diff --git a/src/components/dashboard-navigation.ts b/src/components/dashboard-navigation.ts index 8eb40fa..2a39a61 100644 --- a/src/components/dashboard-navigation.ts +++ b/src/components/dashboard-navigation.ts @@ -8,6 +8,7 @@ import { type LucideIcon, Settings, ShieldCheck, + Tags, Users, UsersRound, } from "lucide-react" @@ -63,6 +64,7 @@ export const dashboardNavigation = [ { title: "Associations", url: "/dashboard/web/associations", icon: Users }, { title: "Guides", url: "/dashboard/web/guides", icon: BookOpen }, { title: "FAQs", url: "/dashboard/web/faqs", icon: CircleQuestionMark }, + { title: "Group labels", url: "/dashboard/web/group-labels", icon: Tags }, ], }, ] as const satisfies readonly DashboardNavigationCategory[] diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 0c0ca19..d6cdc12 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -109,4 +109,83 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { ) } -export { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } +const ComboboxValue = ComboboxPrimitive.Value + +function ComboboxChipsGroup({ className, ...props }: ComboboxPrimitive.InputGroup.Props) { + return ( + + ) +} + +function ComboboxChips({ className, ...props }: ComboboxPrimitive.Chips.Props) { + return ( + + ) +} + +function ComboboxChip({ className, ...props }: ComboboxPrimitive.Chip.Props) { + return ( + + ) +} + +function ComboboxChipRemove({ className, ...props }: ComboboxPrimitive.ChipRemove.Props) { + return ( + + + + ) +} + +function ComboboxChipsInput({ className, ...props }: ComboboxPrimitive.Input.Props) { + return ( + + ) +} + +export { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipRemove, + ComboboxChipsGroup, + ComboboxChipsInput, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, + ComboboxValue, +} diff --git a/src/features/group-labels/group-label-card.tsx b/src/features/group-labels/group-label-card.tsx new file mode 100644 index 0000000..1111db9 --- /dev/null +++ b/src/features/group-labels/group-label-card.tsx @@ -0,0 +1,225 @@ +import { CircleDashed, LoaderCircle, Pencil, Save, Trash2, X } from "lucide-react" +import { useState } from "react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { cn } from "@/lib/utils" + +import { GroupLabelColorPicker } from "./group-label-color-picker" +import { GROUP_LABEL_DESCRIPTION_MAX, GROUP_LABEL_MAX, getGroupLabelColor } from "./group-labels.constants" +import type { GroupLabel, GroupLabelFormValues } from "./types" + +type GroupLabelCardProps = { + groupLabel: GroupLabel + draft: boolean + initialEditActive: boolean + onCancelDraft: () => void + onDelete: () => Promise + onSave: (values: GroupLabelFormValues) => Promise +} + +export function GroupLabelCard({ + groupLabel, + draft, + initialEditActive, + onCancelDraft, + onDelete, + onSave, +}: GroupLabelCardProps) { + const [label, setLabel] = useState(groupLabel.label) + const [editing, setEditing] = useState(initialEditActive) + const [color, setColor] = useState(groupLabel.color) + const [description, setDescription] = useState(groupLabel.description ?? "") + const [saving, setSaving] = useState(false) + const [deleting, setDeleting] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + const canSave = Boolean(label.trim()) + + function resetFields() { + setLabel(groupLabel.label) + setColor(groupLabel.color) + setDescription(groupLabel.description ?? "") + } + + function cancelEdit() { + if (draft) { + onCancelDraft() + return + } + resetFields() + setEditing(false) + } + + async function save() { + if (saving || !canSave) return + setSaving(true) + try { + const saved = await onSave({ label: label.trim(), color, description: description.trim() }) + if (saved) setEditing(false) + } finally { + setSaving(false) + } + } + + async function remove() { + setDeleting(true) + try { + if (await onDelete()) setDeleteOpen(false) + } finally { + setDeleting(false) + } + } + + const swatch = getGroupLabelColor(groupLabel.color) + + return ( + <> + + + + {editing ? ( +
+ {draft && ( + + Unsaved draft + + )} +
+ + {draft ? ( + setLabel(event.target.value)} + className="bg-background text-base font-medium" + maxLength={GROUP_LABEL_MAX} + required + autoFocus + /> + ) : ( + + {label} + + )} +
+
+ ) : ( + + {groupLabel.label} + + )} +
+ + {editing ? ( + <> + + + + ) : ( + <> + + + + )} + +
+ + {editing ? ( +