feat: modernize admin panel UI and add openinary integration
This commit is contained in:
@@ -19,18 +19,21 @@ export default async function CategoriesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Kategoriler</h1>
|
<div>
|
||||||
<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">
|
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Kategoriler</h1>
|
||||||
<Plus size={18} /> Yeni Kategori Ekle
|
<p className="text-sm text-slate-500 mt-1">Menü kategorilerinizi yönetin ve sıralayın.</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/admin/categories/new" className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl font-medium flex items-center gap-2 transition-all shadow-sm hover:shadow-md hover:-translate-y-0.5">
|
||||||
|
<Plus size={18} /> Yeni Kategori
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-50 border-b border-gray-100 text-sm font-semibold text-gray-600">
|
<tr className="bg-slate-50/50 border-b border-slate-100 text-xs uppercase tracking-wider font-semibold text-slate-500">
|
||||||
<th className="py-4 px-6">Kategori Adı</th>
|
<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">Sıra</th>
|
||||||
<th className="py-4 px-6 text-center">Ürün Sayısı</th>
|
<th className="py-4 px-6 text-center">Ürün Sayısı</th>
|
||||||
@@ -38,25 +41,25 @@ export default async function CategoriesPage() {
|
|||||||
<th className="py-4 px-6 text-right">İşlemler</th>
|
<th className="py-4 px-6 text-right">İşlemler</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-slate-100/80">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<tr key={category.id} className="hover:bg-gray-50 transition-colors">
|
<tr key={category.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||||
<td className="py-4 px-6 font-medium text-gray-900">{category.name_tr}</td>
|
<td className="py-4 px-6 font-semibold text-slate-800">{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 text-slate-500 font-medium">{category.order_num}</td>
|
||||||
<td className="py-4 px-6 text-center">
|
<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">
|
<span className="bg-indigo-50 text-indigo-600 py-1 px-3 rounded-lg text-xs font-semibold">
|
||||||
{category._count.products} Ürün
|
{category._count.products} Ürün
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-center">
|
<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 ${
|
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold ${
|
||||||
category.status === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
category.status === 1 ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'
|
||||||
}`}>
|
}`}>
|
||||||
{category.status === 1 ? 'Aktif' : 'Pasif'}
|
{category.status === 1 ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 px-6 text-right">
|
<td className="py-4 px-6 text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-1 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<Link href={`/admin/categories/${category.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
<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} />
|
<Pencil size={18} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -67,8 +70,8 @@ export default async function CategoriesPage() {
|
|||||||
))}
|
))}
|
||||||
{categories.length === 0 && (
|
{categories.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="py-8 text-center text-gray-500">
|
<td colSpan={5} className="py-12 text-center text-slate-500">
|
||||||
Henüz kategori bulunmuyor.
|
Henüz kategori bulunmuyor. Yeni bir kategori ekleyerek başlayın.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import Link from 'next/link'
|
import { Sidebar } from '@/components/admin/Sidebar'
|
||||||
import { LayoutDashboard, LogOut, Package, Settings, Tags, Users2 } from 'lucide-react'
|
import { Header } from '@/components/admin/Header'
|
||||||
import { logout } from '@/lib/auth'
|
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -9,54 +7,18 @@ export default function AdminLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100 overflow-hidden">
|
<div className="flex h-screen bg-[#F8FAFC] overflow-hidden selection:bg-blue-100 selection:text-blue-900">
|
||||||
{/* Sidebar */}
|
<Sidebar />
|
||||||
<aside className="w-64 bg-white shadow-md flex flex-col hidden md:flex">
|
|
||||||
<div className="p-4 border-b">
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
<h2 className="text-xl font-bold text-gray-800">Admin Panel</h2>
|
<Header />
|
||||||
</div>
|
|
||||||
<nav className="flex-1 p-4 space-y-2">
|
<main className="flex-1 overflow-y-auto p-6 md:p-8 lg:p-10">
|
||||||
<Link href="/admin" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
<div className="max-w-6xl mx-auto">
|
||||||
<LayoutDashboard size={20} />
|
{children}
|
||||||
<span>Dashboard</span>
|
</div>
|
||||||
</Link>
|
</main>
|
||||||
<Link href="/admin/categories" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
</div>
|
||||||
<Tags size={20} />
|
|
||||||
<span>Kategoriler</span>
|
|
||||||
</Link>
|
|
||||||
<Link href="/admin/products" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<Package size={20} />
|
|
||||||
<span>Ürünler</span>
|
|
||||||
</Link>
|
|
||||||
<Link href="/admin/settings" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<Settings size={20} />
|
|
||||||
<span>Ayarlar</span>
|
|
||||||
</Link>
|
|
||||||
<Link href="/admin/users" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<Users2 size={20} />
|
|
||||||
<span>Kullanıcılar</span>
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
<div className="p-4 border-t">
|
|
||||||
<form action={async () => {
|
|
||||||
'use server'
|
|
||||||
await logout()
|
|
||||||
redirect('/admin/login')
|
|
||||||
}}>
|
|
||||||
<button className="flex items-center gap-3 w-full px-3 py-2 text-red-600 rounded-lg hover:bg-red-50 transition-colors">
|
|
||||||
<LogOut size={20} />
|
|
||||||
<span>Çıkış Yap</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="flex-1 overflow-y-auto bg-gray-50 p-6 md:p-8">
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Mobile Bottom Nav (Optional, simpler for now just a top bar) */}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { Package, Tags, Eye, TrendingUp } from 'lucide-react'
|
import { Package, Tags, Eye, TrendingUp, ChevronRight } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -14,55 +14,89 @@ export default async function AdminDashboard() {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
<div>
|
||||||
|
<h1 className="text-3xl font-extrabold text-slate-900 tracking-tight">Dashboard</h1>
|
||||||
|
<p className="mt-2 text-slate-500">Moy Beach dijital menü yönetim paneline hoş geldiniz.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
<div className="bg-white p-6 rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 flex items-center gap-5 transition-all duration-300 hover:shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:-translate-y-0.5 group">
|
||||||
<div className="p-3 bg-blue-100 text-blue-600 rounded-lg">
|
<div className="p-4 bg-gradient-to-br from-blue-50 to-blue-100/50 text-blue-600 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
||||||
<Tags size={24} />
|
<Tags size={28} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500 font-medium">Kategori Sayısı</p>
|
<p className="text-sm text-slate-500 font-medium">Kategori Sayısı</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{totalCategories}</p>
|
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{totalCategories}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
<div className="bg-white p-6 rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 flex items-center gap-5 transition-all duration-300 hover:shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:-translate-y-0.5 group">
|
||||||
<div className="p-3 bg-indigo-100 text-indigo-600 rounded-lg">
|
<div className="p-4 bg-gradient-to-br from-indigo-50 to-indigo-100/50 text-indigo-600 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
||||||
<Package size={24} />
|
<Package size={28} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500 font-medium">Toplam Ürün</p>
|
<p className="text-sm text-slate-500 font-medium">Toplam Ürün</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{totalProducts}</p>
|
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{totalProducts}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
|
<div className="bg-white p-6 rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 flex items-center gap-5 transition-all duration-300 hover:shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:-translate-y-0.5 group">
|
||||||
<div className="p-3 bg-green-100 text-green-600 rounded-lg">
|
<div className="p-4 bg-gradient-to-br from-emerald-50 to-emerald-100/50 text-emerald-600 rounded-xl group-hover:scale-110 transition-transform duration-300">
|
||||||
<TrendingUp size={24} />
|
<TrendingUp size={28} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500 font-medium">Aktif Ürünler</p>
|
<p className="text-sm text-slate-500 font-medium">Aktif Ürünler</p>
|
||||||
<p className="text-2xl font-bold text-gray-900">{activeProducts}</p>
|
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{activeProducts}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
|
<div className="bg-white rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 overflow-hidden">
|
||||||
<h2 className="text-lg font-semibold mb-4">Hızlı Kısayollar</h2>
|
<div className="p-6 border-b border-slate-100">
|
||||||
<div className="flex flex-col gap-3">
|
<h2 className="text-lg font-bold text-slate-800">Hızlı Kısayollar</h2>
|
||||||
<Link href="/admin/products" className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2">
|
</div>
|
||||||
<Package size={18} /> Ürünleri Yönet
|
<div className="p-3">
|
||||||
|
<Link href="/admin/products" className="flex items-center justify-between p-4 hover:bg-slate-50 rounded-xl transition-colors group">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2.5 bg-indigo-50 text-indigo-600 rounded-lg group-hover:bg-indigo-100 transition-colors">
|
||||||
|
<Package size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800">Ürünleri Yönet</p>
|
||||||
|
<p className="text-sm text-slate-500">Ürün ekle, düzenle veya fiyat güncelle</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-slate-400 group-hover:text-indigo-600 transition-colors" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/admin/categories" className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2">
|
|
||||||
<Tags size={18} /> Kategorileri Yönet
|
<Link href="/admin/categories" className="flex items-center justify-between p-4 hover:bg-slate-50 rounded-xl transition-colors group">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2.5 bg-blue-50 text-blue-600 rounded-lg group-hover:bg-blue-100 transition-colors">
|
||||||
|
<Tags size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800">Kategorileri Yönet</p>
|
||||||
|
<p className="text-sm text-slate-500">Menü kategorilerini ve sıralamasını düzenle</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-slate-400 group-hover:text-blue-600 transition-colors" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/" target="_blank" className="text-gray-600 hover:text-gray-800 font-medium flex items-center gap-2">
|
|
||||||
<Eye size={18} /> Canlı Menüyü Görüntüle
|
<Link href="/" target="_blank" className="flex items-center justify-between p-4 hover:bg-slate-50 rounded-xl transition-colors group">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2.5 bg-emerald-50 text-emerald-600 rounded-lg group-hover:bg-emerald-100 transition-colors">
|
||||||
|
<Eye size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-800">Canlı Menüyü Görüntüle</p>
|
||||||
|
<p className="text-sm text-slate-500">Müşterilerin gördüğü güncel menüyü aç</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight size={20} className="text-slate-400 group-hover:text-emerald-600 transition-colors" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { Pencil, Plus, Trash2, Image as ImageIcon } from 'lucide-react'
|
import { Pencil, Plus, Image as ImageIcon } from 'lucide-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import DeleteButton from './DeleteButton'
|
import DeleteButton from './DeleteButton'
|
||||||
@@ -19,41 +19,45 @@ export default async function ProductsPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Ürün Yönetimi</h1>
|
<div>
|
||||||
<Link href="/admin/products/new" className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
|
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Ürün Yönetimi</h1>
|
||||||
<Plus size={20} />
|
<p className="text-sm text-slate-500 mt-1">Tüm kategorilerdeki ürünlerinizi görüntüleyin ve düzenleyin.</p>
|
||||||
<span>Yeni Ürün Ekle</span>
|
</div>
|
||||||
|
<Link href="/admin/products/new" className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl font-medium flex items-center gap-2 transition-all shadow-sm hover:shadow-md hover:-translate-y-0.5">
|
||||||
|
<Plus size={18} /> Yeni Ürün Ekle
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category.id} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div key={category.id} className="bg-white rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 overflow-hidden">
|
||||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
<div className="bg-slate-50/50 px-6 py-5 border-b border-slate-100 flex justify-between items-center">
|
||||||
<h2 className="text-lg font-bold text-gray-800">{category.name_tr}</h2>
|
<h2 className="text-lg font-bold text-slate-800 tracking-tight">{category.name_tr}</h2>
|
||||||
<span className="text-sm text-gray-500 font-medium">{category.products.length} ürün</span>
|
<span className="bg-white px-3 py-1 rounded-full text-xs font-semibold text-slate-500 border border-slate-200 shadow-sm">
|
||||||
|
{category.products.length} ürün
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{category.products.length > 0 ? (
|
{category.products.length > 0 ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-100">
|
<tr className="border-b border-slate-100 text-xs uppercase tracking-wider font-semibold text-slate-500">
|
||||||
<th className="py-3 px-6 text-sm font-semibold text-gray-600 w-16">Resim</th>
|
<th className="py-4 px-6 w-16">Resim</th>
|
||||||
<th className="py-3 px-6 text-sm font-semibold text-gray-600">İsim</th>
|
<th className="py-4 px-6">İsim</th>
|
||||||
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Fiyat</th>
|
<th className="py-4 px-6">Fiyat</th>
|
||||||
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Durum</th>
|
<th className="py-4 px-6 text-center">Durum</th>
|
||||||
<th className="py-3 px-6 text-sm font-semibold text-gray-600 text-right">İşlemler</th>
|
<th className="py-4 px-6 text-right">İşlemler</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-slate-100/80">
|
||||||
{category.products.map((product) => (
|
{category.products.map((product) => (
|
||||||
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
|
<tr key={product.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||||
<td className="py-3 px-6">
|
<td className="py-4 px-6">
|
||||||
{product.image_url ? (
|
{product.image_url ? (
|
||||||
<div className="relative w-12 h-12 rounded-lg overflow-hidden border border-gray-200">
|
<div className="relative w-12 h-12 rounded-xl overflow-hidden shadow-sm border border-slate-200 group-hover:scale-105 transition-transform">
|
||||||
<Image
|
<Image
|
||||||
src={product.image_url}
|
src={product.image_url}
|
||||||
alt={product.name_tr}
|
alt={product.name_tr}
|
||||||
@@ -62,20 +66,20 @@ export default async function ProductsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center text-gray-400 border border-gray-200">
|
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-400 border border-slate-200 shadow-sm">
|
||||||
<ImageIcon size={20} />
|
<ImageIcon size={20} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-6 font-medium text-gray-900">{product.name_tr}</td>
|
<td className="py-4 px-6 font-semibold text-slate-800">{product.name_tr}</td>
|
||||||
<td className="py-3 px-6 text-gray-600">₺{product.price.toString()}</td>
|
<td className="py-4 px-6 font-medium text-slate-600">₺{product.price.toString()}</td>
|
||||||
<td className="py-3 px-6">
|
<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 ${product.status === 1 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold ${product.status === 1 ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'}`}>
|
||||||
{product.status === 1 ? 'Aktif' : 'Pasif'}
|
{product.status === 1 ? 'Aktif' : 'Pasif'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-6 text-right">
|
<td className="py-4 px-6 text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-1 opacity-100 sm:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<Link href={`/admin/products/${product.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
<Link href={`/admin/products/${product.id}/edit`} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors" title="Düzenle">
|
||||||
<Pencil size={18} />
|
<Pencil size={18} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -88,8 +92,8 @@ export default async function ProductsPage() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-6 px-6 text-gray-500 text-sm">
|
<div className="py-10 px-6 text-center text-slate-500 text-sm">
|
||||||
Bu kategoride henüz ürün yok.
|
Bu kategoride henüz ürün bulunmuyor.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useActionState, useState, useRef } from 'react'
|
import { useActionState, useState, useRef } from 'react'
|
||||||
import { saveSettings } from './actions'
|
import { saveSettings } from './actions'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Upload, X, Loader2 } from 'lucide-react'
|
import { Upload, X, Loader2, Save } from 'lucide-react'
|
||||||
|
|
||||||
export default function SettingsForm({
|
export default function SettingsForm({
|
||||||
defaultRestaurantName,
|
defaultRestaurantName,
|
||||||
@@ -49,62 +49,62 @@ export default function SettingsForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={action} className="space-y-6">
|
<form action={action} className="space-y-8">
|
||||||
{state?.success && (
|
{state?.success && (
|
||||||
<div className="bg-green-50 text-green-700 p-3 rounded-lg text-sm">
|
<div className="bg-emerald-50 text-emerald-700 px-4 py-3 rounded-xl text-sm font-medium border border-emerald-100/50">
|
||||||
{state.message}
|
{state.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">
|
<div className="bg-red-50 text-red-700 px-4 py-3 rounded-xl text-sm font-medium border border-red-100/50">
|
||||||
{state.error}
|
{state.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Logo Upload */}
|
{/* Logo Upload */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-semibold text-slate-700 mb-3">
|
||||||
Site Logosu
|
Site Logosu
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6">
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
<div className="relative w-40 h-20 rounded-xl overflow-hidden border-2 border-gray-200 bg-stone-900 flex items-center justify-center flex-shrink-0">
|
<div className="relative w-48 h-24 rounded-2xl overflow-hidden border border-slate-200 bg-slate-900 shadow-inner flex items-center justify-center flex-shrink-0 group">
|
||||||
{logoUrl ? (
|
{logoUrl ? (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt="Logo önizleme"
|
alt="Logo önizleme"
|
||||||
fill
|
fill
|
||||||
className="object-contain p-2"
|
className="object-contain p-3"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setLogoUrl(''); if (fileInputRef.current) fileInputRef.current.value = '' }}
|
onClick={() => { setLogoUrl(''); if (fileInputRef.current) fileInputRef.current.value = '' }}
|
||||||
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-0.5 hover:bg-red-600 transition-colors"
|
className="absolute top-2 right-2 bg-red-500/90 text-white rounded-full p-1 hover:bg-red-600 transition-colors opacity-0 group-hover:opacity-100 backdrop-blur-sm shadow-sm"
|
||||||
>
|
>
|
||||||
<X size={12} />
|
<X size={14} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-gray-400 text-center px-2">Logo yok<br />(varsayılan kullanılır)</span>
|
<span className="text-xs text-slate-400 text-center px-4 font-medium">Logo yok<br />(varsayılan kullanılır)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload button */}
|
{/* Upload button */}
|
||||||
<div className="flex flex-col gap-2 justify-center h-20">
|
<div className="flex flex-col gap-2.5 justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
className="flex items-center gap-2 px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-600 hover:border-blue-400 hover:text-blue-600 transition-colors disabled:opacity-50"
|
className="flex items-center gap-2 px-5 py-2.5 border-2 border-dashed border-slate-300 rounded-xl text-sm font-medium text-slate-600 hover:border-blue-400 hover:text-blue-600 transition-colors disabled:opacity-50 hover:bg-blue-50/50"
|
||||||
>
|
>
|
||||||
{uploading
|
{uploading
|
||||||
? <><Loader2 size={16} className="animate-spin" /> Yükleniyor...</>
|
? <><Loader2 size={18} className="animate-spin" /> Yükleniyor...</>
|
||||||
: <><Upload size={16} /> Logo Yükle</>
|
: <><Upload size={18} /> Yeni Logo Seç</>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-gray-500">PNG, SVG, WEBP — Maks. 5MB</p>
|
<p className="text-xs text-slate-400 font-medium ml-1">Önerilen format: Şeffaf arka planlı PNG veya SVG (Maks. 5MB)</p>
|
||||||
{uploadError && <p className="text-xs text-red-600">{uploadError}</p>}
|
{uploadError && <p className="text-xs text-red-600 font-medium ml-1">{uploadError}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,39 +118,41 @@ export default function SettingsForm({
|
|||||||
<input type="hidden" name="logo_url" value={logoUrl} />
|
<input type="hidden" name="logo_url" value={logoUrl} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="border-gray-100" />
|
<hr className="border-slate-100" />
|
||||||
|
|
||||||
{/* Restaurant Name */}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
{/* Restaurant Name */}
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<div>
|
||||||
Restoran / Mekan Adı
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">
|
||||||
</label>
|
Restoran / Mekan Adı
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<input
|
||||||
name="restaurant_name"
|
type="text"
|
||||||
defaultValue={defaultRestaurantName}
|
name="restaurant_name"
|
||||||
required
|
defaultValue={defaultRestaurantName}
|
||||||
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"
|
required
|
||||||
/>
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">
|
||||||
Konum Bilgisi
|
Konum Bilgisi
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="location"
|
name="location"
|
||||||
defaultValue={defaultLocation}
|
defaultValue={defaultLocation}
|
||||||
required
|
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"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Google Review Url */}
|
{/* Google Review Url */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">
|
||||||
Google Yorum Linki
|
Google Yorum Linki
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -158,18 +160,21 @@ export default function SettingsForm({
|
|||||||
name="google_review_url"
|
name="google_review_url"
|
||||||
defaultValue={defaultGoogleReviewUrl}
|
defaultValue={defaultGoogleReviewUrl}
|
||||||
placeholder="https://g.page/r/..."
|
placeholder="https://g.page/r/..."
|
||||||
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"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Eğer buraya link eklerseniz site açılışında ziyaretçilere yorum yapmaları için popup gösterilir.</p>
|
<p className="text-xs text-slate-500 mt-2 font-medium ml-1">Eğer buraya link eklerseniz, ziyaretçilere her 30 dakikada bir işletmenizi değerlendirmelerini isteyen bir açılır pencere (popup) gösterilir.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="pt-4 flex justify-end">
|
||||||
type="submit"
|
<button
|
||||||
disabled={isPending || uploading}
|
type="submit"
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
|
disabled={isPending || uploading}
|
||||||
>
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-8 rounded-xl transition-all shadow-sm hover:shadow hover:-translate-y-0.5 disabled:opacity-70 disabled:hover:translate-y-0 disabled:shadow-none flex items-center gap-2"
|
||||||
{isPending ? 'Kaydediliyor...' : 'Kaydet'}
|
>
|
||||||
</button>
|
<Save size={18} />
|
||||||
|
{isPending ? 'Kaydediliyor...' : 'Değişiklikleri Kaydet'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import SettingsForm from './SettingsForm'
|
import SettingsForm from './SettingsForm'
|
||||||
|
import { Settings as SettingsIcon } from 'lucide-react'
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Genel Ayarlar - Admin',
|
title: 'Genel Ayarlar - Admin',
|
||||||
@@ -18,10 +19,18 @@ export default async function SettingsPage() {
|
|||||||
const googleReviewUrl = settings['google_review_url'] || ''
|
const googleReviewUrl = settings['google_review_url'] || ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Genel Ayarlar</h1>
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-lg shadow-blue-600/20">
|
||||||
|
<SettingsIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Genel Ayarlar</h1>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">Sitenizin temel bilgilerini, logo ve bağlantılarını yapılandırın.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 max-w-2xl">
|
<div className="bg-white p-6 md:p-8 rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 max-w-3xl">
|
||||||
<SettingsForm
|
<SettingsForm
|
||||||
defaultRestaurantName={restaurantName}
|
defaultRestaurantName={restaurantName}
|
||||||
defaultLocation={location}
|
defaultLocation={location}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PageTransition } from '@/components/ui/PageTransition'
|
||||||
|
|
||||||
|
export default function Template({ children }: { children: React.ReactNode }) {
|
||||||
|
return <PageTransition>{children}</PageTransition>
|
||||||
|
}
|
||||||
@@ -8,63 +8,70 @@ export default function CreateUserForm() {
|
|||||||
const [state, formAction, isPending] = useActionState(createUser, undefined)
|
const [state, formAction, isPending] = useActionState(createUser, undefined)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
<div className="bg-white rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 p-6 md:p-8">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-3 mb-6">
|
||||||
<UserPlus size={20} className="text-blue-600" />
|
<div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Yeni Kullanıcı Ekle</h2>
|
<UserPlus size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-slate-800 tracking-tight">Yeni Kullanıcı Ekle</h2>
|
||||||
|
<p className="text-xs text-slate-500">Sisteme yeni bir yönetici veya editör ekleyin.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state?.success && (
|
{state?.success && (
|
||||||
<div className="bg-green-50 text-green-700 p-3 rounded-lg text-sm mb-4">
|
<div className="bg-emerald-50 text-emerald-700 px-4 py-3 rounded-xl text-sm font-medium mb-6 border border-emerald-100/50">
|
||||||
Kullanıcı başarıyla oluşturuldu.
|
Kullanıcı başarıyla oluşturuldu.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm mb-4">
|
<div className="bg-red-50 text-red-700 px-4 py-3 rounded-xl text-sm font-medium mb-6 border border-red-100/50">
|
||||||
{state.error}
|
{state.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form action={formAction} className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<form action={formAction} className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kullanıcı Adı *</label>
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Kullanıcı Adı <span className="text-red-500">*</span></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
required
|
required
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all"
|
||||||
|
placeholder="örn: ahmet"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Şifre * <span className="text-gray-400 font-normal">(min. 6 karakter)</span></label>
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Şifre <span className="text-red-500">*</span> <span className="text-slate-400 font-normal">(min. 6 karakter)</span></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
required
|
required
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all"
|
||||||
|
placeholder="••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Rol</label>
|
<label className="block text-sm font-semibold text-slate-700 mb-1.5">Rol</label>
|
||||||
<select
|
<select
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue="admin"
|
defaultValue="admin"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white outline-none text-sm transition-all appearance-none"
|
||||||
>
|
>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
<option value="editor">Editor</option>
|
<option value="editor">Editor</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-3 flex justify-end">
|
<div className="sm:col-span-3 flex justify-end mt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-5 rounded-lg text-sm transition-colors disabled:opacity-70 flex items-center gap-2"
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-xl text-sm transition-all shadow-sm hover:shadow-md hover:-translate-y-0.5 disabled:opacity-70 disabled:hover:translate-y-0 disabled:hover:shadow-sm flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<UserPlus size={16} />
|
<UserPlus size={18} />
|
||||||
{isPending ? 'Oluşturuluyor...' : 'Kullanıcı Oluştur'}
|
{isPending ? 'Oluşturuluyor...' : 'Kullanıcı Oluştur'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useActionState, useState } from 'react'
|
import { useActionState, useState } from 'react'
|
||||||
import { changePassword, deleteUser } from './actions'
|
import { changePassword, deleteUser } from './actions'
|
||||||
import { KeyRound, Trash2, ChevronDown, ChevronUp, Shield } from 'lucide-react'
|
import { KeyRound, Trash2, ChevronDown, ChevronUp, Shield, User as UserIcon } from 'lucide-react'
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
id: number
|
id: number
|
||||||
@@ -16,44 +16,44 @@ function ChangePasswordForm({ userId }: { userId: number }) {
|
|||||||
const [state, formAction, isPending] = useActionState(action, undefined)
|
const [state, formAction, isPending] = useActionState(action, undefined)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={formAction} className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-3">
|
<form action={formAction} className="p-5 bg-slate-50/80 border-t border-slate-100 space-y-4">
|
||||||
{state?.success && (
|
{state?.success && (
|
||||||
<p className="text-xs text-green-700 bg-green-50 px-3 py-2 rounded-lg">{state.message}</p>
|
<p className="text-sm font-medium text-emerald-700 bg-emerald-50 px-4 py-2.5 rounded-xl border border-emerald-100">{state.message}</p>
|
||||||
)}
|
)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<p className="text-xs text-red-700 bg-red-50 px-3 py-2 rounded-lg">{state.error}</p>
|
<p className="text-sm font-medium text-red-700 bg-red-50 px-4 py-2.5 rounded-xl border border-red-100">{state.error}</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">Yeni Şifre</label>
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5 uppercase tracking-wider">Yeni Şifre</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="new_password"
|
name="new_password"
|
||||||
required
|
required
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="Min. 6 karakter"
|
placeholder="Min. 6 karakter"
|
||||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-1">Şifre Tekrar</label>
|
<label className="block text-xs font-semibold text-slate-600 mb-1.5 uppercase tracking-wider">Şifre Tekrar</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="confirm_password"
|
name="confirm_password"
|
||||||
required
|
required
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="Tekrar giriniz"
|
placeholder="Tekrar giriniz"
|
||||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
className="w-full px-3 py-2 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end pt-1">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium py-1.5 px-4 rounded-lg transition-colors disabled:opacity-70 flex items-center gap-1.5"
|
className="bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium py-2 px-5 rounded-xl transition-all shadow-sm hover:shadow hover:-translate-y-0.5 disabled:opacity-70 disabled:hover:translate-y-0 flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<KeyRound size={14} />
|
<KeyRound size={16} />
|
||||||
{isPending ? 'Güncelleniyor...' : 'Şifreyi Güncelle'}
|
{isPending ? 'Güncelleniyor...' : 'Şifreyi Güncelle'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,43 +71,49 @@ function UserRow({ user }: { user: User }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-gray-100 rounded-xl overflow-hidden">
|
<div className="border border-slate-100 rounded-2xl overflow-hidden bg-white hover:border-slate-200 hover:shadow-[0_2px_15px_-3px_rgba(0,0,0,0.05)] transition-all duration-300">
|
||||||
<div className="flex items-center justify-between px-5 py-4 hover:bg-gray-50 transition-colors">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 sm:px-6 sm:py-5 gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-9 h-9 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-semibold text-sm uppercase">
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-50 to-blue-50 flex items-center justify-center text-indigo-600 font-bold text-lg uppercase shadow-sm border border-indigo-100/50">
|
||||||
{user.username[0]}
|
{user.username[0]}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 text-sm">{user.username}</p>
|
<p className="font-bold text-slate-800 text-base">{user.username}</p>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs font-medium text-slate-400 mt-0.5">
|
||||||
{new Date(user.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
Katılım: {new Date(user.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2 sm:gap-3 w-full sm:w-auto">
|
||||||
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider ${
|
||||||
user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'
|
user.role === 'admin' ? 'bg-purple-50 text-purple-600 border border-purple-100' : 'bg-slate-50 text-slate-500 border border-slate-100'
|
||||||
}`}>
|
}`}>
|
||||||
<Shield size={10} />
|
<Shield size={14} />
|
||||||
{user.role}
|
{user.role}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className="flex-1 sm:hidden"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(v => !v)}
|
onClick={() => setExpanded(v => !v)}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-amber-600 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors font-medium"
|
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-xl transition-all ${
|
||||||
|
expanded
|
||||||
|
? 'bg-amber-100 text-amber-700 shadow-inner'
|
||||||
|
: 'text-amber-600 bg-amber-50 hover:bg-amber-100'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<KeyRound size={13} />
|
<KeyRound size={16} />
|
||||||
Şifre
|
<span className="hidden sm:inline">Şifre İşlemleri</span>
|
||||||
{expanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
|
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-xl transition-colors"
|
||||||
title="Kullanıcıyı Sil"
|
title="Kullanıcıyı Sil"
|
||||||
>
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +125,27 @@ function UserRow({ user }: { user: User }) {
|
|||||||
|
|
||||||
export default function UserList({ users }: { users: User[] }) {
|
export default function UserList({ users }: { users: User[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
<div className="bg-white rounded-2xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-slate-100 p-6 md:p-8">
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Mevcut Kullanıcılar ({users.length})</h2>
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 bg-indigo-50 text-indigo-600 rounded-lg">
|
||||||
|
<UserIcon size={20} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-slate-800 tracking-tight">Sistem Kullanıcıları</h2>
|
||||||
|
<p className="text-xs text-slate-500">Toplam {users.length} kullanıcı kayıtlı.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{users.length === 0 ? (
|
{users.length === 0 ? (
|
||||||
<p className="text-sm text-gray-500 text-center py-6">Henüz kullanıcı yok.</p>
|
<div className="text-center py-12 px-4 border-2 border-dashed border-slate-200 rounded-2xl">
|
||||||
|
<div className="w-12 h-12 bg-slate-50 text-slate-400 rounded-xl flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UserIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-slate-600">Henüz kullanıcı yok.</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">Sisteme giriş yapabilmek için yukarıdan yeni bir kullanıcı ekleyin.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
{users.map(user => (
|
{users.map(user => (
|
||||||
<UserRow key={user.id} user={user} />
|
<UserRow key={user.id} user={user} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ export default async function UsersPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Kullanıcı Yönetimi</h1>
|
<div>
|
||||||
|
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Kullanıcı Yönetimi</h1>
|
||||||
|
<p className="text-sm text-slate-500 mt-1">Admin paneline erişebilecek yetkili kullanıcıları yönetin.</p>
|
||||||
|
</div>
|
||||||
<CreateUserForm />
|
<CreateUserForm />
|
||||||
<UserList users={users} />
|
<UserList users={users} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-14
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import cloudinary from '@/lib/cloudinary'
|
import { uploadToOpeninary } from '@/lib/openinary'
|
||||||
|
import { optimizedImage } from '@/lib/openinary-url'
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -10,21 +11,14 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Dosya bulunamadı.' }, { status: 400 })
|
return NextResponse.json({ error: 'Dosya bulunamadı.' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert file to buffer
|
// Upload to Openinary
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
const result = await uploadToOpeninary(file, 'moyqr');
|
||||||
const buffer = Buffer.from(arrayBuffer)
|
|
||||||
const base64 = `data:${file.type};base64,${buffer.toString('base64')}`
|
|
||||||
|
|
||||||
// Upload to Cloudinary
|
// Generate optimized URL
|
||||||
const result = await cloudinary.uploader.upload(base64, {
|
// e.g. "w_800,h_800,c_limit,q_auto,f_auto" parameters format for Openinary
|
||||||
folder: 'moy-qr/products',
|
const optimizedUrl = optimizedImage(result.path, 'w_800,h_800,c_limit,q_auto,f_auto');
|
||||||
transformation: [
|
|
||||||
{ width: 800, height: 800, crop: 'limit' },
|
|
||||||
{ quality: 'auto', fetch_format: 'auto' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({ url: result.secure_url })
|
return NextResponse.json({ url: optimizedUrl })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error)
|
console.error('Upload error:', error)
|
||||||
return NextResponse.json({ error: 'Yükleme başarısız oldu.' }, { status: 500 })
|
return NextResponse.json({ error: 'Yükleme başarısız oldu.' }, { status: 500 })
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { LogOut, Bell } from 'lucide-react'
|
||||||
|
import { logout } from '@/lib/auth'
|
||||||
|
import { redirect } from 'next/navigation'
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="h-16 bg-white/80 backdrop-blur-md border-b border-slate-200/60 flex items-center justify-between px-6 sticky top-0 z-10">
|
||||||
|
<div className="flex items-center text-sm text-slate-500 font-medium">
|
||||||
|
Hoş Geldiniz, <span className="text-slate-800 font-bold ml-1">Admin</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-colors relative">
|
||||||
|
<Bell size={20} />
|
||||||
|
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-blue-500 rounded-full border-2 border-white"></span>
|
||||||
|
</button>
|
||||||
|
<div className="w-px h-6 bg-slate-200"></div>
|
||||||
|
<form action={async () => {
|
||||||
|
'use server'
|
||||||
|
await logout()
|
||||||
|
redirect('/admin/login')
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-slate-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
<span className="hidden sm:inline">Çıkış Yap</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
import { LayoutDashboard, Package, Settings, Tags, Users2 } from 'lucide-react'
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: '/admin', icon: LayoutDashboard, label: 'Dashboard' },
|
||||||
|
{ href: '/admin/categories', icon: Tags, label: 'Kategoriler' },
|
||||||
|
{ href: '/admin/products', icon: Package, label: 'Ürünler' },
|
||||||
|
{ href: '/admin/users', icon: Users2, label: 'Kullanıcılar' },
|
||||||
|
{ href: '/admin/settings', icon: Settings, label: 'Ayarlar' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-64 bg-white/80 backdrop-blur-xl border-r border-slate-200/60 hidden md:flex flex-col shadow-sm">
|
||||||
|
<div className="p-6 border-b border-slate-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-blue-600 flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold text-lg leading-none">M</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold tracking-tight text-slate-800">Admin</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 p-4 space-y-1.5 overflow-y-auto">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const isActive = pathname === item.href || (item.href !== '/admin' && pathname.startsWith(item.href))
|
||||||
|
const Icon = item.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 font-medium ${
|
||||||
|
isActive
|
||||||
|
? 'bg-blue-50 text-blue-700 shadow-sm shadow-blue-100/50'
|
||||||
|
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon size={20} className={isActive ? 'text-blue-600' : 'text-slate-400'} strokeWidth={isActive ? 2.5 : 2} />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export function PageTransition({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL;
|
||||||
|
|
||||||
|
export function optimizedImage(path: string, params: string) {
|
||||||
|
return `${BASE}/t/${params}/${path}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export async function uploadToOpeninary(file: File, folder: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("files", file);
|
||||||
|
formData.append("folder", folder);
|
||||||
|
|
||||||
|
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error("Openinary upload error:", errorText);
|
||||||
|
throw new Error("Upload başarısız: " + errorText);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return data.files[0]; // { path, url, size, ... }
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'res.cloudinary.com',
|
hostname: 'res.cloudinary.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'media.ayris.tech',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
/* other config options here */
|
/* other config options here */
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 919 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 919 KiB After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,110 @@
|
|||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
dotenv.config({ path: '.env' })
|
||||||
|
dotenv.config({ path: '.env.local' })
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { default: prisma } = await import('../lib/prisma')
|
||||||
|
const { uploadToOpeninary } = await import('../lib/openinary')
|
||||||
|
const { optimizedImage } = await import('../lib/openinary-url')
|
||||||
|
|
||||||
|
console.log("Starting image migration from Cloudinary to Openinary...");
|
||||||
|
|
||||||
|
// FIX PREVIOUSLY CORRUPTED URLS
|
||||||
|
const corruptedProducts = await prisma.products.findMany({
|
||||||
|
where: { image_url: { startsWith: 'undefined/t/' } }
|
||||||
|
});
|
||||||
|
if (corruptedProducts.length > 0) {
|
||||||
|
console.log(`Fixing ${corruptedProducts.length} corrupted products...`);
|
||||||
|
for (const prod of corruptedProducts) {
|
||||||
|
const fixedUrl = prod.image_url?.replace('undefined', process.env.NEXT_PUBLIC_OPENINARY_URL || 'https://media.ayris.tech');
|
||||||
|
if (fixedUrl) {
|
||||||
|
await prisma.products.update({ where: { id: prod.id }, data: { image_url: fixedUrl } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const corruptedLogo = await prisma.settings.findUnique({ where: { key: 'logo_url' } });
|
||||||
|
if (corruptedLogo?.value?.startsWith('undefined/t/')) {
|
||||||
|
const fixedUrl = corruptedLogo.value.replace('undefined', process.env.NEXT_PUBLIC_OPENINARY_URL || 'https://media.ayris.tech');
|
||||||
|
await prisma.settings.update({ where: { key: 'logo_url' }, data: { value: fixedUrl } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAndUpload(url: string, fileName: string): Promise<string> {
|
||||||
|
console.log(`Downloading ${url}...`);
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error(`Failed to download ${url}`);
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
const blob = new Blob([buffer], { type: response.headers.get('content-type') || 'image/jpeg' });
|
||||||
|
const file = new File([blob], fileName, { type: blob.type });
|
||||||
|
|
||||||
|
console.log(`Uploading ${fileName} to Openinary...`);
|
||||||
|
const result = await uploadToOpeninary(file, 'moyqr');
|
||||||
|
|
||||||
|
return optimizedImage(result.path, 'w_800,h_800,c_limit,q_auto,f_auto');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Settings Logo
|
||||||
|
const logoSetting = await prisma.settings.findUnique({
|
||||||
|
where: { key: 'logo_url' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logoSetting?.value && logoSetting.value.includes('res.cloudinary.com')) {
|
||||||
|
try {
|
||||||
|
const newUrl = await downloadAndUpload(logoSetting.value, 'logo.png');
|
||||||
|
await prisma.settings.update({
|
||||||
|
where: { key: 'logo_url' },
|
||||||
|
data: { value: newUrl }
|
||||||
|
});
|
||||||
|
console.log(`Logo updated successfully: ${newUrl}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to migrate logo:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Products
|
||||||
|
const products = await prisma.products.findMany({
|
||||||
|
where: {
|
||||||
|
image_url: {
|
||||||
|
contains: 'res.cloudinary.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${products.length} products with Cloudinary images.`);
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
if (!product.image_url) continue;
|
||||||
|
try {
|
||||||
|
const ext = product.image_url.split('.').pop() || 'jpg';
|
||||||
|
const fileName = `product-${product.id}.${ext}`;
|
||||||
|
const newUrl = await downloadAndUpload(product.image_url, fileName);
|
||||||
|
|
||||||
|
await prisma.products.update({
|
||||||
|
where: { id: product.id },
|
||||||
|
data: { image_url: newUrl }
|
||||||
|
});
|
||||||
|
console.log(`Product ${product.id} updated successfully: ${newUrl}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to migrate product ${product.id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migration finished.");
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
try {
|
||||||
|
const { default: prisma } = await import('../lib/prisma')
|
||||||
|
await prisma.$disconnect()
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user