first commit
This commit is contained in:
@@ -42,3 +42,5 @@ next-env.d.ts
|
||||
|
||||
# brainstorming sessions
|
||||
.superpowers/
|
||||
|
||||
/app/generated/prisma
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
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).
|
||||
# Kozmos QR Menu
|
||||
|
||||
## Getting Started
|
||||
Modern, minimalist ve çok dilli (TR/EN) QR Menü Uygulaması.
|
||||
|
||||
First, run the development server:
|
||||
## Özellikler
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
- **Çift Dil Desteği (i18n):** Türkçe ve İngilizce menü seçenekleri
|
||||
- **Admin Paneli:** Ürün, kategori ekleme/silme ve sıralama (Sürükle-bırak benzeri butonlar ile)
|
||||
- **Minimalist UI Tasarımı:** Kullanıcı dostu, modern arayüz
|
||||
- **PostgreSQL & Prisma:** Güvenli ve performanslı veri yönetimi
|
||||
- **Resim Yükleme:** Admin paneli üzerinden ürünlere görsel ekleyebilme
|
||||
- **Responsive:** Tüm mobil cihazlar ile tam uyumlu görünüm
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
## Kurulum
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
1. Depoyu klonlayın ve bağımlılıkları yükleyin:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
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.
|
||||
2. `.env` ve `.env.local` dosyalarınızı ayarlayın (Veritabanı bağlantısı vb.).
|
||||
|
||||
## Learn More
|
||||
3. Veritabanını güncelleyin ve istemciyi oluşturun:
|
||||
```bash
|
||||
npx prisma db push
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
4. Geliştirme sunucusunu başlatın:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- [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.
|
||||
Uygulamaya `http://localhost:3000` adresinden ulaşabilirsiniz.
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getDictionary, hasLocale } from './dictionaries'
|
||||
import { categories } from '@/data/menu'
|
||||
import { getCategories } from '@/data/menu'
|
||||
import Header from '@/components/Header'
|
||||
import CategorySection from '@/components/CategorySection'
|
||||
import BottomTabBar from '@/components/BottomTabBar'
|
||||
@@ -15,6 +15,7 @@ export default async function MenuPage({
|
||||
if (!hasLocale(lang)) notFound()
|
||||
|
||||
const dict = await getDictionary(lang)
|
||||
const categories = await getCategories()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function login(prevState: any, formData: FormData) {
|
||||
const username = formData.get('username')
|
||||
const password = formData.get('password')
|
||||
|
||||
if (
|
||||
username === process.env.ADMIN_USERNAME &&
|
||||
password === process.env.ADMIN_PASSWORD
|
||||
) {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('admin_session', process.env.ADMIN_SECRET || 'secret', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7 // 1 week
|
||||
})
|
||||
|
||||
redirect('/admin')
|
||||
} else {
|
||||
return { error: 'Geçersiz kullanıcı adı veya şifre' }
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('admin_session')
|
||||
redirect('/admin/login')
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use server'
|
||||
|
||||
import { saveCategories, type Category } from '@/data/menu'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
async function checkAuth() {
|
||||
const cookieStore = await cookies()
|
||||
const session = cookieStore.get('admin_session')?.value
|
||||
if (!session || session !== (process.env.ADMIN_SECRET || 'secret')) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveEntireMenu(categories: Category[]) {
|
||||
await checkAuth()
|
||||
await saveCategories(categories)
|
||||
// Revalidate both language paths
|
||||
revalidatePath('/tr')
|
||||
revalidatePath('/en')
|
||||
return { success: true }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
'use server'
|
||||
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function uploadImage(formData: FormData) {
|
||||
const cookieStore = await cookies()
|
||||
const session = cookieStore.get('admin_session')?.value
|
||||
const secret = process.env.ADMIN_SECRET || 'secret'
|
||||
|
||||
if (!session || session !== secret) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const file = formData.get('file') as File
|
||||
if (!file) {
|
||||
throw new Error('No file uploaded')
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
const extension = path.extname(file.name) || '.jpg'
|
||||
const filename = `${Date.now()}-${Math.random().toString(36).substring(7)}${extension}`
|
||||
|
||||
const uploadDir = path.join(process.cwd(), 'public', 'uploads')
|
||||
await fs.mkdir(uploadDir, { recursive: true })
|
||||
|
||||
const filePath = path.join(uploadDir, filename)
|
||||
await fs.writeFile(filePath, buffer)
|
||||
|
||||
return { url: `/uploads/${filename}` }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { logout } from '@/app/actions/auth'
|
||||
|
||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-cream flex flex-col" style={{ backgroundColor: '#F5F0E8' }}>
|
||||
<header className="bg-coffee-dark px-6 py-4 flex justify-between items-center" style={{ backgroundColor: '#3D2B1F' }}>
|
||||
<h1 className="text-xl font-display font-bold text-cream" style={{ color: '#F5F0E8' }}>Kozmos Yönetim</h1>
|
||||
|
||||
<form action={logout}>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 rounded bg-cream/10 text-cream text-sm font-medium hover:bg-cream/20 transition-colors"
|
||||
>
|
||||
Çıkış Yap
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 max-w-5xl mx-auto w-full">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { login } from '@/app/actions/auth'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [state, formAction, isPending] = useActionState(login, null)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-cream px-4" style={{ backgroundColor: '#F5F0E8' }}>
|
||||
<div className="w-full max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div className="bg-coffee-dark px-6 py-8 text-center" style={{ backgroundColor: '#3D2B1F' }}>
|
||||
<h1 className="text-2xl font-display font-bold text-cream" style={{ color: '#F5F0E8' }}>Kozmos Admin</h1>
|
||||
<p className="text-cream/70 text-sm mt-2">Yönetim paneline giriş yapın</p>
|
||||
</div>
|
||||
|
||||
<form action={formAction} className="px-6 py-8 space-y-5">
|
||||
{state?.error && (
|
||||
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm font-medium border border-red-100">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-coffee mb-1.5">Kullanıcı Adı</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-beige/60 bg-cream/30 focus:outline-none focus:ring-2 focus:ring-teal/50"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-coffee mb-1.5">Şifre</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-beige/60 bg-cream/30 focus:outline-none focus:ring-2 focus:ring-teal/50"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full py-3 rounded-lg text-white font-medium transition-all"
|
||||
style={{
|
||||
backgroundColor: '#4AAFA8',
|
||||
opacity: isPending ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
{isPending ? 'Giriş yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getCategories } from '@/data/menu'
|
||||
import AdminDashboard from '@/components/admin/AdminDashboard'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function AdminPage() {
|
||||
const categories = await getCategories()
|
||||
|
||||
return <AdminDashboard initialCategories={categories} />
|
||||
}
|
||||
@@ -11,25 +11,17 @@ export default function CategorySection({ category, lang, currency }: Props) {
|
||||
const l = lang as 'tr' | 'en'
|
||||
|
||||
return (
|
||||
<section id={category.id} className="category-section category-reveal px-4 pt-9 pb-1">
|
||||
<section id={category.id} className="category-section category-reveal px-4 pt-12 pb-2">
|
||||
{/* Category header */}
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{/* Emoji badge with gradient ring */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
{/* Subtle minimal Emoji badge */}
|
||||
<div className="relative flex-shrink-0">
|
||||
{/* Outer glow ring */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-2xl blur-sm opacity-60"
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center text-2xl shadow-sm"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #4AAFA8, #5C8A5C)',
|
||||
transform: 'scale(1.15)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="relative w-11 h-11 rounded-2xl flex items-center justify-center text-xl"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(74,175,168,0.18), rgba(92,138,92,0.12))',
|
||||
border: '1px solid rgba(74,175,168,0.30)',
|
||||
background: '#FDFCF8',
|
||||
border: '1px solid rgba(212,200,176,0.40)',
|
||||
}}
|
||||
>
|
||||
{category.emoji}
|
||||
@@ -37,75 +29,43 @@ export default function CategorySection({ category, lang, currency }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||
<h2
|
||||
className="font-display font-semibold italic text-coffee leading-tight"
|
||||
style={{ fontSize: '1.2rem' }}
|
||||
className="font-display font-semibold text-coffee uppercase tracking-wide leading-tight"
|
||||
style={{ fontSize: '1.25rem' }}
|
||||
>
|
||||
{category.label[l]}
|
||||
</h2>
|
||||
{/* Item count badge */}
|
||||
<span
|
||||
className="font-body text-[0.65rem] font-semibold tracking-widest text-muted mt-0.5"
|
||||
>
|
||||
({category.items.length})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Item count badge */}
|
||||
<span
|
||||
className="flex-shrink-0 font-body text-[0.6rem] font-semibold tracking-widest uppercase rounded-full px-2.5 py-0.5"
|
||||
style={{
|
||||
color: '#8A7265',
|
||||
background: 'rgba(61,43,31,0.07)',
|
||||
border: '1px solid rgba(61,43,31,0.10)',
|
||||
}}
|
||||
>
|
||||
{category.items.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Decorative divider */}
|
||||
<div className="flex items-center gap-2 ml-1">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-teal opacity-70" />
|
||||
<div
|
||||
className="flex-1 h-px"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(74,175,168,0.40) 0%, rgba(212,200,176,0.40) 60%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
<div className="w-1 h-1 rounded-full bg-gold opacity-50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items card */}
|
||||
<div
|
||||
className="rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
background: 'rgba(253,250,245,0.72)',
|
||||
border: '1px solid rgba(212,200,176,0.60)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
WebkitBackdropFilter: 'blur(8px)',
|
||||
boxShadow:
|
||||
'0 2px 16px rgba(61,43,31,0.06), 0 1px 3px rgba(61,43,31,0.04)',
|
||||
}}
|
||||
>
|
||||
{/* Thin top gradient line */}
|
||||
{/* Minimalist divider */}
|
||||
<div
|
||||
className="h-px w-full"
|
||||
className="w-full h-px"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(90deg, transparent, rgba(74,175,168,0.35), transparent)',
|
||||
background: 'linear-gradient(90deg, rgba(61,43,31,0.15) 0%, transparent 100%)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-1 pt-0.5">
|
||||
{category.items.map((item, i) => (
|
||||
<MenuItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
lang={lang}
|
||||
currency={currency}
|
||||
index={i}
|
||||
isLast={i === category.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Items list (no background card) */}
|
||||
<div className="flex flex-col gap-1">
|
||||
{category.items.map((item, i) => (
|
||||
<MenuItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
lang={lang}
|
||||
currency={currency}
|
||||
index={i}
|
||||
isLast={i === category.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
+34
-26
@@ -13,48 +13,56 @@ export default function MenuItemRow({ item, lang, currency, index, isLast }: Pro
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`menu-item-row flex items-center gap-3.5 py-3.5 rounded-xl px-2 -mx-2 ${
|
||||
!isLast ? 'border-b border-dashed border-beige' : ''
|
||||
className={`menu-item-row flex items-start gap-4 py-4 rounded-xl px-2 -mx-2 transition-colors ${
|
||||
!isLast ? 'border-b border-[rgba(61,43,31,0.06)]' : ''
|
||||
}`}
|
||||
style={{ '--item-index': index } as React.CSSProperties}
|
||||
>
|
||||
{/* Emoji circle with gradient */}
|
||||
{/* Image or Emoji circle */}
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center flex-shrink-0 text-[1.2rem]"
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center flex-shrink-0 text-3xl overflow-hidden shadow-sm mt-0.5"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, #F0E8D8 0%, #E8DCC8 100%)',
|
||||
border: '1px solid rgba(212,200,176,0.80)',
|
||||
boxShadow: 'inset 0 1px 2px rgba(255,255,255,0.8)',
|
||||
background: '#FDFCF8',
|
||||
border: '1px solid rgba(212,200,176,0.30)',
|
||||
}}
|
||||
>
|
||||
{item.emoji || '✨'}
|
||||
{item.image ? (
|
||||
<img src={item.image} alt={item.name[l]} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
item.emoji || '✨'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className="font-display text-coffee font-semibold leading-snug"
|
||||
style={{ fontSize: '0.9rem' }}
|
||||
>
|
||||
{item.name[l]}
|
||||
</p>
|
||||
{/* Text Content */}
|
||||
<div className="flex-1 min-w-0 pt-0.5">
|
||||
<div className="flex justify-between items-start gap-2">
|
||||
<h3
|
||||
className="font-display text-coffee font-semibold leading-tight mb-1"
|
||||
style={{ fontSize: '1.05rem' }}
|
||||
>
|
||||
{item.name[l]}
|
||||
</h3>
|
||||
|
||||
{/* Price */}
|
||||
{item.price !== undefined && (
|
||||
<span
|
||||
className="flex-shrink-0 font-medium text-coffee mt-0.5"
|
||||
style={{ fontSize: '1rem' }}
|
||||
>
|
||||
{item.price} {currency}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{item.description && (
|
||||
<p
|
||||
className="font-body text-muted leading-relaxed mt-0.5 line-clamp-2"
|
||||
style={{ fontSize: '0.72rem' }}
|
||||
className="font-body text-muted leading-relaxed line-clamp-2"
|
||||
style={{ fontSize: '0.85rem' }}
|
||||
>
|
||||
{item.description[l]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price pill */}
|
||||
{item.price !== undefined && (
|
||||
<span className="price-pill flex-shrink-0">
|
||||
{currency}{item.price}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useTransition, useEffect } from 'react'
|
||||
import type { Category, MenuItem } from '@/data/menu'
|
||||
import { saveEntireMenu } from '@/app/actions/menu'
|
||||
import { uploadImage } from '@/app/actions/upload'
|
||||
|
||||
export default function AdminDashboard({ initialCategories }: { initialCategories: Category[] }) {
|
||||
const [categories, setCategories] = useState<Category[]>(initialCategories)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [isSaved, setIsSaved] = useState(false)
|
||||
const [selectedCatId, setSelectedCatId] = useState<string | null>(initialCategories[0]?.id || null)
|
||||
|
||||
// Auto-select first category if selected one is deleted
|
||||
useEffect(() => {
|
||||
if (selectedCatId && !categories.find(c => c.id === selectedCatId) && categories.length > 0) {
|
||||
setSelectedCatId(categories[0].id)
|
||||
}
|
||||
}, [categories, selectedCatId])
|
||||
|
||||
const handleSave = () => {
|
||||
startTransition(async () => {
|
||||
await saveEntireMenu(categories)
|
||||
setIsSaved(true)
|
||||
setTimeout(() => setIsSaved(false), 3000)
|
||||
})
|
||||
}
|
||||
|
||||
const handleImageUpload = async (catId: string, itemId: string, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
const res = await uploadImage(formData)
|
||||
if (res?.url) {
|
||||
updateItem(catId, itemId, 'image', res.url)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert("Resim yüklenemedi.")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Item CRUD ---
|
||||
const updateItem = (catId: string, itemId: string, field: string, value: any, lang?: 'tr' | 'en') => {
|
||||
setCategories(cats => cats.map(cat => {
|
||||
if (cat.id !== catId) return cat
|
||||
return {
|
||||
...cat,
|
||||
items: cat.items.map(item => {
|
||||
if (item.id !== itemId) return item
|
||||
if (lang) {
|
||||
return { ...item, [field]: { ...(item as any)[field], [lang]: value } }
|
||||
}
|
||||
return { ...item, [field]: value }
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const addItem = (catId: string) => {
|
||||
const newItem: MenuItem = {
|
||||
id: `item-${Date.now()}`,
|
||||
name: { tr: 'Yeni Ürün', en: 'New Item' },
|
||||
description: { tr: '', en: '' },
|
||||
price: 0,
|
||||
highlight: false,
|
||||
}
|
||||
setCategories(cats => cats.map(cat => {
|
||||
if (cat.id !== catId) return cat
|
||||
return { ...cat, items: [...cat.items, newItem] }
|
||||
}))
|
||||
}
|
||||
|
||||
const deleteItem = (catId: string, itemId: string) => {
|
||||
if (!confirm('Bu ürünü silmek istediğinize emin misiniz?')) return
|
||||
setCategories(cats => cats.map(cat => {
|
||||
if (cat.id !== catId) return cat
|
||||
return { ...cat, items: cat.items.filter(item => item.id !== itemId) }
|
||||
}))
|
||||
}
|
||||
|
||||
const moveItem = (catId: string, itemIndex: number, direction: 'up' | 'down') => {
|
||||
setCategories(cats => cats.map(cat => {
|
||||
if (cat.id !== catId) return cat
|
||||
const newItems = [...cat.items]
|
||||
if (direction === 'up' && itemIndex > 0) {
|
||||
[newItems[itemIndex - 1], newItems[itemIndex]] = [newItems[itemIndex], newItems[itemIndex - 1]]
|
||||
} else if (direction === 'down' && itemIndex < newItems.length - 1) {
|
||||
[newItems[itemIndex + 1], newItems[itemIndex]] = [newItems[itemIndex], newItems[itemIndex + 1]]
|
||||
}
|
||||
return { ...cat, items: newItems }
|
||||
}))
|
||||
}
|
||||
|
||||
// --- Category CRUD ---
|
||||
const updateCategory = (catId: string, field: string, value: any, lang?: 'tr' | 'en') => {
|
||||
setCategories(cats => cats.map(cat => {
|
||||
if (cat.id !== catId) return cat
|
||||
if (lang && field === 'label') {
|
||||
return { ...cat, label: { ...cat.label, [lang]: value } }
|
||||
}
|
||||
return { ...cat, [field]: value }
|
||||
}))
|
||||
}
|
||||
|
||||
const addCategory = () => {
|
||||
const newCategory: Category = {
|
||||
id: `cat-${Date.now()}`,
|
||||
emoji: '🍽️',
|
||||
label: { tr: 'Yeni Kategori', en: 'New Category' },
|
||||
items: [],
|
||||
}
|
||||
setCategories(cats => [...cats, newCategory])
|
||||
setSelectedCatId(newCategory.id)
|
||||
}
|
||||
|
||||
const deleteCategory = (catId: string) => {
|
||||
if (!confirm('Bu kategoriyi ve içindeki tüm ürünleri silmek istediğinize emin misiniz?')) return
|
||||
setCategories(cats => cats.filter(cat => cat.id !== catId))
|
||||
}
|
||||
|
||||
const moveCategory = (catIndex: number, direction: 'up' | 'down') => {
|
||||
setCategories(cats => {
|
||||
const newCats = [...cats]
|
||||
if (direction === 'up' && catIndex > 0) {
|
||||
[newCats[catIndex - 1], newCats[catIndex]] = [newCats[catIndex], newCats[catIndex - 1]]
|
||||
} else if (direction === 'down' && catIndex < newCats.length - 1) {
|
||||
[newCats[catIndex + 1], newCats[catIndex]] = [newCats[catIndex], newCats[catIndex + 1]]
|
||||
}
|
||||
return newCats
|
||||
})
|
||||
}
|
||||
|
||||
const selectedCategory = categories.find(c => c.id === selectedCatId)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen max-h-screen bg-gray-50 pb-8">
|
||||
{/* Top Bar */}
|
||||
<div className="flex justify-between items-center bg-white p-4 shadow-sm z-10 border-b shrink-0">
|
||||
<h2 className="text-xl font-bold text-coffee" style={{ color: '#3D2B1F' }}>Menü Yönetimi</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isPending}
|
||||
className="px-6 py-2 rounded-lg text-white font-medium shadow-sm transition-all"
|
||||
style={{ backgroundColor: '#4AAFA8', opacity: isPending ? 0.7 : 1 }}
|
||||
>
|
||||
{isPending ? 'Kaydediliyor...' : isSaved ? '✓ Kaydedildi' : 'Değişiklikleri Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Left Sidebar (Categories) */}
|
||||
<div className="w-1/3 md:w-1/4 bg-white border-r border-gray-200 flex flex-col">
|
||||
<div className="p-4 border-b">
|
||||
<button
|
||||
onClick={addCategory}
|
||||
className="w-full py-2 rounded-lg text-emerald-700 bg-emerald-50 border border-emerald-200 font-medium hover:bg-emerald-100 transition-colors text-sm"
|
||||
>
|
||||
+ Yeni Kategori Ekle
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
{categories.map((cat, catIndex) => (
|
||||
<div
|
||||
key={cat.id}
|
||||
className={`flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors group ${
|
||||
selectedCatId === cat.id ? 'bg-emerald-50 border-emerald-200 border' : 'hover:bg-gray-100 border border-transparent'
|
||||
}`}
|
||||
onClick={() => setSelectedCatId(cat.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span className="text-xl">{cat.emoji}</span>
|
||||
<span className={`text-sm font-medium truncate ${selectedCatId === cat.id ? 'text-emerald-800' : 'text-gray-700'}`}>
|
||||
{cat.label.tr}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Category Actions (visible on hover or when selected) */}
|
||||
<div className={`flex items-center opacity-0 group-hover:opacity-100 ${selectedCatId === cat.id ? 'opacity-100' : ''}`}>
|
||||
<button onClick={(e) => { e.stopPropagation(); moveCategory(catIndex, 'up') }} disabled={catIndex === 0} className="p-1 hover:bg-white rounded text-gray-500 disabled:opacity-30">↑</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); moveCategory(catIndex, 'down') }} disabled={catIndex === categories.length - 1} className="p-1 hover:bg-white rounded text-gray-500 disabled:opacity-30">↓</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); deleteCategory(cat.id) }} className="p-1 hover:bg-red-100 text-red-500 rounded ml-1">Sil</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{categories.length === 0 && (
|
||||
<div className="text-center p-4 text-gray-400 text-sm">Kategori bulunamadı.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content (Items) */}
|
||||
<div className="flex-1 overflow-y-auto bg-gray-50 p-6">
|
||||
{selectedCategory ? (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Category Edit Header */}
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4 border-b pb-2">Kategori Ayarları</h3>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={selectedCategory.emoji}
|
||||
onChange={(e) => updateCategory(selectedCategory.id, 'emoji', e.target.value)}
|
||||
className="w-14 h-14 text-center text-3xl border border-gray-300 rounded-xl bg-gray-50"
|
||||
title="Kategori Emojisi"
|
||||
/>
|
||||
<span className="text-[10px] text-gray-400">Emoji</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col md:flex-row gap-4 w-full">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Türkçe İsim</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedCategory.label.tr}
|
||||
onChange={(e) => updateCategory(selectedCategory.id, 'label', e.target.value, 'tr')}
|
||||
className="w-full px-3 py-2 border rounded-md font-bold text-coffee bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">İngilizce İsim</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedCategory.label.en}
|
||||
onChange={(e) => updateCategory(selectedCategory.id, 'label', e.target.value, 'en')}
|
||||
className="w-full px-3 py-2 border rounded-md text-gray-600 bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-end">
|
||||
<h3 className="text-lg font-bold text-coffee">Ürünler ({selectedCategory.items.length})</h3>
|
||||
</div>
|
||||
|
||||
{selectedCategory.items.map((item, itemIndex) => (
|
||||
<div key={item.id} className="border border-gray-200 p-5 rounded-xl bg-white relative hover:border-gray-300 transition-colors shadow-sm">
|
||||
{/* Item Actions (Top Right) */}
|
||||
<div className="absolute top-3 right-3 flex items-center gap-1">
|
||||
<button onClick={() => moveItem(selectedCategory.id, itemIndex, 'up')} disabled={itemIndex === 0} className="p-1.5 bg-gray-100 rounded disabled:opacity-30 hover:bg-gray-200 text-xs">↑</button>
|
||||
<button onClick={() => moveItem(selectedCategory.id, itemIndex, 'down')} disabled={itemIndex === selectedCategory.items.length - 1} className="p-1.5 bg-gray-100 rounded disabled:opacity-30 hover:bg-gray-200 text-xs">↓</button>
|
||||
<button onClick={() => deleteItem(selectedCategory.id, item.id)} className="p-1.5 bg-red-50 text-red-500 rounded hover:bg-red-100 text-xs ml-1 font-medium">Sil</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
||||
{/* TR Fields */}
|
||||
<div className="space-y-3">
|
||||
<div className="font-semibold text-sm text-gray-700 mb-2 border-b pb-1">Türkçe (TR)</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">İsim</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.name.tr}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'name', e.target.value, 'tr')}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Açıklama</label>
|
||||
<textarea
|
||||
value={item.description?.tr || ''}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'description', e.target.value, 'tr')}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* EN Fields */}
|
||||
<div className="space-y-3">
|
||||
<div className="font-semibold text-sm text-gray-700 mb-2 border-b pb-1">İngilizce (EN)</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.name.en}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'name', e.target.value, 'en')}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Description</label>
|
||||
<textarea
|
||||
value={item.description?.en || ''}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'description', e.target.value, 'en')}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-5 pt-4 border-t border-gray-100">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Fiyat (₺)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={item.price || 0}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'price', Number(e.target.value))}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Emoji (Opsiyonel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.emoji || ''}
|
||||
onChange={(e) => updateItem(selectedCategory.id, item.id, 'emoji', e.target.value)}
|
||||
className="w-full text-sm px-3 py-2 border rounded-md bg-gray-50 focus:bg-white text-center text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Resim Ekle</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleImageUpload(selectedCategory.id, item.id, e)}
|
||||
className="w-full text-xs px-2 py-1.5 border rounded-md file:mr-2 file:py-1 file:px-2 file:rounded file:border-0 file:bg-emerald-50 file:text-emerald-700 file:text-xs"
|
||||
/>
|
||||
{item.image && (
|
||||
<div className="mt-2 flex items-center gap-2 bg-gray-50 p-1.5 rounded-md border">
|
||||
<img src={item.image} alt="" className="w-8 h-8 rounded object-cover" />
|
||||
<button onClick={() => updateItem(selectedCategory.id, item.id, 'image', undefined)} className="text-xs text-red-500 hover:underline px-2">Kaldır</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => addItem(selectedCategory.id)}
|
||||
className="w-full py-4 rounded-xl border-2 border-dashed border-gray-300 text-gray-500 font-medium hover:border-emerald-300 hover:text-emerald-600 transition-colors bg-white hover:bg-emerald-50 text-sm mt-4 shadow-sm"
|
||||
>
|
||||
+ Kategoriye Yeni Ürün Ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
Düzenlemek için sol taraftan bir kategori seçin.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1628
File diff suppressed because it is too large
Load Diff
+82
-316
@@ -1,8 +1,11 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export type LocalizedString = { tr: string; en: string }
|
||||
|
||||
export type MenuItem = {
|
||||
id: string
|
||||
emoji?: string
|
||||
image?: string
|
||||
name: LocalizedString
|
||||
description?: LocalizedString
|
||||
price?: number
|
||||
@@ -18,319 +21,82 @@ export type Category = {
|
||||
|
||||
export const WHATSAPP_NUMBER = '905XXXXXXXXXX'
|
||||
|
||||
export const categories: Category[] = [
|
||||
{
|
||||
id: 'kahvaltiliklar',
|
||||
emoji: '🍳',
|
||||
label: { tr: 'Kahvaltılıklar', en: 'Breakfasts' },
|
||||
items: [
|
||||
{ id: 'k1', name: { tr: 'Sucuklu Yumurta', en: 'Eggs with Sucuk' }, description: { tr: 'Kaşar peynirli, çıtır domates', en: 'Kaşar cheese, crispy tomato' } },
|
||||
{ id: 'k2', name: { tr: 'Menemen', en: 'Menemen' }, description: { tr: 'Domates, yeşil biber, 2 yumurta', en: 'Tomato, green pepper, 2 eggs' } },
|
||||
{ id: 'k3', name: { tr: 'Kaşarlı Menemen', en: 'Menemen with Kaşar Cheese' }, description: { tr: 'Rondanın 5 kaşar peyniri, domates, yeşil biber, çeşitli 2 yumurta', en: '5 portions of kaşar cheese, tomato, green pepper, 2 eggs' } },
|
||||
{ id: 'k4', name: { tr: 'Sade Omlet', en: 'Plain Omelette' }, description: { tr: 'Çeşitme 2 yumurta, mezelemeniz kaşar peyniri', en: '2 eggs, kaşar cheese' } },
|
||||
{ id: 'k5', name: { tr: 'Mantarlı Omlet', en: 'Mushroom Omelette' }, description: { tr: 'Mantarlarla zenginleştirilmiş kaşar peyniri, çıtır biber, çimento, yeşil biber, satır, mantarı', en: 'Mushrooms, kaşar cheese, crispy peppers' } },
|
||||
{ id: 'k6', name: { tr: 'Sebzeli Omlet', en: 'Vegetable Omelette' }, description: { tr: 'Mantarlar, zenginleştirilmiş kaşar peyniri, kapıya biber, çimento, yeşil biber ve saman mantarı', en: 'Mushrooms, kaşar cheese, capia pepper, green pepper' } },
|
||||
{ id: 'k7', name: { tr: 'Vegan Kahvaltı', en: 'Vegan Breakfast' }, description: { tr: 'Avokado, çeşit domatesi, zeytin, tost, roka, havuç', en: 'Avocado, cherry tomato, olives, toast, arugula, carrot' } },
|
||||
{ id: 'k8', name: { tr: 'Serpme Kahvaltı – 2 Kişilik', en: 'Mixed Breakfast - For 2' }, description: { tr: 'Kaşar peyniri, bal & kaymak, siyah zeytin, yeşil zeytin, sürüm, hinse türreyi, çeşit domatesi, erikçimler, tatlıpırçık, havuç, maydanoz ve sonsuz çay', en: 'Kaşar cheese, honey & clotted cream, black & green olives, cherry tomato, jams, carrot, parsley and unlimited tea' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'burgerler',
|
||||
emoji: '🍔',
|
||||
label: { tr: 'Burgerler', en: 'Burgers' },
|
||||
items: [
|
||||
{ id: 'b1', name: { tr: 'Kids Smash Burger', en: 'Kids Smash Burger' }, description: { tr: 'Kids Smash burger, cheddar peyniri, çıtır patates, kızartılmış turşu, domates, marul, burger sosu, patates cipsi', en: 'Kids Smash burger, cheddar cheese, crispy fries, fried pickles, tomato, lettuce, burger sauce, potato chips' } },
|
||||
{ id: 'b2', name: { tr: 'Dana Smash Burger', en: 'Beef Smash Burger' }, description: { tr: 'Dana Smash burger, cheddar peyniri, karamelize soğan, turşu, domates, marul, burger sosu, patates cipsi', en: 'Beef Smash burger, cheddar cheese, caramelized onions, pickles, tomato, lettuce, burger sauce, potato chips' } },
|
||||
{ id: 'b3', name: { tr: 'Dana Smash Big Burger', en: 'Beef Smash Big Burger' }, description: { tr: 'Dana Smash burger smash kafası, cheddar peyniri, karamelize soğan, çıtır patates, kızartılmış turşu, domates, marul, burger sosu, patates cipsi', en: 'Beef Smash big burger, cheddar cheese, caramelized onions, crispy fries, fried pickles, tomato, lettuce, burger sauce, potato chips' } },
|
||||
{ id: 'b4', name: { tr: 'Çıtır Tavuk Burger', en: 'Crispy Chicken Burger' }, description: { tr: 'Çıtır Tavuk Burger harika cheddar peyniri, burger sos, patates cipsi', en: 'Crispy Chicken Burger, cheddar cheese, burger sauce, potato chips' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tatlilar',
|
||||
emoji: '🍮',
|
||||
label: { tr: 'Tatlılar', en: 'Desserts' },
|
||||
items: [
|
||||
{ id: 't1', name: { tr: 'Meşhur Akyaka Tatlısı', en: 'Famous Akyaka Dessert' }, description: { tr: 'Deniz tuzlu, bürüme, bitter çikolata, Gökova sütlemi', en: 'Sea salt, bitter chocolate, Gökova milk' } },
|
||||
{ id: 't2', name: { tr: 'Fırın Helva', en: 'Baked Halva' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tostlar',
|
||||
emoji: '🥪',
|
||||
label: { tr: 'Tostlar', en: 'Toasts' },
|
||||
items: [
|
||||
{ id: 'ts1', name: { tr: 'Peynirli Tost', en: 'Cheese Toast' }, description: { tr: 'Kaşar peyniri, çeşit domatesi, zeytin, patates cipsi', en: 'Kaşar cheese, cherry tomato, olives, potato chips' } },
|
||||
{ id: 'ts2', name: { tr: 'Sucuklu Tost', en: 'Sucuk Toast' }, description: { tr: 'Kaşar sucuklu, çeşit domatesi, zeytin, patates cipsi', en: 'Sucuk, kaşar cheese, cherry tomato, olives, potato chips' } },
|
||||
{ id: 'ts3', name: { tr: 'Akdeniz Tost', en: 'Mediterranean Toast' }, description: { tr: 'Kaşar pul, Ezine peyniri, tulum peyniri, kaşar domates, çeşit domatesi, zeytin, patates cipsi', en: 'Ezine cheese, tulum cheese, kaşar cheese, cherry tomato, olives, potato chips' } },
|
||||
{ id: 'ts4', name: { tr: 'Karışık Tost', en: 'Mixed Toast' }, description: { tr: 'Kaşar sucuklu, kaşar peyniri, peri domates, zeytin, salatalık, dilimlen', en: 'Sucuk, kaşar cheese, cherry tomato, olives, cucumber slices' } },
|
||||
{ id: 'ts5', name: { tr: 'Ayvalık Tostu', en: 'Ayvalik Toast' }, description: { tr: 'Ekşi, karamize turşu, kırmızı bilet kaşar peyniri, çeşit domatesi, salatalık, dilimlen', en: 'Sourdough bread, caramelized pickles, red pepper kaşar cheese, cherry tomato, cucumber slices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'salatalar',
|
||||
emoji: '🥗',
|
||||
label: { tr: 'Salatalar', en: 'Salads' },
|
||||
items: [
|
||||
{ id: 'sl1', name: { tr: 'Çoban Salata', en: "Shepherd's Salad" } },
|
||||
{ id: 'sl2', name: { tr: 'Roka Salatası', en: 'Arugula Salad' }, description: { tr: 'Domates, zenginleştirilmiş üzüme bol peyniri', en: 'Tomato, grapes, cheese' } },
|
||||
{ id: 'sl3', name: { tr: 'Yaz Salatası', en: 'Summer Salad' }, description: { tr: 'Kaşar ve her, taze domates, kırmızı soğan, beyaz peynir, fasulye, çeşitli yeşil besinleri, dalya üzümleri, çeşit domatesi', en: 'Fresh tomato, red onion, white cheese, beans, mixed greens, grapes, cherry tomato' } },
|
||||
{ id: 'sl4', name: { tr: 'Avokado Salatası', en: 'Avocado Salad' }, description: { tr: 'Avokado, çeşit domatesi, salatalık, yeşil zeytin', en: 'Avocado, cherry tomato, cucumber, green olives' } },
|
||||
{ id: 'sl5', name: { tr: 'Ton Balıklı Kinoalı Salata', en: 'Tuna & Quinoa Salad' }, description: { tr: 'Siyah kinoa, ton balığı, maydanoz, çeşit domatesi, salatalık, dilimlen', en: 'Black quinoa, tuna fish, parsley, cherry tomato, cucumber slices' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pizzalar',
|
||||
emoji: '🍕',
|
||||
label: { tr: 'Pizzalar', en: 'Pizzas' },
|
||||
items: [
|
||||
{ id: 'pz1', name: { tr: 'Margherita Pizza', en: 'Margherita Pizza' }, description: { tr: 'Pizza sosu, mozzarella peyniri, fesleğen, zeytin makul', en: 'Pizza sauce, mozzarella cheese, basil, olives' } },
|
||||
{ id: 'pz2', name: { tr: 'Akdeniz Pizza', en: 'Mediterranean Pizza' }, description: { tr: 'Kaşar peyniri, yeşil biber, mantar, fesleğen, mozzarella peyniri, piliç sosu', en: 'Kaşar cheese, green pepper, mushroom, basil, mozzarella cheese' } },
|
||||
{ id: 'pz3', name: { tr: 'Sucuk Pizza', en: 'Sucuk Pizza' }, description: { tr: 'Yeşil biber, domates biber, mantar, kaşar', en: 'Green pepper, tomato, pepper, mushroom, kaşar' } },
|
||||
{ id: 'pz4', name: { tr: 'Enginar lı Pizza', en: 'Artichoke Pizza' }, description: { tr: 'Belirli peşik, fesleğen, mozzarella peyniri, domates sosu', en: 'Artichoke, basil, mozzarella cheese, tomato sauce' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wraps',
|
||||
emoji: '🌯',
|
||||
label: { tr: 'Wraps', en: 'Wraps' },
|
||||
items: [
|
||||
{ id: 'w1', name: { tr: 'Sebzeli Wrap', en: 'Veggie Wrap' }, description: { tr: 'Mantar, toflu peynirli, turşu, soğan, samon biber, yeşil biber, kaşar biber', en: 'Mushroom, tofu cheese, pickles, onions, yellow pepper, green pepper, red pepper' } },
|
||||
{ id: 'w2', name: { tr: 'Tavuklu Wrap', en: 'Chicken Wrap' }, description: { tr: 'Salatalık doğası, turşu soğan, kısma biber, marul, kon türfü turşu', en: 'Cucumber, pickled onions, red pepper, lettuce, pickles' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mantilar',
|
||||
emoji: '🥟',
|
||||
label: { tr: 'Mantılar', en: 'Manti & Ravioli' },
|
||||
items: [
|
||||
{ id: 'mt1', name: { tr: 'Ev Yapımı Mantı', en: 'Homemade Manti' }, description: { tr: 'Yoğurt ve sas eşliğince', en: 'Served with yogurt and sauce' } },
|
||||
{ id: 'mt2', name: { tr: 'Cevizli Mantı', en: 'Walnut Manti' }, description: { tr: 'Yoğurt ve sas eşliğince', en: 'Served with yogurt and sauce' } },
|
||||
{ id: 'mt3', name: { tr: 'Akdeniz Mantısı', en: 'Mediterranean Manti' }, description: { tr: 'Fesleğen, maydanoz, cziz, yoğurtlu sos', en: 'Basil, parsley, walnuts, yogurt sauce' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'makarnalar',
|
||||
emoji: '🍝',
|
||||
label: { tr: 'Makarnalar', en: 'Pastas' },
|
||||
items: [
|
||||
{ id: 'mk1', name: { tr: 'Penne Arrabbiata', en: 'Penne Arrabbiata' } },
|
||||
{ id: 'mk2', name: { tr: 'Spaghetti al Pesto', en: 'Spaghetti al Pesto' } },
|
||||
{ id: 'mk3', name: { tr: 'Fettuccine Alfredo', en: 'Fettuccine Alfredo' } },
|
||||
{ id: 'mk4', name: { tr: 'Spagetti Bolonez', en: 'Spaghetti Bolognese' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ana-yemekler',
|
||||
emoji: '🍗',
|
||||
label: { tr: 'Ana Yemekler', en: 'Main Courses' },
|
||||
items: [
|
||||
{ id: 'ay1', name: { tr: 'Tavuk Izgara', en: 'Grilled Chicken' }, description: { tr: 'Tavuk göğsü, karamelize yeşil biber, soğan, yeşillik, domatesli cipsi', en: 'Chicken breast, caramelized green pepper, onions, greens, potato chips' } },
|
||||
{ id: 'ay2', name: { tr: 'Tavuk Pirzola', en: 'Chicken Chops' }, description: { tr: 'Kalemime yeşil biber, soğan, yeşil fık, patates cipsi', en: 'Green pepper, onions, greens, potato chips' } },
|
||||
{ id: 'ay3', name: { tr: 'Köfte', en: 'Meatballs' }, description: { tr: 'Maydanoz, yeşil biber, soğan, yeşillik, patlıcan cipsi', en: 'Parsley, green pepper, onions, greens, eggplant chips' } },
|
||||
{ id: 'ay4', name: { tr: 'Karışık Izgara', en: 'Mixed Grill' }, description: { tr: 'Köfte, tavuk göğsü, kısık pırzola. Kalemima yeşil biber, soğan, yeşillik, patlıcan cipsi', en: 'Meatballs, chicken breast, chicken chops. Green pepper, onions, greens, eggplant chips' } },
|
||||
{ id: 'ay5', name: { tr: 'Sac Kavurma – 2 Kişilik', en: 'Sac Kavurma - For 2' }, description: { tr: 'Mevsim salatası ve patates cipsi eşliğinde', en: 'Served with seasonal salad and potato chips' } },
|
||||
{ id: 'ay6', name: { tr: 'Günlük Balık Çeşitleri', en: 'Daily Fish Catch' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'atistirmaliklar',
|
||||
emoji: '🍟',
|
||||
label: { tr: 'Atıştırmalıklar', en: 'Snacks' },
|
||||
items: [
|
||||
{ id: 'at1', name: { tr: 'Patates Cipsi', en: 'Potato Chips' } },
|
||||
{ id: 'at2', name: { tr: 'Çıtır Tavuk', en: 'Crispy Chicken' }, description: { tr: 'Çıtır tavuk ve patates cipsi', en: 'Crispy chicken and potato chips' } },
|
||||
{ id: 'at3', name: { tr: 'Peynir Tabağı', en: 'Cheese Platter' }, description: { tr: 'Ezine peyniri, tulum peyniri, ham, haşar, çeşit kaşar, kuru domates', en: 'Ezine cheese, tulum cheese, kaşar cheese, dried tomatoes' } },
|
||||
{ id: 'at4', name: { tr: 'Bira Tabağı', en: 'Beer Platter' }, description: { tr: 'Roka & patates cipsi, çıtır tavuk, çeşit kaşar', en: 'Arugula & potato chips, crispy chicken, cheese varieties' } },
|
||||
{ id: 'at5', name: { tr: 'Deniz Tabağı', en: 'Seafood Platter' }, description: { tr: 'Halk kalamar, çıtır karides, roka', en: 'Calamari rings, crispy shrimp, arugula' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sicak-icecekler',
|
||||
emoji: '☕',
|
||||
label: { tr: 'Sıcak İçecekler & Kahveler', en: 'Hot Drinks & Coffees' },
|
||||
items: [
|
||||
{ id: 'si1', name: { tr: 'Çay', en: 'Tea' } },
|
||||
{ id: 'si2', name: { tr: 'Türk Kahvesi', en: 'Turkish Coffee' } },
|
||||
{ id: 'si3', name: { tr: 'Duble Türk Kahvesi', en: 'Double Turkish Coffee' } },
|
||||
{ id: 'si4', name: { tr: 'Filtre Kahve', en: 'Filter Coffee' } },
|
||||
{ id: 'si5', name: { tr: 'Espresso', en: 'Espresso' } },
|
||||
{ id: 'si6', name: { tr: 'Americano', en: 'Americano' } },
|
||||
{ id: 'si7', name: { tr: 'Latte', en: 'Latte' } },
|
||||
{ id: 'si8', name: { tr: 'Cappuccino', en: 'Cappuccino' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'soguk-icecekler',
|
||||
emoji: '🥤',
|
||||
label: { tr: 'Soğuk İçecekler', en: 'Cold Drinks' },
|
||||
items: [
|
||||
{ id: 'so1', name: { tr: 'Su', en: 'Water' } },
|
||||
{ id: 'so2', name: { tr: 'Soda', en: 'Sparkling Water' } },
|
||||
{ id: 'so3', name: { tr: 'Meyveli Soda', en: 'Fruit Sparkling Water' } },
|
||||
{ id: 'so4', name: { tr: 'Ayran', en: 'Ayran' } },
|
||||
{ id: 'so5', name: { tr: 'Limonata', en: 'Lemonade' } },
|
||||
{ id: 'so6', name: { tr: 'Churchill', en: 'Churchill' } },
|
||||
{ id: 'so7', name: { tr: 'Coca-Cola / Fanta / Sprite', en: 'Coca-Cola / Fanta / Sprite' } },
|
||||
{ id: 'so8', name: { tr: 'Ice Tea', en: 'Ice Tea' } },
|
||||
{ id: 'so9', name: { tr: 'Red Bull', en: 'Red Bull' } },
|
||||
{ id: 'so10', name: { tr: 'Vişne Suyu', en: 'Cherry Juice' } },
|
||||
{ id: 'so11', name: { tr: 'Portakal Suyu', en: 'Orange Juice' } },
|
||||
{ id: 'so12', name: { tr: 'Nar Suyu', en: 'Pomegranate Juice' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'soguk-kahveler',
|
||||
emoji: '🧋',
|
||||
label: { tr: 'Soğuk Kahveler', en: 'Cold Coffees' },
|
||||
items: [
|
||||
{ id: 'sk1', name: { tr: 'Iced Americano', en: 'Iced Americano' } },
|
||||
{ id: 'sk2', name: { tr: 'Iced Latte', en: 'Iced Latte' } },
|
||||
{ id: 'sk3', name: { tr: 'Iced Mocha', en: 'Iced Mocha' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'frozen',
|
||||
emoji: '🧊',
|
||||
label: { tr: 'Frozen', en: 'Frozen' },
|
||||
items: [
|
||||
{ id: 'fz1', name: { tr: 'Çilek', en: 'Strawberry' } },
|
||||
{ id: 'fz2', name: { tr: 'Mango', en: 'Mango' } },
|
||||
{ id: 'fz3', name: { tr: 'Orman Meyvesi', en: 'Forest Fruits' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'milkshakes',
|
||||
emoji: '🥛',
|
||||
label: { tr: 'Milkshakes', en: 'Milkshakes' },
|
||||
items: [
|
||||
{ id: 'ms1', name: { tr: 'Muz', en: 'Banana' } },
|
||||
{ id: 'ms2', name: { tr: 'Vanilya', en: 'Vanilla' } },
|
||||
{ id: 'ms3', name: { tr: 'Çilek', en: 'Strawberry' } },
|
||||
{ id: 'ms4', name: { tr: 'Çikolata', en: 'Chocolate' } },
|
||||
{ id: 'ms5', name: { tr: 'Mango', en: 'Mango' } },
|
||||
{ id: 'ms6', name: { tr: 'Orman Meyvesi', en: 'Forest Fruits' } },
|
||||
{ id: 'ms7', name: { tr: 'Espresso', en: 'Espresso' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'refreshers',
|
||||
emoji: '🌿',
|
||||
label: { tr: 'Refreshers', en: 'Refreshers' },
|
||||
items: [
|
||||
{ id: 'rf1', name: { tr: 'Limonata', en: 'Lemonade' } },
|
||||
{ id: 'rf2', name: { tr: 'Nar Suyu', en: 'Pomegranate Juice' } },
|
||||
{ id: 'rf3', name: { tr: 'Portakal Suyu', en: 'Orange Juice' } },
|
||||
{ id: 'rf4', name: { tr: 'Cool Lime', en: 'Cool Lime' } },
|
||||
{ id: 'rf5', name: { tr: 'Rooibos Peach', en: 'Rooibos Peach' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'biralar',
|
||||
emoji: '🍺',
|
||||
label: { tr: 'Biralar', en: 'Beers' },
|
||||
items: [
|
||||
{ id: 'br1', name: { tr: 'Tuborg Gold', en: 'Tuborg Gold' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br2', name: { tr: 'Tuborg Filtresiz', en: 'Tuborg Unfiltered' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br3', name: { tr: 'Tuborg Amber', en: 'Tuborg Amber' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br4', name: { tr: 'Carlsberg', en: 'Carlsberg' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br5', name: { tr: 'Efes Malt', en: 'Efes Malt' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br6', name: { tr: 'Efes Green', en: 'Efes Green' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'br7', name: { tr: 'Stella Artois', en: 'Stella Artois' }, description: { tr: '44 cl', en: '44 cl' } },
|
||||
{ id: 'br8', name: { tr: "Beck's", en: "Beck's" }, description: { tr: '33 cl', en: '33 cl' } },
|
||||
{ id: 'br9', name: { tr: 'Bud', en: 'Bud' }, description: { tr: '33 cl', en: '33 cl' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'saraplar',
|
||||
emoji: '🍷',
|
||||
label: { tr: 'Şaraplar', en: 'Wines' },
|
||||
items: [
|
||||
{ id: 'sr1', name: { tr: 'Beyaz Kadeh', en: 'White Glass' } },
|
||||
{ id: 'sr2', name: { tr: 'Rosé Kadeh', en: 'Rosé Glass' } },
|
||||
{ id: 'sr3', name: { tr: 'Kırmızı Kadeh', en: 'Red Glass' } },
|
||||
{ id: 'sr4', name: { tr: 'Sevilen Colombard Semillon – Beyaz Şişe', en: 'Sevilen Colombard Semillon – White Bottle' } },
|
||||
{ id: 'sr5', name: { tr: 'Sevilen Letter R – Blush Şişe', en: 'Sevilen Letter R – Blush Bottle' } },
|
||||
{ id: 'sr6', name: { tr: 'Sevilen Parsel II – Kırmızı Şişe', en: 'Sevilen Parsel II – Red Bottle' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'alkolsuz-kokteyller',
|
||||
emoji: '🧃',
|
||||
label: { tr: 'Alkolsüz Kokteyller', en: 'Mocktails' },
|
||||
items: [
|
||||
{ id: 'ak1', name: { tr: 'Golden Jüpiter', en: 'Golden Jupiter' }, description: { tr: 'Red Bull Peach Edition, fesleğen, portakal suyu, limon', en: 'Red Bull Peach Edition, basil, orange juice, lemon' } },
|
||||
{ id: 'ak2', name: { tr: 'Venus Wave', en: 'Venus Wave' }, description: { tr: 'Red Bull White Edition, ananas suyu, lime', en: 'Red Bull White Edition, pineapple juice, lime' } },
|
||||
{ id: 'ak3', name: { tr: 'Kozmos Twist', en: 'Kozmos Twist' }, description: { tr: 'Red Bull White Edition, portakal, lime, soda', en: 'Red Bull White Edition, orange, lime, sparkling water' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'house-kokteyller',
|
||||
emoji: '🍹',
|
||||
label: { tr: 'House Kokteyller', en: 'House Cocktails' },
|
||||
items: [
|
||||
{ id: 'hk1', name: { tr: 'Grass', en: 'Grass' }, description: { tr: 'Gin, yeşil erik, buzlu turşu, limon suyu, greyfurt, portakal', en: 'Gin, green plum, ice pickle, lemon juice, grapefruit, orange' } },
|
||||
{ id: 'hk2', name: { tr: 'Strawberry Hibiscus', en: 'Strawberry Hibiscus' }, description: { tr: 'Hibiscus çayı, greyfurt, Cointreau, Casemigos teklila', en: 'Hibiscus tea, grapefruit, Cointreau, Casamigos tequila' } },
|
||||
{ id: 'hk3', name: { tr: 'Satsuma', en: 'Satsuma' }, description: { tr: 'Satsuma, greyfurt, portakal, votka', en: 'Satsuma, grapefruit, orange, vodka' } },
|
||||
{ id: 'hk4', name: { tr: 'Chili Mango', en: 'Chili Mango' }, description: { tr: 'Mango, Antrout taberi, turunç, kamişeri', en: 'Mango, chili pepper, citrus' } },
|
||||
{ id: 'hk5', name: { tr: 'Black Mulberry', en: 'Black Mulberry' }, description: { tr: 'Karadut, lime, sweet & sour, votka', en: 'Black mulberry, lime, sweet & sour, vodka' } },
|
||||
{ id: 'hk6', name: { tr: 'Lunar Bloom', en: 'Lunar Bloom' }, description: { tr: 'Beyaz çay, şeftali, beyaz şarap', en: 'White tea, peach, white wine' } },
|
||||
{ id: 'hk7', name: { tr: 'White Peach', en: 'White Peach' }, description: { tr: 'Beyaz çay, şeftali, rom', en: 'White tea, peach, rum' } },
|
||||
{ id: 'hk8', name: { tr: 'Kozmos', en: 'Kozmos' }, description: { tr: 'Cinzano, sweet & sour, ananas suyu, limon suyu, Kozmos mavi', en: 'Cinzano, sweet & sour, pineapple juice, lemon juice, Kozmos blue' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'raki',
|
||||
emoji: '🥃',
|
||||
label: { tr: 'Rakı', en: 'Raki' },
|
||||
items: [
|
||||
{ id: 'rk1', name: { tr: 'Lokal Rakı Tek', en: 'Local Raki Single' } },
|
||||
{ id: 'rk2', name: { tr: 'Lokal Rakı Duble', en: 'Local Raki Double' } },
|
||||
{ id: 'rk3', name: { tr: 'Yeni Rakı Özel Seri', en: 'Yeni Raki Special Series' }, description: { tr: '35 cl', en: '35 cl' } },
|
||||
{ id: 'rk4', name: { tr: 'Yeni Rakı Özel Seri', en: 'Yeni Raki Special Series' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'rk5', name: { tr: 'Yeni Rakı Özel Seri', en: 'Yeni Raki Special Series' }, description: { tr: '70 cl', en: '70 cl' } },
|
||||
{ id: 'rk6', name: { tr: 'Beylerbeyi Göbek', en: 'Beylerbeyi Gobek' }, description: { tr: '35 cl', en: '35 cl' } },
|
||||
{ id: 'rk7', name: { tr: 'Beylerbeyi Göbek', en: 'Beylerbeyi Gobek' }, description: { tr: '50 cl', en: '50 cl' } },
|
||||
{ id: 'rk8', name: { tr: 'Beylerbeyi Göbek', en: 'Beylerbeyi Gobek' }, description: { tr: '70 cl', en: '70 cl' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'klasik-kokteyller',
|
||||
emoji: '🍸',
|
||||
label: { tr: 'Klasik Kokteyller', en: 'Classic Cocktails' },
|
||||
items: [
|
||||
{ id: 'kk1', name: { tr: 'Espresso Martini', en: 'Espresso Martini' } },
|
||||
{ id: 'kk2', name: { tr: 'Mojito', en: 'Mojito' } },
|
||||
{ id: 'kk3', name: { tr: 'Margarita', en: 'Margarita' } },
|
||||
{ id: 'kk4', name: { tr: 'Aperol Spritz', en: 'Aperol Spritz' } },
|
||||
{ id: 'kk5', name: { tr: 'Lynchburg Lemonade', en: 'Lynchburg Lemonade' } },
|
||||
{ id: 'kk6', name: { tr: 'Whiskey Sour', en: 'Whiskey Sour' } },
|
||||
{ id: 'kk7', name: { tr: 'Long Island Iced Tea', en: 'Long Island Iced Tea' } },
|
||||
{ id: 'kk8', name: { tr: 'Negroni', en: 'Negroni' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'shots',
|
||||
emoji: '🥃',
|
||||
label: { tr: 'Shots', en: 'Shots' },
|
||||
items: [
|
||||
{ id: 'sh1', name: { tr: 'Olmeca Gold', en: 'Olmeca Gold' } },
|
||||
{ id: 'sh2', name: { tr: 'Olmeca Silver', en: 'Olmeca Silver' } },
|
||||
{ id: 'sh3', name: { tr: 'Olmeca Dark Chocolate', en: 'Olmeca Dark Chocolate' } },
|
||||
{ id: 'sh4', name: { tr: 'Aperol Spritz', en: 'Aperol Spritz' } },
|
||||
{ id: 'sh5', name: { tr: 'Lynchburg Lemonade', en: 'Lynchburg Lemonade' } },
|
||||
{ id: 'sh6', name: { tr: 'Whiskey Sour', en: 'Whiskey Sour' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'spirits',
|
||||
emoji: '🫙',
|
||||
label: { tr: 'Spirits', en: 'Spirits' },
|
||||
items: [
|
||||
{ id: 'sp1', name: { tr: 'Absolut Vodka', en: 'Absolut Vodka' } },
|
||||
{ id: 'sp2', name: { tr: 'Absolut Vanilla', en: 'Absolut Vanilla' } },
|
||||
{ id: 'sp3', name: { tr: 'Beefeater Gin', en: 'Beefeater Gin' } },
|
||||
{ id: 'sp4', name: { tr: 'Beefeater Pink', en: 'Beefeater Pink' } },
|
||||
{ id: 'sp5', name: { tr: 'Beefeater Blood Orange', en: 'Beefeater Blood Orange' } },
|
||||
],
|
||||
},
|
||||
]
|
||||
export async function getCategories(): Promise<Category[]> {
|
||||
try {
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
include: {
|
||||
items: {
|
||||
orderBy: { order: 'asc' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return categories.map((c) => ({
|
||||
id: c.id,
|
||||
emoji: c.emoji,
|
||||
label: { tr: c.labelTr, en: c.labelEn },
|
||||
items: c.items.map((i) => ({
|
||||
id: i.id,
|
||||
emoji: i.emoji || undefined,
|
||||
image: i.image || undefined,
|
||||
name: { tr: i.nameTr, en: i.nameEn },
|
||||
description:
|
||||
i.descriptionTr && i.descriptionEn
|
||||
? { tr: i.descriptionTr, en: i.descriptionEn }
|
||||
: undefined,
|
||||
price: i.price !== null ? i.price : undefined,
|
||||
highlight: i.highlight,
|
||||
})),
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Error reading from database:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveCategories(categories: Category[]): Promise<void> {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// 1. Delete all existing records (mimics overwriting the JSON file)
|
||||
await tx.menuItem.deleteMany()
|
||||
await tx.category.deleteMany()
|
||||
|
||||
// 2. Insert new records
|
||||
for (let cIndex = 0; cIndex < categories.length; cIndex++) {
|
||||
const c = categories[cIndex]
|
||||
await tx.category.create({
|
||||
data: {
|
||||
id: c.id,
|
||||
emoji: c.emoji,
|
||||
labelTr: c.label.tr,
|
||||
labelEn: c.label.en || c.label.tr,
|
||||
order: cIndex,
|
||||
},
|
||||
})
|
||||
|
||||
for (let iIndex = 0; iIndex < c.items.length; iIndex++) {
|
||||
const i = c.items[iIndex]
|
||||
await tx.menuItem.create({
|
||||
data: {
|
||||
id: i.id,
|
||||
categoryId: c.id,
|
||||
emoji: i.emoji || null,
|
||||
image: i.image || null,
|
||||
nameTr: i.name.tr,
|
||||
nameEn: i.name.en || i.name.tr,
|
||||
descriptionTr: i.description?.tr || null,
|
||||
descriptionEn: i.description?.en || null,
|
||||
price: i.price ?? null,
|
||||
highlight: i.highlight ?? false,
|
||||
order: iIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error saving to database:', error)
|
||||
throw new Error('Failed to save menu data to database')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient()
|
||||
}
|
||||
|
||||
declare global {
|
||||
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>
|
||||
}
|
||||
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
|
||||
export default prisma
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||
Generated
+949
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,17 @@
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^6.19.3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"prisma": "^6.19.3",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Category {
|
||||
id String @id
|
||||
emoji String
|
||||
labelTr String
|
||||
labelEn String
|
||||
order Int @default(0)
|
||||
items MenuItem[]
|
||||
}
|
||||
|
||||
model MenuItem {
|
||||
id String @id
|
||||
emoji String?
|
||||
image String?
|
||||
nameTr String
|
||||
nameEn String
|
||||
descriptionTr String?
|
||||
descriptionEn String?
|
||||
price Float?
|
||||
highlight Boolean @default(false)
|
||||
order Int @default(0)
|
||||
categoryId String
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
@@ -7,6 +7,26 @@ const defaultLocale = 'tr'
|
||||
export function proxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
// Admin Auth Logic
|
||||
if (pathname.startsWith('/admin')) {
|
||||
const session = request.cookies.get('admin_session')?.value
|
||||
const secret = process.env.ADMIN_SECRET || 'secret'
|
||||
const isLoginPage = pathname === '/admin/login'
|
||||
|
||||
if (!session || session !== secret) {
|
||||
if (!isLoginPage) {
|
||||
return NextResponse.redirect(new URL('/admin/login', request.url))
|
||||
}
|
||||
} else {
|
||||
if (isLoginPage) {
|
||||
return NextResponse.redirect(new URL('/admin', request.url))
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// Locale Logic
|
||||
const hasLocale = locales.some(
|
||||
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)
|
||||
)
|
||||
@@ -15,6 +35,8 @@ export function proxy(request: NextRequest) {
|
||||
request.nextUrl.pathname = `/${defaultLocale}${pathname}`
|
||||
return NextResponse.redirect(request.nextUrl)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
type LocalizedString = { tr: string; en: string }
|
||||
|
||||
type JsonMenuItem = {
|
||||
id: string
|
||||
emoji?: string
|
||||
image?: string
|
||||
name: LocalizedString
|
||||
description?: LocalizedString
|
||||
price?: number
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
type JsonCategory = {
|
||||
id: string
|
||||
emoji: string
|
||||
label: LocalizedString
|
||||
items: JsonMenuItem[]
|
||||
}
|
||||
|
||||
async function migrate() {
|
||||
console.log('Starting migration...')
|
||||
|
||||
const filePath = path.join(process.cwd(), 'data', 'menu.json')
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const categories: JsonCategory[] = JSON.parse(fileContent)
|
||||
|
||||
// Clear existing data (if any)
|
||||
await prisma.menuItem.deleteMany({})
|
||||
await prisma.category.deleteMany({})
|
||||
console.log('Cleared existing data')
|
||||
|
||||
for (let cIndex = 0; cIndex < categories.length; cIndex++) {
|
||||
const cat = categories[cIndex]
|
||||
|
||||
await prisma.category.create({
|
||||
data: {
|
||||
id: cat.id,
|
||||
emoji: cat.emoji,
|
||||
labelTr: cat.label.tr,
|
||||
labelEn: cat.label.en,
|
||||
order: cIndex,
|
||||
items: {
|
||||
create: cat.items.map((item, iIndex) => ({
|
||||
id: item.id,
|
||||
emoji: item.emoji || null,
|
||||
image: item.image || null,
|
||||
nameTr: item.name.tr,
|
||||
nameEn: item.name.en,
|
||||
descriptionTr: item.description?.tr || null,
|
||||
descriptionEn: item.description?.en || null,
|
||||
price: item.price !== undefined ? item.price : null,
|
||||
highlight: item.highlight || false,
|
||||
order: iIndex,
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Migrated category: ${cat.label.tr}`)
|
||||
}
|
||||
|
||||
console.log('Migration complete!')
|
||||
}
|
||||
|
||||
migrate()
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user