Organize/components/projects/project-card.tsx

86 lines
3.5 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { FolderKanban, MoreVertical, Pencil, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog";
import { RenameProjectDialog } from "@/components/projects/rename-project-dialog";
import { useProjects } from "@/components/projects/projects-context";
import type { ProjectDTO } from "@/types/project";
export function ProjectCard({ project }: { project: ProjectDTO }) {
const { removeProject } = useProjects();
const [renameOpen, setRenameOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
return (
<>
{/* The whole tile opens the project; the dropdown sits on top of it
as a sibling (not nested in the link) with pointer-events
re-enabled, so its clicks don't also navigate. `relative` on that
wrapper matters, not just decorative: an absolutely-positioned
element paints after (on top of) non-positioned in-flow content
regardless of DOM order, so without it the Link would still win
every hit-test even under a pointer-events-none ancestor. */}
<div className="group relative flex h-32 flex-col justify-between rounded-2xl border p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md">
<Link
href={`/projects/${project.id}`}
className="absolute inset-0 rounded-xl"
aria-label={`Open ${project.title}`}
/>
<div className="pointer-events-none flex items-start justify-between gap-1">
<FolderKanban className="size-5 shrink-0 text-primary" />
<div className="relative pointer-events-auto">
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
className="flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
aria-label={`More options for ${project.title}`}
>
<MoreVertical className="size-4" />
</button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setRenameOpen(true)}>
<Pencil className="size-4" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => setConfirmOpen(true)}>
<Trash2 className="size-4" />
Delete project
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<h3 className="pointer-events-none min-w-0 truncate font-heading text-[15px] font-semibold tracking-tight">
{project.title}
</h3>
</div>
<RenameProjectDialog project={project} open={renameOpen} onOpenChange={setRenameOpen} />
<ConfirmDeleteDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
title="Delete project?"
description={`Delete "${project.title}" and everything in it -- all of its categories, groups, and to-dos? This can't be undone.`}
onConfirm={async () => {
await removeProject(project.id);
}}
/>
</>
);
}