"use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { ClockIcon, ExternalLinkIcon, MessageSquarePlusIcon, MoreVerticalIcon, Trash2Icon, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { ACTIVITY_LABEL, ACTIVITY_OPTIONS, STAGES, STAGE_LABEL, STAGE_TONE } from "@/lib/stages"; import type { ActivityType, Stage } from "@prisma/client"; import { addActivityAction, deleteApplicationAction, logFollowUpAction, moveStageAction, snoozeFollowUpAction, } from "@/server/actions"; import { cn } from "@/lib/utils"; import { linkHref } from "@/lib/social"; export type ActionTarget = { id: string; company: string; roleTitle: string; stage: Stage; jobUrl: string; }; /** * The verbs, on the object you are already looking at. * * The board had none at all: moving a stage meant dragging, and logging what * happened meant opening the panel, finding the box and closing it again. One * component for the board card and the list row, because two menus offering * different things on the same application is how a product starts feeling * arbitrary. * * Every handler stops the event rather than preventing it. Radix composes its * own trigger handler after the child's and skips it entirely when the event * is already defaultPrevented, so preventDefault here would open nothing — * and the failure is silent. */ export function ApplicationActions({ application, onDone, className, }: { application: ActionTarget; /** Called after any write, so a board can refresh without a full reload. */ onDone?: () => void; className?: string; }) { const router = useRouter(); const [pending, startTransition] = useTransition(); const [logging, setLogging] = useState(false); const [body, setBody] = useState(""); const [type, setType] = useState("NOTE"); const posting = linkHref(application.jobUrl); const run = (work: () => Promise, done: string) => { startTransition(async () => { try { await work(); toast.success(done); onDone?.(); router.refresh(); } catch (error) { toast.error(error instanceof Error ? error.message : "That did not work."); } }); }; return ( <> {/* The menu and the dialog portal out of the card, but React's synthetic events still bubble through the React tree — so a press inside either would reach the draggable that rendered them. */} event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onTouchStart={(event) => event.stopPropagation()} > Move to {STAGES.map((stage) => ( run( () => moveStageAction(application.id, stage), `${application.company} → ${STAGE_LABEL[stage]}`, ) } > {STAGE_LABEL[stage]} ))} setLogging(true)}> Log what happened Follow-up run(() => logFollowUpAction(application.id), "Logged, and back in a week") } > Chased it — log and come back in a week {[3, 7, 14].map((days) => ( run(() => snoozeFollowUpAction(application.id, days), `Snoozed ${days} days`) } > Push out {days} days ))} {posting && ( Open the posting )} { if ( !confirm( `Delete the ${application.company} application? Its timeline and tasks go with it, and you can restore all of it from the archive.`, ) ) return; run(() => deleteApplicationAction(application.id), "Deleted"); }} > Delete event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onTouchStart={(event) => event.stopPropagation()} > Log what happened {application.company} — {application.roleTitle}