first commit
This commit is contained in:
+53
@@ -0,0 +1,53 @@
|
||||
# 1. Base image
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# 2. Dependencies
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
|
||||
|
||||
# 3. Builder
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Environment variables must be present at build time for Next.js
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 4. Runner
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Set the correct permission for prerender cache
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Start the server
|
||||
CMD ["node", "server.js"]
|
||||
@@ -1,36 +1,43 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# Moy Beach Menu
|
||||
|
||||
## Getting Started
|
||||
Moy Beach için modern, şık ve animasyonlu dijital menü uygulaması.
|
||||
|
||||
First, run the development server:
|
||||
## 🚀 Teknolojiler
|
||||
- **Next.js 15** (App Router)
|
||||
- **React 19**
|
||||
- **Tailwind CSS v4**
|
||||
- **Framer Motion** (Yumuşak geçişler ve modal animasyonları)
|
||||
- **TypeScript**
|
||||
|
||||
## 📦 Özellikler
|
||||
- **Kategori Navigasyonu:** Kullanıcı kaydırdıkça hangi kategoride olduğunu takip eden `IntersectionObserver` destekli navigasyon.
|
||||
- **Dinamik Modal Görünümü:** Ürünlere tıklandığında detaylı bilgi gösteren, şık ve tepkisel modal tasarımı.
|
||||
- **Özelleştirilmiş Tasarım Tokenleri:** `app/globals.css` içerisinde renk paletleri ve yazı tipleri (Cormorant, Plus Jakarta Sans) kolayca yönetilebilir.
|
||||
- **Docker Desteği:** `Dockerfile` ile optimize edilmiş `standalone` output desteğiyle kolayca konteynerize edilebilir.
|
||||
|
||||
## 🛠️ Kurulum ve Çalıştırma
|
||||
|
||||
Lokal geliştirme ortamında çalıştırmak için:
|
||||
|
||||
```bash
|
||||
# Bağımlılıkları yükleyin
|
||||
npm install
|
||||
|
||||
# Geliştirme sunucusunu başlatın
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Tarayıcınızda [http://localhost:3000](http://localhost:3000) adresine giderek uygulamayı görüntüleyebilirsiniz.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
## 🐳 Docker ile Çalıştırma
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
```bash
|
||||
# İmajı oluşturun
|
||||
docker build -t moy-qr .
|
||||
|
||||
## Learn More
|
||||
# Konteyneri başlatın
|
||||
docker run -p 3000:3000 moy-qr
|
||||
```
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
## 📝 Menü İçeriğini Güncelleme
|
||||
Menü kategorileri, ürünler, fiyatlar ve resimler `data/menu.ts` dosyasından okunur. Yeni ürün eklemek veya düzenlemek için bu dosyayı güncelleyebilirsiniz.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
+59
-15
@@ -1,26 +1,70 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ── Design Tokens ─────────────────────────────── */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--cream: #FAF8F3;
|
||||
--sand: #F0E8DC;
|
||||
--sand-border: rgba(140, 108, 72, 0.13);
|
||||
--amber: #C2712E;
|
||||
--amber-light: #E49248;
|
||||
--amber-pale: #F6EDDE;
|
||||
--stone-ink: #2E2820;
|
||||
--stone-mid: #6B5E54;
|
||||
--stone-muted: #9C9088;
|
||||
--hero-from: #1E0E06;
|
||||
--hero-via: #7C3312;
|
||||
--hero-to: #C96B28;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
--color-cream: var(--cream);
|
||||
--color-sand: var(--sand);
|
||||
--color-amber: var(--amber);
|
||||
--color-amber-light: var(--amber-light);
|
||||
--color-amber-pale: var(--amber-pale);
|
||||
--color-stone-ink: var(--stone-ink);
|
||||
--color-stone-mid: var(--stone-mid);
|
||||
--color-stone-muted: var(--stone-muted);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
--font-display: var(--font-cormorant);
|
||||
--font-sans: var(--font-jakarta);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: var(--cream);
|
||||
color: var(--stone-ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: var(--font-cormorant), "Garamond", serif;
|
||||
}
|
||||
|
||||
.hero-gradient {
|
||||
background: linear-gradient(
|
||||
170deg,
|
||||
var(--hero-from) 0%,
|
||||
var(--hero-via) 50%,
|
||||
var(--hero-to) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.pill-active {
|
||||
background: linear-gradient(135deg, #C2712E, #9A5320);
|
||||
color: #FFF6E8;
|
||||
box-shadow: 0 2px 10px rgba(194, 113, 46, 0.38);
|
||||
}
|
||||
|
||||
.pill-inactive {
|
||||
background: rgba(120, 90, 58, 0.09);
|
||||
color: var(--stone-mid);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-12
@@ -1,20 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Cormorant_Garamond, Plus_Jakarta_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const cormorant = Cormorant_Garamond({
|
||||
variable: "--font-cormorant",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
style: ["normal", "italic"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const jakarta = Plus_Jakarta_Sans({
|
||||
variable: "--font-jakarta",
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Moy Beach Akyaka | Menü",
|
||||
description: "Moy Beach Akyaka dijital menü.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,11 +28,8 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<html lang="tr" className={`${cormorant.variable} ${jakarta.variable} h-full`}>
|
||||
<body className="min-h-full flex flex-col antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+260
-54
@@ -1,65 +1,271 @@
|
||||
import Image from "next/image";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { menuData, MenuItem as MenuItemType } from "@/data/menu";
|
||||
import { CategoryNav } from "@/components/CategoryNav";
|
||||
import { MenuItem } from "@/components/MenuItem";
|
||||
|
||||
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": "✨",
|
||||
};
|
||||
|
||||
export default function MenuPage() {
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string>(
|
||||
menuData[0]?.id || ""
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<MenuItemType | null>(null);
|
||||
const sectionRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
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]);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
<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"
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
|
||||
{/* 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)" }}
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
Akyaka · Muğla
|
||||
</p>
|
||||
|
||||
<h1
|
||||
className="font-display text-[82px] font-light leading-none tracking-tight"
|
||||
style={{ color: "#FFFAF2" }}
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
Moy
|
||||
</h1>
|
||||
|
||||
<h2
|
||||
className="font-display text-[28px] font-light tracking-[0.22em] uppercase"
|
||||
style={{ color: "rgba(255,200,120,0.85)", marginTop: "-2px" }}
|
||||
>
|
||||
Beach
|
||||
</h2>
|
||||
|
||||
<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)" }}
|
||||
>
|
||||
Akyaka'nın en keyifli menüsü
|
||||
</p>
|
||||
</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={menuData}
|
||||
activeCategoryId={activeCategoryId}
|
||||
icons={CATEGORY_ICONS}
|
||||
/>
|
||||
|
||||
{/* ── Menu Sections ────────────────────────────── */}
|
||||
<main className="pb-28">
|
||||
{menuData.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"
|
||||
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">
|
||||
<span
|
||||
className="font-display text-3xl font-light italic"
|
||||
style={{ color: "rgba(255,220,160,0.85)" }}
|
||||
>
|
||||
Moy Beach
|
||||
</span>
|
||||
<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 Moy Beach. Tüm hakları saklıdır.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
</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)}
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<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-56 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { MenuCategory } from "@/data/menu";
|
||||
|
||||
interface CategoryNavProps {
|
||||
categories: MenuCategory[];
|
||||
activeCategoryId: string;
|
||||
icons: Record<string, string>;
|
||||
}
|
||||
|
||||
export function CategoryNav({ categories, activeCategoryId, icons }: CategoryNavProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeCategoryId || !containerRef.current) return;
|
||||
const el = containerRef.current.querySelector(
|
||||
`[data-id="${activeCategoryId}"]`
|
||||
) as HTMLElement | null;
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||||
}, [activeCategoryId]);
|
||||
|
||||
const scrollTo = (id: string) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const y = el.getBoundingClientRect().top + window.scrollY - 72;
|
||||
window.scrollTo({ top: y, behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sticky top-0 z-50 border-b"
|
||||
style={{
|
||||
background: "rgba(250, 248, 243, 0.93)",
|
||||
backdropFilter: "blur(18px)",
|
||||
WebkitBackdropFilter: "blur(18px)",
|
||||
borderColor: "rgba(140, 108, 72, 0.12)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex overflow-x-auto hide-scrollbar px-3 py-2 gap-1.5 items-center"
|
||||
>
|
||||
{categories.map((cat) => {
|
||||
const active = cat.id === activeCategoryId;
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
data-id={cat.id}
|
||||
onClick={() => scrollTo(cat.id)}
|
||||
className={`
|
||||
flex items-center gap-1.5 whitespace-nowrap px-3 py-1.5 rounded-full
|
||||
text-[11px] font-medium font-sans shrink-0
|
||||
transition-all duration-200 active:scale-95
|
||||
${active ? "pill-active" : "pill-inactive hover:bg-[rgba(120,90,58,0.14)]"}
|
||||
`}
|
||||
>
|
||||
<span className="text-[13px] leading-none select-none">
|
||||
{icons[cat.id] ?? "🍽️"}
|
||||
</span>
|
||||
<span>{cat.title}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { MenuItem as MenuItemType } from "@/data/menu";
|
||||
|
||||
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
|
||||
return (
|
||||
<div
|
||||
className="py-3.5 border-b last:border-b-0 cursor-pointer transition-colors hover:bg-black/5"
|
||||
style={{ borderColor: "rgba(140, 108, 72, 0.08)" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3
|
||||
className="text-[13.5px] font-semibold leading-snug font-sans"
|
||||
style={{ color: "var(--stone-ink)" }}
|
||||
>
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.description && (
|
||||
<p
|
||||
className="mt-0.5 text-[11.5px] leading-relaxed font-sans line-clamp-2"
|
||||
style={{ color: "var(--stone-muted)" }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
{item.price && (
|
||||
<div
|
||||
className="mt-1 text-[13px] font-semibold font-sans tabular-nums"
|
||||
style={{ color: "var(--amber)" }}
|
||||
>
|
||||
{item.price}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.image && (
|
||||
<div className="shrink-0">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="w-20 h-20 object-cover rounded-lg shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1054
File diff suppressed because it is too large
Load Diff
+45
@@ -0,0 +1,45 @@
|
||||
# Moy Beach Akyaka — QR Dijital Menü Yenileme Prompt'u
|
||||
|
||||
## Proje Tanımı
|
||||
|
||||
**URL:** menu.moybeachakyaka.com
|
||||
**Tip:** QR kod ile masadan erişilen tek sayfalık dijital menü
|
||||
**Dil:** Türkçe + İngilizce
|
||||
**Kullanıcı:** Masada oturan misafir, telefonuyla QR okutarak erişiyor
|
||||
|
||||
Mevcut QR menü sitesini kullanım kolaylığı, görsel kalite ve marka tutarlılığı
|
||||
açısından yenile. Site bir e-ticaret ya da rezervasyon platformu değil;
|
||||
**sadece menüyü hızlı ve rahat gösteren bir dijital liste.**
|
||||
|
||||
---
|
||||
|
||||
## Mevcut Sorunlar
|
||||
|
||||
**Navigasyon**
|
||||
- Kategori yok, kullanıcı sayfayı sonuna kadar kaydırmak zorunda
|
||||
- Telefonda çok uzun bir scroll deneyimi oluşuyor
|
||||
|
||||
**Görsel**
|
||||
- Hiçbir üründe fotoğraf yok, hepsi aynı generik çatal-kaşık ikonu gösteriyor
|
||||
- Ürünler birbirinden görsel olarak ayrışmıyor
|
||||
|
||||
**İçerik Tutarsızlıkları**
|
||||
- Bazı ürün açıklamaları İngilizce kalmış
|
||||
(örn. Roka Salatası: *"Pink Tomatoes, Arugula, Parmesan and Balsamic Glaze"*)
|
||||
- Şişe içeceklerin tamamında fiyat **₺0,00** görünüyor
|
||||
- Stella Artois birasının açıklaması hiç yok
|
||||
|
||||
**Mobil Deneyim**
|
||||
- 3'lü grid layout dar telefonlarda okunaksız hale geliyor
|
||||
- Ürün adları ve açıklamalar kırpılıyor (`...` ile bitiyor), tam metin görülemiyor
|
||||
|
||||
---
|
||||
|
||||
## İstenen İyileştirmeler
|
||||
|
||||
### 1. Kategori Navigasyonu
|
||||
|
||||
Sayfanın üstüne sabit (sticky) yatay kaydırılabilir bir kategori çubuğu ekle.
|
||||
Tıklandığında ilgili bölüme smooth scroll ile git.
|
||||
|
||||
Kategoriler (mevcut sırayla):
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
/* other config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+53
@@ -8,6 +8,8 @@
|
||||
"name": "moy-qr",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.40.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
@@ -3762,6 +3764,33 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.40.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
|
||||
"integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.40.0",
|
||||
"motion-utils": "^12.39.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -5032,6 +5061,15 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
|
||||
"integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -5099,6 +5137,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.40.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
|
||||
"integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.39.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.39.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz",
|
||||
"integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.40.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 919 KiB |
Reference in New Issue
Block a user