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:
2026-05-22 11:31:32 +02:00
parent 1d9fa10961
commit 5d785a1c1c
8 changed files with 562 additions and 175 deletions
+17 -1
View File
@@ -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) {