kite-menu-lang

This commit is contained in:
2026-06-11 14:05:36 +03:00
parent dadde7eeea
commit f8ca2034e0
7 changed files with 206 additions and 42 deletions
+34 -5
View File
@@ -2,9 +2,11 @@
import { useEffect, useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useRouter, usePathname } from "next/navigation";
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
import { CategoryNav } from "@/components/CategoryNav";
import { MenuItem } from "@/components/MenuItem";
import { getDictionary } from "@/lib/dictionaries";
export const CATEGORY_ICONS: Record<string, string> = {
"kahvaltiliklar": "🌅",
@@ -35,13 +37,24 @@ type SiteSettings = {
restaurantName: string
}
export default function MenuClient({ initialCategories, siteSettings }: { initialCategories: MenuCategory[], siteSettings: SiteSettings }) {
export default function MenuClient({ initialCategories, siteSettings, lang = "tr" }: { initialCategories: MenuCategory[], siteSettings: SiteSettings, lang?: string }) {
const [activeCategoryId, setActiveCategoryId] = useState<string>(
initialCategories[0]?.id || ""
);
const [selectedItem, setSelectedItem] = useState<MenuItemType | null>(null);
const sectionRefs = useRef<(HTMLElement | null)[]>([]);
const router = useRouter();
const pathname = usePathname();
const t = getDictionary(lang);
const switchLanguage = (newLang: string) => {
if (newLang === lang) return;
// Replace the current locale in the pathname
const newPath = pathname.replace(`/${lang}`, `/${newLang}`);
router.push(newPath || `/${newLang}`);
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
@@ -81,6 +94,22 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
}}
/>
{/* Language Switcher */}
<div className="absolute top-4 right-4 z-20 flex gap-2">
<button
onClick={() => switchLanguage('tr')}
className={`px-3 py-1.5 rounded-full text-[11px] font-sans tracking-widest uppercase transition-colors ${lang === 'tr' ? 'bg-white text-stone-900 font-bold' : 'bg-black/40 text-white/90 backdrop-blur-sm border border-white/20'}`}
>
TR
</button>
<button
onClick={() => switchLanguage('en')}
className={`px-3 py-1.5 rounded-full text-[11px] font-sans tracking-widest uppercase transition-colors ${lang === 'en' ? 'bg-white text-stone-900 font-bold' : 'bg-black/40 text-white/90 backdrop-blur-sm border border-white/20'}`}
>
EN
</button>
</div>
{/* Dark overlay to make text readable */}
<div className="absolute inset-0 z-0 bg-gradient-to-b from-black/60 via-black/40 to-black/80" />
@@ -96,7 +125,7 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
className="text-[10px] tracking-[0.38em] uppercase mb-8 font-sans font-medium"
style={{ color: "rgba(255,220,160,1)" }}
>
Akyaka · Muğla
{siteSettings.location || t.hero.location}
</p>
{/* Logo */}
@@ -120,7 +149,7 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
className="text-xs font-sans tracking-wide"
style={{ color: "rgba(255,218,168,0.5)" }}
>
Akyaka&apos;nın en keyifli menüsü
{t.hero.subtitle}
</p>
</motion.div>
</div>
@@ -203,7 +232,7 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
className="text-[10px] font-sans tracking-[0.3em] uppercase mt-1"
style={{ color: "rgba(255,200,130,0.35)" }}
>
Akyaka, Muğla
{siteSettings.location || t.hero.location}
</p>
<div
className="flex items-center gap-3 my-4"
@@ -217,7 +246,7 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
className="text-[11px] font-sans"
style={{ color: "rgba(120,90,60,0.5)" }}
>
© 2026 KiteBeach Akyaka. Tüm hakları saklıdır.
{t.footer.copyright}
</p>
<a
href="https://ayris.tech"
+17 -4
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Plus_Jakarta_Sans } from "next/font/google";
import "./globals.css";
import Script from "next/script";
import "../globals.css";
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
@@ -22,14 +23,26 @@ export const metadata: Metadata = {
description: "Kite Beach Akyaka dijital menü.",
};
export default function RootLayout({
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ lang: string }>;
}>) {
const { lang } = await params;
return (
<html lang="tr" className={`${cormorant.variable} ${jakarta.variable} h-full`}>
<body className="min-h-full flex flex-col antialiased" suppressHydrationWarning>{children}</body>
<html lang={lang} className={`${cormorant.variable} ${jakarta.variable} h-full`}>
<body className="min-h-full flex flex-col antialiased" suppressHydrationWarning>
{children}
{/* VPS Panel Analytics */}
<Script
src="https://panel.ayris.tech/api/analytics/script"
data-domain="https://menu.kitebeachakyaka.com.tr/"
strategy="afterInteractive"
/>
</body>
</html>
);
}
+15 -9
View File
@@ -1,8 +1,8 @@
import prisma from "@/lib/prisma";
import MenuClient from "./MenuClient";
import MenuClient from "../MenuClient";
import { MenuCategory } from "@/data/menu";
async function getMenuData(): Promise<MenuCategory[]> {
async function getMenuData(lang: string): Promise<MenuCategory[]> {
const dbCategories = await prisma.categories.findMany({
where: { status: 1 },
orderBy: { order_num: "asc" },
@@ -17,12 +17,12 @@ async function getMenuData(): Promise<MenuCategory[]> {
const categoryProducts = dbProducts.filter((p) => p.category_id === cat.id);
return {
id: cat.slug,
title: cat.name_tr,
title: lang === "tr" ? cat.name_tr : cat.name,
items: categoryProducts.map((p) => ({
id: p.id.toString(),
name: p.name_tr,
description: p.description_tr,
price: p.price.toString() + " ₺",
name: lang === "tr" ? p.name_tr : p.name,
description: (lang === "tr" ? p.description_tr : p.description) || "",
price: p.price.toString() + (lang === "tr" ? " ₺" : " ₺"), // Or maybe dynamic currency formatting
image: p.image_url || '/default-product.png',
})),
};
@@ -43,11 +43,17 @@ async function getSiteSettings() {
}
}
export default async function Page() {
export default async function Page({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const [menuData, siteSettings] = await Promise.all([
getMenuData(),
getMenuData(lang),
getSiteSettings(),
]);
return <MenuClient initialCategories={menuData} siteSettings={siteSettings} />;
return <MenuClient initialCategories={menuData} siteSettings={siteSettings} lang={lang} />;
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+62
View File
@@ -0,0 +1,62 @@
# Next.js App Router Internationalization (i18n) Implementation Plan
Bu plan, uygulamanıza İngilizce (en) ve Türkçe (tr) dil desteğini eklemek için yapılacak mimari değişiklikleri detaylandırmaktadır. Paylaştığınız doküman linki eski "Pages Router" yapısına aitti, ancak bu proje modern "App Router" yapısında olduğu için Next.js'in güncel `[lang]` tabanlı App Router i18n standartlarını uygulayacağız.
## Özet ve Hedefler
- `[lang]` dinamik klasör yapısına geçiş.
- Tarayıcı diline göre yönlendirme yapan bir `middleware.ts` dosyası.
- Veritabanındaki `name`/`name_tr` ve `description`/`description_tr` alanlarının seçilen dile göre dinamik getirilmesi.
- Sabit metinler için basit bir sözlük (dictionary) yapısı kurulması.
- Kullanıcının dili manuel olarak değiştirebileceği şık bir Dil Seçici (Language Switcher) bileşeni.
> [!IMPORTANT]
> **User Review Required**
> Lütfen aşağıdaki planı inceleyin ve onay verin veya eklenmesini istediğiniz başka bir dil/detay varsa belirtin.
## Açık Sorular
> [!WARNING]
> - Varsayılan (default) dili **Türkçe** (`tr`) olarak belirliyorum, uygun mudur?
> - Dil seçici butonunu ekranın neresinde göstermek istersiniz? (Öneri: Menü navigasyonunun hemen üzerinde veya sayfanın en üstünde sağ köşede)
---
## Önerilen Değişiklikler
### Routing ve Middleware
Ana sayfayı ve layout'u çoklu dil yapısına uygun hale getireceğiz.
#### [NEW] `middleware.ts`
Uygulamaya gelen istekleri kontrol edip, eğer URL'de `/tr` veya `/en` yoksa kullanıcının tarayıcı tercihine veya varsayılan dile (`tr`) yönlendirecek.
#### [MODIFY] `app/layout.tsx` -> `app/[lang]/layout.tsx`
Layout, artık url'den gelen `params.lang` değerini alacak ve `<html lang={lang}>` özelliğini dinamik olarak ayarlayacak.
#### [MODIFY] `app/page.tsx` -> `app/[lang]/page.tsx`
Sayfa, `lang` parametresine göre çalışacak ve veritabanından verileri çekerken bu dile dikkat edecek.
---
### Veritabanı ve Veri Çekme (Data Fetching)
Mevcut şemada zaten İngilizce ve Türkçe alanlar bulunuyor.
#### [MODIFY] `app/[lang]/page.tsx` (İçerik)
`getMenuData` fonksiyonu güncellenecek:
- Eğer dil `en` ise: `title: cat.name`, `name: p.name`, `description: p.description`
- Eğer dil `tr` ise: `title: cat.name_tr`, `name: p.name_tr`, `description: p.description_tr`
---
### Sabit Metinler (Dictionaries)
Arayüzde veritabanından gelmeyen sabit yazılar için bir sözlük mekanizması kuracağız.
#### [NEW] `lib/dictionaries.ts`
İçerisinde "Akyaka'nın en keyifli menüsü", "Tüm hakları saklıdır" gibi metinlerin hem Türkçe hem İngilizce karşılıklarını tutan basit bir sistem oluşturulacak.
---
### Arayüz (UI)
#### [MODIFY] `app/MenuClient.tsx`
- Sabit metinler (Header yazıları, Footer yazıları) `dictionaries` objesinden beslenecek şekilde `props` olarak alınacak.
- Sayfaya (Örneğin sağ üst köşeye veya menü başlıklarının üstüne) `TR / EN` dilleri arasında geçiş yapmayı sağlayacak bir buton eklenecek.
---
## Doğrulama Planı (Verification Plan)
1. **Middleware Testi**: `http://localhost:3000/` adresine girildiğinde `http://localhost:3000/tr` adresine otomatik yönlendiriliyor mu?
2. **İngilizce Testi**: `http://localhost:3000/en` adresine girildiğinde ürünlerin İngilizce isimleri (`name`) ve açıklamaları (`description`) geliyor mu? Sabit metinler (Copyright vb.) İngilizceye dönüyor mu?
3. **Dil Değiştirici Testi**: UI üzerindeki butonla diller arası geçiş yapıldığında sayfa sorunsuz bir şekilde güncelleniyor mu?
+26
View File
@@ -0,0 +1,26 @@
export const dictionaries = {
tr: {
hero: {
location: "Akyaka · Muğla",
subtitle: "Akyaka'nın en keyifli menüsü",
},
footer: {
copyright: "© 2026 KiteBeach Akyaka. Tüm hakları saklıdır.",
createdBy: "Created by ayris.tech",
},
},
en: {
hero: {
location: "Akyaka · Mugla",
subtitle: "The most delightful menu in Akyaka",
},
footer: {
copyright: "© 2026 KiteBeach Akyaka. All rights reserved.",
createdBy: "Created by ayris.tech",
},
},
};
export const getDictionary = (lang: string) => {
return dictionaries[lang as keyof typeof dictionaries] || dictionaries['tr'];
};
+52 -24
View File
@@ -2,37 +2,65 @@ import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { updateSession, decrypt } from './lib/auth'
const locales = ['en', 'tr'];
const defaultLocale = 'tr';
export async function proxy(request: NextRequest) {
// Always update session expiration
let response = await updateSession(request)
const { pathname } = request.nextUrl;
const isAuthPage = request.nextUrl.pathname.startsWith('/admin/login')
const isAdminPage = request.nextUrl.pathname.startsWith('/admin')
// --- Admin Auth Logic ---
const isAdminPath = pathname.startsWith('/admin');
if (isAdminPath) {
// Always update session expiration for admin
let response = await updateSession(request)
// Check if session exists and is valid
const sessionValue = request.cookies.get('session')?.value
let session = null
if (sessionValue) {
try {
session = await decrypt(sessionValue)
} catch(e) {
session = null
const isAuthPage = pathname.startsWith('/admin/login')
// Check if session exists and is valid
const sessionValue = request.cookies.get('session')?.value
let session = null
if (sessionValue) {
try {
session = await decrypt(sessionValue)
} catch(e) {
session = null
}
}
// If trying to access admin pages (except login) without a valid session
if (!isAuthPage && !session) {
return NextResponse.redirect(new URL('/admin/login', request.url))
}
// If trying to access login page with a valid session
if (isAuthPage && session) {
return NextResponse.redirect(new URL('/admin', request.url))
}
return response || NextResponse.next()
}
// --- i18n Logic ---
const isPublicFile = pathname.includes('.') || pathname.startsWith('/_next') || pathname.startsWith('/api');
if (!isPublicFile) {
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (!pathnameHasLocale) {
// Redirect if there is no locale
request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
}
// If trying to access admin pages (except login) without a valid session
if (isAdminPage && !isAuthPage && !session) {
return NextResponse.redirect(new URL('/admin/login', request.url))
}
// If trying to access login page with a valid session
if (isAuthPage && session) {
return NextResponse.redirect(new URL('/admin', request.url))
}
return response || NextResponse.next()
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*'],
matcher: [
// Apply proxy to all paths except public static assets
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|avif)$).*)',
],
}