diff --git a/app/[lang]/HomeClient.tsx b/app/[lang]/HomeClient.tsx index 930a825..4582cc2 100644 --- a/app/[lang]/HomeClient.tsx +++ b/app/[lang]/HomeClient.tsx @@ -19,11 +19,9 @@ const HERO_BG = const MARQUEE_ITEMS = [ "Akyaka", "Akçapınar", - "Datça", "Beach Bar", "Kite Surf", "Restaurant", - "Bungalov", "Hotel", "Ege Denizi", ]; diff --git a/app/[lang]/galeri/GaleriClient.tsx b/app/[lang]/galeri/GaleriClient.tsx index bf8fa77..ab7acfd 100644 --- a/app/[lang]/galeri/GaleriClient.tsx +++ b/app/[lang]/galeri/GaleriClient.tsx @@ -13,23 +13,23 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any } type Category = string; 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://res.cloudinary.com/du7xohbct/image/upload/v1780592469/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://res.cloudinary.com/du7xohbct/image/upload/v1780592472/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://res.cloudinary.com/du7xohbct/image/upload/v1780592475/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://res.cloudinary.com/du7xohbct/image/upload/v1780592539/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://res.cloudinary.com/du7xohbct/image/upload/v1780592543/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://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://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://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://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://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://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/kitebeach/moyheader-scaled.jpg", cat: t.venues, title: "Kite Beach Genel Görünüm" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_0177-scaled.jpg", cat: "Beach", title: "Sahil Manzarası" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7609-scaled.jpg", cat: "Beach", title: "Kum ve Deniz" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7612-scaled.jpg", cat: "Beach", title: "Kite Sahili" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7630-scaled.jpg", cat: "Beach", title: "Rüzgar ve Deniz" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7684-scaled.jpg", cat: "Beach", title: "Kite Beach Dinlenme" }, + { src: "https://media.ayris.tech/upload/moygrup/kitebeach/images.jpeg", cat: "Beach", title: "Sahil Keyfi" }, + { src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite3.png", cat: "Hotel", title: "Kite Beach Hotel" }, + { src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite4.png", cat: "Hotel", title: "Hotel Odalar" }, + { src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite5.png", cat: "Hotel", title: "Oda İçi Tasarım" }, + { src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite6.png", cat: "Hotel", title: "Oda Detayları" }, + { 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://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://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://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://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://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(t.all); diff --git a/app/[lang]/layout.tsx b/app/[lang]/layout.tsx index c3a7b3f..9767703 100644 --- a/app/[lang]/layout.tsx +++ b/app/[lang]/layout.tsx @@ -48,7 +48,7 @@ export default async function RootLayout({ "@type": "Organization", "name": "Moy Group", "url": "https://moygroup.com.tr", - "logo": "https://moygroup.com.tr/logo.png", + "logo": "https://media.ayris.tech/t/moygrup/logo.png", "contactPoint": { "@type": "ContactPoint", "telephone": "+90-534-465-62-35", diff --git a/app/admin/actions.ts b/app/admin/actions.ts new file mode 100644 index 0000000..90a9b2c --- /dev/null +++ b/app/admin/actions.ts @@ -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 } }); +} diff --git a/app/admin/components/Sidebar.tsx b/app/admin/components/Sidebar.tsx new file mode 100644 index 0000000..10b4d99 --- /dev/null +++ b/app/admin/components/Sidebar.tsx @@ -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 ( + + ); +} diff --git a/app/admin/contact/page.tsx b/app/admin/contact/page.tsx new file mode 100644 index 0000000..cd89294 --- /dev/null +++ b/app/admin/contact/page.tsx @@ -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 ( +
+

İletişim Mesajları

+
+
+ + + + + + + + + + + + + + {messages.map(msg => ( + + + + + + + + + + ))} + {messages.length === 0 && ( + + + + )} + +
TarihGönderenE-postaKonuMesajDurumİşlem
{msg.createdAt.toLocaleDateString('tr-TR')}{msg.name}{msg.email}{msg.subject || '-'}{msg.message} + {msg.isRead ? ( + + Okundu + + ) : ( + + Yeni + + )} + +
+ {!msg.isRead && ( +
{ + 'use server'; + await markContactAsRead(msg.id); + revalidatePath('/admin/contact'); + }}> + +
+ )} +
{ + 'use server'; + await deleteContact(msg.id); + revalidatePath('/admin/contact'); + }}> + +
+
+
Hiç mesaj bulunamadı.
+
+
+
+ ); +} diff --git a/app/admin/gallery/page.tsx b/app/admin/gallery/page.tsx new file mode 100644 index 0000000..00c1be9 --- /dev/null +++ b/app/admin/gallery/page.tsx @@ -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 ( +
+
+

Galeri Yönetimi

+
+ +
+ {/* Ekleme Formu */} +
+
+

Yeni Resim Ekle

+
{ + '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"> +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + {/* Listeleme */} +
+
+

Mevcut Resimler ({galleryItems.length})

+ +
+ {galleryItems.map(item => ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {item.title} +
+
+

{item.title}

+

{item.category}

+
+
+
{ + 'use server'; + await deleteGalleryItem(item.id); + revalidatePath('/admin/gallery'); + }}> + +
+
+
+ ))} + {galleryItems.length === 0 && ( +
+ Galeriye henüz resim eklenmemiş. +
+ )} +
+
+
+
+
+ ); +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..ce82e2e --- /dev/null +++ b/app/admin/layout.tsx @@ -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 ( + + + {session && } +
+
+ {children} +
+
+ + + ); +} diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx new file mode 100644 index 0000000..47f538f --- /dev/null +++ b/app/admin/login/page.tsx @@ -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) => { + 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 ( +
+
+
+
+ +
+
+

+ Admin Girişi +

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+
+ + +
+ +
+
+
+ ); +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..149dcb6 --- /dev/null +++ b/app/admin/page.tsx @@ -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 ( +
+

Dashboard Özeti

+ +
+ +
+
+
+ +
+
+

Galeri Resimleri

+

{galleryCount}

+
+
+
+ + + +
+
+
+ +
+
+

Hizmetlerimiz

+

{serviceCount}

+
+
+
+ + + +
+
+
+ +
+
+

Okunmamış Mesaj

+

{unreadMessages}

+
+
+
+ +
+
+ ); +} diff --git a/app/admin/services/page.tsx b/app/admin/services/page.tsx new file mode 100644 index 0000000..2edd3fa --- /dev/null +++ b/app/admin/services/page.tsx @@ -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 ( +
+
+

Hizmetlerimiz Yönetimi

+
+ +
+ {/* Ekleme Formu */} +
+
+

Yeni Hizmet Ekle

+
{ + '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"> +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + {/* Listeleme */} +
+
+

Mevcut Hizmetler ({services.length})

+ +
+ {services.map(item => ( +
+
+ {item.iconUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {item.title} + ) : ( + + )} +
+
+

{item.title}

+

{item.description}

+
+
+
{ + 'use server'; + await deleteServiceItem(item.id); + revalidatePath('/admin/services'); + }}> + +
+
+
+ ))} + {services.length === 0 && ( +
+ Henüz hizmet eklenmemiş. +
+ )} +
+
+
+
+
+ ); +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 6959234..55b9018 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -10,7 +10,7 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
{/* Brand */}
- Moy Group Logo + Moy Group Logo

{dict.description}

@@ -93,14 +93,27 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
{/* Bottom */} -
-

+

+

© {new Date().getFullYear()} Moy Group. {dict.rights}

-
- - created by ayris.tech +
+ created by + + ayristech + + {/* Google Bot & Arama Motoru İndeksleme Katmanı (Görünmez / Screen-Reader Safe) */} + + Legal AI & Solutions:{" "} + AyrisLegal,{" "} + Ayris Legal AI +
diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index 4ff9ee8..37c895c 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -54,7 +54,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
{/* Logo */} - Moy Group Logo + Moy Group Logo {/* Desktop Nav */} diff --git a/dictionaries/en.json b/dictionaries/en.json index fcdd477..460d17c 100644 --- a/dictionaries/en.json +++ b/dictionaries/en.json @@ -42,7 +42,7 @@ "title": "Moy Beach", "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.", - "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", "link": "http://moybeachakyaka.com" }, @@ -51,7 +51,7 @@ "title": "Kite Beach 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.", - "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", "link": "http://moybeachakyaka.com" }, @@ -60,7 +60,7 @@ "title": "Kite Beach (Surfing)", "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.", - "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", "link": "https://www.kitebeachakyaka.com.tr" } diff --git a/dictionaries/tr.json b/dictionaries/tr.json index c9270d1..7d27ce1 100644 --- a/dictionaries/tr.json +++ b/dictionaries/tr.json @@ -42,7 +42,7 @@ "title": "Moy Beach", "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.", - "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", "link": "http://moybeachakyaka.com" }, @@ -51,7 +51,7 @@ "title": "Kite Beach 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.", - "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", "link": "http://moybeachakyaka.com" }, @@ -60,7 +60,7 @@ "title": "Kite Beach (Surfing)", "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.", - "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", "link": "https://www.kitebeachakyaka.com.tr" } diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..911f26b --- /dev/null +++ b/lib/auth.ts @@ -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 { + 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; +} diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..33c4c2b --- /dev/null +++ b/lib/db.ts @@ -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; diff --git a/lib/openinary-loader.ts b/lib/openinary-loader.ts new file mode 100644 index 0000000..ad51f52 --- /dev/null +++ b/lib/openinary-loader.ts @@ -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}` +} diff --git a/lib/openinary-url.ts b/lib/openinary-url.ts new file mode 100644 index 0000000..d8eb533 --- /dev/null +++ b/lib/openinary-url.ts @@ -0,0 +1,5 @@ +const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL; + +export function optimizedImage(path: string, params: string) { + return `${BASE}/t/${params}/${path}`; +} diff --git a/lib/openinary.ts b/lib/openinary.ts new file mode 100644 index 0000000..36fcb13 --- /dev/null +++ b/lib/openinary.ts @@ -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, ... } +} diff --git a/next.config.ts b/next.config.ts index b994eab..67b606d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,10 +3,12 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", images: { + loader: "custom", + loaderFile: "./lib/openinary-loader.ts", remotePatterns: [ { protocol: "https", - hostname: "res.cloudinary.com", + hostname: "media.ayris.tech", }, { protocol: "https", diff --git a/package-lock.json b/package-lock.json index 331423b..a06e5ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,10 @@ "name": "moygrup", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", "framer-motion": "^12.40.0", + "jose": "^6.2.3", "lucide-react": "^1.17.0", "next": "16.2.7", "react": "19.2.4", @@ -16,12 +19,14 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "cloudinary": "^2.10.0", "eslint": "^9", "eslint-config-next": "16.2.7", + "prisma": "^6.19.3", "tailwindcss": "^4", "typescript": "^5" } @@ -1249,6 +1254,91 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1256,6 +1346,13 @@ "dev": true, "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": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1547,6 +1644,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", @@ -2511,6 +2615,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", @@ -2569,6 +2682,35 @@ "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": { "version": "1.0.9", "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" } }, + "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2712,6 +2880,23 @@ "dev": true, "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2827,6 +3012,16 @@ "dev": true, "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": { "version": "1.1.4", "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" } }, + "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": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2886,6 +3095,19 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2901,6 +3123,17 @@ "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": { "version": "1.5.366", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz", @@ -2915,6 +3148,16 @@ "dev": true, "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": { "version": "5.22.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.1.tgz", @@ -3535,6 +3778,36 @@ "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": { "version": "3.1.3", "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" } }, + "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": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4511,12 +4802,21 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "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", @@ -5207,6 +5507,13 @@ "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": { "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", @@ -5217,6 +5524,31 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5340,6 +5672,13 @@ "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": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5448,6 +5787,20 @@ "dev": true, "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5467,6 +5820,18 @@ "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": { "version": "1.1.0", "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_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": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5538,6 +5929,23 @@ "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": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5559,6 +5967,17 @@ ], "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": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5587,6 +6006,20 @@ "dev": true, "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": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6223,6 +6656,16 @@ "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": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6424,7 +6867,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", diff --git a/package.json b/package.json index dd90e9e..ed3f23f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,10 @@ "lint": "eslint" }, "dependencies": { + "@prisma/client": "^6.19.3", + "bcryptjs": "^3.0.3", "framer-motion": "^12.40.0", + "jose": "^6.2.3", "lucide-react": "^1.17.0", "next": "16.2.7", "react": "19.2.4", @@ -17,12 +20,14 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "cloudinary": "^2.10.0", "eslint": "^9", "eslint-config-next": "16.2.7", + "prisma": "^6.19.3", "tailwindcss": "^4", "typescript": "^5" } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..9b750e2 --- /dev/null +++ b/prisma/schema.prisma @@ -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()) +} diff --git a/proxy.ts b/proxy.ts index 727c249..7b5672e 100644 --- a/proxy.ts +++ b/proxy.ts @@ -10,11 +10,12 @@ function getLocale(request: NextRequest): string { export function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname; - // Skip public files and _next requests + // Skip public files, _next requests, api and admin routes if ( pathname.startsWith('/_next') || pathname.includes('.') || - pathname.startsWith('/api') + pathname.startsWith('/api') || + pathname.startsWith('/admin') ) { return; } @@ -32,6 +33,3 @@ export function proxy(request: NextRequest) { } } -export const config = { - matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], -}; diff --git a/public/moy-beach.jpg b/public/moy-beach.jpg index 21cdbf1..dd62984 100644 Binary files a/public/moy-beach.jpg and b/public/moy-beach.jpg differ diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..ff04343 --- /dev/null +++ b/scripts/seed.ts @@ -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) + }) diff --git a/scripts/upload-logo.js b/scripts/upload-logo.js new file mode 100644 index 0000000..d14ba34 --- /dev/null +++ b/scripts/upload-logo.js @@ -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')); diff --git a/scripts/upload-public.js b/scripts/upload-public.js new file mode 100644 index 0000000..4d55f00 --- /dev/null +++ b/scripts/upload-public.js @@ -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'));