86 lines
3.5 KiB
TypeScript
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);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|