feat: setup database-driven admin user management

This commit is contained in:
2026-06-10 04:25:54 +03:00
parent f542efb324
commit b712760825
23 changed files with 804 additions and 40 deletions
+15
View File
@@ -4,6 +4,21 @@ import { ArrowRight, Clock, Users } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import prisma from "@/lib/prisma";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/aktiviteler", {
title: "Aktiviteler | Kite Beach Akyaka",
description: "Kitesurf, yoga, SUP ve bisiklet kiralama. Akyaka'nın doğasında IKO sertifikalı eğitmenlerle unutulmaz deneyimler.",
}, {
title: "Activities | Kite Beach Akyaka",
description: "Kitesurf, yoga, SUP and bicycle rental. Unforgettable experiences in Akyaka with IKO certified instructors.",
});
}
export default async function AktivitelerPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
+14 -4
View File
@@ -4,11 +4,21 @@ import { ArrowRight, Calendar, Clock, MapPin, Music } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import prisma from "@/lib/prisma";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const metadata = {
title: "Etkinlikler | Kite Beach Akyaka",
description: "Mahmut Orhan konserinden Kitesurf festivallerine, Akyaka'daki en özel etkinlikler ve gün batımı partileri.",
};
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/etkinlikler", {
title: "Etkinlikler | Kite Beach Akyaka",
description: "Mahmut Orhan konserinden Kitesurf festivallerine, Akyaka'daki en özel etkinlikler ve gün batımı partileri.",
}, {
title: "Events | Kite Beach Akyaka",
description: "From Mahmut Orhan concerts to Kitesurf festivals, the most special events and sunset parties in Akyaka.",
});
}
export default async function EventsPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
+19
View File
@@ -0,0 +1,19 @@
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/galeri", {
title: "Galeri | Kite Beach Akyaka",
description: "Kite Beach Akyaka'nın fotoğraf galerisini keşfedin. Odalar, aktiviteler, restoran ve Akyaka'nın eşsiz doğasından kareler.",
}, {
title: "Gallery | Kite Beach Akyaka",
description: "Explore the photo gallery of Kite Beach Akyaka. Rooms, activities, restaurant and moments from the unique nature of Akyaka.",
});
}
export default function GaleriLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
+15
View File
@@ -1,6 +1,21 @@
import Image from "next/image";
import { Leaf, ChefHat, Award } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/hakkimizda", {
title: "Hakkımızda | Kite Beach Akyaka",
description: "10+ yıllık deneyim, 500+ mutlu misafir. Azmak nehri kıyısında doğa, kitesurf tutku ve yerel lezzetlerin buluşma noktası.",
}, {
title: "About Us | Kite Beach Akyaka",
description: "10+ years of experience, 500+ happy guests. Where nature, kitesurf passion and local flavors meet on the banks of Azmak river.",
});
}
const values = [
{
+15
View File
@@ -1,5 +1,20 @@
import { MapPin, Phone, Mail, Clock } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/iletisim", {
title: "İletişim & Rezervasyon | Kite Beach Akyaka",
description: "Kite Beach Akyaka ile iletişime geçin. Muğla Akyaka'da kitesurf dersi, konaklama ve etkinlik rezervasyonu için bize yazın.",
}, {
title: "Contact & Reservation | Kite Beach Akyaka",
description: "Contact Kite Beach Akyaka. Write to us for kitesurf lessons, accommodation and event reservations in Akyaka, Muğla.",
});
}
export default function IletisimPage() {
return (
+94 -4
View File
@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Inter, Playfair_Display } from "next/font/google";
import "../globals.css";
import Navbar from "@/components/Navbar";
@@ -8,22 +8,106 @@ import WhatsAppButton from "@/components/WhatsAppButton";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "swap",
});
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
display: "swap",
});
export const metadata: Metadata = {
title: "Kite Beach Akyaka | Kitesurf & Butik Otel",
description: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Kite Beach Akyaka butik otel ve kitesurf merkezine hoş geldiniz.",
const baseUrl = "https://kitebeachakyaka.com.tr";
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
themeColor: "#0D3D50",
};
export async function generateMetadata(
{ params }: { params: Promise<{ lang: string }> }
): Promise<Metadata> {
const { lang } = await params;
const isEn = lang === "en";
return {
title: isEn
? "Kite Beach Akyaka | Kitesurf & Boutique Hotel"
: "Kite Beach Akyaka | Kitesurf & Butik Otel",
description: isEn
? "Tranquility of the river, energy of the sea — in one spot. Welcome to Kite Beach Akyaka boutique hotel and kitesurf center."
: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Kite Beach Akyaka butik otel ve kitesurf merkezine hoş geldiniz.",
metadataBase: new URL(baseUrl),
openGraph: {
locale: isEn ? "en_US" : "tr_TR",
alternateLocale: isEn ? "tr_TR" : "en_US",
siteName: "Kite Beach Akyaka",
type: "website",
url: `${baseUrl}/${lang}`,
images: [{ url: `${baseUrl}/og-image.jpg`, width: 1200, height: 630, alt: "Kite Beach Akyaka" }],
},
twitter: {
card: "summary_large_image",
images: [`${baseUrl}/og-image.jpg`],
},
};
}
export async function generateStaticParams() {
return [{ lang: 'tr' }, { lang: 'en' }]
}
function buildJsonLd(lang: string) {
const isEn = lang === "en";
return {
"@context": "https://schema.org",
"@graph": [
{
"@type": "LodgingBusiness",
"@id": `${baseUrl}/#business`,
name: "Kite Beach Akyaka",
url: baseUrl,
logo: `${baseUrl}/logo.png`,
image: `${baseUrl}/og-image.jpg`,
description: isEn
? "Boutique hotel and kitesurf center on the banks of Azmak River in Akyaka, Muğla."
: "Akyaka, Muğla'da Azmak Nehri kıyısında butik otel ve kitesurf merkezi.",
telephone: "+905330816181",
email: "info@kitebeachakyaka.com.tr",
address: {
"@type": "PostalAddress",
streetAddress: "Akyaka Mahallesi, Akçapınar Mevkii",
addressLocality: "Akyaka",
addressRegion: "Muğla",
addressCountry: "TR",
},
geo: {
"@type": "GeoCoordinates",
latitude: 37.0543,
longitude: 28.3286,
},
sameAs: [
"https://instagram.com/kitebeachakyaka",
],
amenityFeature: [
{ "@type": "LocationFeatureSpecification", name: "Kitesurf School", value: true },
{ "@type": "LocationFeatureSpecification", name: "Restaurant & Bar", value: true },
{ "@type": "LocationFeatureSpecification", name: "Yoga Platform", value: true },
],
},
{
"@type": "WebSite",
"@id": `${baseUrl}/#website`,
url: baseUrl,
name: "Kite Beach Akyaka",
publisher: { "@id": `${baseUrl}/#business` },
inLanguage: [isEn ? "en" : "tr"],
},
],
};
}
export default async function RootLayout({
children,
params,
@@ -39,6 +123,12 @@ export default async function RootLayout({
suppressHydrationWarning
>
<body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(buildJsonLd(lang)).replace(/</g, "\\u003c"),
}}
/>
<Navbar />
<main className="flex-grow">{children}</main>
<Footer />
+52 -14
View File
@@ -1,9 +1,21 @@
import Image from "next/image";
import Link from "next/link";
import * as Icons from "lucide-react";
import { ArrowRight, Star } from "lucide-react";
import {
ArrowRight, Star, Wind, Anchor, Utensils, BedDouble, Sun,
Waves, Bike, Coffee, Music, MapPin, Camera, Heart, Compass,
Fish, Mountain, Dumbbell, Leaf, Shield, Umbrella, Flame,
type LucideIcon,
} from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
export const revalidate = 3600;
const ICON_MAP: Record<string, LucideIcon> = {
Wind, Anchor, Utensils, BedDouble, Sun, Star, Waves, Bike,
Coffee, Music, MapPin, Camera, Heart, Compass, Fish, Mountain,
Dumbbell, Leaf, Shield, Umbrella, Flame,
};
import HeroVideo from "@/components/HeroVideo";
const defaultFeatures = [
@@ -64,13 +76,32 @@ const testimonials = [
import { getDictionary, Locale } from "./dictionaries";
import prisma from "@/lib/prisma";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ lang: string }> }
): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "", {
title: "Kite Beach Akyaka | Kitesurf & Butik Otel",
description: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Akyaka'nın kalbinde kitesurf ve butik otel deneyimi.",
}, {
title: "Kite Beach Akyaka | Kitesurf & Boutique Hotel",
description: "Tranquility of the river, energy of the sea — in one spot. Kitesurf and boutique hotel experience in the heart of Akyaka.",
});
}
async function getInstagramData() {
if (!process.env.RAPIDAPI_KEY) {
return [];
}
const url = 'https://instagram120.p.rapidapi.com/api/instagram/posts';
const options = {
method: 'POST',
headers: {
'x-rapidapi-key': process.env.RAPIDAPI_KEY || '',
'x-rapidapi-key': process.env.RAPIDAPI_KEY,
'x-rapidapi-host': 'instagram120.p.rapidapi.com',
'Content-Type': 'application/json'
},
@@ -83,11 +114,14 @@ async function getInstagramData() {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error('Instagram fetch failed');
if (!res.ok) {
console.warn('Instagram API error or missing limits. Status:', res.status);
return [];
}
const data = await res.json();
return data.result?.edges?.slice(0, 4).map((edge: any) => edge.node) || [];
} catch (err) {
console.error("Instagram fetch error:", err);
console.warn("Instagram fetch error:", err);
return [];
}
}
@@ -96,7 +130,15 @@ export default async function Home({ params }: { params: Promise<{ lang: string
const { lang } = await params;
const dict = await getDictionary(lang as Locale);
const heroDb = await prisma.heroSection.findUnique({ where: { lang } });
const [heroDb, activities, events, dbFeatures, aboutDb, instagramPosts] = await Promise.all([
prisma.heroSection.findUnique({ where: { lang } }),
prisma.activity.findMany({ where: { lang }, orderBy: { order: "asc" } }),
prisma.event.findMany({ where: { lang }, orderBy: { date: "asc" } }),
prisma.feature.findMany({ where: { lang }, orderBy: { order: "asc" } }),
prisma.aboutSection.findUnique({ where: { lang } }),
getInstagramData(),
]);
const heroData = heroDb || {
location: dict.home.location,
welcome: dict.home.welcome,
@@ -107,11 +149,6 @@ export default async function Home({ params }: { params: Promise<{ lang: string
scroll: dict.home.scroll,
};
const activities = await prisma.activity.findMany({ where: { lang }, orderBy: { order: "asc" } });
const events = await prisma.event.findMany({ where: { lang }, orderBy: { date: "asc" } });
const dbFeatures = await prisma.feature.findMany({ where: { lang }, orderBy: { order: "asc" } });
const aboutDb = await prisma.aboutSection.findUnique({ where: { lang } });
const aboutData = aboutDb || {
badge: lang === "en" ? "Our Story" : "Hikayemiz",
titleLine1: lang === "en" ? "In the" : "Doğanın",
@@ -133,7 +170,6 @@ export default async function Home({ params }: { params: Promise<{ lang: string
};
const displayFeatures = dbFeatures.length > 0 ? dbFeatures : defaultFeatures;
const instagramPosts = await getInstagramData();
return (
<div className="flex flex-col min-h-screen">
@@ -205,7 +241,7 @@ export default async function Home({ params }: { params: Promise<{ lang: string
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-5 divide-x divide-white/8">
{displayFeatures.map(({ icon, label, sub }, i) => {
const IconComponent = (Icons as any)[icon as string] || Icons.Star;
const IconComponent = ICON_MAP[icon as string] || Star;
return (
<div
key={i}
@@ -391,6 +427,7 @@ export default async function Home({ params }: { params: Promise<{ lang: string
src={act.image}
alt={act.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className={`absolute inset-0 bg-gradient-to-t ${i === 0 ? "from-dark/90 via-dark/20" : "from-dark/80"} to-transparent`} />
@@ -453,6 +490,7 @@ export default async function Home({ params }: { params: Promise<{ lang: string
src={ev.image}
alt={ev.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
@@ -522,7 +560,7 @@ export default async function Home({ params }: { params: Promise<{ lang: string
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-dark/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col items-center justify-center text-white p-6 text-center">
<Icons.Camera className="w-8 h-8 mb-3" />
<Camera className="w-8 h-8 mb-3" />
<p className="text-xs line-clamp-3">
{post.caption?.text}
</p>
+15
View File
@@ -2,6 +2,21 @@ import Image from "next/image";
import Link from "next/link";
import { Coffee, Wine, UtensilsCrossed, Clock } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export const revalidate = 3600;
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/restoran", {
title: "Restoran & Bar | Kite Beach Akyaka",
description: "Ege mutfağının taze lezzetleri, gün batımı kokteyller ve Azmak nehri manzarası. Serpme kahvaltı, Ege mezeleri ve akşam yemekleri.",
}, {
title: "Restaurant & Bar | Kite Beach Akyaka",
description: "Fresh flavors of Aegean cuisine, sunset cocktails and views of the Azmak river. Breakfast spread, Aegean meze and dinner.",
});
}
const menuCategories = [
{