feat: modernize admin panel UI and add openinary integration

This commit is contained in:
Mustafa Yildiz
2026-07-12 15:10:50 +03:00
parent 378a1e033a
commit ca1e849e80
20 changed files with 536 additions and 248 deletions
+19 -16
View File
@@ -19,18 +19,21 @@ export default async function CategoriesPage() {
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
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Kategoriler</h1>
<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>
</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">
<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">
<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 text-center">Sıra</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>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tbody className="divide-y divide-slate-100/80">
{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>
<tr key={category.id} className="hover:bg-slate-50/50 transition-colors group">
<td className="py-4 px-6 font-semibold text-slate-800">{category.name_tr}</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">
<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
</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'
<span className={`inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold ${
category.status === 1 ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'
}`}>
{category.status === 1 ? 'Aktif' : 'Pasif'}
</span>
</td>
<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">
<Pencil size={18} />
</Link>
@@ -67,8 +70,8 @@ export default async function CategoriesPage() {
))}
{categories.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
Henüz kategori bulunmuyor.
<td colSpan={5} className="py-12 text-center text-slate-500">
Henüz kategori bulunmuyor. Yeni bir kategori ekleyerek başlayın.
</td>
</tr>
)}
+11 -49
View File
@@ -1,7 +1,5 @@
import Link from 'next/link'
import { LayoutDashboard, LogOut, Package, Settings, Tags, Users2 } from 'lucide-react'
import { logout } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { Sidebar } from '@/components/admin/Sidebar'
import { Header } from '@/components/admin/Header'
export default function AdminLayout({
children,
@@ -9,54 +7,18 @@ export default function AdminLayout({
children: React.ReactNode
}) {
return (
<div className="flex h-screen bg-gray-100 overflow-hidden">
{/* Sidebar */}
<aside className="w-64 bg-white shadow-md flex flex-col hidden md:flex">
<div className="p-4 border-b">
<h2 className="text-xl font-bold text-gray-800">Admin Panel</h2>
</div>
<nav className="flex-1 p-4 space-y-2">
<Link href="/admin" className="flex items-center gap-3 px-3 py-2 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors">
<LayoutDashboard size={20} />
<span>Dashboard</span>
</Link>
<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">
<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>
<div className="flex h-screen bg-[#F8FAFC] overflow-hidden selection:bg-blue-100 selection:text-blue-900">
<Sidebar />
{/* Main Content */}
<main className="flex-1 overflow-y-auto bg-gray-50 p-6 md:p-8">
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Header />
<main className="flex-1 overflow-y-auto p-6 md:p-8 lg:p-10">
<div className="max-w-6xl mx-auto">
{children}
</div>
</main>
{/* Mobile Bottom Nav (Optional, simpler for now just a top bar) */}
</div>
</div>
)
}
+63 -29
View File
@@ -1,5 +1,5 @@
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'
export const metadata = {
@@ -14,55 +14,89 @@ export default async function AdminDashboard() {
])
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<div className="space-y-8">
<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 */}
<div className="grid grid-cols-1 md: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="p-3 bg-blue-100 text-blue-600 rounded-lg">
<Tags size={24} />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<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-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={28} strokeWidth={1.5} />
</div>
<div>
<p className="text-sm text-gray-500 font-medium">Kategori Sayısı</p>
<p className="text-2xl font-bold text-gray-900">{totalCategories}</p>
<p className="text-sm text-slate-500 font-medium">Kategori Sayısı</p>
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{totalCategories}</p>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
<div className="p-3 bg-indigo-100 text-indigo-600 rounded-lg">
<Package size={24} />
<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-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={28} strokeWidth={1.5} />
</div>
<div>
<p className="text-sm text-gray-500 font-medium">Toplam Ürün</p>
<p className="text-2xl font-bold text-gray-900">{totalProducts}</p>
<p className="text-sm text-slate-500 font-medium">Toplam Ürün</p>
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{totalProducts}</p>
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 flex items-center gap-4">
<div className="p-3 bg-green-100 text-green-600 rounded-lg">
<TrendingUp size={24} />
<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-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={28} strokeWidth={1.5} />
</div>
<div>
<p className="text-sm text-gray-500 font-medium">Aktif Ürünler</p>
<p className="text-2xl font-bold text-gray-900">{activeProducts}</p>
<p className="text-sm text-slate-500 font-medium">Aktif Ürünler</p>
<p className="text-3xl font-bold text-slate-900 tracking-tight mt-1">{activeProducts}</p>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h2 className="text-lg font-semibold mb-4">Hızlı Kısayollar</h2>
<div className="flex flex-col gap-3">
<Link href="/admin/products" className="text-blue-600 hover:text-blue-800 font-medium flex items-center gap-2">
<Package size={18} /> Ürünleri Yönet
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<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="p-6 border-b border-slate-100">
<h2 className="text-lg font-bold text-slate-800">Hızlı Kısayollar</h2>
</div>
<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 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 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ü </p>
</div>
</div>
<ChevronRight size={20} className="text-slate-400 group-hover:text-emerald-600 transition-colors" />
</Link>
</div>
</div>
+34 -30
View File
@@ -1,5 +1,5 @@
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 Link from 'next/link'
import DeleteButton from './DeleteButton'
@@ -19,41 +19,45 @@ export default async function ProductsPage() {
})
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">Ürün Yönetimi</h1>
<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">
<Plus size={20} />
<span>Yeni Ürün Ekle</span>
<div className="space-y-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-2xl font-extrabold text-slate-900 tracking-tight">Ürün Yönetimi</h1>
<p className="text-sm text-slate-500 mt-1">Tüm kategorilerdeki ürünlerinizi görüntüleyin ve düzenleyin.</p>
</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>
</div>
<div className="space-y-8">
{categories.map((category) => (
<div key={category.id} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
<h2 className="text-lg font-bold text-gray-800">{category.name_tr}</h2>
<span className="text-sm text-gray-500 font-medium">{category.products.length} ürün</span>
<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-slate-50/50 px-6 py-5 border-b border-slate-100 flex justify-between items-center">
<h2 className="text-lg font-bold text-slate-800 tracking-tight">{category.name_tr}</h2>
<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>
{category.products.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-gray-100">
<th className="py-3 px-6 text-sm font-semibold text-gray-600 w-16">Resim</th>
<th className="py-3 px-6 text-sm font-semibold text-gray-600">İsim</th>
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Fiyat</th>
<th className="py-3 px-6 text-sm font-semibold text-gray-600">Durum</th>
<th className="py-3 px-6 text-sm font-semibold text-gray-600 text-right">İşlemler</th>
<tr className="border-b border-slate-100 text-xs uppercase tracking-wider font-semibold text-slate-500">
<th className="py-4 px-6 w-16">Resim</th>
<th className="py-4 px-6">İsim</th>
<th className="py-4 px-6">Fiyat</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-50">
<tbody className="divide-y divide-slate-100/80">
{category.products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50 transition-colors">
<td className="py-3 px-6">
<tr key={product.id} className="hover:bg-slate-50/50 transition-colors group">
<td className="py-4 px-6">
{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
src={product.image_url}
alt={product.name_tr}
@@ -62,20 +66,20 @@ export default async function ProductsPage() {
/>
</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} />
</div>
)}
</td>
<td className="py-3 px-6 font-medium text-gray-900">{product.name_tr}</td>
<td className="py-3 px-6 text-gray-600">{product.price.toString()}</td>
<td className="py-3 px-6">
<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'}`}>
<td className="py-4 px-6 font-semibold text-slate-800">{product.name_tr}</td>
<td className="py-4 px-6 font-medium text-slate-600">{product.price.toString()}</td>
<td className="py-4 px-6 text-center">
<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'}
</span>
</td>
<td className="py-3 px-6 text-right">
<div className="flex justify-end gap-2">
<td className="py-4 px-6 text-right">
<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">
<Pencil size={18} />
</Link>
@@ -88,8 +92,8 @@ export default async function ProductsPage() {
</table>
</div>
) : (
<div className="py-6 px-6 text-gray-500 text-sm">
Bu kategoride henüz ürün yok.
<div className="py-10 px-6 text-center text-slate-500 text-sm">
Bu kategoride henüz ürün bulunmuyor.
</div>
)}
</div>
+32 -27
View File
@@ -3,7 +3,7 @@
import { useActionState, useState, useRef } from 'react'
import { saveSettings } from './actions'
import Image from 'next/image'
import { Upload, X, Loader2 } from 'lucide-react'
import { Upload, X, Loader2, Save } from 'lucide-react'
export default function SettingsForm({
defaultRestaurantName,
@@ -49,62 +49,62 @@ export default function SettingsForm({
}
return (
<form action={action} className="space-y-6">
<form action={action} className="space-y-8">
{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}
</div>
)}
{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}
</div>
)}
{/* Logo Upload */}
<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
</label>
<div className="flex items-start gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6">
{/* 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 ? (
<>
<Image
src={logoUrl}
alt="Logo önizleme"
fill
className="object-contain p-2"
className="object-contain p-3"
/>
<button
type="button"
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>
</>
) : (
<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>
{/* Upload button */}
<div className="flex flex-col gap-2 justify-center h-20">
<div className="flex flex-col gap-2.5 justify-center">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
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
? <><Loader2 size={16} className="animate-spin" /> Yükleniyor...</>
: <><Upload size={16} /> Logo Yükle</>
? <><Loader2 size={18} className="animate-spin" /> Yükleniyor...</>
: <><Upload size={18} /> Yeni Logo Seç</>
}
</button>
<p className="text-xs text-gray-500">PNG, SVG, WEBP Maks. 5MB</p>
{uploadError && <p className="text-xs text-red-600">{uploadError}</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 font-medium ml-1">{uploadError}</p>}
</div>
</div>
@@ -118,11 +118,12 @@ export default function SettingsForm({
<input type="hidden" name="logo_url" value={logoUrl} />
</div>
<hr className="border-gray-100" />
<hr className="border-slate-100" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Restaurant Name */}
<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">
Restoran / Mekan Adı
</label>
<input
@@ -130,13 +131,13 @@ export default function SettingsForm({
name="restaurant_name"
defaultValue={defaultRestaurantName}
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>
{/* Location */}
<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
</label>
<input
@@ -144,13 +145,14 @@ export default function SettingsForm({
name="location"
defaultValue={defaultLocation}
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>
{/* Google Review Url */}
<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
</label>
<input
@@ -158,18 +160,21 @@ export default function SettingsForm({
name="google_review_url"
defaultValue={defaultGoogleReviewUrl}
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 className="pt-4 flex justify-end">
<button
type="submit"
disabled={isPending || uploading}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors disabled:opacity-70"
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'}
<Save size={18} />
{isPending ? 'Kaydediliyor...' : 'Değişiklikleri Kaydet'}
</button>
</div>
</form>
)
}
+12 -3
View File
@@ -1,5 +1,6 @@
import prisma from '@/lib/prisma'
import SettingsForm from './SettingsForm'
import { Settings as SettingsIcon } from 'lucide-react'
export const metadata = {
title: 'Genel Ayarlar - Admin',
@@ -18,10 +19,18 @@ export default async function SettingsPage() {
const googleReviewUrl = settings['google_review_url'] || ''
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Genel Ayarlar</h1>
<div className="space-y-8">
<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
defaultRestaurantName={restaurantName}
defaultLocation={location}
+5
View File
@@ -0,0 +1,5 @@
import { PageTransition } from '@/components/ui/PageTransition'
export default function Template({ children }: { children: React.ReactNode }) {
return <PageTransition>{children}</PageTransition>
}
+23 -16
View File
@@ -8,63 +8,70 @@ export default function CreateUserForm() {
const [state, formAction, isPending] = useActionState(createUser, undefined)
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<div className="flex items-center gap-2 mb-4">
<UserPlus size={20} className="text-blue-600" />
<h2 className="text-lg font-semibold text-gray-800">Yeni Kullanıcı Ekle</h2>
<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-3 mb-6">
<div className="p-2 bg-blue-50 text-blue-600 rounded-lg">
<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>
{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.
</div>
)}
{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}
</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>
<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
type="text"
name="username"
required
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>
<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
type="password"
name="password"
required
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>
<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
name="role"
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="editor">Editor</option>
</select>
</div>
<div className="sm:col-span-3 flex justify-end">
<div className="sm:col-span-3 flex justify-end mt-2">
<button
type="submit"
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'}
</button>
</div>
+54 -33
View File
@@ -2,7 +2,7 @@
import { useActionState, useState } from 'react'
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 = {
id: number
@@ -16,44 +16,44 @@ function ChangePasswordForm({ userId }: { userId: number }) {
const [state, formAction, isPending] = useActionState(action, undefined)
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 && (
<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 && (
<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>
<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
type="password"
name="new_password"
required
autoComplete="new-password"
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>
<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
type="password"
name="confirm_password"
required
autoComplete="new-password"
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 className="flex justify-end">
<div className="flex justify-end pt-1">
<button
type="submit"
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'}
</button>
</div>
@@ -71,43 +71,49 @@ function UserRow({ user }: { user: User }) {
}
return (
<div className="border border-gray-100 rounded-xl overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 hover:bg-gray-50 transition-colors">
<div className="flex items-center gap-3">
<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="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 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-4">
<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]}
</div>
<div>
<p className="font-medium text-gray-900 text-sm">{user.username}</p>
<p className="text-xs text-gray-400">
{new Date(user.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
<p className="font-bold text-slate-800 text-base">{user.username}</p>
<p className="text-xs font-medium text-slate-400 mt-0.5">
Katılım: {new Date(user.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric' })}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'
<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.5 px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider ${
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}
</span>
<div className="flex-1 sm:hidden"></div>
<button
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} />
Şifre
{expanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
<KeyRound size={16} />
<span className="hidden sm:inline">Şifre İşlemleri</span>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
<button
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"
>
<Trash2 size={16} />
<Trash2 size={18} />
</button>
</div>
</div>
@@ -119,12 +125,27 @@ function UserRow({ user }: { user: User }) {
export default function UserList({ users }: { users: User[] }) {
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
<h2 className="text-lg font-semibold text-gray-800 mb-4">Mevcut Kullanıcılar ({users.length})</h2>
<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-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 ? (
<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 => (
<UserRow key={user.id} user={user} />
))}
+5 -2
View File
@@ -18,8 +18,11 @@ export default async function UsersPage() {
})
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Kullanıcı Yönetimi</h1>
<div className="space-y-8">
<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 />
<UserList users={users} />
</div>
+8 -14
View File
@@ -1,5 +1,6 @@
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) {
try {
@@ -10,21 +11,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Dosya bulunamadı.' }, { status: 400 })
}
// Convert file to buffer
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const base64 = `data:${file.type};base64,${buffer.toString('base64')}`
// Upload to Openinary
const result = await uploadToOpeninary(file, 'moyqr');
// Upload to Cloudinary
const result = await cloudinary.uploader.upload(base64, {
folder: 'moy-qr/products',
transformation: [
{ width: 800, height: 800, crop: 'limit' },
{ quality: 'auto', fetch_format: 'auto' }
]
})
// Generate optimized URL
// e.g. "w_800,h_800,c_limit,q_auto,f_auto" parameters format for Openinary
const optimizedUrl = optimizedImage(result.path, 'w_800,h_800,c_limit,q_auto,f_auto');
return NextResponse.json({ url: result.secure_url })
return NextResponse.json({ url: optimizedUrl })
} catch (error) {
console.error('Upload error:', error)
return NextResponse.json({ error: 'Yükleme başarısız oldu.' }, { status: 500 })
+34
View File
@@ -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>
)
}
+51
View File
@@ -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>
)
}
+18
View File
@@ -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>
)
}
+5
View File
@@ -0,0 +1,5 @@
const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL;
export function optimizedImage(path: string, params: string) {
return `${BASE}/t/${params}/${path}`;
}
+19
View File
@@ -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, ... }
}
+4
View File
@@ -15,6 +15,10 @@ const nextConfig: NextConfig = {
protocol: 'https',
hostname: 'res.cloudinary.com',
},
{
protocol: 'https',
hostname: 'media.ayris.tech',
},
],
},
/* other config options here */
BIN
View File
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

+110
View File
@@ -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
}
})