diff --git a/app/MenuClient.tsx b/app/MenuClient.tsx
index 20f2206..760cfd2 100644
--- a/app/MenuClient.tsx
+++ b/app/MenuClient.tsx
@@ -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 = {
"kahvaltiliklar": "🌅",
@@ -35,12 +37,23 @@ 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(
initialCategories[0]?.id || ""
);
const [selectedItem, setSelectedItem] = useState(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(
@@ -81,6 +94,22 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
}}
/>
+ {/* Language Switcher */}
+
+
+
+
+
{/* Dark overlay to make text readable */}
@@ -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}
{/* 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'nın en keyifli menüsü
+ {t.hero.subtitle}
@@ -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}
- © 2026 KiteBeach Akyaka. Tüm hakları saklıdır.
+ {t.footer.copyright}
;
}>) {
+ const { lang } = await params;
+
return (
-
- {children}
+
+
+ {children}
+ {/* VPS Panel Analytics */}
+
+
);
}
diff --git a/app/page.tsx b/app/[lang]/page.tsx
similarity index 67%
rename from app/page.tsx
rename to app/[lang]/page.tsx
index 906f114..71c5732 100644
--- a/app/page.tsx
+++ b/app/[lang]/page.tsx
@@ -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 {
+async function getMenuData(lang: string): Promise {
const dbCategories = await prisma.categories.findMany({
where: { status: 1 },
orderBy: { order_num: "asc" },
@@ -17,12 +17,12 @@ async function getMenuData(): Promise {
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 ;
+ return ;
}
diff --git a/app/favicon.ico b/app/favicon.ico
index 380789b..fcc1827 100644
Binary files a/app/favicon.ico and b/app/favicon.ico differ
diff --git a/implementation_plan.md b/implementation_plan.md
new file mode 100644
index 0000000..eccb979
--- /dev/null
+++ b/implementation_plan.md
@@ -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 `` ö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?
diff --git a/lib/dictionaries.ts b/lib/dictionaries.ts
new file mode 100644
index 0000000..849a990
--- /dev/null
+++ b/lib/dictionaries.ts
@@ -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'];
+};
diff --git a/proxy.ts b/proxy.ts
index bfde159..891bd5c 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -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)$).*)',
+ ],
}