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 718d6fe..eb4ad89 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ 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}