357 lines
17 KiB
TypeScript
357 lines
17 KiB
TypeScript
'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>
|
||
)
|
||
}
|