feat: integrate Cloudinary image upload, Instagram Feed, and fix Next.js warnings

This commit is contained in:
2026-06-09 21:28:45 +03:00
parent 2b3392dcb1
commit a4ad9344a0
52 changed files with 4300 additions and 263 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ export default async function LocaleLayout({
return (
<html lang={locale} data-scroll-behavior="smooth" className={`${marcellus.variable}`}>
<body className="bg-sand text-midnight font-body antialiased relative">
<body suppressHydrationWarning className="bg-sand text-midnight font-body antialiased relative">
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
+33 -6
View File
@@ -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 (
<main className="min-h-screen bg-cream selection:bg-turquoise selection:text-white">
<Navbar locale={locale} />
<Hero />
<About />
<Accommodation />
<Beach />
<About dbAbout={dbAbout} />
<Beach dbBeach={dbBeach} />
<Dining />
<Events />
<Gallery />
<Contact />
<Events dbEvents={dbEvents} />
<Gallery dbGallery={dbGallery} />
<InstagramFeed posts={instaPosts} />
<Contact dbContact={dbContact} />
<Footer />
<WhatsAppButton />
<WelcomePopup />
</main>
);
}
+67
View File
@@ -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." };
}
}
+103
View File
@@ -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." };
}
}
+44
View File
@@ -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");
}
+63
View File
@@ -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." };
}
}
+57
View File
@@ -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." };
}
}
+104
View File
@@ -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." };
}
}
+97
View File
@@ -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." };
}
}
+68
View File
@@ -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ı." };
}
}
+27
View File
@@ -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 (
<html lang="tr" className={`${marcellus.variable}`}>
<body className="bg-gray-50 text-gray-900 font-body antialiased min-h-screen">
{children}
</body>
</html>
);
}
+13
View File
@@ -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 <LoginClient />;
}
+22
View File
@@ -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 <AboutClient settings={settings as any} />;
}
+12
View File
@@ -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 <AdminsClient initialAdmins={admins} />;
}
+21
View File
@@ -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 <BeachClient settings={settings as any} />;
}
+18
View File
@@ -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 <ContactClient settings={settings as any} />;
}
+18
View File
@@ -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 <EventsClient initialEvents={parsedEvents as any} />;
}
+18
View File
@@ -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 <GalleryClient initialImages={parsedImages as any} />;
}
+124
View File
@@ -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 (
<html lang="tr" className={`${marcellus.variable}`}>
<body className="bg-gray-50 text-gray-900 font-body antialiased flex min-h-screen">
{/* Sidebar */}
<aside className="w-64 bg-charcoal text-cream flex flex-col shadow-2xl">
{/* Header */}
<div className="p-6 border-b border-gray-700 flex items-center justify-center">
<img src="/logo.png" alt="Kozmos Logo" className="h-12 brightness-0 invert opacity-90 object-contain" />
</div>
<nav className="flex-1 p-4 space-y-2">
<Link
href="/admin"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<LayoutDashboard className="w-5 h-5" />
<span>Dashboard</span>
</Link>
<Link
href="/admin/admins"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<Users className="w-5 h-5" />
<span>Admin Yönetimi</span>
</Link>
<Link
href="/admin/events"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
<span>Etkinlik Yönetimi</span>
</Link>
<Link
href="/admin/about"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
<span>Hakkımızda Yönetimi</span>
</Link>
<Link
href="/admin/beach"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 2v20"></path><path d="M2 12h20"></path></svg>
<span>Plaj Yönetimi</span>
</Link>
<Link
href="/admin/gallery"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
<span>Galeri Yönetimi</span>
</Link>
<Link
href="/admin/contact"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
<span>İletişim & Konum</span>
</Link>
</nav>
<div className="p-4 border-t border-gray-700 space-y-2">
<a
href="/"
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-800 transition-colors text-gray-400 hover:text-white"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span>Siteye Git</span>
</a>
<form action={async () => {
"use server";
const { logout } = await import("@/app/actions/auth");
await logout();
}}>
<button
type="submit"
className="w-full flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-red-900/50 transition-colors text-red-400 hover:text-red-300"
>
<LogOut className="w-5 h-5" />
<span>Çıkış Yap</span>
</button>
</form>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
{children}
</main>
</body>
</html>
);
}
+17
View File
@@ -0,0 +1,17 @@
export default function AdminDashboard() {
return (
<div className="p-8">
<h1 className="text-3xl font-serif text-gray-900 mb-8">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h3 className="text-gray-500 text-sm font-medium mb-2">Sisteme Hoş Geldiniz</h3>
<p className="text-2xl font-bold text-gray-900">Kozmos Yönetim Paneli</p>
<p className="text-sm text-gray-500 mt-4">
Sol menüden "Admin Yönetimi" kısmına giderek yeni yönetici ekleyebilir veya silebilirsiniz.
</p>
</div>
</div>
</div>
);
}
+20
View File
@@ -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 });
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+5
View File
@@ -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: '<angle>';