diff --git a/next.config.mjs b/next.config.mjs index 1a67b9f..11f2e90 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', + experimental: { + globalNotFound: true, + }, turbopack: { root: process.cwd(), }, diff --git a/src/app/global-not-found.module.css b/src/app/global-not-found.module.css new file mode 100644 index 0000000..ea401d7 --- /dev/null +++ b/src/app/global-not-found.module.css @@ -0,0 +1,137 @@ +.page { + display: grid; + min-height: 100svh; + padding: var(--space-6) var(--layout-page-gutter); + place-items: center; + overflow: hidden; + background: var(--bg-page); + color: var(--text-primary); +} + +.content { + display: flex; + width: min(100%, 36rem); + align-items: center; + flex-direction: column; + text-align: center; +} + +.persian { + display: none; +} + +:global(html[lang='fa']) .english { + display: none; +} + +:global(html[lang='fa']) .persian { + display: flex; +} + +.code { + color: color-mix(in srgb, var(--accent-default) 38%, var(--bg-page)); + font-size: clamp(6rem, 22vw, 12rem); + font-weight: 850; + letter-spacing: -0.08em; + line-height: 0.82; + user-select: none; +} + +:global(html[dir='rtl']) .code { + letter-spacing: 0; +} + +.divider { + width: 3rem; + height: 0.2rem; + margin-block: var(--space-7) var(--space-5); + border-radius: var(--radius-pill); + background: var(--accent-default); +} + +.content h1 { + max-width: 18ch; + color: var(--text-primary); + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 780; + letter-spacing: -0.04em; + line-height: 1.12; + text-wrap: balance; +} + +:global(html[dir='rtl']) .content h1 { + letter-spacing: 0; +} + +.description { + max-width: 34rem; + margin-top: var(--space-4); + color: var(--text-secondary); + font-size: var(--font-size-body-large); + line-height: var(--line-height-body-large); + text-wrap: pretty; +} + +.homeLink { + display: inline-flex; + min-height: var(--size-button-height); + align-items: center; + justify-content: center; + gap: var(--space-2); + margin-top: var(--space-7); + padding-inline: var(--space-5); + border: 1px solid transparent; + border-radius: var(--radius-pill); + background: var(--action-primary-bg); + color: var(--action-primary-fg); + font-size: var(--font-size-button); + font-weight: 700; + line-height: var(--line-height-button); + transition: + background-color var(--motion-duration-fast) ease, + box-shadow var(--motion-duration-fast) ease, + transform var(--motion-duration-fast) var(--motion-ease-out); +} + +.homeLink svg { + width: var(--size-icon-md); + height: var(--size-icon-md); +} + +.homeLink:active { + transform: scale(0.97); +} + +@media (hover: hover) and (pointer: fine) { + .homeLink:hover { + background: var(--action-primary-hover); + box-shadow: 0 0.25rem 0.75rem var(--shadow-color); + transform: translateY(-1px); + } +} + +@media (max-width: 650px) { + .page { + padding-block: var(--space-5); + } + + .divider { + margin-block: var(--space-6) var(--space-4); + } + + .description { + font-size: var(--font-size-body-default); + line-height: var(--line-height-body-default); + } +} + +@media (prefers-reduced-motion: reduce) { + .homeLink { + transition: background-color 120ms ease, box-shadow 120ms ease; + } + + .homeLink:hover, + .homeLink:active { + transform: none; + } +} diff --git a/src/app/global-not-found.tsx b/src/app/global-not-found.tsx new file mode 100644 index 0000000..707768f --- /dev/null +++ b/src/app/global-not-found.tsx @@ -0,0 +1,72 @@ +import type { Metadata } from 'next'; +import localFont from 'next/font/local'; +import Link from 'next/link'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; +import { messagesByLocale, type Locale } from '@/i18n/content'; +import '@/styles/globals.css'; +import styles from './global-not-found.module.css'; + +const themeAndLocaleScript = ` + try { + const root = document.documentElement; + const isPersian = /^\\/fa(?:\\/|$)/.test(window.location.pathname); + root.lang = isPersian ? 'fa' : 'en'; + root.dir = isPersian ? 'rtl' : 'ltr'; + const savedTheme = localStorage.getItem('portfolio-theme'); + root.dataset.theme = savedTheme === 'dark' ? 'dark' : 'light'; + } catch {} +`; + +const iranYekan = localFont({ + src: '../../public/fonts/IRANYekanXVF.woff2', + variable: '--font-body', + weight: '100 1000', + style: 'normal', + display: 'swap', + preload: false, + fallback: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'], +}); + +export const metadata: Metadata = { + title: '404 | Amirhossein Khalili', + description: 'The requested page could not be found.', +}; + +function NotFoundContent({ locale }: { locale: Locale }) { + const copy = messagesByLocale[locale].notFound; + const isRtl = locale === 'fa'; + + return ( +
+ +
+ ); +} + +export default function GlobalNotFound() { + return ( + + +