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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user