feat(11-04): add client-side search bar to catalog page, remove ServiceForm

- New CatalogSearch client component: filters ServiceWithTags[] by name/tag, case-insensitive, instant (no reload)
- page.tsx now passes filtered services through CatalogSearch -> ServiceTable
- ServiceForm.tsx deleted — quick-add row in ServiceTable replaces its create-service UX
This commit is contained in:
2026-06-13 16:01:17 +02:00
parent c0bedf300e
commit 912a892ba5
3 changed files with 39 additions and 110 deletions
+36
View File
@@ -0,0 +1,36 @@
"use client";
import { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Search } from "lucide-react";
import { ServiceTable } from "@/components/admin/catalog/ServiceTable";
import type { ServiceWithTags } from "@/lib/admin-queries";
export function CatalogSearch({ services }: { services: ServiceWithTags[] }) {
const [query, setQuery] = useState("");
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return services;
return services.filter((s) => {
if (s.name.toLowerCase().includes(q)) return true;
return s.tags.some((tag) => tag.toLowerCase().includes(q));
});
}, [services, query]);
return (
<div className="space-y-4">
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#71717a]" />
<Input
type="text"
placeholder="Cerca per nome o tag..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="pl-9 h-9"
/>
</div>
<ServiceTable services={filtered} />
</div>
);
}
+3 -8
View File
@@ -1,6 +1,5 @@
import { getAllServices } from "@/lib/admin-queries"; import { getAllServices } from "@/lib/admin-queries";
import { ServiceTable } from "@/components/admin/catalog/ServiceTable"; import { CatalogSearch } from "./CatalogSearch";
import { ServiceForm } from "@/components/admin/catalog/ServiceForm";
export const revalidate = 0; export const revalidate = 0;
@@ -13,16 +12,12 @@ export default async function CatalogPage() {
<h1 className="text-2xl font-bold text-[#1a1a1a]">Catalogo Servizi</h1> <h1 className="text-2xl font-bold text-[#1a1a1a]">Catalogo Servizi</h1>
</div> </div>
<div className="mb-6">
<ServiceForm />
</div>
{services.length === 0 ? ( {services.length === 0 ? (
<p className="text-sm text-[#71717a]"> <p className="text-sm text-[#71717a]">
Nessun servizio nel catalogo. Aggiungi il primo servizio qui sopra. Nessun servizio nel catalogo. Scrivi un nome nella riga in fondo alla tabella e premi invio per crearne uno.
</p> </p>
) : ( ) : (
<ServiceTable services={services} /> <CatalogSearch services={services} />
)} )}
</div> </div>
); );
@@ -1,102 +0,0 @@
"use client";
import { useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { createService } from "@/app/admin/catalog/actions";
export function ServiceForm() {
const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
const [, startTransition] = useTransition();
const router = useRouter();
const formRef = useRef<HTMLFormElement>(null);
function handleSubmit(fd: FormData) {
setError(null);
startTransition(async () => {
try {
await createService(fd);
formRef.current?.reset();
setOpen(false);
router.refresh();
} catch (e) {
setError(e instanceof Error ? e.message : "Errore nel salvataggio");
}
});
}
if (!open) {
return (
<Button
onClick={() => setOpen(true)}
className="bg-[#1A463C] text-white hover:bg-[#1A463C]/90"
>
+ Aggiungi servizio
</Button>
);
}
return (
<div className="bg-white border border-[#e5e7eb] rounded-xl p-4 space-y-4">
<h3 className="font-medium text-[#1a1a1a]">Nuovo servizio</h3>
<form ref={formRef} action={handleSubmit} className="space-y-3">
<div className="space-y-1">
<Label htmlFor="name">Nome</Label>
<Input id="name" name="name" placeholder="es. Strategia di brand" required />
</div>
<div className="space-y-1">
<Label htmlFor="description">Descrizione (opzionale)</Label>
<Input
id="description"
name="description"
placeholder="es. Incluso: analisi competitor, posizionamento"
/>
</div>
<div className="space-y-1">
<Label htmlFor="category">Categoria (opzionale)</Label>
<Input
id="category"
name="category"
placeholder="es. Branding, Social media, Consulenza"
/>
</div>
<div className="space-y-1">
<Label htmlFor="unit_price">Prezzo unitario ()</Label>
<Input
id="unit_price"
name="unit_price"
type="number"
step="0.01"
min="0.01"
placeholder="0.00"
required
/>
</div>
{error && <p className="text-xs text-red-600">{error}</p>}
<div className="flex gap-2">
<Button
type="submit"
size="sm"
className="bg-[#1A463C] text-white hover:bg-[#1A463C]/90"
>
Aggiungi
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setOpen(false);
setError(null);
}}
>
Annulla
</Button>
</div>
</form>
</div>
);
}