Files
cadhub/app/web/src/components/IdeToolbar/IdeToolbar.js

423 lines
14 KiB
JavaScript

import { useState } from 'react'
import Popover from '@material-ui/core/Popover'
import OutBound from 'src/components/OutBound'
import ReactGA from 'react-ga'
import { Link, routes, navigate } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import Button from 'src/components/Button'
import ImageUploader from 'src/components/ImageUploader'
import Svg from '../Svg/Svg'
import LoginModal from 'src/components/LoginModal'
import { FORK_PART_MUTATION } from 'src/components/IdePartCell'
import { QUERY as UsersPartsQuery } from 'src/components/PartsOfUserCell'
import useUser from 'src/helpers/hooks/useUser'
import useKeyPress from 'src/helpers/hooks/useKeyPress'
import { captureAndSaveViewport } from 'src/helpers/cloudinary'
const IdeToolbar = ({
canEdit,
isChanges,
onSave,
onExport,
userNamePart,
isDraft,
code,
onCapture,
}) => {
const [anchorEl, setAnchorEl] = useState(null)
const [whichPopup, setWhichPopup] = useState(null)
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false)
const { isAuthenticated, currentUser } = useAuth()
const showForkButton = !(canEdit || isDraft)
const [title, setTitle] = useState('untitled-part')
const [captureState, setCaptureState] = useState(false)
const { user } = useUser()
useKeyPress((e) => {
const rx = /INPUT|SELECT|TEXTAREA/i
const didPressBackspaceOutsideOfInput =
(e.key == 'Backspace' || e.keyCode == 8) && !rx.test(e.target.tagName)
if (didPressBackspaceOutsideOfInput) {
e.preventDefault()
}
})
const [forkPart] = useMutation(FORK_PART_MUTATION, {
refetchQueries: [
{
query: UsersPartsQuery,
variables: { userName: userNamePart?.userName || user?.userName },
},
],
})
const handleClick = ({ event, whichPopup }) => {
setAnchorEl(event.currentTarget)
setWhichPopup(whichPopup)
}
const handleClose = () => {
setAnchorEl(null)
setWhichPopup(null)
}
const saveFork = async () => {
const { publicId } = await captureAndSaveViewport()
return forkPart({
variables: {
input: {
userId: currentUser.sub,
title,
code,
mainImage: publicId,
},
},
})
}
const handleSave = async () => {
if (isDraft && isAuthenticated) {
const { data } = await saveFork()
navigate(
routes.ide({
userName: data?.forkPart?.user?.userName,
partTitle: data?.forkPart?.title,
})
)
toast.success(`Part created with title: ${data?.forkPart?.title}.`)
} else if (isAuthenticated) onSave()
else recordedLogin()
}
const handleSaveAndEdit = async () => {
const { data } = await saveFork()
const {
user: { userName },
title: partTitle,
} = data?.forkPart || { user: {} }
navigate(routes.part({ userName, partTitle }))
}
const recordedLogin = async () => {
ReactGA.event({
category: 'login',
action: 'ideToolbar signup prompt from fork',
})
setIsLoginModalOpen(true)
}
const handleDownload = (url) => {
const aTag = document.createElement('a')
document.body.appendChild(aTag)
aTag.href = url
aTag.style.display = 'none'
aTag.download = `CadHub_${Date.now()}.jpg`
aTag.click()
document.body.removeChild(aTag)
}
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
}
const transformOrigin = {
vertical: 'top',
horizontal: 'center',
}
const id = open ? 'simple-popover' : undefined
return (
<div
id="cadhub-ide-toolbar"
className="flex bg-gradient-to-r from-gray-900 to-indigo-900 pt-1"
>
{!isDraft && (
<>
<div className="flex items-center">
<div className="h-8 w-8 ml-4">
<ImageUploader
className="rounded-full object-cover"
aspectRatio={1}
imageUrl={userNamePart?.image}
width={80}
/>
</div>
<div className="text-indigo-400 ml-2 mr-8">
<Link to={routes.user({ userName: userNamePart?.userName })}>
{userNamePart?.userName}
</Link>
</div>
</div>
<Button
iconName="arrow-left"
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
shouldAnimateHover
onClick={() => {
navigate(routes.part(userNamePart))
}}
>
Part Profile
</Button>
</>
)}
<Button
iconName={showForkButton ? 'fork' : 'save'}
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
shouldAnimateHover
onClick={handleSave}
>
{showForkButton ? 'Fork' : 'Save'}
{isChanges && !isDraft && (
<span className="relative h-4">
<span className="text-pink-400 text-2xl absolute transform -translate-y-3">
*
</span>
</span>
)}
</Button>
{isDraft && isAuthenticated && (
<div className="flex items-center">
<Button
iconName={'save'}
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200 mr-"
shouldAnimateHover
onClick={handleSaveAndEdit}
>
Save & Edit Profile
</Button>
<div className="ml-4 text-indigo-300">title:</div>
<input
className="rounded ml-4 px-2"
value={title}
onChange={({ target }) =>
setTitle(target?.value.replace(/([^a-zA-Z\d_:])/g, '-'))
}
/>
<div className="w-px ml-4 bg-pink-400 h-10"></div>
</div>
)}
<div>
<Button
iconName="logout"
className="ml-3 shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-200"
shouldAnimateHover
aria-describedby={id}
onClick={(event) => handleClick({ event, whichPopup: 'export' })}
>
Export
</Button>
<Popover
id={id}
open={whichPopup === 'export'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<ul className="text-sm py-2 text-gray-500">
{['STEP', 'STL', 'OBJ'].map((exportType) => (
<li key={exportType} className="px-4 py-2 hover:bg-gray-200">
<button onClick={() => onExport(exportType)}>
export
<span className="pl-1 text-base text-indigo-600">
{exportType}
</span>
</button>
</li>
))}
</ul>
</Popover>
</div>
<div className="ml-auto flex items-center">
{/* Capture Screenshot link. Should only appear if part has been saved and is editable. */}
{!isDraft && canEdit && (
<div>
<button
onClick={async (event) => {
handleClick({ event, whichPopup: 'capture' })
setCaptureState(await onCapture())
}}
className="text-indigo-300 flex items-center pr-6"
>
Save Part Image <Svg name="camera" className="pl-2 w-8" />
</button>
<Popover
id={id}
open={whichPopup === 'capture'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-2 text-gray-500">
{!captureState ? (
'Loading...'
) : (
<div className="grid grid-cols-2">
<div
className="rounded m-auto"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div>
<div className="p-2 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<button
className="flex justify-center mb-4"
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg.public_id,
updated: true,
})
}}
>
<Svg
name="refresh"
className="mr-2 w-4 text-indigo-600"
/>{' '}
Update Part Image
</button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Part Image Updated
</div>
)}
<Button
iconName="save"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={() =>
handleDownload(captureState.imageObjectURL)
}
>
Download
</Button>
</div>
</div>
)}
</div>
</Popover>
</div>
)}
<div>
<button
onClick={(event) => handleClick({ event, whichPopup: 'tips' })}
className="text-indigo-300 flex items-center pr-6"
>
Tips <Svg name="lightbulb" className="pl-2 w-8" />
</button>
<Popover
id={id}
open={whichPopup === 'tips'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="p-4">
<div className="text-sm p-2 text-gray-500">
Press F5 to regenerate model
</div>
<OutBound
className="text-gray-600 underline p-2"
to="https://ronie.medium.com/cascade-studio-tutorial-ee2f1c42c829"
>
See the tutorial
</OutBound>
</div>
</Popover>
</div>
<div>
<button
onClick={(event) => handleClick({ event, whichPopup: 'feedback' })}
className="text-indigo-300 flex items-center pr-6"
>
Feedback <Svg name="flag" className="pl-2 w-8" />
</button>
<Popover
id={id}
open={whichPopup === 'feedback'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-2 text-gray-500 max-w-md">
If there's a feature you really want or you found a bug, either
make a{' '}
<OutBound
className="text-gray-600 underline"
to="https://github.com/Irev-Dev/cadhub/issues"
>
github issue
</OutBound>{' '}
or swing by the{' '}
<OutBound
className="text-gray-600 underline"
to="https://discord.gg/SD7zFRNjGH"
>
discord server
</OutBound>
.
</div>
</Popover>
</div>
<div>
<button
onClick={(event) => handleClick({ event, whichPopup: 'issues' })}
className="text-indigo-300 flex items-center pr-6"
>
Known issues <Svg name="exclamation-circle" className="pl-2 w-8" />
</button>
<Popover
id={id}
open={whichPopup === 'issues'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-4 text-gray-500 max-w-md">
<div className="text-base text-gray-700 py-2">
Can't export stl/obj/STEP?
</div>
Currently exports are only working for chrome and edge browsers
<p>
If this problem is frustrating to you, leave a comment on its{' '}
<OutBound
className="text-gray-600 underline"
to="https://github.com/zalo/CascadeStudio/pull/39#issuecomment-766206091"
>
github issue
</OutBound>{' '}
to help prioritize it.
</p>
</div>
</Popover>
</div>
</div>
<LoginModal
open={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
shouldStartWithSignup
/>
</div>
)
}
export default IdeToolbar