feat(04-04): slug resolution + multi-project client dashboard + slug edit
- validate-slug API route: resolves clients.slug for Edge middleware - proxy.ts: slug-first resolution (D-06) — tries slug then falls back to token - client-view.ts: complete rewrite — getClientWithProjectsByToken + getProjectView - No quote_items, no payment amounts in client API (CLAUDE.md security invariants) - client/[token]/page.tsx: multi-project dashboard — 1 project = direct view, 2+ projects = shadcn Tabs with project names (D-09/D-10) - edit/page.tsx: slug field with link preview + unique constraint error handling - actions.ts: updateClient now persists slug, redirects on success/slug_taken Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,12 @@ const clientSchema = z.object({
|
||||
name: z.string().min(1, "Nome richiesto"),
|
||||
brand_name: z.string().min(1, "Brand name richiesto"),
|
||||
brief: z.string(),
|
||||
slug: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]{3,50}$/, "Slug non valido (es. mario-rossi, min 3 max 50 caratteri)")
|
||||
.optional()
|
||||
.or(z.literal(""))
|
||||
.transform((v) => (v === "" || v === undefined ? null : v)),
|
||||
});
|
||||
|
||||
export async function updateClient(clientId: string, formData: FormData) {
|
||||
@@ -54,11 +60,21 @@ export async function updateClient(clientId: string, formData: FormData) {
|
||||
name: formData.get("name"),
|
||||
brand_name: formData.get("brand_name"),
|
||||
brief: formData.get("brief") ?? "",
|
||||
slug: formData.get("slug") ?? "",
|
||||
});
|
||||
if (!parsed.success) throw new Error(parsed.error.issues[0].message);
|
||||
await db.update(clients).set(parsed.data).where(eq(clients.id, clientId));
|
||||
try {
|
||||
await db.update(clients).set(parsed.data).where(eq(clients.id, clientId));
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes("unique") || msg.includes("duplicate")) {
|
||||
redirect(`/admin/clients/${clientId}/edit?error=slug_taken`);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
revalidatePath(`/admin/clients/${clientId}`);
|
||||
revalidatePath("/admin");
|
||||
redirect(`/admin/clients/${clientId}`);
|
||||
}
|
||||
|
||||
export async function deleteClient(clientId: string) {
|
||||
|
||||
@@ -11,10 +11,13 @@ import Link from "next/link";
|
||||
|
||||
export default async function EditClientPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ error?: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const { error } = await searchParams;
|
||||
const [client] = await db.select().from(clients).where(eq(clients.id, id)).limit(1);
|
||||
if (!client) notFound();
|
||||
|
||||
@@ -31,6 +34,12 @@ export default async function EditClientPage({
|
||||
|
||||
<h1 className="text-xl font-bold text-[#1a1a1a] mb-6">Modifica cliente</h1>
|
||||
|
||||
{error === "slug_taken" && (
|
||||
<div className="mb-4 rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
|
||||
Slug già in uso da un altro cliente. Scegline uno diverso.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
action={async (fd: FormData) => {
|
||||
"use server";
|
||||
@@ -64,6 +73,27 @@ export default async function EditClientPage({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="slug">Slug personalizzato (opzionale)</Label>
|
||||
<p className="text-xs text-[#71717a]">
|
||||
Solo lettere minuscole, numeri e trattini (es. mario-rossi). Min 3, max 50 caratteri.
|
||||
</p>
|
||||
<Input
|
||||
id="slug"
|
||||
name="slug"
|
||||
type="text"
|
||||
defaultValue={client.slug ?? ""}
|
||||
pattern="[a-z0-9-]{3,50}"
|
||||
placeholder="mario-rossi"
|
||||
/>
|
||||
<p className="text-xs text-[#71717a]">
|
||||
Link cliente:{" "}
|
||||
<span className="font-mono text-[#1a1a1a]">
|
||||
/client/{client.slug || client.token}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button type="submit">Salva modifiche</Button>
|
||||
<Button asChild variant="ghost">
|
||||
|
||||
Reference in New Issue
Block a user