From 7df4b9c74a237229ef53ff85ff4eab5c51ea44ce Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Mon, 15 Jun 2026 10:25:15 +0200 Subject: [PATCH] feat(12-04): build OfferListClient with category filter, archive toggle, card grid - Category filter chips ("Tutti" + dynamic categoryOptions), active/inactive styling per UI-SPEC (#1A463C / #e5e7eb) - "Mostra offerte archiviate" toggle (default off), filters via useMemo - Responsive card grid (1/2/3 cols) linking to /admin/offers/[id]/edit, with category badge and "Archiviata" indicator - "+ Nuova Offerta" inline form calling createOfferMacro via useTransition + router.refresh() - Empty state with "Nessuna offerta" / "Inizia creando la tua prima offerta" --- .../admin/offers/OfferListClient.tsx | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/components/admin/offers/OfferListClient.tsx diff --git a/src/components/admin/offers/OfferListClient.tsx b/src/components/admin/offers/OfferListClient.tsx new file mode 100644 index 0000000..df9e623 --- /dev/null +++ b/src/components/admin/offers/OfferListClient.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useState, useMemo, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { createOfferMacro } from "@/app/admin/offers/actions"; +import type { OfferListCard } from "@/lib/offer-queries"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; + +export function OfferListClient({ + cards, + categoryOptions, +}: { + cards: OfferListCard[]; + categoryOptions: string[]; +}) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [activeCategory, setActiveCategory] = useState(null); + const [showArchived, setShowArchived] = useState(false); + const [showCreateForm, setShowCreateForm] = useState(false); + const [newOfferName, setNewOfferName] = useState(""); + + const filteredCards = useMemo(() => { + return cards.filter( + (c) => + (activeCategory === null || c.category === activeCategory) && + (showArchived || !c.is_archived) + ); + }, [cards, activeCategory, showArchived]); + + function handleCreateSubmit(formData: FormData) { + startTransition(async () => { + await createOfferMacro(formData); + setShowCreateForm(false); + setNewOfferName(""); + router.refresh(); + }); + } + + const createCta = ( +
+ + {showCreateForm && ( +
+ setNewOfferName(e.target.value)} + className="h-9 w-48" + /> + +
+ )} +
+ ); + + return ( +
+ {/* Page header */} +
+

Offerte

+ {createCta} +
+ + {/* Filter row */} +
+
+ + {categoryOptions.map((category) => ( + + ))} +
+ + +
+ + {/* Card grid / empty state */} + {filteredCards.length === 0 ? ( +
+

Nessuna offerta

+

Inizia creando la tua prima offerta

+
{createCta}
+
+ ) : ( +
+ {filteredCards.map((card) => ( + +

{card.internal_name}

+ {card.description && ( +

{card.description}

+ )} +
+ {card.category && {card.category}} + {card.is_archived && ( + Archiviata + )} +
+ + ))} +
+ )} +
+ ); +}