mirror of
https://github.com/ClipFusion-org/clipfusion.git
synced 2025-08-03 19:15:08 +00:00
ui: implemented project renaming and deleting
This commit is contained in:
parent
7be0181034
commit
5c6aebb5c5
49
package-lock.json
generated
49
package-lock.json
generated
@ -15,12 +15,14 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@react-hook/hotkey": "^3.1.0",
|
"@react-hook/hotkey": "^3.1.0",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
@ -1434,6 +1436,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.2",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.7",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.4",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
||||||
@ -1912,6 +1951,15 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/line-clamp": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
|
||||||
|
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz",
|
||||||
@ -6820,7 +6868,6 @@
|
|||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
|
@ -16,12 +16,14 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@react-hook/hotkey": "^3.1.0",
|
"@react-hook/hotkey": "^3.1.0",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
|
@ -45,8 +45,8 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-geist), sans-serif;
|
font-family: var(--font-geist), sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
|
236
src/app/page.tsx
236
src/app/page.tsx
@ -5,7 +5,7 @@ import { useLiveQuery } from "dexie-react-hooks";
|
|||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CopyIcon, EllipsisIcon, InfoIcon, LetterTextIcon, ListCheckIcon, PencilIcon, PlusIcon, TextIcon, TextQuoteIcon, TrashIcon } from "lucide-react";
|
import { CopyIcon, EditIcon, EllipsisIcon, InfoIcon, ListCheckIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import Search from "@/components/search";
|
import Search from "@/components/search";
|
||||||
import { useIsMobile } from "@/hooks/use-mobile";
|
import { useIsMobile } from "@/hooks/use-mobile";
|
||||||
@ -15,59 +15,202 @@ import Project from "@/types/Project";
|
|||||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, UseFormReturn } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { generateUUID } from "@/lib/uuid";
|
import { generateUUID } from "@/lib/uuid";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
const NewProjectFormSchema = z.object({
|
const ProjectInfoFormSchema = z.object({
|
||||||
title: z.string().nonempty("Title cannot be empty"),
|
title: z.string().nonempty("Title cannot be empty"),
|
||||||
description: z.string().or(z.literal(""))
|
description: z.string().or(z.literal(""))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ProjectInfoForm = ({ form }: { form: UseFormReturn<z.infer<typeof ProjectInfoFormSchema>>}) => (
|
||||||
|
<>
|
||||||
|
<FormField control={form.control} name="title" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
<FormField control={form.control} name="description" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea autoComplete="off" placeholder="Tell something about your project" className="resize-y" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RenameProjectDialog = ({ project }: { project: Project }) => {
|
||||||
|
const renameForm = useForm<z.infer<typeof ProjectInfoFormSchema>>({
|
||||||
|
resolver: zodResolver(ProjectInfoFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
title: project.title,
|
||||||
|
description: project.description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRenameSubmit = async (data: z.infer<typeof ProjectInfoFormSchema>) => {
|
||||||
|
await db.projects.update(project.uuid, {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
editDate: Date.now()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Rename Project</DialogTitle>
|
||||||
|
<DialogDescription>Change the name of your project.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...renameForm}>
|
||||||
|
<form onSubmit={renameForm.handleSubmit(handleRenameSubmit)} className="grid gap-3">
|
||||||
|
<ProjectInfoForm form={renameForm}/>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="submit">Rename</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteProjectDialog = ({ project }: { project: Project }) => {
|
||||||
|
const handleDelete = async () => {
|
||||||
|
await db.projects.delete(project.uuid);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete Project</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete the project "{project.title}"? This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDelete} asChild>
|
||||||
|
<Button variant="destructive">Delete</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectDropdown = ({ project }: { project: Project }): ReactNode => {
|
||||||
|
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||||
|
const [deleteAlertOpen, setDeleteAlertOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDuplicate = async () => {
|
||||||
|
const newProject = {
|
||||||
|
...project,
|
||||||
|
uuid: generateUUID(),
|
||||||
|
creationDate: Date.now(),
|
||||||
|
editDate: Date.now(),
|
||||||
|
title: project.title.includes("Copy of") ? project.title : `Copy of ${project.title}`,
|
||||||
|
origin: project.origin
|
||||||
|
};
|
||||||
|
await db.projects.add(newProject);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<EllipsisIcon/>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="min-w-48">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<DropdownMenuLabel className="font-semibold">{project.title}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem onClick={() => setRenameDialogOpen(true)}>
|
||||||
|
<PencilIcon/> <span className="sr-only">Rename</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator/>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => console.log("Edit Project")}>
|
||||||
|
<EditIcon className="mr-2"/> Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleDuplicate}>
|
||||||
|
<CopyIcon className="mr-2"/> Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem variant="destructive" onClick={() => setDeleteAlertOpen(true)}>
|
||||||
|
<TrashIcon className="mr-2"/> Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator/>
|
||||||
|
<DropdownMenuItem onClick={() => console.log("Project Info")}>
|
||||||
|
<InfoIcon className="mr-2"/> Info
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
|
||||||
|
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
|
||||||
|
<RenameProjectDialog project={project}/>
|
||||||
|
</Dialog>
|
||||||
|
<AlertDialog open={deleteAlertOpen} onOpenChange={setDeleteAlertOpen}>
|
||||||
|
<DeleteProjectDialog project={project}/>
|
||||||
|
</AlertDialog>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectDescription = ({ project }: { project: Project }): ReactNode => {
|
||||||
|
if (!project.description) return <></>;
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<InfoIcon/>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80">
|
||||||
|
<h3 className="font-semibold text-lg">Project Description</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const ProjectContainer = ({
|
const ProjectContainer = ({
|
||||||
project
|
project
|
||||||
}: {
|
}: {
|
||||||
project: Project
|
project: Project
|
||||||
}): ReactNode => {
|
}): ReactNode => {
|
||||||
|
const date = new Date(project.editDate);
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={16 / 9}>
|
<AspectRatio ratio={16 / 9}>
|
||||||
<Card className="relative rounded-lg shadow-md w-full h-full overflow-hidden hover:scale-[101%] hover:drop-shadow-xl duration-100">
|
<Card className="relative rounded-lg shadow-md w-full h-full overflow-hidden hover:scale-[101%] hover:drop-shadow-xl duration-100">
|
||||||
<div className="absolute bottom-0 left-0 h-16 w-full bg-gradient-to-t from-black to-transparent opacity-10"/>
|
<div className="absolute bottom-0 left-0 w-full h-full bg-gradient-to-t from-white dark:from-black to-transparent opacity-50"/>
|
||||||
<div className="absolute bottom-0 left-0 p-2 w-full flex flex-row justify-between items-center">
|
<div className="absolute bottom-0 left-0 p-2 w-full flex flex-row justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold">{project.title}</h3>
|
<h3 className="text-sm sm:text-sm md:text-md lg:text-lg font-semibold line-clamp-1">{project.title}</h3>
|
||||||
{project.creationDate && <p className="text-sm text-secondary-foreground">{new Date(project.creationDate).toLocaleDateString()}</p>}
|
{project.description && <p className="text-sm text-secondary-foreground line-clamp-1">{project.description}</p>}
|
||||||
|
{project.editDate && <p className="text-sm text-secondary-foreground">Last edit date: {date.toLocaleDateString()}, {date.toLocaleTimeString()}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="cursor-pointer m-2">
|
<div className="flex flex-col lg:xl:flex-row items-center gap-1">
|
||||||
<DropdownMenu>
|
<ProjectDescription project={project}/>
|
||||||
<DropdownMenuTrigger>
|
<ProjectDropdown project={project}/>
|
||||||
<EllipsisIcon/> <span className="sr-only">{project.title} additional options</span>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuLabel>{project.title}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<TextIcon/> Rename
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<PencilIcon/> Edit
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<CopyIcon/> Duplicate
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<TrashIcon/> Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator/>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<InfoIcon/> Properties
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@ -83,15 +226,15 @@ export default function Home(): ReactNode {
|
|||||||
return db.projects.filter((project) => project.title.includes(search)).toArray();
|
return db.projects.filter((project) => project.title.includes(search)).toArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
const newProjectForm = useForm<z.infer<typeof NewProjectFormSchema>>({
|
const newProjectForm = useForm<z.infer<typeof ProjectInfoFormSchema>>({
|
||||||
resolver: zodResolver(NewProjectFormSchema),
|
resolver: zodResolver(ProjectInfoFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "New ClipFusion Project",
|
title: "New ClipFusion Project",
|
||||||
description: ""
|
description: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const newProjectSubmit = async (data: z.infer<typeof NewProjectFormSchema>) => {
|
const newProjectSubmit = async (data: z.infer<typeof ProjectInfoFormSchema>) => {
|
||||||
const date = Date.now();
|
const date = Date.now();
|
||||||
await db.projects.add({
|
await db.projects.add({
|
||||||
uuid: generateUUID(),
|
uuid: generateUUID(),
|
||||||
@ -105,7 +248,7 @@ export default function Home(): ReactNode {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 w-full h-full">
|
<div className="p-5 w-full h-full">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2 overscroll-none">
|
||||||
<SidebarTrigger size="lg"/>
|
<SidebarTrigger size="lg"/>
|
||||||
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Project Library</h2>
|
<h2 className="font-bold break-keep text-xl sm:text-2xl md:text-3xl lg:text-4xl leading-none">Project Library</h2>
|
||||||
{projects && <Label className="text-muted-foreground text-sm">(Found {projects.length} projects)</Label>}
|
{projects && <Label className="text-muted-foreground text-sm">(Found {projects.length} projects)</Label>}
|
||||||
@ -129,24 +272,7 @@ export default function Home(): ReactNode {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...newProjectForm}>
|
<Form {...newProjectForm}>
|
||||||
<form onSubmit={newProjectForm.handleSubmit(newProjectSubmit)} className="grid gap-3">
|
<form onSubmit={newProjectForm.handleSubmit(newProjectSubmit)} className="grid gap-3">
|
||||||
<FormField control={newProjectForm.control} name="title" render={({ field }) => (
|
<ProjectInfoForm form={newProjectForm}/>
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Title</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field}/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
|
||||||
<FormField control={newProjectForm.control} name="description" render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Description</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea placeholder="Tell something about your project" className="resize-y" {...field}/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button variant="outline">Cancel</Button>
|
<Button variant="outline">Cancel</Button>
|
||||||
|
@ -87,7 +87,7 @@ export const Dashboard = (): ReactNode => {
|
|||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>
|
<SidebarGroupLabel>
|
||||||
Link
|
Links
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent className="flex flex-row items-center justify-between">
|
<SidebarGroupContent className="flex flex-row items-center justify-between">
|
||||||
<div className="flex flex-row items-center gap-3 pl-2">
|
<div className="flex flex-row items-center gap-3 pl-2">
|
||||||
|
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
3
src/lib/apple.ts
Normal file
3
src/lib/apple.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isIOS(): boolean {
|
||||||
|
return typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
}
|
5
tailwind.config.js
Normal file
5
tailwind.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require("@tailwindcss/line-clamp"),
|
||||||
|
]
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user