kite-qr
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { createCategory, updateCategory } from './actions'
|
||||
import Link from 'next/link'
|
||||
|
||||
type Category = {
|
||||
id: number
|
||||
name_tr: string
|
||||
order_num: number | null
|
||||
status: number | null
|
||||
}
|
||||
|
||||
export default function CategoryForm({ category }: { category?: Category }) {
|
||||
const isEditing = !!category
|
||||
|
||||
// Use appropriate action based on editing state
|
||||
const actionToUse = isEditing
|
||||
? updateCategory.bind(null, category.id)
|
||||
: createCategory
|
||||
|
||||
const [state, formAction, isPending] = useActionState(actionToUse, undefined)
|
||||
|
||||
return (
|
||||
<form action={formAction} className="space-y-6">
|
||||
{state?.error && (
|
||||
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kategori Adı (TR) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name_tr"
|
||||
defaultValue={category?.name_tr || ''}
|
||||
required
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sıra Numarası
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="order_num"
|
||||
defaultValue={category?.order_num || 0}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="status"
|
||||
id="status"
|
||||
defaultChecked={category ? category.status === 1 : true}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="status" className="text-sm font-medium text-gray-700">
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<Link href="/admin/categories" className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors">
|
||||
İptal
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
|
||||
>
|
||||
{isPending ? 'Kaydediliyor...' : 'Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { deleteCategory } from './actions'
|
||||
|
||||
export default function DeleteButton({ id }: { id: number }) {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (confirm('Bu kategoriyi silmek istediğinize emin misiniz?')) {
|
||||
const res = await deleteCategory(id)
|
||||
if (res?.error) {
|
||||
alert(res.error)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import CategoryForm from '../../CategoryForm'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Kategori Düzenle - Admin',
|
||||
}
|
||||
|
||||
export default async function EditCategoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = await params
|
||||
const id = parseInt(resolvedParams.id, 10)
|
||||
|
||||
if (isNaN(id)) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const category = await prisma.categories.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Kategori Düzenle</h1>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
||||
<CategoryForm category={category} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
'use server'
|
||||
|
||||
import prisma from '@/lib/prisma'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function createCategory(_prevState: unknown, formData: FormData) {
|
||||
const name_tr = formData.get('name_tr') as string
|
||||
const name = formData.get('name') as string || name_tr
|
||||
const order_num = parseInt(formData.get('order_num') as string, 10) || 0
|
||||
const status = formData.get('status') === 'on' ? 1 : 0
|
||||
|
||||
if (!name_tr) {
|
||||
return { error: 'Lütfen kategori adını doldurun.' }
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.categories.create({
|
||||
data: {
|
||||
name,
|
||||
name_tr,
|
||||
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
order_num,
|
||||
status,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Create category error:', error)
|
||||
return { error: 'Kategori eklenirken hata oluştu.' }
|
||||
}
|
||||
|
||||
revalidatePath('/admin/categories')
|
||||
revalidatePath('/')
|
||||
redirect('/admin/categories')
|
||||
}
|
||||
|
||||
export async function updateCategory(id: number, _prevState: unknown, formData: FormData) {
|
||||
const name_tr = formData.get('name_tr') as string
|
||||
const name = formData.get('name') as string || name_tr
|
||||
const order_num = parseInt(formData.get('order_num') as string, 10) || 0
|
||||
const status = formData.get('status') === 'on' ? 1 : 0
|
||||
|
||||
if (!name_tr) {
|
||||
return { error: 'Lütfen kategori adını doldurun.' }
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.categories.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
name_tr,
|
||||
slug: name_tr.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||
order_num,
|
||||
status,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Update category error:', error)
|
||||
return { error: 'Kategori güncellenirken hata oluştu.' }
|
||||
}
|
||||
|
||||
revalidatePath('/admin/categories')
|
||||
revalidatePath('/')
|
||||
redirect('/admin/categories')
|
||||
}
|
||||
|
||||
export async function deleteCategory(id: number) {
|
||||
try {
|
||||
// Check if category has products
|
||||
const productsCount = await prisma.products.count({
|
||||
where: { category_id: id }
|
||||
})
|
||||
|
||||
if (productsCount > 0) {
|
||||
return { error: 'Bu kategoriye ait ürünler olduğu için silinemez. Önce ürünleri silin veya başka kategoriye taşıyın.' }
|
||||
}
|
||||
|
||||
await prisma.categories.delete({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
revalidatePath('/admin/categories')
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Delete category error:', error)
|
||||
return { error: 'Silme işlemi başarısız oldu.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import CategoryForm from '../CategoryForm'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Yeni Kategori Ekle - Admin',
|
||||
}
|
||||
|
||||
export default function NewCategoryPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Yeni Kategori Ekle</h1>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
||||
<CategoryForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Pencil, Plus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import DeleteButton from './DeleteButton'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Kategoriler - Admin',
|
||||
}
|
||||
|
||||
export default async function CategoriesPage() {
|
||||
const categories = await prisma.categories.findMany({
|
||||
orderBy: { order_num: 'asc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { products: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Kategoriler</h1>
|
||||
<Link href="/admin/categories/new" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-colors">
|
||||
<Plus size={18} /> Yeni Kategori Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-100 text-sm font-semibold text-gray-600">
|
||||
<th className="py-4 px-6">Kategori Adı</th>
|
||||
<th className="py-4 px-6 text-center">Sıra</th>
|
||||
<th className="py-4 px-6 text-center">Ürün Sayısı</th>
|
||||
<th className="py-4 px-6 text-center">Durum</th>
|
||||
<th className="py-4 px-6 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{categories.map((category) => (
|
||||
<tr key={category.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="py-4 px-6 font-medium text-gray-900">{category.name_tr}</td>
|
||||
<td className="py-4 px-6 text-center text-gray-600">{category.order_num}</td>
|
||||
<td className="py-4 px-6 text-center">
|
||||
<span className="bg-blue-50 text-blue-700 py-1 px-3 rounded-full text-xs font-medium">
|
||||
{category._count.products} Ürün
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-center">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
category.status === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{category.status === 1 ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link href={`/admin/categories/${category.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
||||
<Pencil size={18} />
|
||||
</Link>
|
||||
<DeleteButton id={category.id} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{categories.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-gray-500">
|
||||
Henüz kategori bulunmuyor.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user