langueages

This commit is contained in:
2026-06-29 20:29:41 +03:00
parent 958152a688
commit e30cbfa132
19 changed files with 1014 additions and 40 deletions
+307
View File
@@ -0,0 +1,307 @@
"use client";
import React, { useEffect, useState, useRef, Fragment } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
import { CategoryNav } from "@/components/CategoryNav";
import { MenuItem } from "@/components/MenuItem";
import { useTranslations, useLocale } from "next-intl";
import { useRouter, usePathname } from "@/i18n/routing";
export const CATEGORY_ICONS: Record<string, string> = {
"kahvaltiliklar": "🌅",
"kaseler": "🥗",
"burger-ve-sandvic": "🍔",
"pizzalar": "🍕",
"makarnalar": "🍝",
"baslangiclar": "🧆",
"salatalar": "🥬",
"ana-yemekler": "🍽️",
"tatlilar": "🍮",
"i̇mza-kokteyller": "🍹",
"klasik-kokteyller": "🍸",
"biralar": "🍺",
"shotlar": "🥃",
"sise-ve-kadeh-alkollu": "🥂",
"saraplar": "🍷",
"cerezler": "🥜",
"alkolsuz-i̇cecekler": "🧃",
"sicak-kahveler": "☕",
"soguk-kahveler": "🧊",
"ekstralar": "✨",
};
type SiteSettings = {
logoUrl: string
location: string
restaurantName: string
}
export default function MenuClient({ initialCategories, siteSettings }: { initialCategories: MenuCategory[], siteSettings: SiteSettings }) {
const [activeCategoryId, setActiveCategoryId] = useState<string>(
initialCategories[0]?.id || ""
);
const [selectedItem, setSelectedItem] = useState<MenuItemType | null>(null);
const sectionRefs = useRef<(HTMLElement | null)[]>([]);
const t = useTranslations("Menu");
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const hit = entries.find((e) => e.isIntersecting);
if (hit) setActiveCategoryId(hit.target.id);
},
{ root: null, rootMargin: "-100px 0px -62% 0px", threshold: 0 }
);
sectionRefs.current.forEach((ref) => ref && observer.observe(ref));
return () => observer.disconnect();
}, []);
useEffect(() => {
if (selectedItem) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [selectedItem]);
return (
<div className="min-h-screen" style={{ background: "var(--cream)" }}>
{/* ── Hero ─────────────────────────────────────── */}
<header className="relative overflow-hidden bg-stone-900">
{/* Background Image */}
<div
className="absolute inset-0 z-0 opacity-80"
style={{
backgroundImage: "url('/header-bg.jpg')",
backgroundSize: "cover",
backgroundPosition: "center"
}}
/>
{/* 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" />
{/* hero content */}
<div className="relative z-10 flex flex-col items-center text-center px-6 pt-16 pb-20">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, ease: [0.16, 1, 0.3, 1] }}
className="flex flex-col items-center"
>
<p
className="text-[10px] tracking-[0.38em] uppercase mb-8 font-sans font-medium"
style={{ color: "rgba(255,220,160,1)" }}
>
Akyaka · Muğla
</p>
{/* Logo */}
<img
src="/logo.png"
alt="Moy Beach"
className="w-64 md:w-80 object-contain drop-shadow-lg"
style={{ filter: 'brightness(0) invert(1)' }}
/>
<div
className="flex items-center gap-3 my-5"
style={{ color: "rgba(255,190,100,0.35)" }}
>
<div className="h-px w-14" style={{ background: "currentColor" }} />
<span className="text-base"></span>
<div className="h-px w-14" style={{ background: "currentColor" }} />
</div>
<p
className="text-xs font-sans tracking-wide"
style={{ color: "rgba(255,218,168,0.5)" }}
>
{t('subtitle')}
</p>
<div className="mt-8 flex items-center gap-4">
{['tr', 'en', 'ru', 'de'].map((l, index) => (
<React.Fragment key={l}>
<button
onClick={() => router.replace(pathname, { locale: l })}
className={`text-[11px] font-sans tracking-[0.25em] uppercase transition-all duration-300 ${
locale === l
? 'text-white opacity-100 scale-105'
: 'text-white/40 hover:text-white/70 hover:scale-100'
}`}
>
{l}
</button>
{index < 3 && (
<div className="w-px h-3" style={{ background: "rgba(255,190,100,0.2)" }} />
)}
</React.Fragment>
))}
</div>
</motion.div>
</div>
{/* wave transition */}
<div className="absolute bottom-0 left-0 right-0 translate-y-px">
<svg
viewBox="0 0 1440 52"
preserveAspectRatio="none"
className="w-full h-[52px]"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 52L80 44C160 36 320 20 480 18C640 16 800 28 960 32C1120 36 1280 32 1360 30L1440 28V52H1360C1280 52 1120 52 960 52C800 52 640 52 480 52C320 52 160 52 80 52H0Z"
fill="#FAF8F3"
/>
</svg>
</div>
</header>
{/* ── Category Nav ─────────────────────────────── */}
<CategoryNav
categories={initialCategories}
activeCategoryId={activeCategoryId}
icons={CATEGORY_ICONS}
/>
{/* ── Menu Sections ────────────────────────────── */}
<main className="pb-28">
{initialCategories.map((category, index) => (
<motion.section
key={category.id}
id={category.id}
ref={(el) => { sectionRefs.current[index] = el; }}
className="pt-10 px-4 max-w-lg mx-auto scroll-mt-[62px]"
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
>
{/* section header */}
<div
className="flex items-center gap-2.5 mb-4 pb-3 border-b"
style={{ borderColor: "var(--sand-border)" }}
>
<span className="text-xl leading-none select-none">
{CATEGORY_ICONS[category.id] ?? "🍽️"}
</span>
<h2
className="font-display text-[22px] leading-tight font-semibold italic uppercase tracking-wider"
style={{ color: "var(--stone-ink)" }}
>
{category.title}
</h2>
</div>
<div>
{category.items.map((item, idx) => (
<MenuItem key={idx} item={item} onClick={() => setSelectedItem(item)} />
))}
</div>
</motion.section>
))}
</main>
{/* ── Footer ───────────────────────────────────── */}
<footer
className="py-12 text-center"
style={{ background: "var(--hero-from)" }}
>
<div className="flex flex-col items-center gap-1">
{/* Footer Logo */}
<img
src="/logo.png"
alt="Moy Beach"
className="w-36 object-contain opacity-70 mb-2"
style={{ filter: 'brightness(0) invert(1)' }}
/>
<p
className="text-[10px] font-sans tracking-[0.3em] uppercase mt-1"
style={{ color: "rgba(255,200,130,0.35)" }}
>
Akyaka, Muğla
</p>
<div
className="flex items-center gap-3 my-4"
style={{ color: "rgba(160,100,50,0.4)" }}
>
<div className="h-px w-10" style={{ background: "currentColor" }} />
<span className="text-xs"></span>
<div className="h-px w-10" style={{ background: "currentColor" }} />
</div>
<p
className="text-[11px] font-sans"
style={{ color: "rgba(120,90,60,0.5)" }}
>
© 2026 {siteSettings.restaurantName}. {t('allRightsReserved')}
</p>
<a
href="https://ayris.tech"
target="_blank"
rel="noopener noreferrer"
className="mt-6 text-[10px] font-sans tracking-[0.2em] uppercase transition-colors hover:text-white/80"
style={{ color: "rgba(160,100,50,0.4)" }}
>
Created by <span className="font-semibold">ayris.tech</span>
</a>
</div>
</footer>
{/* ── Modal ────────────────────────────────────── */}
<AnimatePresence>
{selectedItem && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
onClick={() => setSelectedItem(null)}
>
<motion.div
initial={{ y: 50, opacity: 0, scale: 0.95 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
exit={{ y: 20, opacity: 0, scale: 0.95 }}
className="bg-white rounded-2xl overflow-hidden w-full max-w-sm shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{selectedItem.image && (
<div className="w-full h-80 relative">
<img src={selectedItem.image} alt={selectedItem.name} className="w-full h-full object-cover" />
<button
className="absolute top-3 right-3 bg-black/50 text-white w-8 h-8 rounded-full flex items-center justify-center backdrop-blur-md transition-colors hover:bg-black/70"
onClick={() => setSelectedItem(null)}
>
</button>
</div>
)}
<div className="p-5">
<div className="flex justify-between items-start gap-4 mb-2">
<h3 className="text-xl font-semibold font-sans" style={{ color: "var(--stone-ink)" }}>
{selectedItem.name}
</h3>
<span className="shrink-0 text-lg font-semibold font-sans tabular-nums" style={{ color: "var(--amber)" }}>
{selectedItem.price}
</span>
</div>
<p className="text-[14px] leading-relaxed font-sans" style={{ color: "var(--stone-muted)" }}>
{selectedItem.description}
</p>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Plus_Jakarta_Sans } from "next/font/google";
import {NextIntlClientProvider} from 'next-intl';
import {getMessages, setRequestLocale} from 'next-intl/server';
import {routing} from '@/i18n/routing';
import {notFound} from 'next/navigation';
import "../globals.css";
const cormorant = Cormorant_Garamond({
variable: "--font-cormorant",
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
style: ["normal", "italic"],
display: "swap",
});
const jakarta = Plus_Jakarta_Sans({
variable: "--font-jakarta",
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
display: "swap",
});
export const metadata: Metadata = {
title: "Moy Beach Akyaka | Menü",
description: "Moy Beach Akyaka dijital menü.",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}
export default async function LocaleLayout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{locale: string}>;
}) {
const {locale} = await params;
if (!routing.locales.includes(locale as any)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale} className={`${cormorant.variable} ${jakarta.variable} h-full`}>
<body className="min-h-full flex flex-col antialiased" suppressHydrationWarning>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
+63
View File
@@ -0,0 +1,63 @@
import prisma from "@/lib/prisma";
import MenuClient from "./MenuClient";
import { MenuCategory } from "@/data/menu";
import { setRequestLocale } from 'next-intl/server';
import { routing } from '@/i18n/routing';
async function getMenuData(locale: string): Promise<MenuCategory[]> {
const dbCategories = await prisma.categories.findMany({
where: { status: 1 },
orderBy: { order_num: "asc" },
});
const dbProducts = await prisma.products.findMany({
where: { status: 1 },
orderBy: [{ category_id: "asc" }, { id: "asc" }],
});
return dbCategories.map((cat) => {
const categoryProducts = dbProducts.filter((p) => p.category_id === cat.id);
return {
id: cat.slug,
title: (cat.name_i18n as Record<string, string>)?.[locale] || (locale === 'tr' ? cat.name_tr : cat.name),
items: categoryProducts.map((p) => ({
id: p.id.toString(),
name: (p.name_i18n as Record<string, string>)?.[locale] || (locale === 'tr' ? p.name_tr : p.name),
description: (p.description_i18n as Record<string, string>)?.[locale] || (locale === 'tr' ? p.description_tr : (p.description || '')),
price: p.price.toString() + " ₺",
image: p.image_url || '/default-product.png',
})),
};
});
}
async function getSiteSettings() {
const settingsData = await prisma.settings.findMany()
const settings = settingsData.reduce((acc, curr) => {
acc[curr.key] = curr.value
return acc
}, {} as Record<string, string>)
return {
logoUrl: settings['logo_url'] || '/logo.png',
location: settings['location'] || 'Akyaka · Muğla',
restaurantName: settings['restaurant_name'] || 'Moy Beach',
}
}
export default async function Page({
params
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params;
setRequestLocale(locale);
const [menuData, siteSettings] = await Promise.all([
getMenuData(locale),
getSiteSettings(),
]);
return <MenuClient initialCategories={menuData} siteSettings={siteSettings} />;
}