diff --git a/src/app/error.tsx b/src/app/error.tsx index bd08cc93..fb9d7471 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,12 +1,15 @@ "use client"; +import * as Sentry from "@sentry/nextjs"; +import { Home, RefreshCw } from "lucide-react"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { useEffect } from "react"; +import { ErrorScreen } from "@/components/error-screen"; /** * 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 - * the console, never shown to the user. + * stays). Never shows the raw error to the user. */ export default function ErrorPage({ error, @@ -15,51 +18,38 @@ export default function ErrorPage({ error: Error & { digest?: string }; reset: () => void; }) { + const t = useTranslations("pages.error"); + useEffect(() => { console.error(error); + Sentry.captureException(error); }, [error]); return ( -
-
-
- - ⚠️ - -
-

Something went wrong

-

- An unexpected error occurred -

-
-
-
-

- Try again — if the problem persists, please contact a member of - staff. + + + + + {t("backHome")} + + + } + footer={ + error.digest ? ( +

+ {t("reference", { digest: error.digest })}

-
- - - Back home - -
- {error.digest ? ( -

- Reference: {error.digest} -

- ) : null} -
-
-
+ ) : null + } + /> ); } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index df3547a5..4f941e59 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -6,7 +6,7 @@ import { useEffect } from "react"; /** * Last-resort boundary for errors thrown in the root layout itself. It replaces * the whole document, so it must render its own / and can't rely on - * the app shell or its CSS variables. + * the app shell or CSS variables from globals.css. */ export default function GlobalError({ error, @@ -27,35 +27,91 @@ export default function GlobalError({ minHeight: "100vh", display: "grid", placeItems: "center", - fontFamily: "Nunito, system-ui, sans-serif", - background: "#0f1117", - color: "#e5e7eb", + padding: "1.5rem", + fontFamily: + 'Nunito, ui-rounded, "Segoe UI", system-ui, sans-serif', + background: + "radial-gradient(ellipse 80% 60% at 50% -10%, rgba(238, 180, 37, 0.22), transparent 55%), linear-gradient(165deg, #141821 0%, #0c0e14 45%, #10141c 100%)", + color: "#f3f4f6", }} > -
-
⚠️
-

- Something went wrong -

-

- The hotel ran into an unexpected error. Please try again. + +

+

Error

+

Something went wrong

+

+ The hotel ran into an unexpected error. Please try again — if it + keeps happening, contact staff.

- + {error.digest ? ( +

Reference: {error.digest}

+ ) : null}
diff --git a/src/app/globals.css b/src/app/globals.css index 8e12066a..406ced42 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1357,3 +1357,130 @@ details[open] > summary .details-open\:block { details[open] > summary .details-open\:rotate-180 { transform: rotate(180deg); } + +/* ── Public error / 404 screen ───────────────────────────── */ +@keyframes error-screen-rise { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.error-screen { + position: relative; + isolation: isolate; + display: grid; + place-items: center; + min-height: min(70vh, 36rem); + padding: 2rem 1rem 3rem; +} + +.error-screen-glow { + pointer-events: none; + position: absolute; + inset: 10% 15% auto; + height: 12rem; + border-radius: 50%; + background: radial-gradient( + ellipse at center, + color-mix(in srgb, var(--color-primary) 35%, transparent), + transparent 70% + ); + filter: blur(8px); + z-index: 0; + opacity: 0.85; +} + +.error-screen-panel { + position: relative; + z-index: 1; + width: min(100%, 28rem); + text-align: center; + padding: 2.25rem 1.75rem 2rem; + border-radius: 1.25rem; + border: 1px solid color-mix(in srgb, var(--color-primary) 28%, var(--border-subtle)); + background: linear-gradient( + 165deg, + color-mix(in srgb, var(--color-primary) 10%, var(--color-surface)), + var(--color-surface) + ); + box-shadow: + 0 20px 48px color-mix(in srgb, rgb(0, 0, 0) 18%, transparent), + inset 0 1px 0 color-mix(in srgb, var(--color-primary) 18%, transparent); + animation: error-screen-rise 0.45s ease-out both; +} + +.error-screen-code { + margin: 0 0 0.65rem; + font-family: var(--font-pixel), "Courier New", monospace; + font-size: clamp(2.75rem, 9vw, 3.75rem); + font-weight: 700; + letter-spacing: 0.06em; + line-height: 1; + color: var(--color-primary-readable, var(--color-primary)); + text-shadow: 0 0 36px color-mix(in srgb, var(--color-primary) 45%, transparent); +} + +.error-screen-title { + margin: 0 0 0.4rem; + font-size: 1.4rem; + font-weight: 800; + letter-spacing: -0.02em; + color: var(--color-text); +} + +.error-screen-subtitle { + margin: 0 0 0.85rem; + font-size: 1rem; + font-weight: 600; + color: var(--color-text); + opacity: 0.88; +} + +.error-screen-body { + margin: 0 0 1.5rem; + font-size: 0.95rem; + line-height: 1.55; + color: var(--color-text-muted); +} + +.error-screen-actions { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + justify-content: center; +} + +.error-screen-actions .btn { + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.error-screen-btn-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; +} + +.error-screen-footer { + margin-top: 1.25rem; +} + +.error-screen-digest { + margin: 0; + font-size: 0.72rem; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + word-break: break-all; +} + +@media (prefers-reduced-motion: reduce) { + .error-screen-panel { + animation: none; + } +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 774a8994..94e50873 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,22 +1,29 @@ +import { Home, Newspaper } from "lucide-react"; 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 ( -
- -

- The page you’re looking for doesn’t exist or has moved. -

-
+ - Back home + + {t("backHome")} - Latest news + + {t("latestNews")} -
-
-
+ + } + /> ); } diff --git a/src/components/error-screen.tsx b/src/components/error-screen.tsx new file mode 100644 index 00000000..4eedb53d --- /dev/null +++ b/src/components/error-screen.tsx @@ -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 ( +
+
+
+

{code}

+

+ {title} +

+

{subtitle}

+

{body}

+
{actions}
+ {footer ?
{footer}
: null} +
+
+ ); +} diff --git a/src/messages/en.json b/src/messages/en.json index 33c5eea8..a4e1ab6d 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -40,6 +40,23 @@ "darkMode": "Dark mode" }, "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": { "title": "Staff", "subtitle": "The team behind the hotel", diff --git a/src/messages/it.json b/src/messages/it.json index fe0829a5..49bfe931 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -40,6 +40,23 @@ "darkMode": "Tema scuro" }, "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": { "title": "Staff", "subtitle": "Il team dietro l'hotel", diff --git a/src/messages/nl.json b/src/messages/nl.json index 10da2247..cf911bd4 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -40,6 +40,23 @@ "darkMode": "Donkere modus" }, "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": { "title": "Team", "subtitle": "Het team achter het hotel",