Files
lunaqrmenu/app/page.tsx
2026-04-30 01:48:08 +03:00

325 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState, useRef, useCallback } from "react";
import Image from "next/image";
import menuData from "../menu/menu.json";
/* ────── Helpers ────── */
interface MenuItem {
name: string;
ingredients?: string;
taste_profile?: string;
grape_variety?: string;
price:
| string
| { single?: string; double?: string; glass?: string; bottle?: string };
}
interface Category {
id: string;
title: string;
items: MenuItem[];
}
interface MenuData {
restaurant_name: string;
footer_note: string;
categories: Category[];
}
const data = menuData as MenuData;
/* ────── Stars component ────── */
interface Star {
id: number;
left: string;
top: string;
duration: string;
delay: string;
maxOpacity: number;
}
function HeroStars() {
const [stars, setStars] = useState<Star[]>([]);
useEffect(() => {
setStars(
Array.from({ length: 40 }, (_, i) => ({
id: i,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
duration: `${3 + Math.random() * 4}s`,
delay: `${Math.random() * 5}s`,
maxOpacity: 0.3 + Math.random() * 0.5,
}))
);
}, []);
return (
<div className="hero-stars">
{stars.map((s) => (
<span
key={s.id}
className="star"
style={
{
left: s.left,
top: s.top,
"--duration": s.duration,
"--max-opacity": s.maxOpacity,
animationDelay: s.delay,
} as React.CSSProperties
}
/>
))}
</div>
);
}
/* ────── Price renderer ────── */
function PriceDisplay({ price }: { price: MenuItem["price"] }) {
if (typeof price === "string") {
return <span className="item-price">{price}</span>;
}
const entries: [string, string][] = [];
if (price.single) entries.push(["Tek", price.single]);
if (price.double) entries.push(["Dbl", price.double]);
if (price.glass) entries.push(["Kadeh", price.glass]);
if (price.bottle) entries.push(["Şişe", price.bottle]);
return (
<div className="item-price-multi">
{entries.map(([label, val]) => (
<div key={label} className="price-row">
<span className="price-label">{label}</span>
<span className="item-price">{val}</span>
</div>
))}
</div>
);
}
/* ────── Category Icon (decorative) ────── */
function getCategoryIcon(id: string): string {
const icons: Record<string, string> = {
classic_cocktails: "🍸",
lunas_cocktails: "🌙",
shots: "🥃",
gin: "🫒",
whiskey: "🥂",
rum: "🏴‍☠️",
tequila: "🌵",
vodka: "🧊",
cognac: "🍷",
red_wine: "🍷",
white_wine: "🥂",
roze_blush: "🌸",
champagne_prosecco: "🍾",
draft_beer: "🍺",
bottle_beer: "🍻",
coffee: "☕",
soft_drinks: "🧃",
snacks: "🥜",
};
return icons[id] || "✦";
}
/* ────── Main Page ────── */
export default function MenuPage() {
const [activeCategory, setActiveCategory] = useState<string>(
data.categories[0]?.id || ""
);
const [showBackToTop, setShowBackToTop] = useState(false);
const navRef = useRef<HTMLDivElement>(null);
const sectionRefs = useRef<Map<string, HTMLElement>>(new Map());
/* Intersection observer to update active category */
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((e) => e.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (visible.length > 0) {
const id = visible[0].target.getAttribute("data-category-id");
if (id) setActiveCategory(id);
}
},
{ rootMargin: "-50% 0px -50% 0px", threshold: 0 }
);
sectionRefs.current.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
/* Back-to-top visibility */
useEffect(() => {
const onScroll = () => setShowBackToTop(window.scrollY > 600);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
/* Scroll nav button into view */
useEffect(() => {
if (!navRef.current) return;
const btn = navRef.current.querySelector(
`[data-nav-id="${activeCategory}"]`
);
if (btn) {
btn.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
}
}, [activeCategory]);
const scrollToCategory = useCallback((id: string) => {
const el = sectionRefs.current.get(id);
if (el) {
const offset = 56;
const y = el.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: y, behavior: "smooth" });
}
}, []);
const setSectionRef = useCallback(
(id: string) => (el: HTMLElement | null) => {
if (el) sectionRefs.current.set(id, el);
},
[]
);
return (
<>
{/* ───── HERO ───── */}
<section className="hero" id="hero">
<HeroStars />
<div className="hero-moon" />
{/* Background illustration */}
<div className="hero-brand">
<Image
src="/logo1.png"
alt="Luna Cocktail and More"
width={360}
height={360}
className="hero-logo"
priority
/>
<div className="hero-divider" />
<p className="hero-subtitle">MENÜ</p>
</div>
<a
href="#menu"
className="hero-scroll-cta"
onClick={(e) => {
e.preventDefault();
scrollToCategory(data.categories[0]?.id);
}}
>
MENÜYÜ KEŞFET
<span className="scroll-arrow" />
</a>
</section>
{/* ───── CATEGORY NAV ───── */}
<nav className="category-nav" id="menu">
<div className="category-nav-inner" ref={navRef}>
{data.categories.map((cat) => (
<button
key={cat.id}
data-nav-id={cat.id}
className={`category-nav-btn ${activeCategory === cat.id ? "active" : ""
}`}
onClick={() => scrollToCategory(cat.id)}
>
{cat.title.toLocaleUpperCase('en')}
</button>
))}
</div>
</nav>
{/* ───── MENU CONTENT ───── */}
<main className="menu-container">
{data.categories.map((cat) => (
<section
key={cat.id}
className="category-section"
data-category-id={cat.id}
ref={setSectionRef(cat.id)}
>
<div className="category-header">
<h2 className="category-title">
<span style={{ marginRight: "0.5rem", fontSize: "0.85em" }}>
{getCategoryIcon(cat.id)}
</span>
{cat.title}
</h2>
<p className="category-item-count">
{cat.items.length} ÜRÜN
</p>
</div>
<div className="menu-items">
{cat.items.map((item, idx) => (
<div className="menu-item" key={`${cat.id}-${idx}`}>
<div className="item-info">
<h3 className="item-name">{item.name}</h3>
<div className="item-meta">
{item.grape_variety && (
<span className="item-grape">{item.grape_variety}</span>
)}
{item.ingredients && (
<span className="item-ingredients">
{item.ingredients}
</span>
)}
{item.taste_profile && (
<div className="item-taste-profile">
{item.taste_profile.split("-").map((t, i) => (
<span key={i} className="taste-tag">
{t.trim()}
</span>
))}
</div>
)}
</div>
</div>
<div className="item-price-area">
<PriceDisplay price={item.price} />
</div>
</div>
))}
</div>
</section>
))}
</main>
{/* ───── FOOTER ───── */}
<footer className="menu-footer">
<p className="footer-note">{data.footer_note}</p>
<p className="footer-brand">LUNA </p>
</footer>
{/* ───── BACK TO TOP ───── */}
<button
className={`back-to-top ${showBackToTop ? "visible" : ""}`}
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
aria-label="Yukarı git"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
</>
);
}