From 3a4e68dadf58790314184f487f2dd2431f15c57b Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 20 Nov 2025 15:03:31 +0700 Subject: [PATCH] feat: Add coupon edit route and multiselect component Fixed blank coupon edit page and added multiselect component 1. Fixed Missing Route: - Added CouponEdit import in App.tsx - Added route: /coupons/:id -> CouponEdit component - Edit page now loads correctly 2. Created MultiSelect Component: - Shadcn-based multiselect with search - Badge display for selected items - Click badge X to remove - Shows +N more when exceeds maxDisplay - Searchable dropdown with Command component - Keyboard accessible Features: - Selected items shown as badges - Remove item by clicking X on badge - Search/filter options - Checkbox indicators - Max display limit (default 3) - Responsive and accessible Next: Add product/category/brand selectors to coupon form --- admin-spa/src/App.tsx | 2 + admin-spa/src/components/ui/multi-select.tsx | 147 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 admin-spa/src/components/ui/multi-select.tsx diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index 0b3ed51..76418d9 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -20,6 +20,7 @@ import ProductTags from '@/routes/Products/Tags'; import ProductAttributes from '@/routes/Products/Attributes'; import CouponsIndex from '@/routes/Coupons'; import CouponNew from '@/routes/Coupons/New'; +import CouponEdit from '@/routes/Coupons/Edit'; import CustomersIndex from '@/routes/Customers'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { LayoutDashboard, ReceiptText, Package, Tag, Users, Settings as SettingsIcon, Maximize2, Minimize2, Loader2 } from 'lucide-react'; @@ -477,6 +478,7 @@ function AppRoutes() { {/* Coupons */} } /> } /> + } /> {/* Customers */} } /> diff --git a/admin-spa/src/components/ui/multi-select.tsx b/admin-spa/src/components/ui/multi-select.tsx new file mode 100644 index 0000000..67b65dd --- /dev/null +++ b/admin-spa/src/components/ui/multi-select.tsx @@ -0,0 +1,147 @@ +import * as React from "react"; +import { X, Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +export interface MultiSelectOption { + label: string; + value: string; +} + +interface MultiSelectProps { + options: MultiSelectOption[]; + selected: string[]; + onChange: (selected: string[]) => void; + placeholder?: string; + emptyMessage?: string; + className?: string; + maxDisplay?: number; +} + +export function MultiSelect({ + options, + selected, + onChange, + placeholder = "Select items...", + emptyMessage = "No items found.", + className, + maxDisplay = 3, +}: MultiSelectProps) { + const [open, setOpen] = React.useState(false); + + const handleUnselect = (value: string) => { + onChange(selected.filter((s) => s !== value)); + }; + + const handleSelect = (value: string) => { + if (selected.includes(value)) { + onChange(selected.filter((s) => s !== value)); + } else { + onChange([...selected, value]); + } + }; + + const selectedOptions = options.filter((option) => + selected.includes(option.value) + ); + + return ( + + + + + ))} + {selectedOptions.length > maxDisplay && ( + + +{selectedOptions.length - maxDisplay} more + + )} + + + + + + + + {emptyMessage} + + {options.map((option) => ( + handleSelect(option.value)} + > + + {option.label} + + ))} + + + + + ); +}