Files
2026-06-03 04:11:30 +03:00

357 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}