Build Kozmos QR menu site — full implementation

Next.js 16 App Router, TR/EN i18n via [lang] routing and proxy.ts,
Tailwind v4, minimal list menu style, sticky bottom tab bar with
Intersection Observer, PWA manifest, WhatsApp fixed button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mstfyldz
2026-06-03 02:35:14 +03:00
co-authored by Claude Sonnet 4.6
parent f879d65cfc
commit 718e0a6228
16 changed files with 811 additions and 98 deletions
+16
View File
@@ -0,0 +1,16 @@
import 'server-only'
const dictionaries = {
tr: () => import('../../dictionaries/tr.json').then((m) => m.default),
en: () => import('../../dictionaries/en.json').then((m) => m.default),
}
export type Locale = keyof typeof dictionaries
export function hasLocale(locale: string): locale is Locale {
return locale in dictionaries
}
export async function getDictionary(locale: Locale) {
return dictionaries[locale]()
}
+42
View File
@@ -0,0 +1,42 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getDictionary, hasLocale } from './dictionaries'
export async function generateStaticParams() {
return [{ lang: 'tr' }, { lang: 'en' }]
}
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>
}): Promise<Metadata> {
const { lang } = await params
if (!hasLocale(lang)) return {}
const dict = await getDictionary(lang)
return {
title: dict.meta.title,
description: dict.meta.description,
manifest: '/manifest.webmanifest',
appleWebApp: {
capable: true,
statusBarStyle: 'black-translucent',
title: 'Kozmos',
},
other: {
'theme-color': '#3D2B1F',
},
}
}
export default async function LangLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ lang: string }>
}) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
return <>{children}</>
}
+57
View File
@@ -0,0 +1,57 @@
import { notFound } from 'next/navigation'
import { getDictionary, hasLocale } from './dictionaries'
import { categories } from '@/data/menu'
import Header from '@/components/Header'
import CategorySection from '@/components/CategorySection'
import BottomTabBar from '@/components/BottomTabBar'
import WhatsAppButton from '@/components/WhatsAppButton'
export default async function MenuPage({
params,
}: {
params: Promise<{ lang: string }>
}) {
const { lang } = await params
if (!hasLocale(lang)) notFound()
const dict = await getDictionary(lang)
return (
<>
<Header dict={dict} />
<main
className="bg-cream min-h-screen"
style={{
paddingTop: 'var(--header-h)',
paddingBottom: 'calc(var(--tabbar-h) + 8px)',
}}
>
{/* Subtle top fade from header */}
<div
className="h-4 bg-gradient-to-b from-beige/30 to-transparent"
aria-hidden="true"
/>
{categories.map((category) => (
<CategorySection
key={category.id}
category={category}
lang={lang}
currency={dict.currency}
/>
))}
{/* Bottom spacing */}
<div className="h-6" aria-hidden="true" />
</main>
<WhatsAppButton
message={dict.whatsapp.message}
label={dict.whatsapp.label}
/>
<BottomTabBar categories={categories} lang={lang} />
</>
)
}