From 47e59d2926dee7a76ede4172c3c2439f83dc5dce Mon Sep 17 00:00:00 2001 From: Citali Date: Thu, 12 Feb 2026 22:04:42 +0100 Subject: [PATCH] feat(web): polish commission flow and default public navigation --- TODO.md | 1 + .../web/src/app/[locale]/commissions/page.tsx | 19 +++++- .../web/src/components/public-site-header.tsx | 49 ++++++++++----- apps/web/src/messages/de.json | 4 ++ apps/web/src/messages/en.json | 4 ++ apps/web/src/messages/es.json | 4 ++ apps/web/src/messages/fr.json | 4 ++ packages/db/prisma/seed.ts | 61 +++++++++++++++++++ 8 files changed, 128 insertions(+), 18 deletions(-) diff --git a/TODO.md b/TODO.md index f1d36c3..4646042 100644 --- a/TODO.md +++ b/TODO.md @@ -325,6 +325,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Page editor now supports locale translations in `/pages/:id`; public page rendering uses locale-aware page lookup with base-content fallback. - [2026-02-12] Public rendering integration advanced with locale-aware navigation/news translations and a new public commission request entry route (`/[locale]/commissions`) that creates/reuses customer records and opens a `new` commission. - [2026-02-12] Public portfolio baseline added with `/{locale}/portfolio` and `/{locale}/portfolio/{slug}`, including published-artwork filters (gallery/album/category/tag), rendition image streaming via web `/api/media/file/:id`, and media-aware artwork detail rendering. +- [2026-02-12] Public UX pass: commission request flow now reports explicit invalid budget range errors, and header navigation now falls back to localized defaults (`home`, `portfolio`, `news`, `commissions`) when no CMS menu exists; seed data now creates those default menu entries. ## How We Use This File diff --git a/apps/web/src/app/[locale]/commissions/page.tsx b/apps/web/src/app/[locale]/commissions/page.tsx index ba11093..46cd106 100644 --- a/apps/web/src/app/[locale]/commissions/page.tsx +++ b/apps/web/src/app/[locale]/commissions/page.tsx @@ -69,6 +69,13 @@ export default async function PublicCommissionRequestPage({ async function submitCommissionRequestAction(formData: FormData) { "use server" + const budgetMin = readNullableNumber(formData, "budgetMin") + const budgetMax = readNullableNumber(formData, "budgetMax") + + if (budgetMin != null && budgetMax != null && budgetMax < budgetMin) { + redirect(buildRedirect(locale, { error: "budget_range_invalid" })) + } + try { await createPublicCommissionRequest({ customerName: readInputString(formData, "customerName"), @@ -77,8 +84,8 @@ export default async function PublicCommissionRequestPage({ customerInstagram: readNullableString(formData, "customerInstagram"), title: readInputString(formData, "title"), description: readNullableString(formData, "description"), - budgetMin: readNullableNumber(formData, "budgetMin"), - budgetMax: readNullableNumber(formData, "budgetMax"), + budgetMin, + budgetMax, }) } catch { redirect(buildRedirect(locale, { error: "submission_failed" })) @@ -110,6 +117,11 @@ export default async function PublicCommissionRequestPage({ {t("error")} ) : null} + {error === "budget_range_invalid" ? ( +
+ {t("budgetRangeError")} +
+ ) : null}
@@ -117,6 +129,7 @@ export default async function PublicCommissionRequestPage({ {t("fields.customerName")} @@ -126,6 +139,7 @@ export default async function PublicCommissionRequestPage({ @@ -137,6 +151,7 @@ export default async function PublicCommissionRequestPage({ {t("fields.customerPhone")} diff --git a/apps/web/src/components/public-site-header.tsx b/apps/web/src/components/public-site-header.tsx index 6b12afb..975740b 100644 --- a/apps/web/src/components/public-site-header.tsx +++ b/apps/web/src/components/public-site-header.tsx @@ -1,5 +1,5 @@ import { listPublicNavigation } from "@cms/db" -import { getLocale } from "next-intl/server" +import { getLocale, getTranslations } from "next-intl/server" import { Link } from "@/i18n/navigation" @@ -7,7 +7,33 @@ import { LanguageSwitcher } from "./language-switcher" export async function PublicSiteHeader() { const locale = await getLocale() - const navItems = await listPublicNavigation("header", locale) + const [navItems, t] = await Promise.all([ + listPublicNavigation("header", locale), + getTranslations("Layout.nav"), + ]) + const fallbackNavItems = [ + { + id: "fallback-home", + href: "/", + label: t("home"), + }, + { + id: "fallback-portfolio", + href: "/portfolio", + label: t("portfolio"), + }, + { + id: "fallback-news", + href: "/news", + label: t("news"), + }, + { + id: "fallback-commissions", + href: "/commissions", + label: t("commissions"), + }, + ] + const resolvedNavItems = navItems.length > 0 ? navItems : fallbackNavItems return (
@@ -20,24 +46,15 @@ export async function PublicSiteHeader() { diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index db12efe..0e9c9b3 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -21,6 +21,9 @@ "brand": "CMS Web", "nav": { "home": "Start", + "portfolio": "Portfolio", + "news": "News", + "commissions": "Aufträge", "about": "Über uns", "contact": "Kontakt" }, @@ -49,6 +52,7 @@ "description": "Teile deine Idee und Projektdetails. Wir prüfen die Anfrage und melden uns zeitnah.", "success": "Deine Auftragsanfrage wurde übermittelt.", "error": "Übermittlung fehlgeschlagen. Bitte prüfe die Eingaben und versuche es erneut.", + "budgetRangeError": "Das maximale Budget muss größer oder gleich dem minimalen Budget sein.", "submit": "Anfrage senden", "fields": { "customerName": "Name", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 022895e..29a1e2f 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -21,6 +21,9 @@ "brand": "CMS Web", "nav": { "home": "Home", + "portfolio": "Portfolio", + "news": "News", + "commissions": "Commissions", "about": "About", "contact": "Contact" }, @@ -49,6 +52,7 @@ "description": "Share your idea and project details. We will review and reply as soon as possible.", "success": "Your commission request was submitted.", "error": "Submission failed. Please review your data and try again.", + "budgetRangeError": "Budget max must be greater than or equal to budget min.", "submit": "Submit request", "fields": { "customerName": "Name", diff --git a/apps/web/src/messages/es.json b/apps/web/src/messages/es.json index 6c7436c..ec9df7b 100644 --- a/apps/web/src/messages/es.json +++ b/apps/web/src/messages/es.json @@ -21,6 +21,9 @@ "brand": "CMS Web", "nav": { "home": "Inicio", + "portfolio": "Portafolio", + "news": "Noticias", + "commissions": "Comisiones", "about": "Acerca de", "contact": "Contacto" }, @@ -49,6 +52,7 @@ "description": "Comparte tu idea y detalles del proyecto. Revisaremos la solicitud y responderemos pronto.", "success": "Tu solicitud de comisión fue enviada.", "error": "No se pudo enviar la solicitud. Revisa los datos e inténtalo de nuevo.", + "budgetRangeError": "El presupuesto máximo debe ser mayor o igual al mínimo.", "submit": "Enviar solicitud", "fields": { "customerName": "Nombre", diff --git a/apps/web/src/messages/fr.json b/apps/web/src/messages/fr.json index b5314de..909ab53 100644 --- a/apps/web/src/messages/fr.json +++ b/apps/web/src/messages/fr.json @@ -21,6 +21,9 @@ "brand": "CMS Web", "nav": { "home": "Accueil", + "portfolio": "Portfolio", + "news": "Actualités", + "commissions": "Commissions", "about": "À propos", "contact": "Contact" }, @@ -49,6 +52,7 @@ "description": "Partagez votre idée et les détails du projet. Nous examinerons la demande et répondrons rapidement.", "success": "Votre demande de commission a été envoyée.", "error": "Échec de l'envoi. Vérifiez les données et réessayez.", + "budgetRangeError": "Le budget max doit être supérieur ou égal au budget min.", "submit": "Envoyer la demande", "fields": { "customerName": "Nom", diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts index 863bfa2..36f1813 100644 --- a/packages/db/prisma/seed.ts +++ b/packages/db/prisma/seed.ts @@ -159,6 +159,67 @@ async function main() { }) } + const defaultHeaderItems = [ + { + label: "Portfolio", + href: "/portfolio", + sortOrder: 1, + pageId: null, + }, + { + label: "News", + href: "/news", + sortOrder: 2, + pageId: null, + }, + { + label: "Commissions", + href: "/commissions", + sortOrder: 3, + pageId: null, + }, + ] as const + + for (const item of defaultHeaderItems) { + const existingItem = await db.navigationItem.findFirst({ + where: { + menuId: primaryMenu.id, + parentId: null, + href: item.href, + }, + select: { + id: true, + }, + }) + + if (existingItem) { + await db.navigationItem.update({ + where: { + id: existingItem.id, + }, + data: { + label: item.label, + sortOrder: item.sortOrder, + isVisible: true, + pageId: item.pageId, + }, + }) + continue + } + + await db.navigationItem.create({ + data: { + menuId: primaryMenu.id, + label: item.label, + href: item.href, + pageId: item.pageId, + parentId: null, + sortOrder: item.sortOrder, + isVisible: true, + }, + }) + } + const existingCustomer = await db.customer.findFirst({ where: { email: "collector@example.com",