From a4ad9344a08015b3dd53e1afdf8e5ba5cad98ea9 Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Tue, 9 Jun 2026 21:28:45 +0300 Subject: [PATCH] feat: integrate Cloudinary image upload, Instagram Feed, and fix Next.js warnings --- .gitignore | 2 + app/[locale]/layout.tsx | 2 +- app/[locale]/page.tsx | 39 +- app/actions/about.ts | 67 ++ app/actions/admin.ts | 103 ++ app/actions/auth.ts | 44 + app/actions/beach.ts | 63 ++ app/actions/contact.ts | 57 + app/actions/events.ts | 104 ++ app/actions/gallery.ts | 97 ++ app/actions/instagram.ts | 68 ++ app/admin/(auth)/layout.tsx | 27 + app/admin/(auth)/login/page.tsx | 13 + app/admin/(dashboard)/about/page.tsx | 22 + app/admin/(dashboard)/admins/page.tsx | 12 + app/admin/(dashboard)/beach/page.tsx | 21 + app/admin/(dashboard)/contact/page.tsx | 18 + app/admin/(dashboard)/events/page.tsx | 18 + app/admin/(dashboard)/gallery/page.tsx | 18 + app/admin/(dashboard)/layout.tsx | 124 +++ app/admin/(dashboard)/page.tsx | 17 + app/api/cloudinary/route.ts | 20 + app/favicon.ico | Bin 25931 -> 15406 bytes app/globals.css | 5 + components/About.tsx | 42 +- components/Beach.tsx | 38 +- components/Contact.tsx | 20 +- components/Events.tsx | 343 +++--- components/Footer.tsx | 18 +- components/Gallery.tsx | 7 +- components/InstagramFeed.tsx | 94 ++ components/Navbar.tsx | 50 +- components/WelcomePopup.tsx | 84 ++ components/admin/AboutClient.tsx | 187 ++++ components/admin/AdminsClient.tsx | 207 ++++ components/admin/BeachClient.tsx | 157 +++ components/admin/ContactClient.tsx | 129 +++ components/admin/EventsClient.tsx | 256 +++++ components/admin/GalleryClient.tsx | 215 ++++ components/admin/ImageUpload.tsx | 73 ++ components/admin/LoginClient.tsx | 81 ++ lib/auth.ts | 44 + lib/prisma.ts | 18 + middleware.ts | 17 - next.config.ts | 8 + package-lock.json | 1365 +++++++++++++++++++++++- package.json | 11 + prisma.config.ts | 14 + prisma/schema.prisma | 78 ++ proxy.ts | 46 + public/icon-192x192.png | Bin 0 -> 341116 bytes public/logo.png | Bin 0 -> 341116 bytes 52 files changed, 4300 insertions(+), 263 deletions(-) create mode 100644 app/actions/about.ts create mode 100644 app/actions/admin.ts create mode 100644 app/actions/auth.ts create mode 100644 app/actions/beach.ts create mode 100644 app/actions/contact.ts create mode 100644 app/actions/events.ts create mode 100644 app/actions/gallery.ts create mode 100644 app/actions/instagram.ts create mode 100644 app/admin/(auth)/layout.tsx create mode 100644 app/admin/(auth)/login/page.tsx create mode 100644 app/admin/(dashboard)/about/page.tsx create mode 100644 app/admin/(dashboard)/admins/page.tsx create mode 100644 app/admin/(dashboard)/beach/page.tsx create mode 100644 app/admin/(dashboard)/contact/page.tsx create mode 100644 app/admin/(dashboard)/events/page.tsx create mode 100644 app/admin/(dashboard)/gallery/page.tsx create mode 100644 app/admin/(dashboard)/layout.tsx create mode 100644 app/admin/(dashboard)/page.tsx create mode 100644 app/api/cloudinary/route.ts create mode 100644 components/InstagramFeed.tsx create mode 100644 components/WelcomePopup.tsx create mode 100644 components/admin/AboutClient.tsx create mode 100644 components/admin/AdminsClient.tsx create mode 100644 components/admin/BeachClient.tsx create mode 100644 components/admin/ContactClient.tsx create mode 100644 components/admin/EventsClient.tsx create mode 100644 components/admin/GalleryClient.tsx create mode 100644 components/admin/ImageUpload.tsx create mode 100644 components/admin/LoginClient.tsx create mode 100644 lib/auth.ts create mode 100644 lib/prisma.ts delete mode 100644 middleware.ts create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma create mode 100644 proxy.ts create mode 100644 public/icon-192x192.png create mode 100644 public/logo.png diff --git a/.gitignore b/.gitignore index 5ef6a52..45254b6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/app/generated/prisma diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index bed88d9..c19a7e8 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -37,7 +37,7 @@ export default async function LocaleLayout({ return ( - + {children} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 93ab098..081e099 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -9,6 +9,14 @@ import Gallery from '@/components/Gallery'; import Contact from '@/components/Contact'; import Footer from '@/components/Footer'; import WhatsAppButton from '@/components/WhatsAppButton'; +import WelcomePopup from '@/components/WelcomePopup'; +import { getEvents } from '@/app/actions/events'; +import { getGalleryImages } from '@/app/actions/gallery'; +import { getContactSettings } from '@/app/actions/contact'; +import { getBeachSettings } from '@/app/actions/beach'; +import { getAboutSettings } from '@/app/actions/about'; +import { getInstagramPosts } from '@/app/actions/instagram'; +import InstagramFeed from '@/components/InstagramFeed'; export default async function Page({ params @@ -16,20 +24,39 @@ export default async function Page({ params: Promise<{ locale: string }> }) { const { locale } = await params; + const eventsResult = await getEvents(); + const dbEvents = eventsResult.success ? eventsResult.data : []; + + const galleryResult = await getGalleryImages(); + const dbGallery = galleryResult.success ? galleryResult.data : []; + + const contactResult = await getContactSettings(); + const dbContact = contactResult.success ? contactResult.data : null; + + const beachResult = await getBeachSettings(); + const dbBeach = beachResult.success ? beachResult.data : null; + + const aboutResult = await getAboutSettings(); + const dbAbout = aboutResult.success ? aboutResult.data : null; + + const instaResult = await getInstagramPosts(); + const instaPosts = instaResult.success ? instaResult.data : []; return (
- - - + + + - - - + + + +
+
); } diff --git a/app/actions/about.ts b/app/actions/about.ts new file mode 100644 index 0000000..47e6dbe --- /dev/null +++ b/app/actions/about.ts @@ -0,0 +1,67 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function getAboutSettings() { + try { + let settings = await prisma.aboutSettings.findUnique({ + where: { id: "default" } + }); + + if (!settings) { + settings = await prisma.aboutSettings.create({ + data: { + id: "default", + title: "Sonsuz Mavilikte Bir Kaçış", + description: "Kozmos Beach & More, denizin esintisiyle doğanın ritmini bir araya getiren eşsiz bir deneyim sunuyor. Gündüz güneşin ve berrak denizin tadını çıkarırken, gün batımıyla birlikte DJ performansları ve özel kokteyller eşliğinde unutulmaz anılar biriktirin.", + card1Title: "Kusursuz Sahil", + card1Desc: "İncecik altın kumu ve turkuaz sularıyla kendinizi tamamen yenileyin.", + card2Title: "Gastronomi", + card2Desc: "Dünya mutfağından özenle seçilmiş lezzetler ve imza kokteyller.", + card3Title: "Canlı Eğlence", + card3Desc: "Her akşam gün batımında başlayan benzersiz DJ performansları.", + image: "https://images.unsplash.com/photo-1544148103-0773bf10d330?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + } + }); + } + + return { success: true, data: settings }; + } catch (error) { + console.error("Error fetching about settings:", error); + return { success: false, error: "Hakkımızda ayarları alınırken bir hata oluştu." }; + } +} + +export async function updateAboutSettings(formData: FormData) { + try { + const title = formData.get("title") as string; + const description = formData.get("description") as string; + const card1Title = formData.get("card1Title") as string; + const card1Desc = formData.get("card1Desc") as string; + const card2Title = formData.get("card2Title") as string; + const card2Desc = formData.get("card2Desc") as string; + const card3Title = formData.get("card3Title") as string; + const card3Desc = formData.get("card3Desc") as string; + const image = formData.get("image") as string; + + if (!title || !description || !card1Title || !card1Desc || !card2Title || !card2Desc || !card3Title || !card3Desc || !image) { + return { success: false, error: "Tüm alanların doldurulması zorunludur." }; + } + + await prisma.aboutSettings.upsert({ + where: { id: "default" }, + update: { title, description, card1Title, card1Desc, card2Title, card2Desc, card3Title, card3Desc, image }, + create: { id: "default", title, description, card1Title, card1Desc, card2Title, card2Desc, card3Title, card3Desc, image } + }); + + revalidatePath("/admin/about"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error updating about settings:", error); + return { success: false, error: "Hakkımızda ayarları güncellenirken bir hata oluştu." }; + } +} diff --git a/app/actions/admin.ts b/app/actions/admin.ts new file mode 100644 index 0000000..f1d59cb --- /dev/null +++ b/app/actions/admin.ts @@ -0,0 +1,103 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import bcrypt from "bcryptjs"; +import { revalidatePath } from "next/cache"; + +export async function getAdmins() { + try { + const admins = await prisma.admin.findMany({ + select: { + id: true, + name: true, + email: true, + createdAt: true, + updatedAt: true, + }, + orderBy: { createdAt: 'desc' } + }); + return { success: true, data: admins }; + } catch (error) { + console.error("Error fetching admins:", error); + return { success: false, error: "Adminleri çekerken bir hata oluştu." }; + } +} + +export async function createAdmin(formData: FormData) { + try { + const name = formData.get("name") as string; + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + if (!name || !email || !password) { + return { success: false, error: "Lütfen tüm alanları doldurun." }; + } + + const existingAdmin = await prisma.admin.findUnique({ + where: { email }, + }); + + if (existingAdmin) { + return { success: false, error: "Bu e-posta adresi zaten kullanımda." }; + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await prisma.admin.create({ + data: { + name, + email, + password: hashedPassword, + }, + }); + + revalidatePath("/admin/admins"); + return { success: true }; + } catch (error) { + console.error("Error creating admin:", error); + return { success: false, error: "Admin oluşturulurken bir hata oluştu." }; + } +} + +export async function updateAdmin(id: string, formData: FormData) { + try { + const name = formData.get("name") as string; + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + if (!name || !email) { + return { success: false, error: "İsim ve e-posta zorunludur." }; + } + + const dataToUpdate: any = { name, email }; + + if (password) { + dataToUpdate.password = await bcrypt.hash(password, 10); + } + + await prisma.admin.update({ + where: { id }, + data: dataToUpdate, + }); + + revalidatePath("/admin/admins"); + return { success: true }; + } catch (error) { + console.error("Error updating admin:", error); + return { success: false, error: "Admin güncellenirken bir hata oluştu." }; + } +} + +export async function deleteAdmin(id: string) { + try { + await prisma.admin.delete({ + where: { id }, + }); + + revalidatePath("/admin/admins"); + return { success: true }; + } catch (error) { + console.error("Error deleting admin:", error); + return { success: false, error: "Admin silinirken bir hata oluştu." }; + } +} diff --git a/app/actions/auth.ts b/app/actions/auth.ts new file mode 100644 index 0000000..174eb4d --- /dev/null +++ b/app/actions/auth.ts @@ -0,0 +1,44 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import bcrypt from "bcryptjs"; +import { signToken, setAuthCookie, removeAuthCookie } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +export async function login(formData: FormData) { + try { + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + if (!email || !password) { + return { success: false, error: "Lütfen email ve şifre giriniz." }; + } + + const admin = await prisma.admin.findUnique({ + where: { email }, + }); + + if (!admin) { + return { success: false, error: "Geçersiz e-posta veya şifre." }; + } + + const isMatch = await bcrypt.compare(password, admin.password); + if (!isMatch) { + return { success: false, error: "Geçersiz e-posta veya şifre." }; + } + + // Başarılı giriş, token oluştur ve cookie'ye kaydet + const token = await signToken({ id: admin.id, email: admin.email, name: admin.name }); + await setAuthCookie(token); + + return { success: true }; + } catch (error) { + console.error("Login error:", error); + return { success: false, error: "Giriş yapılırken beklenmeyen bir hata oluştu." }; + } +} + +export async function logout() { + await removeAuthCookie(); + redirect("/admin/login"); +} diff --git a/app/actions/beach.ts b/app/actions/beach.ts new file mode 100644 index 0000000..a543039 --- /dev/null +++ b/app/actions/beach.ts @@ -0,0 +1,63 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function getBeachSettings() { + try { + let settings = await prisma.beachSettings.findUnique({ + where: { id: "default" } + }); + + if (!settings) { + settings = await prisma.beachSettings.create({ + data: { + id: "default", + title: "Ayrıcalıklı Bir Gevşeme Alanı", + description: "Tertemiz sularla buluşan özel plajımız, kendinizi tamamen yenileyebilmeniz için rahatlığın merkezinde. Altın kumların ve berrak denizin tadını çıkarırken, dünya standartlarındaki hizmetimizle her anınızın özel hissettirmesini sağlıyoruz.", + reservationInfo: "VIP cabana ve ön sıra şezlonglar yoğun talep görmektedir. Yeriniz garantilemek için en az bir gün önceden bizimle iletişime geçmenizi öneririz.", + image1: "https://images.unsplash.com/photo-1544148103-0773bf10d330?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image2: "https://images.unsplash.com/photo-1519046904884-53103b34b206?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image3: "https://images.unsplash.com/photo-1499793983690-e29da59ef1c2?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image4: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + } + }); + } + + return { success: true, data: settings }; + } catch (error) { + console.error("Error fetching beach settings:", error); + return { success: false, error: "Plaj ayarları alınırken bir hata oluştu." }; + } +} + +export async function updateBeachSettings(formData: FormData) { + try { + const title = formData.get("title") as string; + const description = formData.get("description") as string; + const reservationInfo = formData.get("reservationInfo") as string; + const image1 = formData.get("image1") as string; + const image2 = formData.get("image2") as string; + const image3 = formData.get("image3") as string; + const image4 = formData.get("image4") as string; + + if (!title || !description || !reservationInfo || !image1 || !image2 || !image3 || !image4) { + return { success: false, error: "Tüm alanların doldurulması zorunludur." }; + } + + await prisma.beachSettings.upsert({ + where: { id: "default" }, + update: { title, description, reservationInfo, image1, image2, image3, image4 }, + create: { id: "default", title, description, reservationInfo, image1, image2, image3, image4 } + }); + + revalidatePath("/admin/beach"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error updating beach settings:", error); + return { success: false, error: "Plaj ayarları güncellenirken bir hata oluştu." }; + } +} diff --git a/app/actions/contact.ts b/app/actions/contact.ts new file mode 100644 index 0000000..77e81c8 --- /dev/null +++ b/app/actions/contact.ts @@ -0,0 +1,57 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function getContactSettings() { + try { + let settings = await prisma.contactSettings.findUnique({ + where: { id: "default" } + }); + + if (!settings) { + settings = await prisma.contactSettings.create({ + data: { + id: "default", + address: "Akyaka, Gökova Körfezi, Muğla", + phone: "905000000000", + instagram: "https://www.instagram.com/kozmosbeach/", + mapUrl: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d539.7850236154288!2d28.2165012!3d37.0413013527448!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x14bfa1006d98f019%3A0x85a41319cb3b8be!2sKozmos%20Beach%20%26%20More!5e1!3m2!1sen!2str!4v1781027409075!5m2!1sen!2str" + } + }); + } + + return { success: true, data: settings }; + } catch (error) { + console.error("Error fetching contact settings:", error); + return { success: false, error: "İletişim ayarları alınırken bir hata oluştu." }; + } +} + +export async function updateContactSettings(formData: FormData) { + try { + const address = formData.get("address") as string; + const phone = formData.get("phone") as string; + const instagram = formData.get("instagram") as string; + const mapUrl = formData.get("mapUrl") as string; + + if (!address || !phone || !instagram || !mapUrl) { + return { success: false, error: "Tüm alanların doldurulması zorunludur." }; + } + + await prisma.contactSettings.upsert({ + where: { id: "default" }, + update: { address, phone, instagram, mapUrl }, + create: { id: "default", address, phone, instagram, mapUrl } + }); + + revalidatePath("/admin/contact"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error updating contact settings:", error); + return { success: false, error: "İletişim ayarları güncellenirken bir hata oluştu." }; + } +} diff --git a/app/actions/events.ts b/app/actions/events.ts new file mode 100644 index 0000000..d614f4c --- /dev/null +++ b/app/actions/events.ts @@ -0,0 +1,104 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function getEvents() { + try { + const events = await prisma.event.findMany({ + orderBy: { createdAt: 'desc' } + }); + return { success: true, data: events }; + } catch (error) { + console.error("Error fetching events:", error); + return { success: false, error: "Etkinlikleri çekerken bir hata oluştu." }; + } +} + +export async function createEvent(formData: FormData) { + try { + const title = formData.get("title") as string; + const time = formData.get("time") as string; + const tag = formData.get("tag") as string; + const iconType = formData.get("iconType") as string || "music"; + const colorTheme = formData.get("colorTheme") as string || "cyan"; + const isFeatured = formData.get("isFeatured") === "on"; + + if (!title || !time || !tag) { + return { success: false, error: "Başlık, zaman ve etiket alanları zorunludur." }; + } + + await prisma.event.create({ + data: { + title, + time, + tag, + iconType, + colorTheme, + isFeatured, + }, + }); + + revalidatePath("/admin/events"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error creating event:", error); + return { success: false, error: "Etkinlik oluşturulurken bir hata oluştu." }; + } +} + +export async function updateEvent(id: string, formData: FormData) { + try { + const title = formData.get("title") as string; + const time = formData.get("time") as string; + const tag = formData.get("tag") as string; + const iconType = formData.get("iconType") as string || "music"; + const colorTheme = formData.get("colorTheme") as string || "cyan"; + const isFeatured = formData.get("isFeatured") === "on"; + + if (!title || !time || !tag) { + return { success: false, error: "Başlık, zaman ve etiket alanları zorunludur." }; + } + + await prisma.event.update({ + where: { id }, + data: { + title, + time, + tag, + iconType, + colorTheme, + isFeatured, + }, + }); + + revalidatePath("/admin/events"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error updating event:", error); + return { success: false, error: "Etkinlik güncellenirken bir hata oluştu." }; + } +} + +export async function deleteEvent(id: string) { + try { + await prisma.event.delete({ + where: { id }, + }); + + revalidatePath("/admin/events"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error deleting event:", error); + return { success: false, error: "Etkinlik silinirken bir hata oluştu." }; + } +} diff --git a/app/actions/gallery.ts b/app/actions/gallery.ts new file mode 100644 index 0000000..850b4ac --- /dev/null +++ b/app/actions/gallery.ts @@ -0,0 +1,97 @@ +"use server"; + +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function getGalleryImages() { + try { + const images = await prisma.galleryImage.findMany({ + orderBy: [ + { order: 'asc' }, + { createdAt: 'desc' } + ] + }); + return { success: true, data: images }; + } catch (error) { + console.error("Error fetching gallery images:", error); + return { success: false, error: "Galeri görsellerini çekerken bir hata oluştu." }; + } +} + +export async function createGalleryImage(formData: FormData) { + try { + const src = formData.get("src") as string; + const alt = formData.get("alt") as string || ""; + const orderStr = formData.get("order") as string; + const order = orderStr ? parseInt(orderStr, 10) : 0; + + if (!src) { + return { success: false, error: "Görsel URL'si zorunludur." }; + } + + await prisma.galleryImage.create({ + data: { + src, + alt, + order, + }, + }); + + revalidatePath("/admin/gallery"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error creating gallery image:", error); + return { success: false, error: "Galeri görseli eklenirken bir hata oluştu." }; + } +} + +export async function updateGalleryImage(id: string, formData: FormData) { + try { + const src = formData.get("src") as string; + const alt = formData.get("alt") as string || ""; + const orderStr = formData.get("order") as string; + const order = orderStr ? parseInt(orderStr, 10) : 0; + + if (!src) { + return { success: false, error: "Görsel URL'si zorunludur." }; + } + + await prisma.galleryImage.update({ + where: { id }, + data: { + src, + alt, + order, + }, + }); + + revalidatePath("/admin/gallery"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error updating gallery image:", error); + return { success: false, error: "Galeri görseli güncellenirken bir hata oluştu." }; + } +} + +export async function deleteGalleryImage(id: string) { + try { + await prisma.galleryImage.delete({ + where: { id }, + }); + + revalidatePath("/admin/gallery"); + revalidatePath("/"); + revalidatePath("/tr"); + revalidatePath("/en"); + return { success: true }; + } catch (error) { + console.error("Error deleting gallery image:", error); + return { success: false, error: "Galeri görseli silinirken bir hata oluştu." }; + } +} diff --git a/app/actions/instagram.ts b/app/actions/instagram.ts new file mode 100644 index 0000000..88092ab --- /dev/null +++ b/app/actions/instagram.ts @@ -0,0 +1,68 @@ +"use server"; + +export async function getInstagramPosts() { + try { + const response = await fetch('https://instagram120.p.rapidapi.com/api/instagram/posts?v=1', { + method: 'POST', + headers: { + 'x-rapidapi-key': process.env.RAPIDAPI_KEY || '762dc41c1dmshc120ecf29aa240ap11dc66jsn87f410b870f5', + 'x-rapidapi-host': 'instagram120.p.rapidapi.com', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username: 'kozmosbeach', maxId: '' }), + // CRITICAL ADIM: Veriyi 24 saat boyunca önbellekte tut (Saniye cinsinden: 24 * 60 * 60) + next: { revalidate: 86400 } + }); + + if (!response.ok) { + throw new Error("Instagram API request failed"); + } + + const data = await response.json(); + + // The RapidAPI endpoint usually returns an array of posts or { data: { items: [...] } } + // Based on standard Instagram120 response, it returns { result: { edges: [{node: {}}] } } + let posts = []; + if (data && data.result && data.result.edges && Array.isArray(data.result.edges)) { + posts = data.result.edges.map((edge: any) => edge.node); + } else if (data && Array.isArray(data)) { + posts = data; + } else if (data && data.data && Array.isArray(data.data)) { + posts = data.data; + } else if (data && data.items && Array.isArray(data.items)) { + posts = data.items; + } + + // Map to a cleaner format and take first 4 + const formattedPosts = posts.slice(0, 4).map((post: any) => { + // Instagram API has multiple variants for post schema + const id = post.id || post.pk; + const code = post.code; + + // Try to find the highest res image + let imageUrl = ""; + if (post.image_versions2 && post.image_versions2.candidates) { + imageUrl = post.image_versions2.candidates[0].url; + } else if (post.carousel_media && post.carousel_media.length > 0) { + imageUrl = post.carousel_media[0].image_versions2.candidates[0].url; + } else if (post.display_url) { + imageUrl = post.display_url; + } + + const caption = post.caption?.text || post.caption || ""; + const url = code ? `https://instagram.com/p/${code}` : `https://instagram.com/kozmosbeach`; + + return { + id, + imageUrl, + url, + caption + }; + }); + + return { success: true, data: formattedPosts }; + } catch (error) { + console.error("Error fetching Instagram posts:", error); + return { success: false, error: "Instagram gönderileri alınamadı." }; + } +} diff --git a/app/admin/(auth)/layout.tsx b/app/admin/(auth)/layout.tsx new file mode 100644 index 0000000..767eb8a --- /dev/null +++ b/app/admin/(auth)/layout.tsx @@ -0,0 +1,27 @@ +import { Marcellus } from 'next/font/google'; +import '@/app/globals.css'; + +const marcellus = Marcellus({ + subsets: ['latin'], + variable: '--font-marcellus', + weight: ['400'], +}); + +export const metadata = { + title: 'Kozmos Admin - Giriş Yap', + description: 'Kozmos Beach & More - Yönetim Paneli Girişi', +}; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/app/admin/(auth)/login/page.tsx b/app/admin/(auth)/login/page.tsx new file mode 100644 index 0000000..b59d839 --- /dev/null +++ b/app/admin/(auth)/login/page.tsx @@ -0,0 +1,13 @@ +import LoginClient from "@/components/admin/LoginClient"; +import { getAuthCookie } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +export default async function LoginPage() { + const admin = await getAuthCookie(); + + if (admin) { + redirect("/admin"); + } + + return ; +} diff --git a/app/admin/(dashboard)/about/page.tsx b/app/admin/(dashboard)/about/page.tsx new file mode 100644 index 0000000..45c15e4 --- /dev/null +++ b/app/admin/(dashboard)/about/page.tsx @@ -0,0 +1,22 @@ +import { getAboutSettings } from "@/app/actions/about"; +import AboutClient from "@/components/admin/AboutClient"; + +export const dynamic = "force-dynamic"; + +export default async function AboutPage() { + const result = await getAboutSettings(); + + const settings = result.success && result.data ? result.data : { + title: "Sonsuz Mavilikte Bir Kaçış", + description: "Kozmos Beach & More, denizin esintisiyle doğanın ritmini bir araya getiren eşsiz bir deneyim sunuyor. Gündüz güneşin ve berrak denizin tadını çıkarırken, gün batımıyla birlikte DJ performansları ve özel kokteyller eşliğinde unutulmaz anılar biriktirin.", + card1Title: "Kusursuz Sahil", + card1Desc: "İncecik altın kumu ve turkuaz sularıyla kendinizi tamamen yenileyin.", + card2Title: "Gastronomi", + card2Desc: "Dünya mutfağından özenle seçilmiş lezzetler ve imza kokteyller.", + card3Title: "Canlı Eğlence", + card3Desc: "Her akşam gün batımında başlayan benzersiz DJ performansları.", + image: "https://images.unsplash.com/photo-1544148103-0773bf10d330?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + }; + + return ; +} diff --git a/app/admin/(dashboard)/admins/page.tsx b/app/admin/(dashboard)/admins/page.tsx new file mode 100644 index 0000000..1f62fbf --- /dev/null +++ b/app/admin/(dashboard)/admins/page.tsx @@ -0,0 +1,12 @@ +import { getAdmins } from "@/app/actions/admin"; +import AdminsClient from "@/components/admin/AdminsClient"; + +// Vercel / Next.js için dinamik rendering (cache'lemeyi kapatmak için) +export const dynamic = "force-dynamic"; + +export default async function AdminsPage() { + const result = await getAdmins(); + const admins = result.success && result.data ? result.data : []; + + return ; +} diff --git a/app/admin/(dashboard)/beach/page.tsx b/app/admin/(dashboard)/beach/page.tsx new file mode 100644 index 0000000..4cb890e --- /dev/null +++ b/app/admin/(dashboard)/beach/page.tsx @@ -0,0 +1,21 @@ +import { getBeachSettings } from "@/app/actions/beach"; +import BeachClient from "@/components/admin/BeachClient"; + +export const dynamic = "force-dynamic"; + +export default async function BeachPage() { + const result = await getBeachSettings(); + + // result.data will always be populated if success because we upsert "default" + const settings = result.success && result.data ? result.data : { + title: "Ayrıcalıklı Bir Gevşeme Alanı", + description: "Tertemiz sularla buluşan özel plajımız, kendinizi tamamen yenileyebilmeniz için rahatlığın merkezinde. Altın kumların ve berrak denizin tadını çıkarırken, dünya standartlarındaki hizmetimizle her anınızın özel hissettirmesini sağlıyoruz.", + reservationInfo: "VIP cabana ve ön sıra şezlonglar yoğun talep görmektedir. Yeriniz garantilemek için en az bir gün önceden bizimle iletişime geçmenizi öneririz.", + image1: "https://images.unsplash.com/photo-1544148103-0773bf10d330?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image2: "https://images.unsplash.com/photo-1519046904884-53103b34b206?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image3: "https://images.unsplash.com/photo-1499793983690-e29da59ef1c2?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + image4: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", + }; + + return ; +} diff --git a/app/admin/(dashboard)/contact/page.tsx b/app/admin/(dashboard)/contact/page.tsx new file mode 100644 index 0000000..a8f9709 --- /dev/null +++ b/app/admin/(dashboard)/contact/page.tsx @@ -0,0 +1,18 @@ +import { getContactSettings } from "@/app/actions/contact"; +import ContactClient from "@/components/admin/ContactClient"; + +export const dynamic = "force-dynamic"; + +export default async function ContactPage() { + const result = await getContactSettings(); + + // result.data will always be populated if success because we upsert "default" + const settings = result.success && result.data ? result.data : { + address: "Akyaka, Gökova Körfezi, Muğla", + phone: "905000000000", + instagram: "https://www.instagram.com/kozmosbeach/", + mapUrl: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d12754.717654763133!2d28.3144!3d36.9507!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x14bf76ccf312ba37%3A0xcdaaa97b102b4bb7!2sAkyaka%2C%20Ula%2FMu%C4%9Fla!5e0!3m2!1str!2str!4v1700000000000!5m2!1str!2str" + }; + + return ; +} diff --git a/app/admin/(dashboard)/events/page.tsx b/app/admin/(dashboard)/events/page.tsx new file mode 100644 index 0000000..b9de0a9 --- /dev/null +++ b/app/admin/(dashboard)/events/page.tsx @@ -0,0 +1,18 @@ +import { getEvents } from "@/app/actions/events"; +import EventsClient from "@/components/admin/EventsClient"; + +export const dynamic = "force-dynamic"; + +export default async function EventsPage() { + const result = await getEvents(); + const events = result.success && result.data ? result.data : []; + + // Data parsing for Client Component + const parsedEvents = events.map(e => ({ + ...e, + createdAt: e.createdAt.toISOString(), + updatedAt: e.updatedAt.toISOString(), + })); + + return ; +} diff --git a/app/admin/(dashboard)/gallery/page.tsx b/app/admin/(dashboard)/gallery/page.tsx new file mode 100644 index 0000000..db3fbad --- /dev/null +++ b/app/admin/(dashboard)/gallery/page.tsx @@ -0,0 +1,18 @@ +import { getGalleryImages } from "@/app/actions/gallery"; +import GalleryClient from "@/components/admin/GalleryClient"; + +export const dynamic = "force-dynamic"; + +export default async function GalleryPage() { + const result = await getGalleryImages(); + const images = result.success && result.data ? result.data : []; + + // Data parsing for Client Component + const parsedImages = images.map(img => ({ + ...img, + createdAt: img.createdAt.toISOString(), + updatedAt: img.updatedAt.toISOString(), + })); + + return ; +} diff --git a/app/admin/(dashboard)/layout.tsx b/app/admin/(dashboard)/layout.tsx new file mode 100644 index 0000000..0474035 --- /dev/null +++ b/app/admin/(dashboard)/layout.tsx @@ -0,0 +1,124 @@ +import { Marcellus } from 'next/font/google'; +import '@/app/globals.css'; +import Link from 'next/link'; +import { Users, LayoutDashboard, LogOut } from 'lucide-react'; + +const marcellus = Marcellus({ + subsets: ['latin'], + variable: '--font-marcellus', + weight: ['400'], +}); + +export const metadata = { + title: 'Kozmos Admin', + description: 'Kozmos Beach & More - Yönetim Paneli', +}; + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {/* Sidebar */} + + + {/* Main Content */} +
+ {children} +
+ + + + ); +} diff --git a/app/admin/(dashboard)/page.tsx b/app/admin/(dashboard)/page.tsx new file mode 100644 index 0000000..9751e52 --- /dev/null +++ b/app/admin/(dashboard)/page.tsx @@ -0,0 +1,17 @@ +export default function AdminDashboard() { + return ( +
+

Dashboard

+ +
+
+

Sisteme Hoş Geldiniz

+

Kozmos Yönetim Paneli

+

+ Sol menüden "Admin Yönetimi" kısmına giderek yeni yönetici ekleyebilir veya silebilirsiniz. +

+
+
+
+ ); +} diff --git a/app/api/cloudinary/route.ts b/app/api/cloudinary/route.ts new file mode 100644 index 0000000..1174dbb --- /dev/null +++ b/app/api/cloudinary/route.ts @@ -0,0 +1,20 @@ +import { v2 as cloudinary } from "cloudinary"; +import { NextResponse } from "next/server"; + +cloudinary.config({ + cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +export async function POST(request: Request) { + const body = await request.json(); + const { paramsToSign } = body; + + const signature = cloudinary.utils.api_sign_request( + paramsToSign, + process.env.CLOUDINARY_API_SECRET as string + ); + + return NextResponse.json({ signature }); +} diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..eb4ad890f6108d6dd939257c9f5be685da36ff59 100644 GIT binary patch literal 15406 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 LrJugUA?W`A8`#=m diff --git a/app/globals.css b/app/globals.css index 545e0db..75dcdbf 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,6 +19,7 @@ --color-turquoise: #00BFA5; --color-forest: #0E1B30; --color-dark-brown: #080D18; + --color-charcoal: #111827; --font-heading: var(--font-marcellus); --font-body: var(--font-marcellus); @@ -38,6 +39,10 @@ html { scroll-behavior: smooth; } +html[data-scroll-behavior="smooth"] { + scroll-behavior: smooth; +} + /* ── CSS custom property for animated border ──── */ @property --border-angle { syntax: ''; diff --git a/components/About.tsx b/components/About.tsx index 714652e..84cfb01 100644 --- a/components/About.tsx +++ b/components/About.tsx @@ -11,9 +11,34 @@ const features = [ { icon: Music, titleKey: 'card3_title', descKey: 'card3_desc', color: 'text-coral', bg: 'bg-coral/10' }, ]; -export default function About() { +export default function About({ dbAbout }: { dbAbout?: any }) { const t = useTranslations('About'); + const titleVal = dbAbout?.title || t('title'); + const descVal = dbAbout?.description || t('description'); + const imgUrl = dbAbout?.image || "https://picsum.photos/800/1100?random=2"; + + const featuresList = [ + { + icon: Waves, + title: dbAbout?.card1Title || t('card1_title'), + desc: dbAbout?.card1Desc || t('card1_desc'), + color: 'text-aqua', bg: 'bg-aqua/10' + }, + { + icon: Leaf, + title: dbAbout?.card2Title || t('card2_title'), + desc: dbAbout?.card2Desc || t('card2_desc'), + color: 'text-amber', bg: 'bg-amber/10' + }, + { + icon: Music, + title: dbAbout?.card3Title || t('card3_title'), + desc: dbAbout?.card3Desc || t('card3_desc'), + color: 'text-coral', bg: 'bg-coral/10' + }, + ]; + return (
@@ -32,15 +57,15 @@ export default function About() { — Beach & More

- {t('title')} + {titleVal}

-

{t('description')}

+

{descVal}

- {features.map(({ icon: Icon, titleKey, descKey, color, bg }, i) => ( + {featuresList.map(({ icon: Icon, title, desc, color, bg }, i) => (
-

{t(titleKey)}

-

{t(descKey)}

+

{title}

+

{desc}

))} @@ -74,9 +99,10 @@ export default function About() { />
Kozmos atmosferi diff --git a/components/Beach.tsx b/components/Beach.tsx index bd6a1ee..4e94fd0 100644 --- a/components/Beach.tsx +++ b/components/Beach.tsx @@ -5,9 +5,18 @@ import { motion } from 'framer-motion'; import Image from 'next/image'; import { Sun } from 'lucide-react'; -export default function Beach() { +export default function Beach({ dbBeach }: { dbBeach?: any }) { const t = useTranslations('Beach'); + const titleVal = dbBeach?.title || t('title'); + const descVal = dbBeach?.description || t('description'); + const resInfoVal = dbBeach?.reservationInfo || t('reservation_info'); + + const img1 = dbBeach?.image1 || "https://picsum.photos/400/600?random=10"; + const img2 = dbBeach?.image2 || "https://picsum.photos/400/400?random=11"; + const img3 = dbBeach?.image3 || "https://picsum.photos/400/400?random=12"; + const img4 = dbBeach?.image4 || "https://picsum.photos/400/600?random=13"; + const openWhatsApp = () => { const phone = process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '905000000000'; window.open( @@ -37,19 +46,20 @@ export default function Beach() { >
Plaj
Şezlong
@@ -64,16 +74,16 @@ export default function Beach() { >
Deniz
Koy

- {t('title')} + {titleVal}

-

{t('description')}

+

{descVal}

{/* Stats */}
@@ -114,7 +124,7 @@ export default function Beach() { {/* Info & CTA */}
-

{t('reservation_info')}

+

{resInfoVal}