"use client"; import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; import { addProject, db, deleteProject } from "@/lib/db"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { ALargeSmallIcon, ArrowDownIcon, ArrowUpIcon, CalendarIcon, ClockIcon, CopyIcon, EditIcon, EllipsisIcon, Grid2X2CheckIcon, Grid2X2XIcon, InfoIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react"; import { Toggle } from "@/components/ui/toggle"; import Search from "@/components/search"; import { useIsMobile } from "@/hooks/use-mobile"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import Project from "@/types/Project"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { z } from "zod"; import { useForm, UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Textarea } from "@/components/ui/textarea"; import { generateUUID } from "@/lib/uuid"; import { DropdownMenu, DropdownMenuCheckboxItem, 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"; import { useDebounce } from "use-debounce"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import { TooltipTrigger } from "@radix-ui/react-tooltip"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { Separator } from "@/components/ui/separator"; import StaticSidebarTrigger from "@/components/static-sidebar-trigger"; import SidebarTriggerAdjustable from "@/components/sidebar-trigger-adjustable"; import ScrollFadingTitle from "@/components/scroll-fading-title"; import AscendingCard from "@/components/ascending-card"; type SortingType = "byCreationDate" | "byEditDate" | "byTitle"; const defaultSortingType: SortingType = "byCreationDate"; const sortProjects = (a: Project, b: Project, sortingType: SortingType) => { switch (sortingType) { case "byCreationDate": return b.creationDate - a.creationDate; case "byEditDate": return b.editDate - a.editDate; case "byTitle": return a.title.localeCompare(b.title); } }; const sortingTypeToString = (sortingType: SortingType) => { switch (sortingType) { case "byCreationDate": return "By Creation Date"; case "byEditDate": return "By Edit Date"; case "byTitle": return "By Title"; } }; const SortingTypeIcon = ({ sortingType }: { sortingType: SortingType }) => { switch (sortingType) { case "byCreationDate": return ; case "byEditDate": return ; case "byTitle": return ; } }; const SortingTypeMenuItem = ({ sortingType, currentSortingType, setSortingType }: { sortingType: SortingType, currentSortingType: SortingType setSortingType: Dispatch>, }) => ( setSortingType(sortingType)}> {sortingTypeToString(sortingType)} ); interface SelectContextData { selecting: boolean; setSelecting: Dispatch>; selectedProjects: string[]; setSelectedProjects: Dispatch>; }; const SelectContext = createContext(null); const useSelectContext = (): SelectContextData => { const context = useContext(SelectContext); if (!context) throw new Error("SelectContext is not provided"); return context; }; const ProjectInfoFormSchema = z.object({ title: z.string().nonempty("Title cannot be empty"), description: z.string().or(z.literal("")) }); const ProjectInfoForm = ({ form }: { form: UseFormReturn> }) => ( <> ( Title )} /> ( Description )} /> > ); const RenameProjectDialog = ({ project }: { project: Project }) => { const renameForm = useForm>({ resolver: zodResolver(ProjectInfoFormSchema), defaultValues: { title: project.title, description: project.description } }); const handleRenameSubmit = async (data: z.infer) => { await db.projects.update(project.uuid, { title: data.title, description: data.description, editDate: Date.now() }); }; return ( Rename Project Change the name of your project. Cancel Rename ); } const DeleteProjectDialog = ({ project }: { project: Project }) => { const handleDelete = async () => { deleteProject(project.uuid); }; return ( Delete Project Are you sure you want to delete the project "{project.title}"? This action cannot be undone. Cancel Delete ); } const ProjectDropdown = ({ project, selected }: { project: Project, selected: boolean }): ReactNode => { const { selecting, setSelecting, selectedProjects, setSelectedProjects } = useSelectContext(); const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [deleteAlertOpen, setDeleteAlertOpen] = useState(false); const isMobile = useIsMobile(); const handleDuplicate = async () => { let originProject = await db.projects.where('uuid').equals(project.origin).first(); if (!originProject) originProject = project; let newProjectTitle = originProject.title; const duplication = await db.duplications.where('uuid').equals(originProject.uuid).first(); if (!duplication) return; newProjectTitle = `${originProject.title} (${duplication.count + 1})`; await db.duplications.update(duplication.uuid, { ...duplication, count: duplication.count + 1 }); const newProject = { ...project, uuid: generateUUID(), creationDate: Date.now(), editDate: Date.now(), title: newProjectTitle, origin: originProject.uuid }; addProject(newProject as Project); }; const handleSelect = () => { if (!selecting) { setSelecting(true); setSelectedProjects([]); } const index = selectedProjects.indexOf(project.uuid); if (index >= 0) { selectedProjects.splice(index, 1); setSelectedProjects([...selectedProjects]); } else { setSelectedProjects([...selectedProjects, project.uuid]); } }; if (isMobile) { return ( {project.title} setRenameDialogOpen(true)}> Rename Additional Options for the Project {selected ? "Deselect" : "Select"} console.log("Edit Project")}> Edit Duplicate setDeleteAlertOpen(true)}> Delete console.log("Project Info")}> Info ); } return ( {project.title} setRenameDialogOpen(true)}> Rename {selected ? "Deselect" : "Select"} console.log("Edit Project")}> Edit Duplicate setDeleteAlertOpen(true)}> Delete console.log("Project Info")}> Info ); }; const ProjectDescription = ({ project }: { project: Project }): ReactNode => { const isMobile = useIsMobile(); if (!project.description) return <>>; if (isMobile) { return ( {project.title} Description Additional Information About the Project {project.description} ); } return ( {project.title} Description {project.description} ); } const ProjectContainer = ({ project }: { project: Project }): ReactNode => { const { selecting, selectedProjects, setSelectedProjects } = useSelectContext(); const date = new Date(project.editDate); // TODO: data-selectable is a really dirty way of deciding whether to trigger selection or not should be reworked in the future const handleCheck = (e: React.MouseEvent) => { if ((e.target as HTMLDivElement).getAttribute("data-selectable") != "true") return; e.stopPropagation(); if (selecting) { const index = selectedProjects.indexOf(project.uuid); if (index >= 0) { selectedProjects.splice(index, 1); setSelectedProjects([...selectedProjects]); } else { setSelectedProjects([...selectedProjects, project.uuid]); } } }; return ( {project.title} {project.description && {project.description}} {project.editDate && Last Edit Date: {date.toLocaleDateString()}, {date.toLocaleTimeString()}} {!selecting && } {selecting && ( )} ) }; export default function Home(): ReactNode { const isMobile = useIsMobile(); const [search, setSearch] = useState(''); const [debouncedSearch] = useDebounce(search, 300); const [selecting, setSelecting] = useState(false); const [selectedProjects, setSelectedProjects] = useState([]); const [sortingType, setSortingType] = useState(defaultSortingType); const [descendingSort, setDescendingSort] = useState(false); const [showDeleteSelectedAlert, setShowDeleteSelectedAlert] = useState(false); const projects = useLiveQuery(() => ( db.projects.toArray() )); const filteredProjects = projects && ( projects.filter((project) => project.title.toLowerCase().includes(debouncedSearch.toLowerCase())) .sort((a, b) => sortProjects(a, b, sortingType) * (descendingSort ? -1 : 1)) ); const newProjectForm = useForm>({ resolver: zodResolver(ProjectInfoFormSchema), defaultValues: { title: "New ClipFusion Project", description: "" } }); const newProjectSubmit = async (data: z.infer) => { const date = Date.now(); addProject({ uuid: generateUUID(), creationDate: date, editDate: date, title: data.title, description: data.description, origin: "" } as Project); }; const handleDeleteSelected = () => { selectedProjects.map((uuid) => deleteProject(uuid)); setSelectedProjects([]); setSelecting(false); }; const context: SelectContextData = { selecting, setSelecting, selectedProjects, setSelectedProjects }; return ( Project Library {projects && (Found {projects.length} projects)} {!isMobile && "New Project"} Create New Project Fill in the information about your project. You can change it at any time later. Cancel Create setSearch(e.target.value)} className={isMobile ? "w-full" : "w-60"} /> {isMobile ? "Sort" : sortingTypeToString(sortingType)} Sort Projects setDescendingSort(pressed)}> {descendingSort ? : } {descendingSort ? "Descending" : "Ascending"} setSelecting(pressed)}> {isMobile ? "Select" : "Select Projects"} {filteredProjects && filteredProjects.map((project) => )} {(projects != undefined && projects.length == 0) && ( Nothing to Show )} {selecting && ( <> setShowDeleteSelectedAlert(true)}> {!isMobile && "Delete All"} {selectedProjects.length} Projects Selected {!isMobile && "More Options"} filteredProjects && (selectedProjects.length == 0 ? setSelectedProjects(filteredProjects.map((project) => project.uuid)) : setSelectedProjects([]))}> {selectedProjects.length == 0 ? : } {selectedProjects.length == 0 ? "Select All" : "Deselect All"} > )} Delete {selectedProjects.length} Projects This action cannot be undone. Cancel Delete ); };
{project.description}
Last Edit Date: {date.toLocaleDateString()}, {date.toLocaleTimeString()}