feat: add prisma support, admin panel and auth

This commit is contained in:
AyrisAI
2026-05-15 19:11:17 +03:00
parent 31c3deb2da
commit 09a105cd1e
29 changed files with 3606 additions and 441 deletions

150
app/admin/actions.ts Normal file
View File

@@ -0,0 +1,150 @@
'use server';
import { prisma } from '@/app/lib/prisma';
import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import bcrypt from 'bcryptjs';
import { encrypt } from '@/app/lib/auth';
/* ────── Auth Actions ────── */
export async function authenticate(formData: FormData) {
const username = formData.get('username') as string;
const password = formData.get('password') as string;
const user = await prisma.user.findUnique({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
return { error: 'Geçersiz kullanıcı adı veya şifre' };
}
// Create session
const expires = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours
const session = await encrypt({ userId: user.id, username: user.username, expires });
(await cookies()).set('session', session, { expires, httpOnly: true });
redirect('/admin');
}
export async function signout() {
(await cookies()).set('session', '', { expires: new Date(0) });
redirect('/login');
}
export async function createCategory(formData: FormData) {
const title = formData.get('title') as string;
const externalId = formData.get('externalId') as string;
await prisma.category.create({
data: {
title,
externalId: externalId || null,
restaurantId: (await prisma.restaurant.findFirst())?.id || '',
},
});
revalidatePath('/admin/categories');
revalidatePath('/');
}
export async function updateCategory(id: string, formData: FormData) {
const title = formData.get('title') as string;
const externalId = formData.get('externalId') as string;
await prisma.category.update({
where: { id },
data: {
title,
externalId: externalId || null,
},
});
revalidatePath('/admin/categories');
revalidatePath('/');
}
export async function deleteCategory(id: string) {
// Optional: check if there are items first, or let prisma handles cascade if configured
await prisma.item.deleteMany({ where: { categoryId: id } });
await prisma.category.delete({ where: { id } });
revalidatePath('/admin/categories');
revalidatePath('/');
}
/* ────── Item Actions ────── */
export async function createItem(formData: FormData) {
const name = formData.get('name') as string;
const categoryId = formData.get('categoryId') as string;
const ingredients = formData.get('ingredients') as string;
const tasteProfile = formData.get('tasteProfile') as string;
const grapeVariety = formData.get('grapeVariety') as string;
const priceInput = formData.get('price') as string;
let price;
try {
// Try to parse as JSON if it looks like an object, otherwise keep as string
price = (priceInput.startsWith('{') || priceInput.startsWith('['))
? JSON.parse(priceInput)
: priceInput;
} catch (e) {
price = priceInput;
}
await prisma.item.create({
data: {
name,
categoryId,
ingredients: ingredients || null,
tasteProfile: tasteProfile || null,
grapeVariety: grapeVariety || null,
price,
},
});
revalidatePath('/admin/items');
revalidatePath('/');
}
export async function updateItem(id: string, formData: FormData) {
const name = formData.get('name') as string;
const categoryId = formData.get('categoryId') as string;
const ingredients = formData.get('ingredients') as string;
const tasteProfile = formData.get('tasteProfile') as string;
const grapeVariety = formData.get('grapeVariety') as string;
const priceInput = formData.get('price') as string;
let price;
try {
price = (priceInput.startsWith('{') || priceInput.startsWith('['))
? JSON.parse(priceInput)
: priceInput;
} catch (e) {
price = priceInput;
}
await prisma.item.update({
where: { id },
data: {
name,
categoryId,
ingredients: ingredients || null,
tasteProfile: tasteProfile || null,
grapeVariety: grapeVariety || null,
price,
},
});
revalidatePath('/admin/items');
revalidatePath('/');
}
export async function deleteItem(id: string) {
await prisma.item.delete({ where: { id } });
revalidatePath('/admin/items');
revalidatePath('/');
}

322
app/admin/admin.css Normal file
View File

@@ -0,0 +1,322 @@
.admin-layout {
display: flex;
min-height: 100vh;
background: var(--background);
color: var(--foreground);
}
.admin-sidebar {
width: 280px;
background: var(--surface);
border-right: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
}
.admin-sidebar-header {
padding: 2rem;
border-bottom: 1px solid var(--border-subtle);
}
.admin-logo {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
color: #fff;
text-decoration: none;
letter-spacing: 0.1em;
}
.admin-logo span {
color: var(--gold);
font-size: 0.8rem;
font-family: 'Inter', sans-serif;
letter-spacing: 0.3em;
margin-left: 0.5rem;
}
.admin-nav {
padding: 1.5rem 0;
flex: 1;
}
.admin-nav-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.85rem 2rem;
color: var(--text-secondary);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.3s;
border-left: 3px solid transparent;
}
.admin-nav-item svg {
width: 18px;
height: 18px;
}
.admin-nav-item:hover {
color: #fff;
background: rgba(255, 255, 255, 0.03);
}
.admin-nav-item.active {
color: var(--gold);
background: rgba(201, 169, 110, 0.05);
border-left-color: var(--gold);
}
.admin-sidebar-footer {
padding: 1.5rem 2rem;
border-top: 1px solid var(--border-subtle);
}
.admin-view-site {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-muted);
text-decoration: none;
font-size: 0.8rem;
transition: color 0.3s;
}
.admin-view-site:hover {
color: var(--gold);
}
.admin-view-site svg {
width: 14px;
height: 14px;
}
.admin-main {
flex: 1;
display: flex;
flex-direction: column;
}
.admin-header {
height: 70px;
padding: 0 2rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
background: rgba(10, 10, 15, 0.5);
backdrop-filter: blur(10px);
}
.admin-header-title {
font-size: 1.1rem;
font-weight: 500;
color: #fff;
}
.admin-user {
font-size: 0.85rem;
color: var(--text-secondary);
}
.admin-content {
padding: 2rem;
overflow-y: auto;
}
/* Dashboard Cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.stat-card {
background: var(--surface-elevated);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid var(--border-subtle);
transition: transform 0.3s, border-color 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
border-color: var(--border-glow);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 600;
color: var(--gold);
}
/* Tables & Lists */
.admin-card {
background: var(--surface);
border-radius: 16px;
border: 1px solid var(--border-subtle);
overflow: hidden;
}
.card-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-subtle);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.1rem;
color: #fff;
}
.admin-btn {
background: var(--gold);
color: #000;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.admin-btn:hover {
background: var(--gold-light);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(201, 169, 110, 0.2);
}
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th {
text-align: left;
padding: 1rem 2rem;
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border-subtle);
}
.admin-table td {
padding: 1.25rem 2rem;
font-size: 0.9rem;
border-bottom: 1px solid var(--border-subtle);
}
.admin-table tr:last-child td {
border-bottom: none;
}
.admin-table tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.actions-cell {
display: flex;
gap: 0.75rem;
}
.action-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.3s;
}
.action-btn:hover {
color: var(--gold);
}
.action-btn.delete:hover {
color: var(--accent-rose);
}
.badge {
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
background: rgba(201, 169, 110, 0.1);
color: var(--gold);
}
/* Forms */
.admin-form {
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.85rem;
color: var(--text-secondary);
}
.admin-input, .admin-select, .admin-textarea {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 0.75rem 1rem;
color: #fff;
font-family: inherit;
transition: all 0.3s;
}
.admin-input:focus, .admin-select:focus, .admin-textarea:focus {
outline: none;
border-color: var(--gold);
background: rgba(255, 255, 255, 0.08);
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 2rem;
}
.modal-content {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 20px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}

View File

@@ -0,0 +1,73 @@
'use client';
import React, { useState } from 'react';
import CategoryForm from '../components/CategoryForm';
import DeleteButton from '../components/DeleteButton';
import { deleteCategory } from '../actions';
interface CategoryManagerProps {
categories: any[];
}
export default function CategoryManager({ categories }: CategoryManagerProps) {
const [showForm, setShowForm] = useState(false);
const [editingCategory, setEditingCategory] = useState<any>(null);
const handleEdit = (category: any) => {
setEditingCategory(category);
setShowForm(true);
};
const handleClose = () => {
setShowForm(false);
setEditingCategory(null);
};
return (
<div className="admin-categories">
<div className="admin-page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<div>
<h1>Kategoriler</h1>
<p>Menü kategorilerini yönetin</p>
</div>
<button className="admin-btn" onClick={() => setShowForm(true)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{width: '16px'}}><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
Yeni Kategori
</button>
</div>
<div className="admin-card">
<table className="admin-table">
<thead>
<tr>
<th>Başlık</th>
<th>Harici ID</th>
<th>Ürün Sayısı</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
{categories.map((cat) => (
<tr key={cat.id}>
<td style={{ fontWeight: 500, color: '#fff' }}>{cat.title}</td>
<td><code className="badge">{cat.externalId}</code></td>
<td>{cat._count?.items || 0} Ürün</td>
<td className="actions-cell">
<button className="action-btn" onClick={() => handleEdit(cat)}>Düzenle</button>
<DeleteButton onDelete={async () => { await deleteCategory(cat.id); }} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{showForm && (
<CategoryForm
initialData={editingCategory}
onClose={handleClose}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { prisma } from '@/app/lib/prisma';
import CategoryManager from './CategoryManager';
export const dynamic = 'force-dynamic';
export default async function AdminCategoriesPage() {
const categories = await prisma.category.findMany({
include: {
_count: {
select: { items: true }
}
},
orderBy: { createdAt: 'asc' }
});
return <CategoryManager categories={categories} />;
}

View File

@@ -0,0 +1,22 @@
'use client';
import React from 'react';
import Link from 'next/use-pathname';
import { usePathname } from 'next/navigation';
import NextLink from 'next/link';
interface NavItemProps {
href: string;
children: React.ReactNode;
}
export default function AdminNavItem({ href, children }: NavItemProps) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<NextLink href={href} className={`admin-nav-item ${isActive ? 'active' : ''}`}>
{children}
</NextLink>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import React, { useState } from 'react';
import { createCategory, updateCategory } from '../actions';
interface CategoryFormProps {
initialData?: any;
onClose: () => void;
}
export default function CategoryForm({ initialData, onClose }: CategoryFormProps) {
const [loading, setLoading] = useState(false);
async function handleSubmit(formData: FormData) {
setLoading(true);
if (initialData) {
await updateCategory(initialData.id, formData);
} else {
await createCategory(formData);
}
setLoading(false);
onClose();
}
return (
<div className="modal-overlay">
<div className="modal-content">
<div className="card-header">
<h2 className="card-title">{initialData ? 'Kategoriyi Düzenle' : 'Yeni Kategori Ekle'}</h2>
<button onClick={onClose} className="action-btn"></button>
</div>
<form action={handleSubmit} className="admin-form">
<div className="form-group">
<label htmlFor="title">Kategori Başlığı</label>
<input
id="title"
name="title"
defaultValue={initialData?.title}
className="admin-input"
placeholder="Örn: Klasik Kokteyller"
required
/>
</div>
<div className="form-group">
<label htmlFor="externalId">Harici ID (İkon eşleşmesi için)</label>
<input
id="externalId"
name="externalId"
defaultValue={initialData?.externalId}
className="admin-input"
placeholder="Örn: classic_cocktails"
/>
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
<button type="submit" className="admin-btn" disabled={loading}>
{loading ? 'Kaydediliyor...' : 'Kaydet'}
</button>
<button type="button" onClick={onClose} className="admin-btn" style={{ background: 'rgba(255,255,255,0.05)', color: '#fff' }}>
İptal
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
'use client';
import React from 'react';
interface DeleteButtonProps {
onDelete: () => Promise<void>;
}
export default function DeleteButton({ onDelete }: DeleteButtonProps) {
const [loading, setLoading] = React.useState(false);
async function handleClick() {
if (confirm('Bu öğeyi silmek istediğinizden emin misiniz?')) {
setLoading(true);
await onDelete();
setLoading(false);
}
}
return (
<button
onClick={handleClick}
className="action-btn delete"
disabled={loading}
>
{loading ? '...' : 'Sil'}
</button>
);
}

View File

@@ -0,0 +1,112 @@
'use client';
import React, { useState } from 'react';
import { createItem, updateItem } from '../actions';
interface ItemFormProps {
categories: any[];
initialData?: any;
onClose: () => void;
}
export default function ItemForm({ categories, initialData, onClose }: ItemFormProps) {
const [loading, setLoading] = useState(false);
async function handleSubmit(formData: FormData) {
setLoading(true);
if (initialData) {
await updateItem(initialData.id, formData);
} else {
await createItem(formData);
}
setLoading(false);
onClose();
}
return (
<div className="modal-overlay">
<div className="modal-content">
<div className="card-header">
<h2 className="card-title">{initialData ? 'Ürünü Düzenle' : 'Yeni Ürün Ekle'}</h2>
<button onClick={onClose} className="action-btn"></button>
</div>
<form action={handleSubmit} className="admin-form">
<div className="form-group">
<label htmlFor="name">Ürün Adı</label>
<input
id="name"
name="name"
defaultValue={initialData?.name}
className="admin-input"
placeholder="Örn: Mojito"
required
/>
</div>
<div className="form-group">
<label htmlFor="categoryId">Kategori</label>
<select
id="categoryId"
name="categoryId"
defaultValue={initialData?.categoryId || categories[0]?.id}
className="admin-select"
required
>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.title}</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="price">Fiyat (Metin veya JSON)</label>
<textarea
id="price"
name="price"
defaultValue={typeof initialData?.price === 'object' ? JSON.stringify(initialData.price) : initialData?.price}
className="admin-textarea"
placeholder='Örn: 590 tl veya {"single": "370 tl", "double": "560 tl"}'
required
/>
</div>
<div className="form-group">
<label htmlFor="ingredients">İçerik</label>
<input
id="ingredients"
name="ingredients"
defaultValue={initialData?.ingredients}
className="admin-input"
placeholder="Örn: Rom, Nane, Lime"
/>
</div>
<div className="form-group">
<label htmlFor="tasteProfile">Tat Profili</label>
<input
id="tasteProfile"
name="tasteProfile"
defaultValue={initialData?.tasteProfile}
className="admin-input"
placeholder="Örn: Tatlı-Ekşi-Ferah"
/>
</div>
<div className="form-group">
<label htmlFor="grapeVariety">Üzüm Çeşidi (Şaraplar için)</label>
<input
id="grapeVariety"
name="grapeVariety"
defaultValue={initialData?.grapeVariety}
className="admin-input"
placeholder="Örn: Merlot"
/>
</div>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
<button type="submit" className="admin-btn" disabled={loading}>
{loading ? 'Kaydediliyor...' : 'Kaydet'}
</button>
<button type="button" onClick={onClose} className="admin-btn" style={{ background: 'rgba(255,255,255,0.05)', color: '#fff' }}>
İptal
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import React from 'react';
import { signout } from '../actions';
export default function SignOutButton() {
return (
<button
onClick={() => signout()}
className="action-btn"
style={{ fontSize: '0.8rem', color: 'var(--accent-rose)' }}
>
Çıkış Yap
</button>
);
}

View File

@@ -0,0 +1,91 @@
'use client';
import React, { useState } from 'react';
import ItemForm from '../components/ItemForm';
import DeleteButton from '../components/DeleteButton';
import { deleteItem } from '../actions';
interface ItemManagerProps {
items: any[];
categories: any[];
}
export default function ItemManager({ items, categories }: ItemManagerProps) {
const [showForm, setShowForm] = useState(false);
const [editingItem, setEditingItem] = useState<any>(null);
const handleEdit = (item: any) => {
setEditingItem(item);
setShowForm(true);
};
const handleClose = () => {
setShowForm(false);
setEditingItem(null);
};
return (
<div className="admin-items">
<div className="admin-page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<div>
<h1>Ürünler</h1>
<p>Tüm menü öğelerini yönetin</p>
</div>
<button className="admin-btn" onClick={() => setShowForm(true)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{width: '16px'}}><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
Yeni Ürün Ekle
</button>
</div>
<div className="admin-card">
<table className="admin-table">
<thead>
<tr>
<th>Ürün Adı</th>
<th>Kategori</th>
<th>Fiyat</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id}>
<td>
<div style={{ fontWeight: 500, color: '#fff' }}>{item.name}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{item.ingredients ? (item.ingredients.length > 50 ? item.ingredients.substring(0, 50) + '...' : item.ingredients) : 'İçerik yok'}
</div>
</td>
<td>
<span className="badge" style={{ background: 'rgba(124, 92, 191, 0.1)', color: 'var(--accent-purple)', borderColor: 'rgba(124, 92, 191, 0.2)' }}>
{item.category.title}
</span>
</td>
<td>
<div style={{ color: 'var(--gold)', fontWeight: 600 }}>
{typeof item.price === 'string'
? item.price
: Object.entries(item.price as object).map(([k, v]) => `${k}: ${v}`).join(', ')
}
</div>
</td>
<td className="actions-cell">
<button className="action-btn" onClick={() => handleEdit(item)}>Düzenle</button>
<DeleteButton onDelete={async () => { await deleteItem(item.id); }} />
</td>
</tr>
))}
</tbody>
</table>
</div>
{showForm && (
<ItemForm
categories={categories}
initialData={editingItem}
onClose={handleClose}
/>
)}
</div>
);
}

20
app/admin/items/page.tsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import { prisma } from '@/app/lib/prisma';
import ItemManager from './ItemManager';
export const dynamic = 'force-dynamic';
export default async function AdminItemsPage() {
const items = await prisma.item.findMany({
include: {
category: true
},
orderBy: { createdAt: 'desc' }
});
const categories = await prisma.category.findMany({
orderBy: { title: 'asc' }
});
return <ItemManager items={items} categories={categories} />;
}

58
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,58 @@
import React from 'react';
import Link from 'next/link';
import AdminNavItem from './components/AdminNavItem';
import SignOutButton from './components/SignOutButton';
import { getSession } from '@/app/lib/auth';
import './admin.css';
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
return (
<div className="admin-layout">
<aside className="admin-sidebar">
<div className="admin-sidebar-header">
<Link href="/admin" className="admin-logo">
LUNA <span>ADMIN</span>
</Link>
</div>
<nav className="admin-nav">
<AdminNavItem href="/admin">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /></svg>
Dashboard
</AdminNavItem>
<AdminNavItem href="/admin/categories">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" /></svg>
Kategoriler
</AdminNavItem>
<AdminNavItem href="/admin/items">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /></svg>
Ürünler
</AdminNavItem>
</nav>
<div className="admin-sidebar-footer">
<Link href="/" target="_blank" className="admin-view-site">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></svg>
Siteyi Görüntüle
</Link>
</div>
</aside>
<main className="admin-main">
<header className="admin-header">
<div className="admin-header-title">Panel</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
<div className="admin-user">{session?.username || 'Admin'}</div>
<SignOutButton />
</div>
</header>
<div className="admin-content">
{children}
</div>
</main>
</div>
);
}

44
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,44 @@
import React from 'react';
import { prisma } from '@/app/lib/prisma';
export const dynamic = 'force-dynamic';
export default async function AdminDashboard() {
const categoryCount = await prisma.category.count();
const itemCount = await prisma.item.count();
const restaurant = await prisma.restaurant.findFirst();
return (
<div className="admin-dashboard">
<div className="admin-page-header">
<h1>Dashboard</h1>
<p>Luna Cocktail & More - Menü Özeti</p>
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Toplam Kategori</div>
<div className="stat-value">{categoryCount}</div>
</div>
<div className="stat-card">
<div className="stat-label">Toplam Ürün</div>
<div className="stat-value">{itemCount}</div>
</div>
<div className="stat-card">
<div className="stat-label">Restoran İsmi</div>
<div className="stat-value" style={{ fontSize: '1.2rem' }}>{restaurant?.name || 'Yükleniyor...'}</div>
</div>
</div>
<div className="admin-card">
<div className="card-header">
<h2 className="card-title">Hızlı İşlemler</h2>
</div>
<div style={{ padding: '2rem', display: 'flex', gap: '1rem' }}>
<button className="admin-btn">Yeni Kategori Ekle</button>
<button className="admin-btn">Yeni Ürün Ekle</button>
</div>
</div>
</div>
);
}