Polish public error and 404 screens with brand atmosphere.
Local Build and Deploy / deploy (push) Successful in 1m31s

Shared ErrorScreen composition, i18n for en/nl/it, Sentry on route errors, and a self-contained global-error fallback that matches the hotel gold theme.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-18 20:00:04 +02:00
1 parent b22725d3a9
commit 1f552c2822
8 files changed
+345 -77

No files matched your search

+29 -39
View File
@@ -1,12 +1,15 @@
"use client"; "use client";
import * as Sentry from "@sentry/nextjs";
import { Home, RefreshCw } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useTranslations } from "next-intl";
import { useEffect } from "react"; import { useEffect } from "react";
import { ErrorScreen } from "@/components/error-screen";
/** /**
* Route-segment error boundary. Renders inside the root layout (so the shell * Route-segment error boundary. Renders inside the root layout (so the shell
* stays), shows a friendly card, and offers a reset. The raw error is logged to * stays). Never shows the raw error to the user.
* the console, never shown to the user.
*/ */
export default function ErrorPage({ export default function ErrorPage({
error, error,
@@ -15,51 +18,38 @@ export default function ErrorPage({
error: Error & { digest?: string }; error: Error & { digest?: string };
reset: () => void; reset: () => void;
}) { }) {
const t = useTranslations("pages.error");
useEffect(() => { useEffect(() => {
console.error(error); console.error(error);
Sentry.captureException(error);
}, [error]); }, [error]);
return ( return (
<main style={{ display: "grid", gap: "1.5rem" }}> <ErrorScreen
<div className="content-card"> code={t("code")}
<div className="content-card-head"> title={t("title")}
<span className="content-card-icon" aria-hidden> subtitle={t("subtitle")}
⚠️ body={t("body")}
</span> actions={
<div className="content-card-head-text"> <>
<p className="content-card-title">Something went wrong</p> <button type="button" className="btn btn-primary" onClick={() => reset()}>
<p className="content-card-subtitle"> <RefreshCw className="error-screen-btn-icon" aria-hidden />
An unexpected error occurred {t("tryAgain")}
</p>
</div>
</div>
<div className="content-card-body">
<p className="muted" style={{ marginTop: 0 }}>
Try again — if the problem persists, please contact a member of
staff.
</p>
<div style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap" }}>
<button
type="button"
className="btn btn-primary"
onClick={() => reset()}
>
Try again
</button> </button>
<Link className="btn btn-outline" href="/"> <Link className="btn btn-outline" href="/">
Back home <Home className="error-screen-btn-icon" aria-hidden />
{t("backHome")}
</Link> </Link>
</div> </>
{error.digest ? ( }
<p footer={
className="muted" error.digest ? (
style={{ marginTop: "0.75rem", fontSize: "0.75rem" }} <p className="error-screen-digest">
> {t("reference", { digest: error.digest })}
Reference: {error.digest}
</p> </p>
) : null} ) : null
</div> }
</div> />
</main>
); );
} }
File diff suppressed because it is too large. Load diff
+127
View File
File diff suppressed because it is too large. Load diff
+20 -13
View File
@@ -1,22 +1,29 @@
import { Home, Newspaper } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { ContentCard } from "@/components/public/ui"; import { getTranslations } from "next-intl/server";
import { ErrorScreen } from "@/components/error-screen";
export default async function NotFound() {
const t = await getTranslations("pages.notFound");
export default function NotFound() {
return ( return (
<main style={{ display: "grid", gap: "1.5rem" }}> <ErrorScreen
<ContentCard icon="🧭" title="Page not found" subtitle="Error 404"> code={t("code")}
<p className="muted" style={{ marginTop: 0 }}> title={t("title")}
The page you’re looking for doesn’t exist or has moved. subtitle={t("subtitle")}
</p> body={t("body")}
<div style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap" }}> actions={
<>
<Link className="btn btn-primary" href="/"> <Link className="btn btn-primary" href="/">
Back home <Home className="error-screen-btn-icon" aria-hidden />
{t("backHome")}
</Link> </Link>
<Link className="btn btn-outline" href="/news"> <Link className="btn btn-outline" href="/news">
Latest news <Newspaper className="error-screen-btn-icon" aria-hidden />
{t("latestNews")}
</Link> </Link>
</div> </>
</ContentCard> }
</main> />
); );
} }
+37
View File
@@ -0,0 +1,37 @@
import type { ReactNode } from "react";
/**
* Shared public error / empty-route composition.
* Used by route error.tsx and not-found.tsx (inside the app shell).
*/
export function ErrorScreen({
code,
title,
subtitle,
body,
actions,
footer,
}: {
code: string;
title: string;
subtitle: string;
body: string;
actions: ReactNode;
footer?: ReactNode;
}) {
return (
<main className="error-screen">
<div className="error-screen-glow" aria-hidden />
<section className="error-screen-panel" aria-labelledby="error-screen-title">
<p className="error-screen-code">{code}</p>
<h1 id="error-screen-title" className="error-screen-title">
{title}
</h1>
<p className="error-screen-subtitle">{subtitle}</p>
<p className="error-screen-body">{body}</p>
<div className="error-screen-actions">{actions}</div>
{footer ? <div className="error-screen-footer">{footer}</div> : null}
</section>
</main>
);
}
+17
View File
@@ -40,6 +40,23 @@
"darkMode": "Dark mode" "darkMode": "Dark mode"
}, },
"pages": { "pages": {
"error": {
"code": "Error",
"title": "Something went wrong",
"subtitle": "The hotel hit an unexpected snag.",
"body": "Try again in a moment. If it keeps happening, contact staff with the reference below.",
"tryAgain": "Try again",
"backHome": "Back home",
"reference": "Reference: {digest}"
},
"notFound": {
"code": "404",
"title": "Page not found",
"subtitle": "This room doesn’t exist — or the furniture was moved.",
"body": "Check the link, or head back to the lobby.",
"backHome": "Back home",
"latestNews": "Latest news"
},
"staff": { "staff": {
"title": "Staff", "title": "Staff",
"subtitle": "The team behind the hotel", "subtitle": "The team behind the hotel",
+17
View File
@@ -40,6 +40,23 @@
"darkMode": "Tema scuro" "darkMode": "Tema scuro"
}, },
"pages": { "pages": {
"error": {
"code": "Errore",
"title": "Qualcosa è andato storto",
"subtitle": "L'hotel ha incontrato un imprevisto.",
"body": "Riprova tra poco. Se continua, contatta lo staff indicando il riferimento qui sotto.",
"tryAgain": "Riprova",
"backHome": "Torna alla home",
"reference": "Riferimento: {digest}"
},
"notFound": {
"code": "404",
"title": "Pagina non trovata",
"subtitle": "Questa stanza non esiste — o i mobili sono stati spostati.",
"body": "Controlla il link, oppure torna alla lobby.",
"backHome": "Torna alla home",
"latestNews": "Ultime notizie"
},
"staff": { "staff": {
"title": "Staff", "title": "Staff",
"subtitle": "Il team dietro l'hotel", "subtitle": "Il team dietro l'hotel",
+17
View File
@@ -40,6 +40,23 @@
"darkMode": "Donkere modus" "darkMode": "Donkere modus"
}, },
"pages": { "pages": {
"error": {
"code": "Fout",
"title": "Er ging iets mis",
"subtitle": "Het hotel liep tegen een onverwachte snag aan.",
"body": "Probeer het zo opnieuw. Blijft het probleem? Neem contact op met de staff en noem de referentie hieronder.",
"tryAgain": "Opnieuw proberen",
"backHome": "Terug naar home",
"reference": "Referentie: {digest}"
},
"notFound": {
"code": "404",
"title": "Pagina niet gevonden",
"subtitle": "Deze kamer bestaat niet — of de meubels zijn verplaatst.",
"body": "Controleer de link, of ga terug naar de lobby.",
"backHome": "Terug naar home",
"latestNews": "Laatste nieuws"
},
"staff": { "staff": {
"title": "Team", "title": "Team",
"subtitle": "Het team achter het hotel", "subtitle": "Het team achter het hotel",