first commit

This commit is contained in:
mstfyldz
2026-06-03 04:11:30 +03:00
parent d369c9e258
commit 7b4b3a85af
20 changed files with 3446 additions and 442 deletions
+32 -72
View File
@@ -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
View File
@@ -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>
)
}
+356
View File
@@ -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>
)
}