feat: add prisma support, admin panel and auth
This commit is contained in:
353
app/page.tsx
353
app/page.tsx
@@ -1,324 +1,43 @@
|
||||
"use client";
|
||||
import { prisma } from "@/app/lib/prisma";
|
||||
import MenuView from "./components/MenuView";
|
||||
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import Image from "next/image";
|
||||
import menuData from "../menu/menu.json";
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/* ────── Helpers ────── */
|
||||
interface MenuItem {
|
||||
name: string;
|
||||
ingredients?: string;
|
||||
taste_profile?: string;
|
||||
grape_variety?: string;
|
||||
price:
|
||||
| string
|
||||
| { single?: string; double?: string; glass?: string; bottle?: string };
|
||||
}
|
||||
export default async function Page() {
|
||||
const restaurant = await prisma.restaurant.findFirst({
|
||||
include: {
|
||||
categories: {
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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>;
|
||||
if (!restaurant) {
|
||||
return <div>Veritabanı yükleniyor...</div>;
|
||||
}
|
||||
|
||||
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: "🥜",
|
||||
// Map database structure to the structure expected by MenuView
|
||||
const data = {
|
||||
restaurant_name: restaurant.name,
|
||||
footer_note: restaurant.footerNote || "",
|
||||
categories: restaurant.categories.map(cat => ({
|
||||
id: cat.id,
|
||||
title: cat.title,
|
||||
externalId: cat.externalId,
|
||||
items: cat.items.map(item => ({
|
||||
name: item.name,
|
||||
ingredients: item.ingredients,
|
||||
tasteProfile: item.tasteProfile,
|
||||
grapeVariety: item.grapeVariety,
|
||||
price: item.price,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
return <MenuView data={data} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user