implemented barebones project selection ui

This commit is contained in:
corgifist 2025-07-26 02:36:11 +03:00
parent 2d55a7092c
commit 43542f2c34
2 changed files with 84 additions and 11 deletions

View File

@ -48,7 +48,7 @@ export default async function RootLayout({
<ThemeProvider> <ThemeProvider>
<SidebarProvider> <SidebarProvider>
<Dashboard/> <Dashboard/>
<main className="w-full h-full"> <main className="w-full">
<PersistenceProvider> <PersistenceProvider>
{children} {children}
</PersistenceProvider> </PersistenceProvider>

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import { ReactNode, useState } from "react"; import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState } from "react";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { addProject, db, deleteProject } from "@/lib/db"; import { addProject, db, deleteProject } 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, EditIcon, EllipsisIcon, InfoIcon, ListCheckIcon, PencilIcon, PlusIcon, TrashIcon } from "lucide-react"; import { CopyIcon, EditIcon, EllipsisIcon, Grid2X2CheckIcon, 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";
@ -24,6 +24,22 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem,
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
import { Checkbox } from "@/components/ui/checkbox";
interface SelectContextData {
selecting: boolean;
setSelecting: Dispatch<SetStateAction<boolean>>;
selectedProjects: string[];
setSelectedProjects: Dispatch<SetStateAction<string[]>>;
};
const SelectContext = createContext<SelectContextData | null>(null);
const useSelectContext = (): SelectContextData => {
const context = useContext(SelectContext);
if (!context) throw new Error("SelectContext is not provided");
return context;
};
const ProjectInfoFormSchema = z.object({ const ProjectInfoFormSchema = z.object({
title: z.string().nonempty("Title cannot be empty"), title: z.string().nonempty("Title cannot be empty"),
@ -116,7 +132,16 @@ const DeleteProjectDialog = ({ project }: { project: Project }) => {
); );
} }
const ProjectDropdown = ({ project }: { project: Project }): ReactNode => { const ProjectDropdown = ({
project,
selected,
setSelected
}: {
project: Project,
selected: boolean,
setSelected: Dispatch<SetStateAction<boolean>>
}): ReactNode => {
const { selecting, setSelecting } = useSelectContext();
const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [deleteAlertOpen, setDeleteAlertOpen] = useState(false); const [deleteAlertOpen, setDeleteAlertOpen] = useState(false);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -145,6 +170,11 @@ const ProjectDropdown = ({ project }: { project: Project }): ReactNode => {
addProject(newProject as Project); addProject(newProject as Project);
}; };
const handleSelect = () => {
if (!selecting) setSelecting(true);
setSelected(!selected);
};
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -161,6 +191,9 @@ const ProjectDropdown = ({ project }: { project: Project }): ReactNode => {
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem onClick={handleSelect}>
<Grid2X2CheckIcon className="mr-2" /> Select
</DropdownMenuItem>
<DropdownMenuItem onClick={() => console.log("Edit Project")}> <DropdownMenuItem onClick={() => console.log("Edit Project")}>
<EditIcon className="mr-2" /> Edit <EditIcon className="mr-2" /> Edit
</DropdownMenuItem> </DropdownMenuItem>
@ -211,9 +244,35 @@ const ProjectContainer = ({
}: { }: {
project: Project project: Project
}): ReactNode => { }): ReactNode => {
const { selecting, selectedProjects, setSelectedProjects } = useSelectContext();
const [selected, setSelected] = useState(false);
const [mounted, setMounted] = useState(false);
if (!selecting && selected) setSelected(false);
const date = new Date(project.editDate); const date = new Date(project.editDate);
const handleClick = () => {
if (selecting) {
setSelected(!selected);
}
};
// Automatically add UUID to the selectedProjects
useEffect(() => {
if (mounted) {
if (selected) {
setSelectedProjects([...selectedProjects, project.uuid]);
} else {
const newSelectedProject = selectedProjects;
newSelectedProject.splice(newSelectedProject.indexOf(project.uuid), 1);
setSelectedProjects(newSelectedProject);
}
}
setMounted(true);
}, [selected]);
return ( return (
<AspectRatio ratio={16 / 9}> <AspectRatio ratio={16 / 9} onClick={handleClick}>
<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 w-full h-full bg-gradient-to-t from-white dark:from-black to-transparent opacity-50" /> <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">
@ -224,9 +283,14 @@ const ProjectContainer = ({
</div> </div>
<div className="flex flex-col lg:xl:flex-row items-center gap-1"> <div className="flex flex-col lg:xl:flex-row items-center gap-1">
<ProjectDescription project={project} /> <ProjectDescription project={project} />
<ProjectDropdown project={project} /> <ProjectDropdown selected={selected} setSelected={setSelected} project={project} />
</div> </div>
</div> </div>
{selecting && (
<div className="absolute top-0 right-0 p-2">
<Checkbox checked={selected} onCheckedChange={(checked) => setSelected(checked as boolean)}/>
</div>
)}
</Card> </Card>
</AspectRatio> </AspectRatio>
) )
@ -235,6 +299,8 @@ const ProjectContainer = ({
export default function Home(): ReactNode { export default function Home(): ReactNode {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selecting, setSelecting] = useState(false);
const [selectedProjects, setSelectedProjects] = useState<string[]>([]);
const [debouncedSearch] = useDebounce(search, 300); const [debouncedSearch] = useDebounce(search, 300);
const projects = useLiveQuery(() => ( const projects = useLiveQuery(() => (
@ -261,15 +327,22 @@ export default function Home(): ReactNode {
} as Project); } as Project);
}; };
const context: SelectContextData = {
selecting,
setSelecting,
selectedProjects,
setSelectedProjects
};
return ( return (
<> <SelectContext.Provider value={context}>
<div className="p-5 w-full h-full"> <div className="p-5">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<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>}
</div> </div>
<div className="flex flex-row items-center justify-between sticky top-safe bg-background gap-2 mt-3 pb-2 pt-2 w-full z-20"> <div className="flex flex-row items-center justify-between sticky top-safe bg-background gap-2 mt-3 pb-2 pt-2 w-full z-10">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -303,7 +376,7 @@ export default function Home(): ReactNode {
</Dialog> </Dialog>
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<Toggle variant="outline"> <Toggle variant="outline" pressed={selecting} onPressedChange={(pressed: boolean) => setSelecting(pressed)}>
<ListCheckIcon /> {!isMobile && "Select Projects"} <ListCheckIcon /> {!isMobile && "Select Projects"}
</Toggle> </Toggle>
<Search placeholder="Search Projects" value={search} onChange={(e) => setSearch(e.target.value)} /> <Search placeholder="Search Projects" value={search} onChange={(e) => setSearch(e.target.value)} />
@ -318,6 +391,6 @@ export default function Home(): ReactNode {
</div> </div>
)} )}
</div> </div>
</> </SelectContext.Provider>
); );
}; };