feat: add dynamic site settings, hero media, admin panels, and database integration
This commit is contained in:
+20
-6
@@ -7,21 +7,35 @@ import InstagramFeed from "@/components/InstagramFeed";
|
||||
import Contact from "@/components/Contact";
|
||||
import Footer from "@/components/Footer";
|
||||
import { getDictionary } from "../dictionaries";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export default async function Home({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
const dict = await getDictionary(lang);
|
||||
|
||||
const dbPhotos = await prisma.gallery.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const dbServices = await prisma.service.findMany({
|
||||
orderBy: { createdAt: 'asc' }
|
||||
});
|
||||
|
||||
const heroMediaList = await prisma.heroMedia.findMany();
|
||||
const dbHeroMedia = heroMediaList.length > 0 ? heroMediaList[0] : null;
|
||||
|
||||
const dbSettings = await prisma.siteSettings.findFirst();
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-brand-white">
|
||||
<Header dict={dict} lang={lang} />
|
||||
<Hero dict={dict} />
|
||||
<Header dict={dict} lang={lang} dbSettings={dbSettings} />
|
||||
<Hero dict={dict} dbHeroMedia={dbHeroMedia} />
|
||||
<About dict={dict} />
|
||||
<Services dict={dict} />
|
||||
<Gallery dict={dict} />
|
||||
<Services dict={dict} dbServices={dbServices} />
|
||||
<Gallery dict={dict} dbPhotos={dbPhotos} />
|
||||
<InstagramFeed dict={dict} />
|
||||
<Contact dict={dict} />
|
||||
<Footer dict={dict} />
|
||||
<Contact dict={dict} dbSettings={dbSettings} />
|
||||
<Footer dict={dict} dbSettings={dbSettings} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@/lib/db';
|
||||
import { createSession, deleteSession } from '@/lib/auth';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Auth Actions
|
||||
export async function login(formData: FormData) {
|
||||
const username = formData.get('username') as string;
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
if (!username || !password) {
|
||||
return { error: 'Kullanıcı adı ve şifre zorunludur' };
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
return { error: 'Hatalı kullanıcı adı veya şifre' };
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) {
|
||||
return { error: 'Hatalı kullanıcı adı veya şifre' };
|
||||
}
|
||||
|
||||
await createSession(user.id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await deleteSession();
|
||||
}
|
||||
|
||||
// Contact Actions
|
||||
export async function markContactAsRead(id: string) {
|
||||
await prisma.contactMessage.update({
|
||||
where: { id },
|
||||
data: { isRead: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteContact(id: string) {
|
||||
await prisma.contactMessage.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Gallery Actions
|
||||
export async function addGalleryItem(data: { title: string, category: string, imageUrl: string }) {
|
||||
await prisma.gallery.create({ data });
|
||||
}
|
||||
|
||||
export async function deleteGalleryItem(id: string) {
|
||||
await prisma.gallery.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Service Actions
|
||||
export async function addServiceItem(data: { title: string, description: string, iconUrl?: string }) {
|
||||
await prisma.service.create({ data });
|
||||
}
|
||||
|
||||
export async function deleteServiceItem(id: string) {
|
||||
await prisma.service.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function updateServiceItem(id: string, data: { title: string, description: string, iconUrl?: string }) {
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// Hero Actions
|
||||
export async function updateHeroMedia(url: string, type: string) {
|
||||
// We only want one hero media. We can delete all and create a new one.
|
||||
await prisma.heroMedia.deleteMany({});
|
||||
await prisma.heroMedia.create({
|
||||
data: { url, type }
|
||||
});
|
||||
}
|
||||
|
||||
// Site Settings Actions
|
||||
export async function updateSiteSettings(data: {
|
||||
logoUrl?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
addressText1?: string;
|
||||
addressText2?: string;
|
||||
workingHours?: string;
|
||||
instagramUrl?: string;
|
||||
facebookUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
whatsappNumber?: string;
|
||||
}) {
|
||||
const existing = await prisma.siteSettings.findFirst();
|
||||
if (existing) {
|
||||
await prisma.siteSettings.update({
|
||||
where: { id: existing.id },
|
||||
data
|
||||
});
|
||||
} else {
|
||||
await prisma.siteSettings.create({ data });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { LayoutDashboard, Image as ImageIcon, Briefcase, Mail, LogOut, Settings } from 'lucide-react';
|
||||
import { logout } from '../actions';
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
||||
{ name: 'Hero (Ana Ekran)', href: '/admin/hero', icon: ImageIcon },
|
||||
{ name: 'Galeri', href: '/admin/gallery', icon: ImageIcon },
|
||||
{ name: 'Hizmetler', href: '/admin/services', icon: Briefcase },
|
||||
{ name: 'İletişim Mesajları', href: '/admin/contact', icon: Mail },
|
||||
{ name: 'Site Ayarları', href: '/admin/settings', icon: Settings },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
router.push('/admin/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 hidden md:flex flex-col h-screen sticky top-0">
|
||||
<div className="p-6 border-b border-slate-200 dark:border-slate-800">
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Admin Panel</h2>
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-medium'
|
||||
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-slate-600 hover:bg-red-50 hover:text-red-600 dark:text-slate-400 dark:hover:bg-red-900/30 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, CheckCircle } from 'lucide-react';
|
||||
import { deleteContact, markContactAsRead } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function ContactAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const messages = await prisma.contactMessage.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-8">İletişim Mesajları</h1>
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800">
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Tarih</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Gönderen</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">E-posta</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Konu</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Mesaj</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Durum</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{messages.map(msg => (
|
||||
<tr key={msg.id} className="border-b border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
||||
<td className="p-4 text-sm">{msg.createdAt.toLocaleDateString('tr-TR')}</td>
|
||||
<td className="p-4 text-sm font-medium">{msg.name}</td>
|
||||
<td className="p-4 text-sm text-slate-500 dark:text-slate-400">{msg.email}</td>
|
||||
<td className="p-4 text-sm">{msg.subject || '-'}</td>
|
||||
<td className="p-4 text-sm max-w-xs truncate" title={msg.message}>{msg.message}</td>
|
||||
<td className="p-4 text-sm">
|
||||
{msg.isRead ? (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
Okundu
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
Yeni
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{!msg.isRead && (
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await markContactAsRead(msg.id);
|
||||
revalidatePath('/admin/contact');
|
||||
}}>
|
||||
<button type="submit" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" title="Okundu İşaretle">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteContact(msg.id);
|
||||
revalidatePath('/admin/contact');
|
||||
}}>
|
||||
<button type="submit" className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" title="Sil">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{messages.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-slate-500">Hiç mesaj bulunamadı.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, Plus, Image as ImageIcon } from 'lucide-react';
|
||||
import { addGalleryItem, deleteGalleryItem } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { uploadImage } from '@/lib/cloudinary';
|
||||
|
||||
export default async function GalleryAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const galleryItems = await prisma.gallery.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Galeri Yönetimi</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Ekleme Formu */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 sticky top-8">
|
||||
<h2 className="text-xl font-bold mb-6">Yeni Resim Ekle</h2>
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const title = formData.get('title') as string;
|
||||
const category = formData.get('category') as string;
|
||||
const imageFile = formData.get('image') as File;
|
||||
|
||||
if (title && category && imageFile && imageFile.size > 0) {
|
||||
const imageUrl = await uploadImage(imageFile);
|
||||
await addGalleryItem({ title, category, imageUrl });
|
||||
revalidatePath('/admin/gallery');
|
||||
}
|
||||
}} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Başlık</label>
|
||||
<input type="text" name="title" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Kategori</label>
|
||||
<select name="category" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">Seçiniz...</option>
|
||||
<option value="beach">Plaj (Beach)</option>
|
||||
<option value="restaurant">Restoran (Restaurant)</option>
|
||||
<option value="events">Etkinlikler (Events)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Resim Yükle</label>
|
||||
<input type="file" name="image" accept="image/*" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" />
|
||||
</div>
|
||||
<button type="submit" className="w-full flex items-center justify-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-lg transition-colors">
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Ekle</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listeleme */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h2 className="text-xl font-bold mb-6">Mevcut Resimler ({galleryItems.length})</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{galleryItems.map(item => (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<div className="aspect-video w-full overflow-hidden bg-slate-200 dark:bg-slate-700">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-slate-800 dark:text-white truncate">{item.title}</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{item.category}</p>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteGalleryItem(item.id);
|
||||
revalidatePath('/admin/gallery');
|
||||
}}>
|
||||
<button type="submit" className="p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg shadow-lg transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{galleryItems.length === 0 && (
|
||||
<div className="col-span-full py-8 text-center text-slate-500">
|
||||
Galeriye henüz resim eklenmemiş.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Image as ImageIcon, Video, Upload } from 'lucide-react';
|
||||
import { updateHeroMedia } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { uploadImage } from '@/lib/cloudinary';
|
||||
|
||||
export default async function HeroAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const heroMediaList = await prisma.heroMedia.findMany();
|
||||
const currentHero = heroMediaList.length > 0 ? heroMediaList[0] : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Hero (Ana Ekran) Yönetimi</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Yükleme Formu */}
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 h-fit">
|
||||
<h2 className="text-xl font-bold mb-6">Yeni Medya Yükle</h2>
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const file = formData.get('media') as File;
|
||||
|
||||
if (file && file.size > 0) {
|
||||
const url = await uploadImage(file);
|
||||
// Cloudinary returns video format URLs or we can infer from file type
|
||||
const type = file.type.startsWith('video/') ? 'video' : 'image';
|
||||
await updateHeroMedia(url, type);
|
||||
revalidatePath('/admin/hero');
|
||||
}
|
||||
}} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Resim veya Video Seçin (Maksimum 50MB)
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="media"
|
||||
accept="image/*,video/*"
|
||||
required
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
Yeni bir dosya yüklediğinizde, mevcut olan otomatik olarak silinir ve yerini alır. Sadece bir adet Hero medyası bulunabilir.
|
||||
</p>
|
||||
</div>
|
||||
<button type="submit" className="w-full flex items-center justify-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-lg transition-colors">
|
||||
<Upload className="w-5 h-5" />
|
||||
<span>Yükle ve Güncelle</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Mevcut Medya */}
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h2 className="text-xl font-bold mb-6">Aktif Medya</h2>
|
||||
{currentHero ? (
|
||||
<div className="rounded-xl overflow-hidden border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<div className="aspect-[16/9] w-full overflow-hidden bg-slate-200 dark:bg-slate-700 relative">
|
||||
{currentHero.type === 'video' ? (
|
||||
<video
|
||||
src={currentHero.url}
|
||||
controls
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={currentHero.url} alt="Hero" className="w-full h-full object-cover" />
|
||||
)}
|
||||
|
||||
<div className="absolute top-2 right-2 bg-black/70 backdrop-blur-md text-white text-xs px-2 py-1 rounded flex items-center space-x-1">
|
||||
{currentHero.type === 'video' ? <Video className="w-3 h-3" /> : <ImageIcon className="w-3 h-3" />}
|
||||
<span>{currentHero.type === 'video' ? 'Video' : 'Görsel'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-12 text-center text-slate-500 bg-slate-50 dark:bg-slate-800 rounded-xl border border-dashed border-slate-300 dark:border-slate-700">
|
||||
Henüz medya yüklenmemiş. Varsayılan resim gösteriliyor.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import '../globals.css';
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const session = await getSession();
|
||||
|
||||
return (
|
||||
<html lang="tr">
|
||||
<body>
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-50 flex">
|
||||
{session && <Sidebar />}
|
||||
<main className="flex-1 overflow-x-hidden p-6 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { login } from '../actions';
|
||||
import { Lock } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const res = await login(formData);
|
||||
|
||||
if (res?.error) {
|
||||
setError(res.error);
|
||||
setLoading(false);
|
||||
} else {
|
||||
router.push('/admin');
|
||||
router.refresh(); // Refresh to update layout sidebar
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[80vh]">
|
||||
<div className="w-full max-w-md bg-white dark:bg-slate-900 shadow-xl rounded-2xl p-8 border border-slate-200 dark:border-slate-800">
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-full text-blue-600 dark:text-blue-400">
|
||||
<Lock className="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center text-slate-800 dark:text-white mb-8">
|
||||
Admin Girişi
|
||||
</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm text-center mb-6 border border-red-100 dark:border-red-800">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Kullanıcı Adı
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-slate-900 dark:text-white transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Şifre
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-slate-900 dark:text-white transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Giriş Yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { ImageIcon, Briefcase, Mail } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
const galleryCount = await prisma.gallery.count();
|
||||
const serviceCount = await prisma.service.count();
|
||||
const unreadMessages = await prisma.contactMessage.count({ where: { isRead: false } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-8">Dashboard Özeti</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Link href="/admin/gallery" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-blue-500 dark:hover:border-blue-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-xl">
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Galeri Resimleri</p>
|
||||
<p className="text-2xl font-bold">{galleryCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/services" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-purple-500 dark:hover:border-purple-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-xl">
|
||||
<Briefcase className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Hizmetlerimiz</p>
|
||||
<p className="text-2xl font-bold">{serviceCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/contact" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-green-500 dark:hover:border-green-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded-xl">
|
||||
<Mail className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Okunmamış Mesaj</p>
|
||||
<p className="text-2xl font-bold">{unreadMessages}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, Plus, Briefcase, Edit, X } from 'lucide-react';
|
||||
import { addServiceItem, deleteServiceItem, updateServiceItem } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { uploadImage } from '@/lib/cloudinary';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function ServicesAdminPage({ searchParams }: { searchParams: Promise<{ edit?: string }> }) {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const { edit } = await searchParams;
|
||||
|
||||
const services = await prisma.service.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const editingService = edit ? services.find(s => s.id === edit) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Hizmetlerimiz Yönetimi</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Ekleme / Düzenleme Formu */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 sticky top-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold">{editingService ? "Hizmeti Düzenle" : "Yeni Hizmet Ekle"}</h2>
|
||||
{editingService && (
|
||||
<Link href="/admin/services" className="text-slate-400 hover:text-slate-600">
|
||||
<X className="w-5 h-5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const id = formData.get('id') as string;
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
const iconFile = formData.get('icon') as File;
|
||||
|
||||
if (title && description) {
|
||||
let iconUrl = editingService?.iconUrl || '';
|
||||
if (iconFile && iconFile.size > 0) {
|
||||
iconUrl = await uploadImage(iconFile);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
await updateServiceItem(id, { title, description, iconUrl });
|
||||
} else {
|
||||
await addServiceItem({ title, description, iconUrl });
|
||||
}
|
||||
|
||||
revalidatePath('/admin/services');
|
||||
redirect('/admin/services');
|
||||
}
|
||||
}} className="space-y-4">
|
||||
|
||||
{editingService && <input type="hidden" name="id" value={editingService.id} />}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Başlık</label>
|
||||
<input type="text" name="title" defaultValue={editingService?.title || ""} required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Açıklama</label>
|
||||
<textarea name="description" defaultValue={editingService?.description || ""} required rows={4} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Resim Yükle {editingService ? "(Değiştirmek için seçin)" : ""}</label>
|
||||
<input type="file" name="icon" accept="image/*" required={!editingService} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-purple-50 file:text-purple-700 hover:file:bg-purple-100" />
|
||||
{editingService?.iconUrl && (
|
||||
<div className="mt-2 text-xs text-slate-500">Mevcut resim korunuyor. Yeni resim seçerseniz değiştirilecektir.</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="submit" className="w-full flex items-center justify-center space-x-2 bg-purple-600 hover:bg-purple-700 text-white py-2.5 rounded-lg transition-colors">
|
||||
{editingService ? <Edit className="w-5 h-5" /> : <Plus className="w-5 h-5" />}
|
||||
<span>{editingService ? "Güncelle" : "Ekle"}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listeleme */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h2 className="text-xl font-bold mb-6">Mevcut Hizmetler ({services.length})</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{services.map(item => (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<div className="aspect-video w-full overflow-hidden bg-slate-200 dark:bg-slate-700">
|
||||
{item.iconUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={item.iconUrl} alt={item.title} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Briefcase className="w-8 h-8 text-slate-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-semibold text-slate-800 dark:text-white text-lg">{item.title}</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1 whitespace-pre-wrap line-clamp-3">{item.description}</p>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 flex space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Link href={`/admin/services?edit=${item.id}`} className="p-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg shadow-lg transition-colors">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteServiceItem(item.id);
|
||||
revalidatePath('/admin/services');
|
||||
}}>
|
||||
<button type="submit" className="p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg shadow-lg transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{services.length === 0 && (
|
||||
<div className="col-span-full py-8 text-center text-slate-500">
|
||||
Henüz hizmet eklenmemiş.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Save, Upload } from 'lucide-react';
|
||||
import { updateSiteSettings } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { uploadImage } from '@/lib/cloudinary';
|
||||
|
||||
export default async function SettingsAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const settings = await prisma.siteSettings.findFirst();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Site Ayarları</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
|
||||
const data: any = {
|
||||
phone: formData.get('phone') as string,
|
||||
email: formData.get('email') as string,
|
||||
addressText1: formData.get('addressText1') as string,
|
||||
addressText2: formData.get('addressText2') as string,
|
||||
workingHours: formData.get('workingHours') as string,
|
||||
instagramUrl: formData.get('instagramUrl') as string,
|
||||
facebookUrl: formData.get('facebookUrl') as string,
|
||||
youtubeUrl: formData.get('youtubeUrl') as string,
|
||||
whatsappNumber: formData.get('whatsappNumber') as string,
|
||||
};
|
||||
|
||||
const logoFile = formData.get('logo') as File;
|
||||
if (logoFile && logoFile.size > 0) {
|
||||
data.logoUrl = await uploadImage(logoFile);
|
||||
}
|
||||
|
||||
await updateSiteSettings(data);
|
||||
revalidatePath('/', 'layout');
|
||||
redirect('/admin/settings');
|
||||
}} className="space-y-8">
|
||||
|
||||
{/* Logo Section */}
|
||||
<section>
|
||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">Logo Ayarları</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Yeni Logo Yükle
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="logo"
|
||||
accept="image/*"
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
|
||||
/>
|
||||
</div>
|
||||
{settings?.logoUrl && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Mevcut Logo</label>
|
||||
<div className="p-4 bg-slate-800 rounded-lg w-fit">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={settings.logoUrl} alt="Logo" className="h-12 w-auto object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Section */}
|
||||
<section>
|
||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">İletişim Bilgileri</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Telefon</label>
|
||||
<input type="text" name="phone" defaultValue={settings?.phone || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="+90 534 465 62 35" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">E-posta</label>
|
||||
<input type="email" name="email" defaultValue={settings?.email || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="info@moybeachakyaka.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Çalışma Saatleri</label>
|
||||
<input type="text" name="workingHours" defaultValue={settings?.workingHours || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="Her Gün: 09:00 - 02:00" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">WhatsApp Numarası</label>
|
||||
<input type="text" name="whatsappNumber" defaultValue={settings?.whatsappNumber || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="905344656235 (Sadece rakam, + olmadan)" />
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Adres Satırı 1</label>
|
||||
<input type="text" name="addressText1" defaultValue={settings?.addressText1 || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="Akyaka Mah. Gümüş Sok." />
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Adres Satırı 2</label>
|
||||
<input type="text" name="addressText2" defaultValue={settings?.addressText2 || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="Ula, Muğla / Türkiye" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Social Media */}
|
||||
<section>
|
||||
<h2 className="text-xl font-bold mb-4 border-b pb-2">Sosyal Medya Linkleri</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Instagram URL</label>
|
||||
<input type="url" name="instagramUrl" defaultValue={settings?.instagramUrl || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://instagram.com/moybeachakyaka" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Facebook URL</label>
|
||||
<input type="url" name="facebookUrl" defaultValue={settings?.facebookUrl || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://facebook.com/moybeachakyaka" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">YouTube URL</label>
|
||||
<input type="url" name="youtubeUrl" defaultValue={settings?.youtubeUrl || ""} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://youtube.com/@moybeach" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="pt-4 flex justify-end">
|
||||
<button type="submit" className="flex items-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg transition-colors font-medium">
|
||||
<Save className="w-5 h-5" />
|
||||
<span>Ayarları Kaydet</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-6
@@ -2,7 +2,7 @@
|
||||
|
||||
import { MapPin, Phone, Mail, Clock } from "lucide-react";
|
||||
|
||||
export default function Contact({ dict }: { dict: any }) {
|
||||
export default function Contact({ dict, dbSettings }: { dict: any, dbSettings?: any }) {
|
||||
return (
|
||||
<section id="contact" className="py-24 bg-sand-light text-sea-dark relative">
|
||||
<div className="container mx-auto px-6 max-w-7xl">
|
||||
@@ -22,8 +22,8 @@ export default function Contact({ dict }: { dict: any }) {
|
||||
<div>
|
||||
<h4 className="font-bold text-xl mb-2 font-serif">{dict.contact.address.title}</h4>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
{dict.contact.address.text1}<br />
|
||||
{dict.contact.address.text2}
|
||||
{dbSettings?.addressText1 || dict.contact.address.text1}<br />
|
||||
{dbSettings?.addressText2 || dict.contact.address.text2}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,9 @@ export default function Contact({ dict }: { dict: any }) {
|
||||
<div>
|
||||
<h4 className="font-bold text-xl mb-2 font-serif">{dict.contact.phone}</h4>
|
||||
<p className="text-gray-600">
|
||||
<a href="tel:+905344656235" className="hover:text-coral transition-colors">+90 534 465 62 35</a>
|
||||
<a href={`tel:${dbSettings?.phone?.replace(/\s+/g, '') || '+905344656235'}`} className="hover:text-coral transition-colors">
|
||||
{dbSettings?.phone || '+90 534 465 62 35'}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +49,9 @@ export default function Contact({ dict }: { dict: any }) {
|
||||
<div>
|
||||
<h4 className="font-bold text-xl mb-2 font-serif">{dict.contact.email}</h4>
|
||||
<p className="text-gray-600">
|
||||
<a href="mailto:info@moybeachakyaka.com" className="hover:text-coral transition-colors">info@moybeachakyaka.com</a>
|
||||
<a href={`mailto:${dbSettings?.email || 'info@moybeachakyaka.com'}`} className="hover:text-coral transition-colors">
|
||||
{dbSettings?.email || 'info@moybeachakyaka.com'}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +63,7 @@ export default function Contact({ dict }: { dict: any }) {
|
||||
<div>
|
||||
<h4 className="font-bold text-xl mb-2 font-serif">{dict.contact.hours.title}</h4>
|
||||
<p className="text-gray-600">
|
||||
{dict.contact.hours.text}
|
||||
{dbSettings?.workingHours || dict.contact.hours.text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+17
-10
@@ -33,7 +33,7 @@ const socials = [
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer({ dict }: { dict: any }) {
|
||||
export default function Footer({ dict, dbSettings }: { dict: any, dbSettings?: any }) {
|
||||
const links = [
|
||||
{ label: dict.nav.about, href: "#about" },
|
||||
{ label: dict.nav.services, href: "#services" },
|
||||
@@ -41,6 +41,13 @@ export default function Footer({ dict }: { dict: any }) {
|
||||
{ label: dict.nav.contact, href: "#contact" },
|
||||
];
|
||||
|
||||
const dynamicSocials = socials.map(s => {
|
||||
if (s.label === "Instagram" && dbSettings?.instagramUrl) return { ...s, href: dbSettings.instagramUrl };
|
||||
if (s.label === "Facebook" && dbSettings?.facebookUrl) return { ...s, href: dbSettings.facebookUrl };
|
||||
if (s.label === "YouTube" && dbSettings?.youtubeUrl) return { ...s, href: dbSettings.youtubeUrl };
|
||||
return s;
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className="relative bg-sea-dark text-sand-light overflow-hidden">
|
||||
|
||||
@@ -67,7 +74,7 @@ export default function Footer({ dict }: { dict: any }) {
|
||||
|
||||
{/* Social icons */}
|
||||
<div className="flex gap-3">
|
||||
{socials.map((s) => (
|
||||
{dynamicSocials.map((s) => (
|
||||
<a
|
||||
key={s.label}
|
||||
href={s.href}
|
||||
@@ -105,20 +112,20 @@ export default function Footer({ dict }: { dict: any }) {
|
||||
<h4 className="text-[11px] uppercase tracking-[0.25em] text-sand-dark mb-5">{dict.footer.contact}</h4>
|
||||
<ul className="space-y-4 text-sm text-sand-light/65">
|
||||
<li className="leading-relaxed">
|
||||
{dict.contact.address.text1}<br />
|
||||
{dict.contact.address.text2}
|
||||
{dbSettings?.addressText1 || dict.contact.address.text1}<br />
|
||||
{dbSettings?.addressText2 || dict.contact.address.text2}
|
||||
</li>
|
||||
<li>
|
||||
<a href="tel:+905344656235" className="hover:text-white transition-colors">
|
||||
+90 534 465 62 35
|
||||
<a href={`tel:${dbSettings?.phone?.replace(/\s+/g, '') || '+905344656235'}`} className="hover:text-white transition-colors">
|
||||
{dbSettings?.phone || '+90 534 465 62 35'}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="mailto:info@moybeachakyaka.com" className="hover:text-white transition-colors">
|
||||
info@moybeachakyaka.com
|
||||
<a href={`mailto:${dbSettings?.email || 'info@moybeachakyaka.com'}`} className="hover:text-white transition-colors">
|
||||
{dbSettings?.email || 'info@moybeachakyaka.com'}
|
||||
</a>
|
||||
</li>
|
||||
<li className="text-sand-light/45">{dict.contact.hours.text}</li>
|
||||
<li className="text-sand-light/45">{dbSettings?.workingHours || dict.contact.hours.text}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +155,7 @@ export default function Footer({ dict }: { dict: any }) {
|
||||
|
||||
{/* Floating WhatsApp Button */}
|
||||
<a
|
||||
href="https://wa.me/905344656235"
|
||||
href={`https://wa.me/${dbSettings?.whatsappNumber || '905344656235'}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="fixed bottom-6 right-6 z-50 bg-[#25D366] text-white p-3.5 rounded-full shadow-xl hover:scale-110 transition-transform"
|
||||
|
||||
@@ -7,29 +7,31 @@ import Image from "next/image";
|
||||
|
||||
const photos = [
|
||||
{ id: 1, categoryKey: "beach", url: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&q=80&w=1200" },
|
||||
{ id: 2, categoryKey: "rooms", url: "https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&q=80&w=1200" },
|
||||
{ id: 3, categoryKey: "restaurant", url: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?auto=format&fit=crop&q=80&w=1200" },
|
||||
{ id: 4, categoryKey: "beach", url: "https://images.unsplash.com/photo-1519046904884-53103b34b206?auto=format&fit=crop&q=80&w=1200" },
|
||||
{ id: 5, categoryKey: "events", url: "https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&q=80&w=1200" },
|
||||
{ id: 6, categoryKey: "restaurant", url: "https://images.unsplash.com/photo-1555396273-367ea4eb4db5?auto=format&fit=crop&q=80&w=1200" },
|
||||
];
|
||||
|
||||
export default function Gallery({ dict }: { dict: any }) {
|
||||
export default function Gallery({ dict, dbPhotos }: { dict: any, dbPhotos?: any[] }) {
|
||||
const categories = [
|
||||
{ key: "all", label: dict.gallery.categories.all },
|
||||
{ key: "beach", label: dict.gallery.categories.beach },
|
||||
{ key: "restaurant", label: dict.gallery.categories.restaurant },
|
||||
{ key: "rooms", label: dict.gallery.categories.rooms },
|
||||
{ key: "events", label: dict.gallery.categories.events }
|
||||
];
|
||||
|
||||
const [activeCategoryKey, setActiveCategoryKey] = useState("all");
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||
|
||||
const displayPhotos = dbPhotos && dbPhotos.length > 0
|
||||
? dbPhotos.map(p => ({ id: p.id, categoryKey: p.category, url: p.imageUrl, title: p.title }))
|
||||
: photos;
|
||||
|
||||
const filteredPhotos =
|
||||
activeCategoryKey === "all"
|
||||
? photos
|
||||
: photos.filter((p) => p.categoryKey === activeCategoryKey);
|
||||
? displayPhotos
|
||||
: displayPhotos.filter((p) => p.categoryKey === activeCategoryKey);
|
||||
|
||||
return (
|
||||
<section id="gallery" className="py-24 bg-sand-light text-sea-dark">
|
||||
@@ -76,7 +78,7 @@ export default function Gallery({ dict }: { dict: any }) {
|
||||
>
|
||||
<Image
|
||||
src={photo.url}
|
||||
alt={dict.gallery.categories[photo.categoryKey]}
|
||||
alt={(photo as any).title || dict.gallery.categories[photo.categoryKey] || "Galeri Görseli"}
|
||||
fill
|
||||
className="object-cover transform group-hover:scale-108 transition-transform duration-700"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Menu, X } from "lucide-react";
|
||||
|
||||
export default function Header({ dict, lang }: { dict: any, lang: string }) {
|
||||
export default function Header({ dict, lang, dbSettings }: { dict: any, lang: string, dbSettings?: any }) {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
@@ -31,9 +31,9 @@ export default function Header({ dict, lang }: { dict: any, lang: string }) {
|
||||
<div className="container mx-auto px-4 md:px-6 flex justify-between items-center">
|
||||
<a href="#" className="flex items-center">
|
||||
<img
|
||||
src="/logo.png"
|
||||
src={dbSettings?.logoUrl || "/logo.png"}
|
||||
alt="Moy Beach Logo"
|
||||
className={`h-12 w-auto transition-all duration-300 ${!isScrolled ? "brightness-0 invert opacity-90" : ""}`}
|
||||
className={`h-12 w-auto transition-all duration-300 ${(!isScrolled && !dbSettings?.logoUrl) ? "brightness-0 invert opacity-90" : ""}`}
|
||||
/>
|
||||
</a>
|
||||
|
||||
|
||||
+21
-10
@@ -4,22 +4,33 @@ import { motion } from "framer-motion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Hero({ dict }: { dict: any }) {
|
||||
export default function Hero({ dict, dbHeroMedia }: { dict: any, dbHeroMedia?: any }) {
|
||||
const words = ["MOY", "BEACH"];
|
||||
|
||||
return (
|
||||
<section className="relative h-screen w-full flex items-center justify-center overflow-hidden">
|
||||
|
||||
{/* Background Image */}
|
||||
{/* Background Media */}
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1590523277543-a94d2e4eb00b?auto=format&fit=crop&q=85&w=2400"
|
||||
alt="Moy Beach Akyaka"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
sizes="100vw"
|
||||
/>
|
||||
{dbHeroMedia?.type === 'video' ? (
|
||||
<video
|
||||
src={dbHeroMedia.url}
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-full h-full object-cover object-center"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={dbHeroMedia?.url || "https://images.unsplash.com/photo-1590523277543-a94d2e4eb00b?auto=format&fit=crop&q=85&w=2400"}
|
||||
alt="Moy Beach Akyaka"
|
||||
fill
|
||||
className="object-cover object-center"
|
||||
priority
|
||||
sizes="100vw"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Multi-layer gradient for depth */}
|
||||
|
||||
+23
-7
@@ -4,8 +4,8 @@ import { motion } from "framer-motion";
|
||||
import { Waves, UtensilsCrossed, Hotel, CalendarHeart } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Services({ dict }: { dict: any }) {
|
||||
const services = [
|
||||
export default function Services({ dict, dbServices }: { dict: any, dbServices?: any[] }) {
|
||||
const defaultServices = [
|
||||
{
|
||||
title: dict.services.items.beach.title,
|
||||
description: dict.services.items.beach.description,
|
||||
@@ -32,6 +32,15 @@ export default function Services({ dict }: { dict: any }) {
|
||||
},
|
||||
];
|
||||
|
||||
const displayServices = dbServices && dbServices.length > 0
|
||||
? dbServices.map(s => ({
|
||||
title: s.title,
|
||||
description: s.description,
|
||||
icon: null, // Dynamic services don't have static icons right now
|
||||
image: s.iconUrl || "https://images.unsplash.com/photo-1519046904884-53103b34b206?auto=format&fit=crop&q=80&w=800"
|
||||
}))
|
||||
: defaultServices;
|
||||
|
||||
return (
|
||||
<section id="services" className="py-24 bg-white text-sea-dark">
|
||||
<div className="container mx-auto px-6 max-w-7xl">
|
||||
@@ -45,8 +54,13 @@ export default function Services({ dict }: { dict: any }) {
|
||||
<h3 className="text-4xl md:text-5xl font-serif">{dict.services.heading}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<div className={`grid gap-8 justify-center ${
|
||||
displayServices.length === 1 ? 'grid-cols-1 max-w-sm mx-auto' :
|
||||
displayServices.length === 2 ? 'md:grid-cols-2 max-w-3xl mx-auto' :
|
||||
displayServices.length === 3 ? 'md:grid-cols-3 max-w-5xl mx-auto' :
|
||||
'md:grid-cols-2 lg:grid-cols-4'
|
||||
}`}>
|
||||
{displayServices.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.title}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -72,9 +86,11 @@ export default function Services({ dict }: { dict: any }) {
|
||||
/>
|
||||
|
||||
{/* Icon badge */}
|
||||
<div className="absolute top-4 left-4 z-20 bg-white/90 backdrop-blur-sm p-3 rounded-full shadow-lg ring-2 ring-coral/10 group-hover:ring-coral/50 transition-all duration-300">
|
||||
<service.icon className="w-5 h-5 text-coral" />
|
||||
</div>
|
||||
{service.icon && (
|
||||
<div className="absolute top-4 left-4 z-20 bg-white/90 backdrop-blur-sm p-3 rounded-full shadow-lg ring-2 ring-coral/10 group-hover:ring-coral/50 transition-all duration-300">
|
||||
<service.icon className="w-5 h-5 text-coral" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const secretKey = process.env.JWT_SECRET || 'super-secret-key-replace-me-in-production';
|
||||
const key = new TextEncoder().encode(secretKey);
|
||||
|
||||
export async function encrypt(payload: any) {
|
||||
return await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('24h')
|
||||
.sign(key);
|
||||
}
|
||||
|
||||
export async function decrypt(input: string): Promise<any> {
|
||||
try {
|
||||
const { payload } = await jwtVerify(input, key, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
return payload;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSession() {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session')?.value;
|
||||
if (!session) return null;
|
||||
return await decrypt(session);
|
||||
}
|
||||
|
||||
export async function createSession(userId: string) {
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const session = await encrypt({ userId, expires });
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session', session, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
expires: expires,
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteSession() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('session');
|
||||
}
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
const session = request.cookies.get('session')?.value;
|
||||
if (!session) return;
|
||||
|
||||
const parsed = await decrypt(session);
|
||||
if (!parsed) return;
|
||||
|
||||
parsed.expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
const res = NextResponse.next();
|
||||
res.cookies.set({
|
||||
name: 'session',
|
||||
value: await encrypt(parsed),
|
||||
httpOnly: true,
|
||||
expires: parsed.expires,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { v2 as cloudinary } from 'cloudinary';
|
||||
|
||||
export async function uploadImage(file: File): Promise<string> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const uploadStream = cloudinary.uploader.upload_stream(
|
||||
{ folder: 'moybeach', resource_type: 'auto' },
|
||||
(error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result!.secure_url);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
uploadStream.end(buffer);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
@@ -8,8 +8,17 @@ const nextConfig: NextConfig = {
|
||||
protocol: "https",
|
||||
hostname: "images.unsplash.com",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "res.cloudinary.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '50mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
Generated
+639
-3
@@ -8,7 +8,11 @@
|
||||
"name": "moybeach",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.4.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cloudinary": "^2.10.0",
|
||||
"framer-motion": "^12.40.0",
|
||||
"jose": "^6.2.3",
|
||||
"lucide-react": "^1.17.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
@@ -16,11 +20,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"prisma": "^6.4.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
@@ -311,6 +317,422 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||
@@ -1308,6 +1730,78 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz",
|
||||
"integrity": "sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*",
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz",
|
||||
"integrity": "sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz",
|
||||
"integrity": "sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.4.1",
|
||||
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
|
||||
"@prisma/fetch-engine": "6.4.1",
|
||||
"@prisma/get-platform": "6.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d.tgz",
|
||||
"integrity": "sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz",
|
||||
"integrity": "sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.4.1",
|
||||
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
|
||||
"@prisma/get-platform": "6.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz",
|
||||
"integrity": "sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -1618,6 +2112,13 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@@ -2612,6 +3113,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
@@ -2773,6 +3283,18 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cloudinary": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz",
|
||||
"integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.23"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=9"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2894,7 +3416,7 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -3194,6 +3716,61 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-register": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
|
||||
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"esbuild": ">=0.12 <1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -3791,6 +4368,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -4605,6 +5196,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -5031,6 +5631,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -5156,7 +5762,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -5609,6 +6215,36 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz",
|
||||
"integrity": "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "6.4.1",
|
||||
"esbuild": ">=0.12 <1",
|
||||
"esbuild-register": "3.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -6517,7 +7153,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.4.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cloudinary": "^2.10.0",
|
||||
"framer-motion": "^12.40.0",
|
||||
"jose": "^6.2.3",
|
||||
"lucide-react": "^1.17.0",
|
||||
"next": "16.2.7",
|
||||
"react": "19.2.4",
|
||||
@@ -17,11 +21,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.7",
|
||||
"prisma": "^6.4.1",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
password String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Gallery {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
category String
|
||||
imageUrl String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Service {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String
|
||||
iconUrl String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model ContactMessage {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String
|
||||
subject String?
|
||||
message String
|
||||
isRead Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model HeroMedia {
|
||||
id String @id @default(cuid())
|
||||
url String
|
||||
type String // 'image' or 'video'
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model SiteSettings {
|
||||
id String @id @default(cuid())
|
||||
logoUrl String?
|
||||
phone String?
|
||||
email String?
|
||||
addressText1 String?
|
||||
addressText2 String?
|
||||
workingHours String?
|
||||
instagramUrl String?
|
||||
facebookUrl String?
|
||||
youtubeUrl String?
|
||||
whatsappNumber String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -4,10 +4,14 @@ import type { NextRequest } from "next/server";
|
||||
const locales = ["tr", "en"];
|
||||
const defaultLocale = "tr";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
export function proxy(request: NextRequest) {
|
||||
// Check if there is any supported locale in the pathname
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
if (pathname.startsWith('/admin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathnameHasLocale = locales.some(
|
||||
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
async function main() {
|
||||
const password = await bcrypt.hash('admin123', 10)
|
||||
const user = await prisma.user.upsert({
|
||||
where: { username: 'admin' },
|
||||
update: { password }, // update password if already exists
|
||||
create: {
|
||||
username: 'admin',
|
||||
password,
|
||||
},
|
||||
})
|
||||
console.log('Admin user created:', user.username)
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e)
|
||||
await prisma.$disconnect()
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
const cloudinary = require('cloudinary').v2;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: 'du7xohbct',
|
||||
api_key: '525922573613433',
|
||||
api_secret: 'cJ0NDcaoQhSTAxBMv6jNMFupt3k'
|
||||
});
|
||||
|
||||
const uploadImages = async () => {
|
||||
const foldersToUpload = ['kitehotel', 'kitesurf'];
|
||||
const results = {};
|
||||
|
||||
for (const folderName of foldersToUpload) {
|
||||
const publicDir = path.join(__dirname, '..', 'public', folderName);
|
||||
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
console.log(`Directory does not exist: ${publicDir}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(publicDir);
|
||||
const images = files.filter(file => file.endsWith('.jpg') || file.endsWith('.jpeg') || file.endsWith('.png') || file.endsWith('.webp'));
|
||||
|
||||
for (const file of images) {
|
||||
console.log(`Uploading ${folderName}/${file}...`);
|
||||
try {
|
||||
const filePath = path.join(publicDir, file);
|
||||
const result = await cloudinary.uploader.upload(filePath, {
|
||||
folder: `moygrup/${folderName}`,
|
||||
use_filename: true,
|
||||
unique_filename: false
|
||||
});
|
||||
console.log(`Uploaded ${file} -> ${result.secure_url}`);
|
||||
results[`${folderName}/${file}`] = result.secure_url;
|
||||
} catch (error) {
|
||||
console.error(`Error uploading ${folderName}/${file}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n--- Upload Results ---');
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
};
|
||||
|
||||
uploadImages();
|
||||
Reference in New Issue
Block a user