Home page touch-ups (#2135)
* Save part images when navigating home * Load part images in project cards if available * Polish home page * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Merge branch 'main' into franknoirot/project-images * Mostly restored link + form functionality * Working cards with images * Comment out project image stuff * Little style tweaks * Remove unused imports * More minor styling tweaks * Merge branch 'main' into franknoirot/project-images * Was using the wrong imported `Project` type * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Revert any docs changes * Revert wasm-lib divergences * Move ProjectCard into its component folder * Remove unused hook useSaveVideoFrame * Remove "hideOnLevel" config from theme setting --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Binary file not shown.
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 40 KiB |
@ -395,6 +395,14 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
trash: (
|
||||||
|
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.5 6H5V8H6M8.5 6V4H11.5V6M8.5 6H11.5M11.5 6H15V8H14M6 8V15.5H8M6 8H14M14 8V15.5H12M8 15.5V10M8 15.5H10M12 15.5V10M12 15.5H10M10 15.5V12"
|
||||||
|
stroke="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
vertical: (
|
vertical: (
|
||||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
@ -1,235 +0,0 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
|
||||||
import { paths } from 'lib/paths'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { ActionButton } from './ActionButton'
|
|
||||||
import {
|
|
||||||
faCheck,
|
|
||||||
faPenAlt,
|
|
||||||
faTrashAlt,
|
|
||||||
faX,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { FILE_EXT } from 'lib/constants'
|
|
||||||
import { Dialog } from '@headlessui/react'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
import Tooltip from './Tooltip'
|
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
function ProjectCard({
|
|
||||||
project,
|
|
||||||
handleRenameProject,
|
|
||||||
handleDeleteProject,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
project: Project
|
|
||||||
handleRenameProject: (
|
|
||||||
e: FormEvent<HTMLFormElement>,
|
|
||||||
f: Project
|
|
||||||
) => Promise<void>
|
|
||||||
handleDeleteProject: (f: Project) => Promise<void>
|
|
||||||
}) {
|
|
||||||
useHotkeys('esc', () => setIsEditing(false))
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
|
||||||
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
|
||||||
|
|
||||||
let inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
|
|
||||||
function handleSave(e: FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault()
|
|
||||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayedTime(dateStr: string) {
|
|
||||||
const date = new Date(dateStr)
|
|
||||||
const startOfToday = new Date()
|
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
|
||||||
return date.getTime() < startOfToday.getTime()
|
|
||||||
? date.toLocaleDateString()
|
|
||||||
: date.toLocaleTimeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getNumberOfFiles() {
|
|
||||||
setNumberOfFiles(project.kcl_file_count)
|
|
||||||
setNumberOfFolders(project.directory_count)
|
|
||||||
}
|
|
||||||
void getNumberOfFiles()
|
|
||||||
}, [project.kcl_file_count, project.directory_count])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus()
|
|
||||||
inputRef.current.select()
|
|
||||||
}
|
|
||||||
}, [inputRef.current])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
{...props}
|
|
||||||
className="group relative min-h-[5em] p-1 rounded-sm border border-chalkboard-20 dark:border-chalkboard-80 hover:!border-primary"
|
|
||||||
>
|
|
||||||
{isEditing ? (
|
|
||||||
<form onSubmit={handleSave} className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
className="min-w-0 p-1 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
|
|
||||||
type="text"
|
|
||||||
id="newProjectName"
|
|
||||||
name="newProjectName"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
defaultValue={project.name}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
type="submit"
|
|
||||||
iconStart={{
|
|
||||||
icon: faCheck,
|
|
||||||
size: 'sm',
|
|
||||||
className: 'p-1',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Rename project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: faX,
|
|
||||||
size: 'sm',
|
|
||||||
iconClassName: 'dark:!text-chalkboard-20',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
className: 'p-1',
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
onClick={() => setIsEditing(false)}
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Cancel
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
className="relative z-0 flex flex-col h-full gap-2 p-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10"
|
|
||||||
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
|
|
||||||
data-testid="project-link"
|
|
||||||
>
|
|
||||||
<div className="flex-1">{project.name?.replace(FILE_EXT, '')}</div>
|
|
||||||
<span className="text-xs text-chalkboard-60">
|
|
||||||
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
|
||||||
{numberOfFolders > 0 &&
|
|
||||||
`/ ${numberOfFolders} folder${
|
|
||||||
numberOfFolders === 1 ? '' : 's'
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-chalkboard-60">
|
|
||||||
Edited{' '}
|
|
||||||
{project.metadata && project.metadata?.modified
|
|
||||||
? getDisplayedTime(project.metadata.modified)
|
|
||||||
: 'never'}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: faPenAlt,
|
|
||||||
className: 'p-1',
|
|
||||||
iconClassName: 'dark:!text-chalkboard-20',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
size: 'xs',
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.nativeEvent.stopPropagation()
|
|
||||||
setIsEditing(true)
|
|
||||||
}}
|
|
||||||
className="!p-0"
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Rename project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
iconStart={{
|
|
||||||
icon: faTrashAlt,
|
|
||||||
className: 'p-1',
|
|
||||||
size: 'xs',
|
|
||||||
bgClassName: '!bg-transparent',
|
|
||||||
iconClassName: '!text-destroy-70',
|
|
||||||
}}
|
|
||||||
className="!p-0 hover:border-destroy-40 dark:hover:border-destroy-40"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
e.nativeEvent.stopPropagation()
|
|
||||||
setIsConfirmingDelete(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip position="left" delay={1000}>
|
|
||||||
Delete project
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<Dialog
|
|
||||||
open={isConfirmingDelete}
|
|
||||||
onClose={() => setIsConfirmingDelete(false)}
|
|
||||||
className="relative z-50"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
|
|
||||||
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
|
|
||||||
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
|
|
||||||
Delete File
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
This will permanently delete "{project.name || 'this file'}".
|
|
||||||
</Dialog.Description>
|
|
||||||
|
|
||||||
<p className="my-4">
|
|
||||||
Are you sure you want to delete "{project.name || 'this file'}
|
|
||||||
"? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={async () => {
|
|
||||||
await handleDeleteProject(project)
|
|
||||||
setIsConfirmingDelete(false)
|
|
||||||
}}
|
|
||||||
iconStart={{
|
|
||||||
icon: faTrashAlt,
|
|
||||||
bgClassName: 'bg-destroy-80',
|
|
||||||
className: 'p-1',
|
|
||||||
size: 'sm',
|
|
||||||
iconClassName: '!text-destroy-70 dark:!text-destroy-40',
|
|
||||||
}}
|
|
||||||
className="hover:border-destroy-40 dark:hover:border-destroy-40"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={() => setIsConfirmingDelete(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProjectCard
|
|
53
src/components/ProjectCard/DeleteProjectDialog.tsx
Normal file
53
src/components/ProjectCard/DeleteProjectDialog.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
import { ActionButton } from 'components/ActionButton'
|
||||||
|
|
||||||
|
interface DeleteProjectDialogProps {
|
||||||
|
projectName: string
|
||||||
|
onConfirm: () => void
|
||||||
|
onDismiss: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteProjectDialog({
|
||||||
|
projectName,
|
||||||
|
onConfirm,
|
||||||
|
onDismiss,
|
||||||
|
}: DeleteProjectDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onClose={onDismiss} className="relative z-50">
|
||||||
|
<div className="fixed inset-0 grid bg-chalkboard-110/80 place-content-center">
|
||||||
|
<Dialog.Panel className="max-w-2xl p-4 border rounded bg-chalkboard-10 dark:bg-chalkboard-100 border-destroy-80">
|
||||||
|
<Dialog.Title as="h2" className="mb-4 text-2xl font-bold">
|
||||||
|
Delete File
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
This will permanently delete "{projectName || 'this file'}
|
||||||
|
".
|
||||||
|
</Dialog.Description>
|
||||||
|
|
||||||
|
<p className="my-4">
|
||||||
|
Are you sure you want to delete "{projectName || 'this file'}
|
||||||
|
"? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={onConfirm}
|
||||||
|
iconStart={{
|
||||||
|
icon: 'trash',
|
||||||
|
bgClassName: 'bg-destroy-10 dark:bg-destroy-80',
|
||||||
|
iconClassName: '!text-destroy-80 dark:!text-destroy-20',
|
||||||
|
}}
|
||||||
|
className="hover:border-destroy-40 dark:hover:border-destroy-40 hover:bg-destroy-10/20 dark:hover:bg-destroy-80/20"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton Element="button" onClick={onDismiss}>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
176
src/components/ProjectCard/ProjectCard.tsx
Normal file
176
src/components/ProjectCard/ProjectCard.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
|
import { paths } from 'lib/paths'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ActionButton } from '../ActionButton'
|
||||||
|
import { FILE_EXT } from 'lib/constants'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import Tooltip from '../Tooltip'
|
||||||
|
import { DeleteProjectDialog } from './DeleteProjectDialog'
|
||||||
|
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
|
function ProjectCard({
|
||||||
|
project,
|
||||||
|
handleRenameProject,
|
||||||
|
handleDeleteProject,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
project: Project
|
||||||
|
handleRenameProject: (
|
||||||
|
e: FormEvent<HTMLFormElement>,
|
||||||
|
f: Project
|
||||||
|
) => Promise<void>
|
||||||
|
handleDeleteProject: (f: Project) => Promise<void>
|
||||||
|
}) {
|
||||||
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
|
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||||
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
// const [imageUrl, setImageUrl] = useState('')
|
||||||
|
|
||||||
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
function handleSave(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayedTime(dateStr: string) {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const startOfToday = new Date()
|
||||||
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
|
return date.getTime() < startOfToday.getTime()
|
||||||
|
? date.toLocaleDateString()
|
||||||
|
: date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getNumberOfFiles() {
|
||||||
|
setNumberOfFiles(project.kcl_file_count)
|
||||||
|
setNumberOfFolders(project.directory_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function setupImageUrl() {
|
||||||
|
// const projectImagePath = await join(project.path, PROJECT_IMAGE_NAME)
|
||||||
|
// if (await exists(projectImagePath)) {
|
||||||
|
// const imageData = await readFile(projectImagePath)
|
||||||
|
// const blob = new Blob([imageData], { type: 'image/jpg' })
|
||||||
|
// const imageUrl = URL.createObjectURL(blob)
|
||||||
|
// setImageUrl(imageUrl)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void getNumberOfFiles()
|
||||||
|
// void setupImageUrl()
|
||||||
|
}, [project.kcl_file_count, project.directory_count])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current && isEditing) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}
|
||||||
|
}, [isEditing, inputRef.current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...props}
|
||||||
|
className="group relative flex flex-col rounded-sm border border-primary/40 dark:border-chalkboard-80 hover:!border-primary"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
data-testid="project-link"
|
||||||
|
to={`${paths.FILE}/${encodeURIComponent(project.default_file)}`}
|
||||||
|
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
|
||||||
|
>
|
||||||
|
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
|
||||||
|
{imageUrl && (
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt=""
|
||||||
|
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div> */}
|
||||||
|
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
|
||||||
|
{isEditing ? (
|
||||||
|
<ProjectCardRenameForm
|
||||||
|
onSubmit={handleSave}
|
||||||
|
className="flex items-center gap-2 p-2"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
project={project}
|
||||||
|
onDismiss={() => setIsEditing(false)}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h3 className="font-sans relative z-0 p-2">
|
||||||
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<span className="px-2 text-chalkboard-60 text-xs">
|
||||||
|
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
||||||
|
{numberOfFolders > 0 &&
|
||||||
|
`/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 text-chalkboard-60 text-xs">
|
||||||
|
Edited{' '}
|
||||||
|
{project.metadata && project.metadata?.modified
|
||||||
|
? getDisplayedTime(project.metadata.modified)
|
||||||
|
: 'never'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{!isEditing && (
|
||||||
|
<div className="absolute z-10 flex items-center gap-1 opacity-0 bottom-2 right-2 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'sketch',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsEditing(true)
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
>
|
||||||
|
<Tooltip position="top-right" delay={1000}>
|
||||||
|
Rename project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'trash',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-30',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.nativeEvent.stopPropagation()
|
||||||
|
setIsConfirmingDelete(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip position="top-right" delay={1000}>
|
||||||
|
Delete project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isConfirmingDelete && (
|
||||||
|
<DeleteProjectDialog
|
||||||
|
projectName={project.name}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await handleDeleteProject(project)
|
||||||
|
setIsConfirmingDelete(false)
|
||||||
|
}}
|
||||||
|
onDismiss={() => setIsConfirmingDelete(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCard
|
67
src/components/ProjectCard/ProjectCardRenameForm.tsx
Normal file
67
src/components/ProjectCard/ProjectCardRenameForm.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { ActionButton } from 'components/ActionButton'
|
||||||
|
import Tooltip from 'components/Tooltip'
|
||||||
|
import { HTMLProps, forwardRef } from 'react'
|
||||||
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
|
interface ProjectCardRenameFormProps extends HTMLProps<HTMLFormElement> {
|
||||||
|
project: Project
|
||||||
|
onDismiss: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectCardRenameForm = forwardRef(
|
||||||
|
(
|
||||||
|
{ project, onDismiss, ...props }: ProjectCardRenameFormProps,
|
||||||
|
ref: React.Ref<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<form {...props}>
|
||||||
|
<input
|
||||||
|
className="min-w-0 dark:bg-chalkboard-80 dark:border-chalkboard-40 focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
id="newProjectName"
|
||||||
|
onClickCapture={(e) => e.preventDefault()}
|
||||||
|
name="newProjectName"
|
||||||
|
required
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
defaultValue={project.name}
|
||||||
|
ref={ref}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
type="submit"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'checkmark',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Rename project
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'close',
|
||||||
|
iconClassName: 'dark:!text-chalkboard-20',
|
||||||
|
bgClassName: '!bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0"
|
||||||
|
onClick={onDismiss}
|
||||||
|
>
|
||||||
|
<Tooltip position="left" delay={1000}>
|
||||||
|
Cancel
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
@ -24,6 +24,8 @@ export const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
|||||||
export const FILE_EXT = '.kcl'
|
export const FILE_EXT = '.kcl'
|
||||||
/** Default file to open when a project is opened */
|
/** Default file to open when a project is opened */
|
||||||
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
|
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
|
||||||
|
/** Thumbnail file name */
|
||||||
|
export const PROJECT_IMAGE_NAME = `main.jpg` as const
|
||||||
/** The localStorage key for last-opened projects */
|
/** The localStorage key for last-opened projects */
|
||||||
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
||||||
/** The default name given to new kcl files in a project */
|
/** The default name given to new kcl files in a project */
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import {
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
faArrowDown,
|
|
||||||
faArrowUp,
|
|
||||||
faCircle,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||||
|
|
||||||
const DESC = ':desc'
|
const DESC = ':desc'
|
||||||
|
|
||||||
export function getSortIcon(currentSort: string, newSort: string) {
|
export function getSortIcon(
|
||||||
|
currentSort: string,
|
||||||
|
newSort: string
|
||||||
|
): CustomIconName {
|
||||||
if (currentSort === newSort) {
|
if (currentSort === newSort) {
|
||||||
return faArrowUp
|
return 'arrowUp'
|
||||||
} else if (currentSort === newSort + DESC) {
|
} else if (currentSort === newSort + DESC) {
|
||||||
return faArrowDown
|
return 'arrowDown'
|
||||||
}
|
}
|
||||||
return faCircle
|
return 'horizontalDash'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextSearchParams(currentSort: string, newSort: string) {
|
export function getNextSearchParams(currentSort: string, newSort: string) {
|
||||||
|
@ -4,16 +4,15 @@ import {
|
|||||||
getNextProjectIndex,
|
getNextProjectIndex,
|
||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
} from '../lib/tauriFS'
|
} from 'lib/tauriFS'
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from 'components/AppHeader'
|
||||||
import ProjectCard from '../components/ProjectCard'
|
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { type HomeLoaderData } from 'lib/types'
|
import { type HomeLoaderData } from 'lib/types'
|
||||||
import Loading from '../components/Loading'
|
import Loading from 'components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
import { ContextFrom, EventFrom } from 'xstate'
|
import { ContextFrom, EventFrom } from 'xstate'
|
||||||
@ -187,9 +186,11 @@ const Home = () => {
|
|||||||
new FormData(e.target as HTMLFormElement)
|
new FormData(e.target as HTMLFormElement)
|
||||||
)
|
)
|
||||||
|
|
||||||
send('Rename project', {
|
if (newProjectName !== project.name) {
|
||||||
data: { oldName: project.name, newName: newProjectName },
|
send('Rename project', {
|
||||||
})
|
data: { oldName: project.name, newName: newProjectName },
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
async function handleDeleteProject(project: Project) {
|
||||||
@ -199,71 +200,93 @@ const Home = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col h-screen overflow-hidden">
|
<div className="relative flex flex-col h-screen overflow-hidden">
|
||||||
<AppHeader showToolbar={false} />
|
<AppHeader showToolbar={false} />
|
||||||
<div className="w-full max-w-5xl px-4 mx-auto my-24 overflow-y-auto lg:px-0">
|
<div className="w-full flex flex-col overflow-hidden max-w-5xl px-4 mx-auto mt-24 lg:px-2">
|
||||||
<section className="flex justify-between">
|
<section>
|
||||||
<h1 className="text-3xl font-bold">Your Projects</h1>
|
<div className="flex justify-between items-baseline select-none">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-8 items-baseline">
|
||||||
<small>Sort by</small>
|
<h1 className="text-3xl font-bold">Your Projects</h1>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
className={
|
onClick={() => send('Create project')}
|
||||||
'text-sm ' +
|
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
||||||
(!sort.includes('name')
|
iconStart={{
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
icon: 'plus',
|
||||||
: '')
|
bgClassName: '!bg-transparent rounded-sm',
|
||||||
}
|
iconClassName:
|
||||||
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
|
'!text-chalkboard-10 transition-transform group-active:rotate-90',
|
||||||
iconStart={{
|
}}
|
||||||
icon: getSortIcon(sort, 'name'),
|
data-testid="home-new-file"
|
||||||
className: 'p-1.5',
|
>
|
||||||
iconClassName: !sort.includes('name')
|
New project
|
||||||
? '!text-chalkboard-40'
|
</ActionButton>
|
||||||
: '',
|
</div>
|
||||||
size: 'sm',
|
<div className="flex gap-2 items-center">
|
||||||
}}
|
<small>Sort by</small>
|
||||||
>
|
<ActionButton
|
||||||
Name
|
Element="button"
|
||||||
</ActionButton>
|
className={
|
||||||
<ActionButton
|
'text-xs border-primary/10 ' +
|
||||||
Element="button"
|
(!sort.includes('name')
|
||||||
className={
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
'text-sm ' +
|
: '')
|
||||||
(!isSortByModified
|
}
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
onClick={() =>
|
||||||
: '')
|
setSearchParams(getNextSearchParams(sort, 'name'))
|
||||||
}
|
}
|
||||||
onClick={() =>
|
iconStart={{
|
||||||
setSearchParams(getNextSearchParams(sort, 'modified'))
|
icon: getSortIcon(sort, 'name'),
|
||||||
}
|
bgClassName: 'bg-transparent',
|
||||||
iconStart={{
|
iconClassName: !sort.includes('name')
|
||||||
icon: sort ? getSortIcon(sort, 'modified') : faArrowDown,
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
||||||
className: 'p-1.5',
|
: '',
|
||||||
iconClassName: !isSortByModified ? '!text-chalkboard-40' : '',
|
}}
|
||||||
size: 'sm',
|
>
|
||||||
}}
|
Name
|
||||||
>
|
</ActionButton>
|
||||||
Last Modified
|
<ActionButton
|
||||||
</ActionButton>
|
Element="button"
|
||||||
|
className={
|
||||||
|
'text-xs border-primary/10 ' +
|
||||||
|
(!isSortByModified
|
||||||
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
setSearchParams(getNextSearchParams(sort, 'modified'))
|
||||||
|
}
|
||||||
|
iconStart={{
|
||||||
|
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
iconClassName: !isSortByModified
|
||||||
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Last Modified
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
<section data-testid="home-section">
|
|
||||||
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
<p className="my-4 text-sm text-chalkboard-80 dark:text-chalkboard-30">
|
||||||
Loaded from{' '}
|
Loaded from{' '}
|
||||||
<span className="text-chalkboard-90 dark:text-chalkboard-20">
|
<Link
|
||||||
|
to="settings?tab=user#projectDirectory"
|
||||||
|
className="text-chalkboard-90 dark:text-chalkboard-20 underline underline-offset-2"
|
||||||
|
>
|
||||||
{settings.app.projectDirectory.current}
|
{settings.app.projectDirectory.current}
|
||||||
</span>
|
|
||||||
.{' '}
|
|
||||||
<Link to="settings" className="underline underline-offset-2">
|
|
||||||
Edit in settings
|
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
data-testid="home-section"
|
||||||
|
className="flex-1 overflow-y-auto pr-2 pb-24"
|
||||||
|
>
|
||||||
{state.matches('Reading projects') ? (
|
{state.matches('Reading projects') ? (
|
||||||
<Loading>Loading your Projects...</Loading>
|
<Loading>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<ul className="grid w-full grid-cols-4 gap-4 my-8">
|
<ul className="grid w-full grid-cols-4 gap-4">
|
||||||
{projects.sort(getSortFunction(sort)).map((project) => (
|
{projects.sort(getSortFunction(sort)).map((project) => (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.name}
|
key={project.name}
|
||||||
@ -278,14 +301,6 @@ const Home = () => {
|
|||||||
No Projects found, ready to make your first one?
|
No Projects found, ready to make your first one?
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={() => send('Create project')}
|
|
||||||
iconStart={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
|
|
||||||
data-testid="home-new-file"
|
|
||||||
>
|
|
||||||
New project
|
|
||||||
</ActionButton>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
Reference in New Issue
Block a user