diff --git a/src/components/board/board-column.tsx b/src/components/board/board-column.tsx new file mode 100644 index 0000000..c13ef88 --- /dev/null +++ b/src/components/board/board-column.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { cn } from "@/lib/cn"; +import type { ColumnRecord } from "@/lib/pb/types"; + +export function BoardColumn({ + column, + count, + isOver, + children, + setDroppableRef, +}: { + column: ColumnRecord; + count: number; + isOver?: boolean; + children: React.ReactNode; + setDroppableRef?: (element: HTMLElement | null) => void; +}) { + return ( +
+
+

+ {column.name} +

+ + {count} + +
+ +
+ {children} + + {count === 0 && ( +

+ {column.is_inbox ? "No requests" : "Nothing here"} +

+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/board/card-tile.tsx b/src/components/board/card-tile.tsx new file mode 100644 index 0000000..760a389 --- /dev/null +++ b/src/components/board/card-tile.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { forwardRef } from "react"; +import { Inbox, CalendarDays } from "lucide-react"; + +import { Pill } from "@/components/ui/pill"; +import { cn } from "@/lib/cn"; +import { classifyDueDate, formatDueDate } from "@/lib/board/dates"; +import type { CardRecord, Priority } from "@/lib/pb/types"; + +const PRIORITY_TONE: Record = { + critical: "critical", + high: "high", + medium: "medium", + low: "low", +}; + +const PRIORITY_LABEL: Record = { + critical: "Critical", + high: "High", + medium: "Medium", + low: "Low", +}; + +export const CardTile = forwardRef< + HTMLDivElement, + { + card: CardRecord; + dragging?: boolean; + className?: string; + children?: React.ReactNode; + } & React.HTMLAttributes +>(function CardTile({ card, dragging, className, children, ...props }, ref) { + const due = classifyDueDate(card.target_date); + const dueLabel = formatDueDate(card.target_date); + + return ( +
+

+ {card.title} +

+ +
+ + {PRIORITY_LABEL[card.priority]} + + + {dueLabel && ( + // The warm accent's second sanctioned use: an overdue date. Anything + // not yet late stays neutral, or "late" stops registering. + + + {dueLabel} + + )} + + {card.created_by_role === "client" && !card.accepted && ( + + + Requested + + )} +
+ + {children} +
+ ); +}); \ No newline at end of file diff --git a/src/components/board/client-card-controls.tsx b/src/components/board/client-card-controls.tsx new file mode 100644 index 0000000..3628691 --- /dev/null +++ b/src/components/board/client-card-controls.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; + +import { + Dropdown, + DropdownContent, + DropdownItem, + DropdownLabel, + DropdownTrigger, +} from "@/components/ui/dropdown"; +import type { CardRecord, Priority } from "@/lib/pb/types"; + +const PRIORITIES: Array<{ value: Priority; label: string }> = [ + { value: "critical", label: "Critical" }, + { value: "high", label: "High" }, + { value: "medium", label: "Medium" }, + { value: "low", label: "Low" }, +]; + +/** + * The only two fields a client manager may change. + * + * Both go through PATCH /api/client/cards/:id, never straight to PocketBase. + * Their token cannot write to the cards collection at all — see Plan 1a — so + * a direct SDK call from here would simply fail. The route is the path. + */ +export function ClientCardControls({ + card, + onPatched, +}: { + card: CardRecord; + onPatched: (card: CardRecord) => void; +}) { + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + async function patch(body: Record) { + setSaving(true); + setError(null); + + const previous = { ...card }; + onPatched({ ...card, ...body } as CardRecord); + + const response = await fetch(`/api/client/cards/${card.id}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + + setSaving(false); + + if (!response.ok) { + onPatched(previous); + setError("Could not save"); + return; + } + + const { card: updated } = await response.json(); + onPatched({ ...card, ...updated } as CardRecord); + } + + const currentPriority = + PRIORITIES.find((p) => p.value === card.priority) ?? PRIORITIES[2]; + + return ( +
+ + + + + + Priority + {PRIORITIES.map((option) => ( + patch({ priority: option.value })} + > + {option.label} + + ))} + + + + + + {error && ( + + {error} + + )} +
+ ); +} \ No newline at end of file