Files
lunaqrmenu/app/components/MenuView.tsx
2026-05-15 19:11:17 +03:00

306 lines
8.6 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";
/* ────── Helpers ────── */
interface MenuItem {
name: string;
ingredients?: string | null;
tasteProfile?: string | null;
grapeVariety?: string | null;
price: any;
}
interface Category {
id: string;
title: string;
externalId?: string | null;
items: MenuItem[];
}
interface MenuData {
restaurant_name: string;
footer_note: string;
categories: Category[];
}
/* ────── 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] || "✦";
}
export default function MenuView({ data }: { data: MenuData }) {
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());
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();
}, []);
useEffect(() => {
const onScroll = () => setShowBackToTop(window.scrollY > 600);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
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 (
<>
<section className="hero" id="hero">
<HeroStars />
<div className="hero-moon" />
<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>
<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>
<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.externalId || 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.grapeVariety && (
<span className="item-grape">{item.grapeVariety}</span>
)}
{item.ingredients && (
<span className="item-ingredients">
{item.ingredients}
</span>
)}
{item.tasteProfile && (
<div className="item-taste-profile">
{item.tasteProfile.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 className="menu-footer">
<p className="footer-note">{data.footer_note}</p>
<p className="footer-brand">LUNA </p>
</footer>
<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}
>
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
</>
);
}