From 77cc9dca8712f6e394c568bf7bb5d89db7da5d0c Mon Sep 17 00:00:00 2001 From: MOH Date: Tue, 10 Mar 2026 17:43:26 +0100 Subject: [PATCH] Add Sonner-based notifications and UX polish --- app/[locale]/(site)/contact/actions.ts | 20 ++++-- app/[locale]/(site)/contact/page.tsx | 14 +--- app/layout.tsx | 8 ++- app/root/maintenance/actions.ts | 5 +- app/root/marquee/page.tsx | 25 +------ app/root/media/page.tsx | 23 +----- app/root/portfolio/categories/page.tsx | 25 +------ app/root/portfolio/page.tsx | 14 ---- app/root/portfolio/projects/[id]/page.tsx | 18 ----- app/root/portfolio/projects/new/page.tsx | 17 +---- app/root/portfolio/projects/page.tsx | 16 ----- app/root/site-settings/page.tsx | 25 +------ app/root/smtp/contact-protection/page.tsx | 25 +------ app/root/smtp/page.tsx | 23 +----- components/layout/floating-preferences.tsx | 9 +++ components/layout/locale-toggle.tsx | 16 +++++ components/root/flash-message.tsx | 67 ----------------- components/root/form-save-button.tsx | 11 +-- components/root/media-library-manager.tsx | 2 +- components/root/query-toast-bridge.tsx | 84 ++++++++++++++++++++++ components/root/root-dashboard-shell.tsx | 13 +++- components/sound-toggle.tsx | 12 +++- components/theme-toggle.tsx | 6 ++ components/ui/sonner.tsx | 50 +++++++++++++ lib/media-service.ts | 4 +- lib/media-storage.ts | 1 + messages/ar.json | 10 ++- messages/de.json | 10 ++- messages/en.json | 10 ++- package-lock.json | 11 +++ package.json | 1 + tests/media-storage.test.ts | 14 ++++ 32 files changed, 279 insertions(+), 310 deletions(-) delete mode 100644 components/root/flash-message.tsx create mode 100644 components/root/query-toast-bridge.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 tests/media-storage.test.ts diff --git a/app/[locale]/(site)/contact/actions.ts b/app/[locale]/(site)/contact/actions.ts index d1ce63a..d7c2aaa 100644 --- a/app/[locale]/(site)/contact/actions.ts +++ b/app/[locale]/(site)/contact/actions.ts @@ -40,9 +40,15 @@ const contactErrorMessages = { }, } as const; -function withError(pathname: string, message: string) { +const contactSuccessMessages = { + ar: "شكراً على رسالتك.", + en: "Thank you for your message.", + de: "Vielen Dank fuer deine Nachricht.", +} as const; + +function withMessage(pathname: string, type: "success" | "error", message: string) { const params = new URLSearchParams(); - params.set("error", message); + params.set(type, message); return `${pathname}?${params.toString()}`; } @@ -80,18 +86,18 @@ export async function submitContactFormAction(formData: FormData) { message: values.message, }); - redirect(getLocalizedPath(locale, "/success")); + redirect(withMessage(getLocalizedPath(locale, "/success"), "success", contactSuccessMessages[locale])); } catch (error) { if (isRedirectError(error)) { throw error; } if (error instanceof z.ZodError) { - redirect(withError(contactPath, contactErrorMessages[locale].invalid)); + redirect(withMessage(contactPath, "error", contactErrorMessages[locale].invalid)); } if (error instanceof Error && error.message === "Too many contact requests. Please try again later.") { - redirect(withError(contactPath, contactErrorMessages[locale].blocked)); + redirect(withMessage(contactPath, "error", contactErrorMessages[locale].blocked)); } if ( @@ -100,10 +106,10 @@ export async function submitContactFormAction(formData: FormData) { error.message === "Turnstile verification failed." || error.message === "Turnstile verification request failed.") ) { - redirect(withError(contactPath, contactErrorMessages[locale].verification)); + redirect(withMessage(contactPath, "error", contactErrorMessages[locale].verification)); } console.error("Contact form delivery failed.", error); - redirect(withError(contactPath, contactErrorMessages[locale].failed)); + redirect(withMessage(contactPath, "error", contactErrorMessages[locale].failed)); } } diff --git a/app/[locale]/(site)/contact/page.tsx b/app/[locale]/(site)/contact/page.tsx index 1496380..f12f273 100644 --- a/app/[locale]/(site)/contact/page.tsx +++ b/app/[locale]/(site)/contact/page.tsx @@ -5,7 +5,6 @@ import { getTranslations } from "next-intl/server"; import { Container } from "@/components/layout/container"; import { PageHero } from "@/components/layout/page-hero"; import { MotionFade } from "@/components/motion-fade"; -import { FlashMessage } from "@/components/root/flash-message"; import { ContactForm } from "@/components/site/contact-form"; import { buildLocalizedMetadata } from "@/lib/metadata"; import { getPublicContactProtectionSettings } from "@/lib/app-config"; @@ -19,9 +18,6 @@ type ContactPageProps = { params: { locale: string; }; - searchParams?: { - error?: string; - }; }; export async function generateMetadata({ @@ -38,10 +34,7 @@ export async function generateMetadata({ }); } -export default async function ContactPage({ - params: { locale }, - searchParams, -}: ContactPageProps) { +export default async function ContactPage({ params: { locale } }: ContactPageProps) { const localeKey = resolveLocale(locale); const [t, protection] = await Promise.all([ getTranslations({ locale: localeKey, namespace: "contactPage" }), @@ -86,11 +79,6 @@ export default async function ContactPage({ - {searchParams?.error ? ( -
- -
- ) : null} - {children} + + {children} + + +