feat: add admin panel, Openinary media integration and standardized footer backlink
This commit is contained in:
@@ -19,11 +19,9 @@ const HERO_BG =
|
|||||||
const MARQUEE_ITEMS = [
|
const MARQUEE_ITEMS = [
|
||||||
"Akyaka",
|
"Akyaka",
|
||||||
"Akçapınar",
|
"Akçapınar",
|
||||||
"Datça",
|
|
||||||
"Beach Bar",
|
"Beach Bar",
|
||||||
"Kite Surf",
|
"Kite Surf",
|
||||||
"Restaurant",
|
"Restaurant",
|
||||||
"Bungalov",
|
|
||||||
"Hotel",
|
"Hotel",
|
||||||
"Ege Denizi",
|
"Ege Denizi",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,23 +13,23 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
|
|||||||
type Category = string;
|
type Category = string;
|
||||||
|
|
||||||
const IMAGES = [
|
const IMAGES = [
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592478/moygrup/kitebeach/moyheader-scaled.jpg", cat: t.venues, title: "Kite Beach Genel Görünüm" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/moyheader-scaled.jpg", cat: t.venues, title: "Kite Beach Genel Görünüm" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592469/moygrup/kitebeach/A1_0177-scaled.jpg", cat: "Beach", title: "Sahil Manzarası" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_0177-scaled.jpg", cat: "Beach", title: "Sahil Manzarası" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592471/moygrup/kitebeach/A1_7609-scaled.jpg", cat: "Beach", title: "Kum ve Deniz" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7609-scaled.jpg", cat: "Beach", title: "Kum ve Deniz" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592472/moygrup/kitebeach/A1_7612-scaled.jpg", cat: "Beach", title: "Kite Sahili" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7612-scaled.jpg", cat: "Beach", title: "Kite Sahili" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592473/moygrup/kitebeach/A1_7630-scaled.jpg", cat: "Beach", title: "Rüzgar ve Deniz" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7630-scaled.jpg", cat: "Beach", title: "Rüzgar ve Deniz" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592475/moygrup/kitebeach/A1_7684-scaled.jpg", cat: "Beach", title: "Kite Beach Dinlenme" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7684-scaled.jpg", cat: "Beach", title: "Kite Beach Dinlenme" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592477/moygrup/kitebeach/images.jpg", cat: "Beach", title: "Sahil Keyfi" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/images.jpeg", cat: "Beach", title: "Sahil Keyfi" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592539/moygrup/kitehotel/kite3.png", cat: "Hotel", title: "Kite Beach Hotel" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite3.png", cat: "Hotel", title: "Kite Beach Hotel" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592542/moygrup/kitehotel/kite4.png", cat: "Hotel", title: "Hotel Odalar" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite4.png", cat: "Hotel", title: "Hotel Odalar" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592543/moygrup/kitehotel/kite5.png", cat: "Hotel", title: "Oda İçi Tasarım" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite5.png", cat: "Hotel", title: "Oda İçi Tasarım" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592546/moygrup/kitehotel/kite6.png", cat: "Hotel", title: "Oda Detayları" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite6.png", cat: "Hotel", title: "Oda Detayları" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592548/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-1.jpg", cat: "Kite Surf", title: "Kitesurf Heyecanı" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-1.jpg", cat: "Kite Surf", title: "Kitesurf Heyecanı" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592549/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-2.jpg", cat: "Kite Surf", title: "Dalgalarla Dans" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-2.jpg", cat: "Kite Surf", title: "Dalgalarla Dans" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592549/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-3.jpg", cat: "Kite Surf", title: "Profesyonel Kitesurf" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-3.jpg", cat: "Kite Surf", title: "Profesyonel Kitesurf" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592550/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-4.jpg", cat: "Kite Surf", title: "Rüzgarla Sörf" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-4.jpg", cat: "Kite Surf", title: "Rüzgarla Sörf" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592552/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-5.jpg", cat: "Kite Surf", title: "Deniz ve Özgürlük" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-5.jpg", cat: "Kite Surf", title: "Deniz ve Özgürlük" },
|
||||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592553/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee.jpg", cat: "Kite Surf", title: "Kitesurf Eğitimi" },
|
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee.jpg", cat: "Kite Surf", title: "Kitesurf Eğitimi" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [active, setActive] = useState<Category>(t.all);
|
const [active, setActive] = useState<Category>(t.all);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default async function RootLayout({
|
|||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "Moy Group",
|
"name": "Moy Group",
|
||||||
"url": "https://moygroup.com.tr",
|
"url": "https://moygroup.com.tr",
|
||||||
"logo": "https://moygroup.com.tr/logo.png",
|
"logo": "https://media.ayris.tech/t/moygrup/logo.png",
|
||||||
"contactPoint": {
|
"contactPoint": {
|
||||||
"@type": "ContactPoint",
|
"@type": "ContactPoint",
|
||||||
"telephone": "+90-534-465-62-35",
|
"telephone": "+90-534-465-62-35",
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'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 } });
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
import { LayoutDashboard, Image as ImageIcon, Briefcase, Mail, LogOut } from 'lucide-react';
|
||||||
|
import { logout } from '../actions';
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
||||||
|
{ name: 'Galeri', href: '/admin/gallery', icon: ImageIcon },
|
||||||
|
{ name: 'Hizmetler', href: '/admin/services', icon: Briefcase },
|
||||||
|
{ name: 'İletişim', href: '/admin/contact', icon: Mail },
|
||||||
|
];
|
||||||
|
|
||||||
|
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,98 @@
|
|||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { Trash2, Plus } from 'lucide-react';
|
||||||
|
import { addGalleryItem, deleteGalleryItem } from '../actions';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
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 imageUrl = formData.get('imageUrl') as string;
|
||||||
|
|
||||||
|
if (title && category && imageUrl) {
|
||||||
|
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 (Örn: Beach, Hotel, Kite Surf)</label>
|
||||||
|
<input type="text" 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" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Resim URL (Cloudinary vs)</label>
|
||||||
|
<input type="url" name="imageUrl" 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>
|
||||||
|
<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,20 @@
|
|||||||
|
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 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>
|
||||||
|
</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,102 @@
|
|||||||
|
import { prisma } from '@/lib/db';
|
||||||
|
import { getSession } from '@/lib/auth';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { Trash2, Plus, Briefcase } from 'lucide-react';
|
||||||
|
import { addServiceItem, deleteServiceItem } from '../actions';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
export default async function ServicesAdminPage() {
|
||||||
|
const session = await getSession();
|
||||||
|
if (!session) redirect('/admin/login');
|
||||||
|
|
||||||
|
const services = await prisma.service.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 Hizmet Ekle</h2>
|
||||||
|
<form action={async (formData: FormData) => {
|
||||||
|
'use server';
|
||||||
|
const title = formData.get('title') as string;
|
||||||
|
const description = formData.get('description') as string;
|
||||||
|
const iconUrl = formData.get('iconUrl') as string;
|
||||||
|
|
||||||
|
if (title && description) {
|
||||||
|
await addServiceItem({ title, description, iconUrl });
|
||||||
|
revalidatePath('/admin/services');
|
||||||
|
}
|
||||||
|
}} 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">Açıklama</label>
|
||||||
|
<textarea name="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">İkon URL (Opsiyonel)</label>
|
||||||
|
<input type="url" name="iconUrl" 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>
|
||||||
|
<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">
|
||||||
|
<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 Hizmetler ({services.length})</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{services.map(item => (
|
||||||
|
<div key={item.id} className="group relative flex items-start space-x-4 p-4 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||||
|
<div className="flex-shrink-0 p-3 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700">
|
||||||
|
{item.iconUrl ? (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={item.iconUrl} alt={item.title} className="w-8 h-8 object-contain" />
|
||||||
|
) : (
|
||||||
|
<Briefcase className="w-8 h-8 text-slate-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<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">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<form action={async () => {
|
||||||
|
'use server';
|
||||||
|
await deleteServiceItem(item.id);
|
||||||
|
revalidatePath('/admin/services');
|
||||||
|
}}>
|
||||||
|
<button type="submit" className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{services.length === 0 && (
|
||||||
|
<div className="py-8 text-center text-slate-500">
|
||||||
|
Henüz hizmet eklenmemiş.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Image src="/logo.png" alt="Moy Group Logo" width={180} height={48} className="h-12 w-auto mb-6" />
|
<Image src="https://media.ayris.tech/upload/moygrup/logo.png" alt="Moy Group Logo" width={180} height={48} className="h-12 w-auto mb-6" />
|
||||||
<p className="text-gray-400 text-sm leading-relaxed">
|
<p className="text-gray-400 text-sm leading-relaxed">
|
||||||
{dict.description}
|
{dict.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -93,14 +93,27 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom */}
|
{/* Bottom */}
|
||||||
<div className="pt-8 border-t border-gray-800 flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="pt-8 border-t border-gray-800 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-gray-500">
|
||||||
<p className="text-gray-500 text-xs">
|
<p>
|
||||||
© {new Date().getFullYear()} Moy Group. {dict.rights}
|
© {new Date().getFullYear()} Moy Group. {dict.rights}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex items-center gap-1.5">
|
||||||
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-[var(--color-moy-gold)] transition-colors text-xs flex items-center gap-1">
|
<span className="opacity-75">created by</span>
|
||||||
created by <span className="font-medium">ayris.tech</span>
|
<a
|
||||||
|
href="https://ayris.tech"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="font-medium text-gray-400 hover:text-white transition-colors duration-200 hover:underline underline-offset-4"
|
||||||
|
>
|
||||||
|
ayristech
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{/* Google Bot & Arama Motoru İndeksleme Katmanı (Görünmez / Screen-Reader Safe) */}
|
||||||
|
<span className="sr-only">
|
||||||
|
Legal AI & Solutions:{" "}
|
||||||
|
<a href="https://ayris.legal" rel="noopener">AyrisLegal</a>,{" "}
|
||||||
|
<a href="https://ayrislegal.com" rel="noopener">Ayris Legal AI</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href={`/${lang}`} className="flex-shrink-0">
|
<Link href={`/${lang}`} className="flex-shrink-0">
|
||||||
<Image src="/logo.png" alt="Moy Group Logo" width={160} height={40} className="h-10 w-auto" priority />
|
<Image src="https://media.ayris.tech/upload/moygrup/logo.png" alt="Moy Group Logo" width={160} height={40} className="h-10 w-auto" priority />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Desktop Nav */}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"title": "Moy Beach",
|
"title": "Moy Beach",
|
||||||
"tag": "Beach & Bar",
|
"tag": "Beach & Bar",
|
||||||
"desc": "An exclusive beach and gastronomy experience amidst the unique nature and azure waters of Akyaka. The point where local aromas meet international flavors.",
|
"desc": "An exclusive beach and gastronomy experience amidst the unique nature and azure waters of Akyaka. The point where local aromas meet international flavors.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592404/moygrup/moy-beach.jpg",
|
"img": "https://media.ayris.tech/upload/moygrup/moy-beach.jpg",
|
||||||
"color": "from-amber-900/60",
|
"color": "from-amber-900/60",
|
||||||
"link": "http://moybeachakyaka.com"
|
"link": "http://moybeachakyaka.com"
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"title": "Kite Beach Hotel",
|
"title": "Kite Beach Hotel",
|
||||||
"tag": "Hotel",
|
"tag": "Hotel",
|
||||||
"desc": "A boutique hotel right on the clear waters of Akyaka, offering the perfect balance of both sports and relaxation. Swim in the sea in the mornings, enjoy the restaurant in the evenings.",
|
"desc": "A boutique hotel right on the clear waters of Akyaka, offering the perfect balance of both sports and relaxation. Swim in the sea in the mornings, enjoy the restaurant in the evenings.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592402/moygrup/kite-beach-hotel.jpg",
|
"img": "https://media.ayris.tech/upload/moygrup/kite-beach-hotel.jpg",
|
||||||
"color": "from-blue-900/60",
|
"color": "from-blue-900/60",
|
||||||
"link": "http://moybeachakyaka.com"
|
"link": "http://moybeachakyaka.com"
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"title": "Kite Beach (Surfing)",
|
"title": "Kite Beach (Surfing)",
|
||||||
"tag": "Kite & Surf",
|
"tag": "Kite & Surf",
|
||||||
"desc": "Professional training and equipment rental at Akçapınar Kite Beach, Turkey's best kitesurf spot. Discover freedom in the diamond waters of the Gulf of Gökova.",
|
"desc": "Professional training and equipment rental at Akçapınar Kite Beach, Turkey's best kitesurf spot. Discover freedom in the diamond waters of the Gulf of Gökova.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592405/moygrup/volkswagen-kite.png",
|
"img": "https://media.ayris.tech/upload/moygrup/volkswagen-kite.png",
|
||||||
"color": "from-green-900/60",
|
"color": "from-green-900/60",
|
||||||
"link": "https://www.kitebeachakyaka.com.tr"
|
"link": "https://www.kitebeachakyaka.com.tr"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"title": "Moy Beach",
|
"title": "Moy Beach",
|
||||||
"tag": "Beach & Bar",
|
"tag": "Beach & Bar",
|
||||||
"desc": "Akyaka'nın eşsiz doğası ve masmavi suları arasında, seçkin bir plaj ve gastronomi deneyimi. Yerel aromalar ile uluslararası lezzetlerin buluştuğu nokta.",
|
"desc": "Akyaka'nın eşsiz doğası ve masmavi suları arasında, seçkin bir plaj ve gastronomi deneyimi. Yerel aromalar ile uluslararası lezzetlerin buluştuğu nokta.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592404/moygrup/moy-beach.jpg",
|
"img": "https://media.ayris.tech/upload/moygrup/moy-beach.jpg",
|
||||||
"color": "from-amber-900/60",
|
"color": "from-amber-900/60",
|
||||||
"link": "http://moybeachakyaka.com"
|
"link": "http://moybeachakyaka.com"
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"title": "Kite Beach Hotel",
|
"title": "Kite Beach Hotel",
|
||||||
"tag": "Hotel",
|
"tag": "Hotel",
|
||||||
"desc": "Akyaka'nın berrak sularına sıfır, hem sporun hem de dinlenmenin mükemmel dengesini sunan butik otel. Sabahları denizde yüzün, akşamları restoranda keyif yapın.",
|
"desc": "Akyaka'nın berrak sularına sıfır, hem sporun hem de dinlenmenin mükemmel dengesini sunan butik otel. Sabahları denizde yüzün, akşamları restoranda keyif yapın.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592402/moygrup/kite-beach-hotel.jpg",
|
"img": "https://media.ayris.tech/upload/moygrup/kite-beach-hotel.jpg",
|
||||||
"color": "from-blue-900/60",
|
"color": "from-blue-900/60",
|
||||||
"link": "http://moybeachakyaka.com"
|
"link": "http://moybeachakyaka.com"
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"title": "Kite Beach (Surfing)",
|
"title": "Kite Beach (Surfing)",
|
||||||
"tag": "Kite & Surf",
|
"tag": "Kite & Surf",
|
||||||
"desc": "Akçapınar Kite Sahili'nde, Türkiye'nin en iyi kitesurf noktasında profesyonel eğitim ve ekipman kiralama. Gökova Körfezi'nin pırlanta sularında özgürlüğü keşfedin.",
|
"desc": "Akçapınar Kite Sahili'nde, Türkiye'nin en iyi kitesurf noktasında profesyonel eğitim ve ekipman kiralama. Gökova Körfezi'nin pırlanta sularında özgürlüğü keşfedin.",
|
||||||
"img": "https://res.cloudinary.com/du7xohbct/image/upload/v1780592405/moygrup/volkswagen-kite.png",
|
"img": "https://media.ayris.tech/upload/moygrup/volkswagen-kite.png",
|
||||||
"color": "from-green-900/60",
|
"color": "from-green-900/60",
|
||||||
"link": "https://www.kitebeachakyaka.com.tr"
|
"link": "https://www.kitebeachakyaka.com.tr"
|
||||||
}
|
}
|
||||||
|
|||||||
+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,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;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
export default function openinaryLoader({ src, width, quality }: { src: string, width: number, quality?: number }) {
|
||||||
|
// Handle already absolute openinary URLs
|
||||||
|
let path = src;
|
||||||
|
if (src.startsWith('https://media.ayris.tech/t/')) {
|
||||||
|
// format: https://media.ayris.tech/t/w_800,h_800/moyqr/img.jpg
|
||||||
|
const parts = src.split('/');
|
||||||
|
path = parts.slice(5).join('/');
|
||||||
|
} else if (src.startsWith('https://media.ayris.tech/upload/')) {
|
||||||
|
// format: https://media.ayris.tech/upload/moyqr/img.jpg
|
||||||
|
const parts = src.split('/');
|
||||||
|
path = parts.slice(4).join('/');
|
||||||
|
} else if (src.includes('res.cloudinary.com')) {
|
||||||
|
// Correctly apply width & quality for unmigrated Cloudinary URLs
|
||||||
|
// e.g. https://res.cloudinary.com/domain/image/upload/v1234/path.jpg
|
||||||
|
// becomes: https://res.cloudinary.com/domain/image/upload/w_800,f_webp,q_75/v1234/path.jpg
|
||||||
|
const parts = src.split('/upload/');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return `${parts[0]}/upload/w_${width},f_webp,q_${quality || 75}/${parts[1]}`;
|
||||||
|
}
|
||||||
|
return src;
|
||||||
|
} else if (src.startsWith('http')) {
|
||||||
|
// For other external URLs, we append the width as a query parameter to satisfy Next.js.
|
||||||
|
const url = new URL(src);
|
||||||
|
url.searchParams.set('w', width.toString());
|
||||||
|
if (quality) {
|
||||||
|
url.searchParams.set('q', quality.toString());
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any leading slash
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://media.ayris.tech/t/w_${width},f_webp,q_${quality || 75}/${path}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL;
|
||||||
|
|
||||||
|
export function optimizedImage(path: string, params: string) {
|
||||||
|
return `${BASE}/t/${params}/${path}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export async function uploadToOpeninary(file: File, folder: string) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("files", file);
|
||||||
|
formData.append("folder", folder);
|
||||||
|
|
||||||
|
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error("Openinary upload error:", errorText);
|
||||||
|
throw new Error("Upload başarısız: " + errorText);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return data.files[0]; // { path, url, size, ... }
|
||||||
|
}
|
||||||
+3
-1
@@ -3,10 +3,12 @@ import type { NextConfig } from "next";
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
|
loader: "custom",
|
||||||
|
loaderFile: "./lib/openinary-loader.ts",
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "res.cloudinary.com",
|
hostname: "media.ayris.tech",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
|
|||||||
Generated
+445
-2
@@ -8,7 +8,10 @@
|
|||||||
"name": "moygrup",
|
"name": "moygrup",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.19.3",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"next": "16.2.7",
|
"next": "16.2.7",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -16,12 +19,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"cloudinary": "^2.10.0",
|
"cloudinary": "^2.10.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.7",
|
"eslint-config-next": "16.2.7",
|
||||||
|
"prisma": "^6.19.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@@ -1249,6 +1254,91 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/client": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==",
|
||||||
|
"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/config": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"c12": "3.1.0",
|
||||||
|
"deepmerge-ts": "7.1.5",
|
||||||
|
"effect": "3.21.0",
|
||||||
|
"empathic": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/debug": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
|
||||||
|
"devOptional": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.3",
|
||||||
|
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"@prisma/fetch-engine": "6.19.3",
|
||||||
|
"@prisma/get-platform": "6.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/engines-version": {
|
||||||
|
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||||
|
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/fetch-engine": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.3",
|
||||||
|
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||||
|
"@prisma/get-platform": "6.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/get-platform": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.19.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -1256,6 +1346,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -1547,6 +1644,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -2511,6 +2615,15 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||||
@@ -2569,6 +2682,35 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"confbox": "^0.2.2",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"dotenv": "^16.6.1",
|
||||||
|
"exsolve": "^1.0.7",
|
||||||
|
"giget": "^2.0.0",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"ohash": "^2.0.11",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pkg-types": "^2.2.0",
|
||||||
|
"rc9": "^2.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"magicast": "^0.3.5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"magicast": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||||
@@ -2666,6 +2808,32 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/citty": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"consola": "^3.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
@@ -2712,6 +2880,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/confbox": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/consola": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -2827,6 +3012,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge-ts": {
|
||||||
|
"version": "7.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
|
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
@@ -2863,6 +3058,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/defu": {
|
||||||
|
"version": "6.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||||
|
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/destr": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -2886,6 +3095,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2901,6 +3123,17 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/effect": {
|
||||||
|
"version": "3.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
|
||||||
|
"integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"fast-check": "^3.23.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.366",
|
"version": "1.5.366",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz",
|
||||||
@@ -2915,6 +3148,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/empathic": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.22.1",
|
"version": "5.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz",
|
||||||
@@ -3535,6 +3778,36 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/exsolve": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-check": {
|
||||||
|
"version": "3.23.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||||
|
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||||
|
"devOptional": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/dubzzz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fast-check"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pure-rand": "^6.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3834,6 +4107,24 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/giget": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"citty": "^0.1.6",
|
||||||
|
"consola": "^3.4.0",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"node-fetch-native": "^1.6.6",
|
||||||
|
"nypm": "^0.6.0",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"giget": "dist/cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -4511,12 +4802,21 @@
|
|||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5207,6 +5507,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch-native": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.47",
|
"version": "2.0.47",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
||||||
@@ -5217,6 +5524,31 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nypm": {
|
||||||
|
"version": "0.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz",
|
||||||
|
"integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"citty": "^0.2.2",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"tinyexec": "^1.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nypm": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nypm/node_modules/citty": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5340,6 +5672,13 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ohash": {
|
||||||
|
"version": "2.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||||
|
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -5448,6 +5787,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/perfect-debounce": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5467,6 +5820,18 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pkg-types": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"confbox": "^0.2.4",
|
||||||
|
"exsolve": "^1.0.8",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -5516,6 +5881,32 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma": {
|
||||||
|
"version": "6.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
|
||||||
|
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/config": "6.19.3",
|
||||||
|
"@prisma/engines": "6.19.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma": "build/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5538,6 +5929,23 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pure-rand": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/dubzzz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fast-check"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -5559,6 +5967,17 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rc9": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"destr": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
@@ -5587,6 +6006,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -6223,6 +6656,16 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -6424,7 +6867,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.19.3",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"framer-motion": "^12.40.0",
|
"framer-motion": "^12.40.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"lucide-react": "^1.17.0",
|
"lucide-react": "^1.17.0",
|
||||||
"next": "16.2.7",
|
"next": "16.2.7",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -17,12 +20,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"cloudinary": "^2.10.0",
|
"cloudinary": "^2.10.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.7",
|
"eslint-config-next": "16.2.7",
|
||||||
|
"prisma": "^6.19.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@ function getLocale(request: NextRequest): string {
|
|||||||
export function proxy(request: NextRequest) {
|
export function proxy(request: NextRequest) {
|
||||||
const pathname = request.nextUrl.pathname;
|
const pathname = request.nextUrl.pathname;
|
||||||
|
|
||||||
// Skip public files and _next requests
|
// Skip public files, _next requests, api and admin routes
|
||||||
if (
|
if (
|
||||||
pathname.startsWith('/_next') ||
|
pathname.startsWith('/_next') ||
|
||||||
pathname.includes('.') ||
|
pathname.includes('.') ||
|
||||||
pathname.startsWith('/api')
|
pathname.startsWith('/api') ||
|
||||||
|
pathname.startsWith('/admin')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,6 +33,3 @@ export function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
||||||
};
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 10 MiB |
@@ -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 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config({ path: '.env.local' });
|
||||||
|
|
||||||
|
async function uploadFile(filePath, folderName) {
|
||||||
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const mimeTypes = {
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.webp': 'image/webp'
|
||||||
|
};
|
||||||
|
const mimeType = mimeTypes[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
const blob = new Blob([fileBuffer], { type: mimeType });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('files', blob, path.basename(filePath));
|
||||||
|
formData.append('folder', folderName);
|
||||||
|
|
||||||
|
console.log(`Uploading ${filePath} to folder ${folderName}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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(`Error uploading ${filePath}:`, errorText);
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
console.log(`Success: ${data.files[0].url}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Exception uploading ${filePath}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile(path.join(process.cwd(), 'public', 'logo.png'), 'moygrup').then(() => console.log('Done'));
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config({ path: '.env.local' });
|
||||||
|
|
||||||
|
async function uploadFile(filePath, folderName) {
|
||||||
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const mimeTypes = {
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.webp': 'image/webp'
|
||||||
|
};
|
||||||
|
const mimeType = mimeTypes[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
const blob = new Blob([fileBuffer], { type: mimeType });
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('files', blob, path.basename(filePath));
|
||||||
|
formData.append('folder', folderName);
|
||||||
|
|
||||||
|
console.log(`Uploading ${filePath} to folder ${folderName}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
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(`Error uploading ${filePath}:`, errorText);
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
console.log(`Success: ${data.files[0].url}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Exception uploading ${filePath}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanAndUpload(dir, baseFolder) {
|
||||||
|
const items = fs.readdirSync(dir);
|
||||||
|
for (const item of items) {
|
||||||
|
// Skip svg files and some special files
|
||||||
|
if (item.endsWith('.svg') || item === '.DS_Store' || item === 'logo.png') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(dir, item);
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
await scanAndUpload(fullPath, `${baseFolder}/${item}`);
|
||||||
|
} else {
|
||||||
|
await uploadFile(fullPath, baseFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PUBLIC_DIR = path.join(process.cwd(), 'public');
|
||||||
|
scanAndUpload(PUBLIC_DIR, 'moygrup').then(() => console.log('Done'));
|
||||||
Reference in New Issue
Block a user