Compare commits

...
15 Commits
67 changed files with 2580 additions and 1180 deletions
+27 -24
View File
@@ -4,41 +4,45 @@ 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 profesyonel eğitmenlerle unutulmaz deneyimler.",
}, {
title: "Activities | Kite Beach Akyaka",
description: "Kitesurf, yoga, SUP and bicycle rental. Unforgettable experiences in Akyaka with professional instructors.",
});
}
export default async function AktivitelerPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const isEn = lang === "en";
const activities = await prisma.activity.findMany({ where: { lang }, orderBy: { order: "asc" } });
return (
<div className="min-h-screen bg-background">
<div className="min-h-screen bg-background pt-40 pb-10">
{/* Page Header */}
<section className="relative h-[55vh] w-full flex items-end overflow-hidden">
<div className="absolute inset-0">
<Image
src="https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop"
alt="Aktiviteler"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/85 via-dark/30 to-black/20" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-14 w-full">
<span className="block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-3 animate-fade-up">
Deneyimler
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4 animate-fade-up">
{isEn ? "Experiences" : "Deneyimler"}
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
Aktiviteler
<h1 className="font-serif text-5xl md:text-6xl text-dark font-bold animate-fade-up delay-100 mb-6">
{isEn ? "Activities" : "Aktiviteler"}
</h1>
<p className="text-white/60 text-lg mt-3 font-light animate-fade-up delay-200">
Rüzgarı hisset, doğayla bir ol.
<p className="text-muted text-lg max-w-2xl mx-auto animate-fade-up delay-200">
{isEn ? "Feel the wind, be one with nature." : "Rüzgarı hisset, doğayla bir ol."}
</p>
</div>
</section>
{/* Activities */}
<section className="py-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{activities.length > 0 ? activities.map((act, i) => (
<ScrollReveal key={act.id} delay={i * 80}>
<div className="card-lift group bg-white rounded-3xl overflow-hidden border border-sand flex flex-col h-full">
@@ -47,6 +51,7 @@ export default async function AktivitelerPage({ params }: { params: Promise<{ la
src={act.image}
alt={act.title}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
{act.badge && (
@@ -61,9 +66,7 @@ export default async function AktivitelerPage({ params }: { params: Promise<{ la
<p className="text-gray-500 text-[14px] leading-relaxed mb-6 flex-grow">{act.description}</p>
<div className="flex justify-between items-center pt-6 border-t border-sand">
<div>
<span className="text-2xl font-bold text-dark">{act.price}</span>
</div>
<div className="flex-1"></div>
<Link
href={`/${lang}/iletisim`}
className="inline-flex items-center gap-2 text-accent font-bold text-[12px] tracking-wider uppercase hover:text-primary transition-colors group/link"
+37 -30
View File
@@ -4,14 +4,25 @@ 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 = {
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;
const isEn = lang === "en";
const events = await prisma.event.findMany({ where: { lang }, orderBy: { date: "asc" } });
return (
@@ -20,32 +31,19 @@ export default async function EventsPage({ params }: { params: Promise<{ lang: s
{/* ──────────────────────────────────────────
HERO
────────────────────────────────────────── */}
<section className="relative h-[60vh] min-h-[500px] w-full flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1514525253161-7a46d19cd819?q=80&w=2874&auto=format&fit=crop"
alt="Kite Beach Akyaka Etkinlikler"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-dark/60" />
</div>
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto mt-16">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.35em] uppercase mb-4 font-bold">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16 pt-40">
<span className="inline-block text-accent text-xs tracking-[0.35em] uppercase font-bold mb-4 animate-fade-up">
Kite Beach Akyaka
</span>
<h1 className="font-serif text-5xl md:text-7xl font-bold text-white mb-6">
Etkinlikler
<h1 className="font-serif text-5xl md:text-6xl text-dark font-bold animate-fade-up delay-100 mb-6">
{isEn ? "Events" : "Etkinlikler"}
</h1>
<p className="text-white/80 text-lg md:text-xl font-light max-w-2xl mx-auto leading-relaxed">
Müzik, spor ve doğanın içe geçtiği unutulmaz deneyimler.
<p className="text-muted text-lg max-w-2xl mx-auto animate-fade-up delay-200">
{isEn
? "Unforgettable experiences where music, sports, and nature intertwine."
: "Müzik, spor ve doğanın iç içe geçtiği unutulmaz deneyimler."}
</p>
</ScrollReveal>
</div>
</section>
{/* ──────────────────────────────────────────
EVENTS LIST
@@ -64,6 +62,7 @@ export default async function EventsPage({ params }: { params: Promise<{ lang: s
src={event.image}
alt={event.title}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
{event.badge && (
@@ -104,7 +103,7 @@ export default async function EventsPage({ params }: { params: Promise<{ lang: s
href={`/${lang}/iletisim`}
className="inline-flex items-center gap-2 text-xs font-bold tracking-widest uppercase transition-colors bg-accent text-white px-6 py-3 rounded-full hover:bg-accent/90"
>
Bize Ulaşın <ArrowRight className="w-4 h-4" />
{isEn ? "Contact Us" : "Bize Ulaşın"} <ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
@@ -112,7 +111,9 @@ export default async function EventsPage({ params }: { params: Promise<{ lang: s
</div>
</ScrollReveal>
)) : (
<div className="text-center py-20 text-gray-500">Henüz etkinlik eklenmedi.</div>
<div className="text-center py-20 text-gray-500">
{isEn ? "No events added yet." : "Henüz etkinlik eklenmedi."}
</div>
)}
</div>
@@ -122,16 +123,22 @@ export default async function EventsPage({ params }: { params: Promise<{ lang: s
{/* CTA BAND */}
<section className="bg-dark text-white py-24 text-center px-4">
<ScrollReveal>
<span className="block text-accent text-xs font-bold tracking-[0.25em] uppercase mb-4">Organizasyon</span>
<h2 className="font-serif text-4xl md:text-5xl font-bold mb-6">Kendi Etkinliğinizi Planlayın</h2>
<span className="block text-accent text-xs font-bold tracking-[0.25em] uppercase mb-4">
{isEn ? "Organization" : "Organizasyon"}
</span>
<h2 className="font-serif text-4xl md:text-5xl font-bold mb-6">
{isEn ? "Plan Your Own Event" : "Kendi Etkinliğinizi Planlayın"}
</h2>
<p className="text-white/60 max-w-xl mx-auto mb-10 leading-relaxed text-sm">
Özel partiler, düğünler veya kurumsal etkinlikler için Kite Beach Akyaka'nın eşsiz atmosferini kiralayabilirsiniz. Detaylar için bizimle iletişime geçin.
{isEn
? "You can rent the unique atmosphere of Kite Beach Akyaka for private parties, weddings, or corporate events. Contact us for details."
: "Özel partiler, düğünler veya kurumsal etkinlikler için Kite Beach Akyaka'nın eşsiz atmosferini kiralayabilirsiniz. Detaylar için bizimle iletişime geçin."}
</p>
<Link
href="/iletisim"
href={`/${lang}/iletisim`}
className="inline-block border-2 border-white/20 hover:border-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase transition-all"
>
Bize Ulaşın
{isEn ? "Contact Us" : "Bize Ulaşın"}
</Link>
</ScrollReveal>
</section>
+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}</>;
}
+52 -14
View File
@@ -1,32 +1,66 @@
"use client";
import Image from "next/image";
import { useState } from "react";
import { useState, use } from "react";
const filters = ["Tümü", "Odalar", "Aktiviteler", "Restoran", "Doğa"] as const;
type Filter = (typeof filters)[number];
const getAltText = (alt: string, isEn: boolean) => {
if (!isEn) return alt;
const translations: Record<string, string> = {
"Standart Oda": "Standard Room",
"Kitesurf": "Kitesurfing",
"Nehir Doğa": "River Nature",
"Nehir Manzaralı Oda": "River View Room",
"Restoran": "Restaurant",
"Yoga": "Yoga",
"Bungalow": "Bungalow",
"SUP": "Stand Up Paddleboarding",
"Kahvaltı": "Breakfast Spread",
"Bisiklet": "Bicycle Rental",
"Restoran Ambiyans": "Restaurant Ambience",
"Gün Batımı": "Sunset View",
};
return translations[alt] || alt;
};
const gallery: { src: string; alt: string; category: Exclude<Filter, "Tümü"> }[] = [
{ src: "https://images.unsplash.com/photo-1582719478250-c89404bb8a0e?q=80&w=2940&auto=format&fit=crop", alt: "Standart Oda", category: "Odalar" },
const gallery: { src: string; alt: string; category: "Odalar" | "Aktiviteler" | "Restoran" | "Doğa" }[] = [
{ src: "https://images.unsplash.com/photo-1618773928121-c32242e63f39?q=80&w=2940&auto=format&fit=crop", alt: "Standart Oda", category: "Odalar" },
{ src: "https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop", alt: "Kitesurf", category: "Aktiviteler" },
{ src: "https://images.unsplash.com/photo-1544551763-46a013bb70d5?q=80&w=2940&auto=format&fit=crop", alt: "Nehir Doğa", category: "Doğa" },
{ src: "https://images.unsplash.com/photo-1566665797739-1674de7a421a?q=80&w=2874&auto=format&fit=crop", alt: "Nehir Manzaralı Oda",category: "Odalar" },
{ src: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=2940&auto=format&fit=crop", alt: "Restoran", category: "Restoran" },
{ src: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?q=80&w=2940&auto=format&fit=crop", alt: "Yoga", category: "Aktiviteler" },
{ src: "https://images.unsplash.com/photo-1499955085172-a104c9463ece?q=80&w=2940&auto=format&fit=crop", alt: "Bungalow", category: "Odalar" },
{ src: "https://images.unsplash.com/photo-1522883392419-7d8cb2435728?q=80&w=2924&auto=format&fit=crop", alt: "SUP", category: "Aktiviteler" },
{ src: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=2940&auto=format&fit=crop", alt: "SUP", category: "Aktiviteler" },
{ src: "https://images.unsplash.com/photo-1551504734-5ee1c4a1479b?q=80&w=2940&auto=format&fit=crop", alt: "Kahvaltı", category: "Restoran" },
{ src: "https://images.unsplash.com/photo-1511994298241-608e28f14fde?q=80&w=2940&auto=format&fit=crop", alt: "Bisiklet", category: "Aktiviteler" },
{ src: "https://images.unsplash.com/photo-1544148103-0773bf10d330?q=80&w=2940&auto=format&fit=crop", alt: "Restoran Ambiyans", category: "Restoran" },
{ src: "https://images.unsplash.com/photo-1498623116890-37e912163d5d?q=80&w=2940&auto=format&fit=crop", alt: "Gün Batımı", category: "Doğa" },
];
export default function GaleriPage() {
const [active, setActive] = useState<Filter>("Tümü");
export default function GaleriPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = use(params);
const isEn = lang === "en";
const filters = isEn
? ["All", "Rooms", "Activities", "Restaurant", "Nature"]
: ["Tümü", "Odalar", "Aktiviteler", "Restoran", "Doğa"];
const [active, setActive] = useState<string>(isEn ? "All" : "Tümü");
const [lightbox, setLightbox] = useState<string | null>(null);
const getInternalCategory = (filterName: string) => {
if (filterName === "All" || filterName === "Tümü") return "Tümü";
if (filterName === "Rooms") return "Odalar";
if (filterName === "Activities") return "Aktiviteler";
if (filterName === "Restaurant") return "Restoran";
if (filterName === "Nature") return "Doğa";
return filterName;
};
const internalActive = getInternalCategory(active);
const filtered =
active === "Tümü" ? gallery : gallery.filter((g) => g.category === active);
internalActive === "Tümü" ? gallery : gallery.filter((g) => g.category === internalActive);
return (
<div className="min-h-screen bg-background pt-28 pb-20">
@@ -35,11 +69,13 @@ export default function GaleriPage() {
{/* Heading */}
<div className="text-center mb-12">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Fotoğraflar
{isEn ? "Photos" : "Fotoğraflar"}
</span>
<h1 className="font-serif text-5xl font-bold text-dark mb-4">Galeri</h1>
<h1 className="font-serif text-5xl font-bold text-dark mb-4">{isEn ? "Gallery" : "Galeri"}</h1>
<p className="text-muted max-w-md mx-auto text-sm leading-relaxed">
Kite Beach Akyaka'nın eşsiz doğasını, modern odalarını ve heyecan verici aktivitelerini keşfedin.
{isEn
? "Discover the unique nature and exciting activities of Kite Beach Akyaka."
: "Kite Beach Akyaka'nın eşsiz doğasını ve heyecan verici aktivitelerini keşfedin."}
</p>
</div>
@@ -72,13 +108,14 @@ export default function GaleriPage() {
<div className="relative w-full" style={{ paddingBottom: idx % 3 === 0 ? "133%" : "75%" }}>
<Image
src={img.src}
alt={img.alt}
alt={getAltText(img.alt, isEn)}
fill
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-dark/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<span className="text-white font-semibold text-[13px] tracking-wider bg-dark/60 backdrop-blur-sm px-4 py-2 rounded-full">
{img.alt}
{getAltText(img.alt, isEn)}
</span>
</div>
</div>
@@ -99,6 +136,7 @@ export default function GaleriPage() {
src={lightbox}
alt="Galeri"
fill
sizes="100vw"
className="object-contain rounded-xl"
/>
</div>
+4 -4
View File
@@ -8,11 +8,11 @@ export default async function PrivacyPage({ params }: { params: Promise<{ lang:
text: `Kite Beach Akyaka (“Biz”, “Şirketimiz” veya “Otel”) olarak, web sitemizi ziyaret eden ve hizmetlerimizden faydalanan misafirlerimizin ("Kullanıcı" veya "Misafir") gizliliğini korumayı taahhüt ediyoruz. Bu Gizlilik Politikası, web sitemiz (www.kitebeachakyaka.com.tr) üzerinden toplanan bilgilerin nasıl kullanıldığını ve korunduğunu açıklar.
1. Hangi Bilgileri Topluyoruz?
Size daha iyi hizmet verebilmek amacıyla adınız, soyadınız, e-posta adresiniz, telefon numaranız ve rezervasyon detaylarınız gibi bilgileri doğrudan sizden talep etmekteyiz. Ayrıca web sitemizi ziyaret ettiğinizde otomatik olarak IP adresi, tarayıcı türü, ziyaret süresi gibi standart log bilgileri de toplanabilmektedir.
Size daha iyi hizmet verebilmek amacıyla adınız, soyadınız, e-posta adresiniz, telefon numaranız ve talep detaylarınız gibi bilgileri doğrudan sizden talep etmekteyiz. Ayrıca web sitemizi ziyaret ettiğinizde otomatik olarak IP adresi, tarayıcı türü, ziyaret süresi gibi standart log bilgileri de toplanabilmektedir.
2. Bilgilerinizi Nasıl Kullanıyoruz?
Topladığımız bilgiler aşağıdaki amaçlar için kullanılmaktadır:
- Rezervasyon işlemlerinizi gerçekleştirmek ve onaylamak,
- Taleplerinizi değerlendirmek ve yanıtlamak,
- Sizinle iletişim kurmak ve taleplerinize yanıt vermek,
- Otel ve kitesurf okulu hizmetlerimizi geliştirmek,
- Pazarlama ve tanıtım bildirimleri göndermek (Yalnızca açık rızanız olması halinde),
@@ -34,11 +34,11 @@ Bu Gizlilik Politikası, zaman zaman güncellenebilir. Herhangi bir değişiklik
text: `As Kite Beach Akyaka ("We", "Our Company" or "Hotel"), we are committed to protecting the privacy of our guests ("User" or "Guest") who visit our website and benefit from our services. This Privacy Policy explains how the information collected through our website is used and protected.
1. What Information Do We Collect?
In order to serve you better, we request information such as your name, surname, email address, phone number, and reservation details directly from you. In addition, standard log information such as IP address, browser type, and visit duration may be automatically collected when you visit our website.
In order to serve you better, we request information such as your name, surname, email address, phone number, and request details directly from you. In addition, standard log information such as IP address, browser type, and visit duration may be automatically collected when you visit our website.
2. How Do We Use Your Information?
The information we collect is used for the following purposes:
- Processing and confirming your reservations,
- Processing and responding to your requests,
- Communicating with you and responding to your requests,
- Improving our hotel and kitesurf school services,
- Sending marketing and promotional notifications (Only with your explicit consent),
+81 -41
View File
@@ -1,8 +1,23 @@
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";
const values = [
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 valuesTr = [
{
icon: Leaf,
title: "Doğaya Saygı",
@@ -16,44 +31,63 @@ const values = [
{
icon: Award,
title: "Premium Deneyim",
desc: "IKO sertifikalı kitesurf okulumuzdandan odalarımıza kadar her noktada en yüksek kalite standartlarını sunuyoruz.",
desc: "Kitesurf okulumuzdan restoranımıza kadar her noktada en yüksek kalite standartlarını sunuyoruz.",
},
];
const stats = [
const valuesEn = [
{
icon: Leaf,
title: "Respect for Nature",
desc: "We adopt zero waste and sustainability principles in all processes of our facility. We offer an environmentally conscious stay experience.",
},
{
icon: ChefHat,
title: "Local Kitchen",
desc: "We source a large portion of our ingredients from local producers and Aegean villages. Fresh, seasonal and sustainable.",
},
{
icon: Award,
title: "Premium Experience",
desc: "From our kitesurf school to our restaurant, we offer the highest quality standards at every point.",
},
];
const statsTr = [
{ value: "10+", label: "Yıllık Deneyim" },
{ value: "500+", label: "Mutlu Misafir" },
{ value: "3", label: "IKO Sertifikalı Eğitmen" },
{ value: "3", label: "Profesyonel Eğitmen" },
{ value: "4", label: "Benzersiz Aktivite" },
];
export default function HakkimizdaPage() {
return (
<div className="min-h-screen bg-background">
const statsEn = [
{ value: "10+", label: "Years Experience" },
{ value: "500+", label: "Happy Guests" },
{ value: "3", label: "Professional Instructor" },
{ value: "4", label: "Unique Activity" },
];
export default async function HakkimizdaPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const isEn = lang === "en";
const displayValues = isEn ? valuesEn : valuesTr;
const displayStats = isEn ? statsEn : statsTr;
return (
<div className="min-h-screen bg-background pt-40 pb-10">
{/* Page Header */}
<section className="relative h-[55vh] w-full flex items-end overflow-hidden">
<div className="absolute inset-0">
<Image
src="https://images.unsplash.com/photo-1544551763-46a013bb70d5?q=80&w=2940&auto=format&fit=crop"
alt="Hakkımızda"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/85 via-dark/30 to-black/20" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-14 w-full">
<span className="block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-3 animate-fade-up">
Biz Kimiz
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4 animate-fade-up">
{isEn ? "Who We Are" : "Biz Kimiz"}
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
Hikayemiz
<h1 className="font-serif text-5xl md:text-6xl text-dark font-bold animate-fade-up delay-100 mb-6">
{isEn ? "Our Story" : "Hikayemiz"}
</h1>
<p className="text-white/60 text-lg mt-3 font-light animate-fade-up delay-200">
Rüzgarın, denizin ve nehrin kesiştiği yerde.
<p className="text-muted text-lg max-w-2xl mx-auto animate-fade-up delay-200">
{isEn ? "Where the wind, sea and river meet." : "Rüzgarın, denizin ve nehrin kesiştiği yerde."}
</p>
</div>
</section>
{/* Story */}
<section className="py-24 bg-white">
@@ -61,22 +95,25 @@ export default function HakkimizdaPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
Akyaka'nın Kalbinde
{isEn ? "In the Heart of Akyaka" : "Akyaka'nın Kalbinde"}
</span>
<h2 className="font-serif text-4xl font-bold text-dark mb-6 leading-tight">
Bir Tutku<br />Yolculuğu
{isEn ? <>A Journey<br />of Passion</> : <>Bir Tutku<br />Yolculuğu</>}
</h2>
<div className="space-y-4 text-gray-500 leading-relaxed text-[15px]">
<p>
Kite Beach Akyaka, Gökova Körfezi'nin kristal sularına dökülen Azmak Nehri'nin
hemen kıyısında doğdu. Amacımız, misafirlerimize sıradan bir konaklamadan çok
daha fazlasını sunmak; rüzgar sörfü tutkusunu doğayla, konforla ve iyi yemekle
bütünleştiren eşsiz bir yaşam alanı yaratmaktı.
{isEn ? (
"Kite Beach Akyaka was born right on the banks of the Azmak River flowing into the crystal waters of the Gulf of Gokova. Our goal was to offer our guests much more than a typical stay; to create a unique living space that integrates the passion for kitesurfing with nature, comfort, and good food."
) : (
"Kite Beach Akyaka, Gökova Körfezi'nin kristal sularına dökülen Azmak Nehri'nin hemen kıyısında doğdu. Amacımız, misafirlerimize sıradan bir konaklamadan çok daha fazlasını sunmak; rüzgar sörfü tutkusunu doğayla, konforla ve iyi yemekle bütünleştiren eşsiz bir yaşam alanı yaratmaktı."
)}
</p>
<p>
Doğaya saygılı mimarimiz, lokal üreticileri destekleyen mutfağımız ve IKO
standartlarındaki eğitim merkezimizle, Akyaka'nın sürdürülebilir turizm yüzü
olmayı hedefliyoruz.
{isEn ? (
"With our nature-friendly architecture, our kitchen supporting local producers, and our professional training center, we aim to be the face of sustainable tourism in Akyaka."
) : (
"Doğaya saygılı mimarimiz, lokal üreticileri destekleyen mutfağımız ve profesyonel eğitim merkezimizle, Akyaka'nın sürdürülebilir turizm yüzü olmayı hedefliyoruz."
)}
</p>
</div>
</ScrollReveal>
@@ -84,9 +121,10 @@ export default function HakkimizdaPage() {
<ScrollReveal delay={150}>
<div className="relative h-[460px] rounded-3xl overflow-hidden">
<Image
src="https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop"
src="/kitesurf.png"
alt="Kitesurf"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
@@ -99,7 +137,7 @@ export default function HakkimizdaPage() {
<section className="py-20 bg-primary">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal stagger className="grid grid-cols-2 md:grid-cols-4 gap-0 divide-x divide-white/10">
{stats.map((s, i) => (
{displayStats.map((s, i) => (
<div key={i} className="text-center py-8 px-6">
<span className="block font-serif text-4xl font-bold text-white mb-2">{s.value}</span>
<span className="block text-white/50 text-[12px] uppercase tracking-wider">{s.label}</span>
@@ -114,13 +152,13 @@ export default function HakkimizdaPage() {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-14">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Ne İnanıyoruz
{isEn ? "What We Believe" : "Ne İnanıyoruz"}
</span>
<h2 className="font-serif text-4xl font-bold text-dark">Değerlerimiz</h2>
<h2 className="font-serif text-4xl font-bold text-dark">{isEn ? "Our Values" : "Değerlerimiz"}</h2>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 md:grid-cols-3 gap-6">
{values.map((v, i) => {
{displayValues.map((v, i) => {
const Icon = v.icon;
return (
<div key={i} className="bg-white rounded-2xl p-8 border border-sand card-lift">
@@ -141,10 +179,12 @@ export default function HakkimizdaPage() {
<div className="max-w-3xl mx-auto px-4 text-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
Vizyonumuz
{isEn ? "Our Vision" : "Vizyonumuz"}
</span>
<blockquote className="font-serif text-3xl md:text-4xl font-bold leading-tight mb-8">
"Akyaka'nın sürdürülebilir turizm yüzü olmak ve her misafirimize benzersiz bir kaçış sunmak."
{isEn
? "\"To be the face of sustainable tourism in Akyaka and to offer each of our guests a unique escape.\""
: "\"Akyaka'nın sürdürülebilir turizm yüzü olmak ve her misafirimize benzersiz bir kaçış sunmak.\""}
</blockquote>
<div className="section-line mx-auto" />
</ScrollReveal>
+163 -111
View File
@@ -1,115 +1,51 @@
import { MapPin, Phone, Mail, Clock } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
export default function IletisimPage() {
return (
<div className="min-h-screen bg-background pt-28 pb-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
export const revalidate = 3600;
{/* Heading */}
<div className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Bize Ulaşın
</span>
<h1 className="font-serif text-5xl font-bold text-dark mb-4">
İletişim & Rezervasyon
</h1>
<p className="text-muted max-w-lg mx-auto text-sm leading-relaxed">
Sorularınız veya rezervasyon talepleriniz için bizimle iletişime geçin.
Ekibimiz en kısa sürede dönüş yapacaktır.
</p>
</div>
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params;
return buildMeta(lang, "/iletisim", {
title: "İletişim | Kite Beach Akyaka",
description: "Kite Beach Akyaka ile iletişime geçin. Muğla Akyaka'da kitesurf dersi, konaklama ve etkinlikler için bize yazın.",
}, {
title: "Contact | Kite Beach Akyaka",
description: "Contact Kite Beach Akyaka. Write to us for kitesurf lessons, accommodation and events in Akyaka, Muğla.",
});
}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-10">
export default async function IletisimPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const isEn = lang === "en";
{/* Form — takes 3 cols */}
<ScrollReveal className="lg:col-span-3">
<div className="bg-white rounded-3xl p-10 md:p-12 border border-sand shadow-sm">
<h2 className="font-serif text-2xl font-bold text-dark mb-8">Mesaj Gönderin</h2>
<form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<label htmlFor="name" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
Ad Soyad
</label>
<input
type="text"
id="name"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder="Adınız Soyadınız"
/>
</div>
<div>
<label htmlFor="phone" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
Telefon
</label>
<input
type="tel"
id="phone"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder="+90 (5XX) XXX XX XX"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
E-posta
</label>
<input
type="email"
id="email"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder="ornek@email.com"
/>
</div>
<div>
<label htmlFor="subject" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
Konu
</label>
<select
id="subject"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px] text-foreground"
>
<option>Oda Rezervasyonu</option>
<option>Kitesurf Dersi / Kiralama</option>
<option>Restoran Rezervasyonu</option>
<option>Aktivite Bilgisi</option>
<option>Diğer</option>
</select>
</div>
<div>
<label htmlFor="message" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
Mesajınız
</label>
<textarea
id="message"
rows={5}
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px] resize-none"
placeholder="Talebinizi buraya yazabilirsiniz..."
/>
</div>
<button
type="button"
className="btn-shimmer w-full bg-accent text-white py-4 rounded-xl text-[12px] font-bold tracking-[0.15em] uppercase hover:bg-accent/90 transition-colors shadow-lg"
>
Mesaj Gönder
</button>
</form>
</div>
</ScrollReveal>
{/* Sidebar — takes 2 cols */}
<ScrollReveal delay={200} className="lg:col-span-2 flex flex-col gap-6">
{/* Contact info card */}
<div className="bg-primary rounded-3xl p-8 text-white">
<h3 className="font-serif text-xl font-bold mb-7">İletişim Bilgileri</h3>
<ul className="space-y-6">
{[
const contactItems = isEn ? [
{
icon: MapPin,
label: "Address",
value: "Akyaka District, Akcapinar Location, Mugla, Turkey",
href: undefined,
},
{
icon: Phone,
label: "Phone / WhatsApp",
value: "+90 544 504 03 10",
href: "tel:+905445040310",
},
{
icon: Mail,
label: "Email",
value: "info@kitebeachakyaka.com.tr",
href: "mailto:info@kitebeachakyaka.com.tr",
},
{
icon: Clock,
label: "Reception",
value: "Every day 08:00 22:00",
href: undefined,
},
] : [
{
icon: MapPin,
label: "Adres",
@@ -119,8 +55,8 @@ export default function IletisimPage() {
{
icon: Phone,
label: "Telefon / WhatsApp",
value: "+90 533 081 6181",
href: "tel:+905330816181",
value: "+90 544 504 03 10",
href: "tel:+905445040310",
},
{
icon: Mail,
@@ -134,7 +70,123 @@ export default function IletisimPage() {
value: "Her gün 08:00 22:00",
href: undefined,
},
].map(({ icon: Icon, label, value, href }) => (
];
return (
<div className="min-h-screen bg-background pt-28 pb-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Heading */}
<div className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
{isEn ? "Contact Us" : "Bize Ulaşın"}
</span>
<h1 className="font-serif text-5xl font-bold text-dark mb-4">
{isEn ? "Contact" : "İletişim"}
</h1>
<p className="text-muted max-w-lg mx-auto text-sm leading-relaxed">
{isEn ? (
"Get in touch with us for your questions or requests. Our team will get back to you as soon as possible."
) : (
"Sorularınız veya talepleriniz için bizimle iletişime geçin. Ekibimiz en kısa sürede dönüş yapacaktır."
)}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-10">
{/* Form — takes 3 cols */}
<ScrollReveal className="lg:col-span-3">
<div className="bg-white rounded-3xl p-10 md:p-12 border border-sand shadow-sm">
<h2 className="font-serif text-2xl font-bold text-dark mb-8">
{isEn ? "Send a Message" : "Mesaj Gönderin"}
</h2>
<form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<label htmlFor="name" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
{isEn ? "Full Name" : "Ad Soyad"}
</label>
<input
type="text"
id="name"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder={isEn ? "Your Full Name" : "Adınız Soyadınız"}
/>
</div>
<div>
<label htmlFor="phone" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
{isEn ? "Phone" : "Telefon"}
</label>
<input
type="tel"
id="phone"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder="+90 (5XX) XXX XX XX"
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
{isEn ? "Email" : "E-posta"}
</label>
<input
type="email"
id="email"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px]"
placeholder="ornek@email.com"
/>
</div>
<div>
<label htmlFor="subject" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
{isEn ? "Subject" : "Konu"}
</label>
<select
id="subject"
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px] text-foreground"
>
<option>{isEn ? "Room Information" : "Oda Bilgisi"}</option>
<option>{isEn ? "Kitesurf Lesson / Rental" : "Kitesurf Dersi / Kiralama"}</option>
<option>{isEn ? "Restaurant Information" : "Restoran Bilgisi"}</option>
<option>{isEn ? "Activity Information" : "Aktivite Bilgisi"}</option>
<option>{isEn ? "Other" : "Diğer"}</option>
</select>
</div>
<div>
<label htmlFor="message" className="block text-[12px] font-semibold text-foreground/60 mb-2 tracking-wider uppercase">
{isEn ? "Your Message" : "Mesajınız"}
</label>
<textarea
id="message"
rows={5}
className="w-full px-4 py-3 rounded-xl border border-sand focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all bg-background text-[14px] resize-none"
placeholder={isEn ? "You can write your request here..." : "Talebinizi buraya yazabilirsiniz..."}
/>
</div>
<button
type="button"
className="btn-shimmer w-full bg-accent text-white py-4 rounded-xl text-[12px] font-bold tracking-[0.15em] uppercase hover:bg-accent/90 transition-colors shadow-lg"
>
{isEn ? "Send Message" : "Mesaj Gönder"}
</button>
</form>
</div>
</ScrollReveal>
{/* Sidebar — takes 2 cols */}
<ScrollReveal delay={200} className="lg:col-span-2 flex flex-col gap-6">
{/* Contact info card */}
<div className="bg-primary rounded-3xl p-8 text-white">
<h3 className="font-serif text-xl font-bold mb-7">
{isEn ? "Contact Information" : "İletişim Bilgileri"}
</h3>
<ul className="space-y-6">
{contactItems.map(({ icon: Icon, label, value, href }) => (
<li key={label} className="flex items-start gap-4">
<div className="w-9 h-9 bg-white/10 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<Icon size={15} className="text-accent" />
@@ -159,13 +211,13 @@ export default function IletisimPage() {
{/* Map */}
<div className="rounded-3xl overflow-hidden border border-sand flex-grow min-h-[220px] relative">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d12759.395175968393!2d28.3245!3d37.0564!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x14bf76cd1f1f1d1f%3A0x1d1f1f1f1f1f1f1f!2sAkyaka%2C%20Ula%2FMuğla!5e0!3m2!1str!2str!4v1650000000000!5m2!1str!2str"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1570.4884121408588!2d28.328470723755615!3d37.03786201891436!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x14bf9f4829fe7243%3A0xe3747440b7bf7a8f!2sKite%20Beach%20Akyaka!5e0!3m2!1str!2str!4v1784112071594!5m2!1str!2str"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
referrerPolicy="strict-origin-when-cross-origin"
className="absolute inset-0"
/>
</div>
+4 -4
View File
@@ -11,14 +11,14 @@ export default async function KVKKPage({ params }: { params: Promise<{ lang: str
Kişisel verileriniz;
- Otelimizde konaklama ve kitesurf okulumuzdaki faaliyetlerin gerçekleştirilebilmesi,
- Sizlere sunulan ürün ve hizmetlerin beğeni, kullanım alışkanlıkları ve ihtiyaçlarınıza göre özelleştirilerek önerilmesi,
- Rezervasyon, iletişim ve müşteri memnuniyeti süreçlerinin yürütülmesi,
- İletişim ve müşteri memnuniyeti süreçlerinin yürütülmesi,
- İlgili mevzuat gereği yasal yükümlülüklerin yerine getirilmesi amacıyla işlenmektedir.
2. İşlenen Kişisel Verilerin Kimlere ve Hangi Amaçla Aktarılabileceği
Toplanan kişisel verileriniz; yasal yükümlülüklerimizin yerine getirilmesi, hizmetlerimizin sağlanabilmesi ve müşteri memnuniyeti süreçleri dahilinde; iş ortaklarımıza, tedarikçilerimize, kanunen yetkili kamu kurumlarına ve özel kişilere Kanunun 8. ve 9. maddelerinde belirtilen şartlar dahilinde aktarılabilecektir.
3. Kişisel Veri Toplamanın Yöntemi ve Hukuki Sebebi
Kişisel verileriniz, web sitemiz, rezervasyon formları, e-posta iletişimleri ve otel resepsiyonu gibi kanallar aracılığıyla fiziksel veya elektronik ortamda toplanmaktadır.
Kişisel verileriniz, web sitemiz, iletişim formları, e-posta iletişimleri ve otel resepsiyonu gibi kanallar aracılığıyla fiziksel veya elektronik ortamda toplanmaktadır.
4. Veri Sahibinin Hakları
KVKKnın 11. maddesi uyarınca;
@@ -39,14 +39,14 @@ Detaylı bilgi ve talepleriniz için info@kitebeachakyaka.com.tr adresi üzerind
Your personal data is processed to;
- Carry out accommodation in our hotel and activities in our kitesurf school,
- Customize and recommend the products and services offered to you according to your tastes, usage habits, and needs,
- Manage reservation, communication, and customer satisfaction processes,
- Manage communication, and customer satisfaction processes,
- Fulfill legal obligations in accordance with the relevant legislation.
2. To Whom and for What Purpose the Processed Personal Data Can Be Transferred
Your collected personal data may be transferred to our business partners, suppliers, legally authorized public institutions, and private persons within the conditions specified in Articles 8 and 9 of the Law in order to fulfill our legal obligations, provide our services, and within customer satisfaction processes.
3. Method and Legal Reason for Collecting Personal Data
Your personal data is collected in physical or electronic environment through channels such as our website, reservation forms, email communications, and hotel reception.
Your personal data is collected in physical or electronic environment through channels such as our website, contact forms, email communications, and hotel reception.
4. Rights of the Data Subject
In accordance with Article 11 of the KVKK, you have the right to;
+100 -6
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Inter, Playfair_Display } from "next/font/google";
import "../globals.css";
import Script from "next/script";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import WhatsAppButton from "@/components/WhatsAppButton";
@@ -8,22 +9,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: "+905445040310",
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,
@@ -38,10 +123,19 @@ export default async function RootLayout({
className={`${inter.variable} ${playfair.variable} h-full antialiased scroll-smooth`}
suppressHydrationWarning
>
<head>
<Script defer src="/st.js" data-domain="kitebeachakyaka.com.tr" strategy="afterInteractive" />
</head>
<body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
<Navbar />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(buildJsonLd(lang)).replace(/</g, "\\u003c"),
}}
/>
<Navbar lang={lang} />
<main className="flex-grow">{children}</main>
<Footer />
<Footer lang={lang} />
<WhatsAppButton />
</body>
</html>
+70 -130
View File
@@ -1,160 +1,100 @@
import Image from "next/image";
import Link from "next/link";
import { Check, Wifi, Wind, Coffee, Eye, Trees } from "lucide-react";
import { Check, Info } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import BentoGallery from "@/components/BentoGallery";
const rooms = [
{
img: "https://images.unsplash.com/photo-1582719478250-c89404bb8a0e?q=80&w=2940&auto=format&fit=crop",
tag: "Standart",
name: "Standart Oda",
desc: "Konforlu ve modern tasarımlı standart odalarımızda rahatlığın tadını çıkarın. Çift kişilik yatak ve şık dekorasyon ile dinlendirici bir atmosfer.",
sqm: "20 m²",
price: "₺4.500",
features: [
{ icon: Wifi, label: "Ücretsiz Wi-Fi" },
{ icon: Wind, label: "Klima" },
{ icon: Coffee, label: "Minibar" },
{ icon: Eye, label: "Bahçe Manzarası" },
],
reversed: false,
},
{
img: "https://images.unsplash.com/photo-1566665797739-1674de7a421a?q=80&w=2874&auto=format&fit=crop",
tag: "Popüler",
name: "Nehir Manzaralı Oda",
desc: "Güne Azmak nehrinin eşsiz manzarası eşliğinde uyanacağınız ferah odalar. Balkonunuzda sabah kahvenizi yudumlarken doğanın sesini dinleyin.",
sqm: "25 m²",
price: "₺5.500",
features: [
{ icon: Eye, label: "Nehir Manzarası" },
{ icon: Trees, label: "Özel Balkon" },
{ icon: Coffee, label: "Nespresso Makinesi" },
{ icon: Wifi, label: "Ücretsiz Wi-Fi" },
],
reversed: true,
},
{
img: "https://images.unsplash.com/photo-1499955085172-a104c9463ece?q=80&w=2940&auto=format&fit=crop",
tag: "Premium",
name: "Ahşap Bungalow",
desc: "Doğayla tamamen iç içe, bağımsız girişli ve teraslı ahşap tasarımlı yaşam alanı. Gizlilik ve huzur arayan çiftler için ideal.",
sqm: "35 m²",
price: "₺7.000",
features: [
{ icon: Trees, label: "Özel Teras" },
{ icon: Eye, label: "Doğa Manzarası" },
{ icon: Coffee, label: "Premium İkramlar" },
{ icon: Wind, label: "Klima" },
],
reversed: false,
},
const galleryImages = [
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.15.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.18.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.19.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.20%20(1).jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.20.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.21.jpeg",
];
export default function OdalarPage() {
return (
<div className="min-h-screen bg-background">
export default async function OdalarPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const isEn = lang === "en";
const displayAmenities = isEn
? ["Organic Breakfast Spread", "Free Parking", "Beach & River Access", "Sunbed & Towel", "Wi-Fi", "24/7 Reception", "Air Conditioning", "Minibar"]
: ["Organik Serpme Kahvaltı", "Ücretsiz Otopark", "Plaj & Nehir Erişimi", "Şezlong & Havlu", "Wi-Fi", "24/7 Resepsiyon", "Klima", "Minibar"];
return (
<div className="min-h-screen bg-background pt-40 pb-10">
{/* Page Header */}
<section className="relative h-[55vh] w-full flex items-end overflow-hidden">
<div className="absolute inset-0">
<Image
src="https://images.unsplash.com/photo-1582719478250-c89404bb8a0e?q=80&w=2940&auto=format&fit=crop"
alt="Odalarımız"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/85 via-dark/30 to-black/20" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-14 w-full">
<span className="block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-3 animate-fade-up">
Konaklama
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4 animate-fade-up">
{isEn ? "Accommodation" : "Konaklama"}
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
Odalarımız
<h1 className="font-serif text-5xl md:text-6xl text-dark font-bold animate-fade-up delay-100 mb-6">
{isEn ? "Our Rooms" : "Odalarımız"}
</h1>
<p className="text-white/60 text-lg mt-3 font-light animate-fade-up delay-200">
Doğanın içinde, konforlu bir mola.
<p className="text-muted text-lg max-w-2xl mx-auto animate-fade-up delay-200">
{isEn ? "A comfortable break in the heart of nature." : "Doğanın içinde, konforlu bir mola."}
</p>
</div>
</section>
{/* Room Listings */}
<section className="py-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="space-y-16">
{rooms.map((room, i) => (
<ScrollReveal key={i}>
<div
className={`grid grid-cols-1 md:grid-cols-2 gap-0 bg-white rounded-3xl overflow-hidden shadow-sm border border-sand ${
room.reversed ? "" : ""
}`}
>
{/* Image */}
<div className={`relative h-80 md:h-auto ${room.reversed ? "md:order-2" : ""}`}>
<Image
src={room.img}
alt={room.name}
fill
className="object-cover"
/>
{/* Tag */}
<span className="absolute top-5 left-5 bg-dark/80 backdrop-blur-sm text-white text-[10px] font-bold px-3 py-1.5 rounded-full tracking-widest uppercase">
{room.tag}
{/* Intro & Price */}
<section className="py-20 max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-12">
<h2 className="font-serif text-4xl font-bold text-dark mb-6">
{isEn ? "Comfort & Serenity" : "Konfor & Huzur"}
</h2>
<p className="text-muted text-[15px] leading-relaxed mb-10 max-w-2xl mx-auto">
{isEn
? "All our rooms are carefully designed to offer you the ultimate relaxation, harmonizing with the natural beauty surrounding Akyaka. Enjoy modern amenities with a touch of Aegean warmth."
: "Tüm odalarımız, Akyaka'yı çevreleyen doğal güzelliklerle uyum içinde, size en üst düzeyde rahatlama sunmak için özenle tasarlandı. Ege sıcaklığının dokunuşuyla modern olanakların keyfini çıkarın."}
</p>
<div className="inline-flex items-center justify-center gap-4 bg-primary/10 border-2 border-primary/20 text-primary px-8 py-5 rounded-3xl shadow-sm">
<Info size={24} className="text-accent" />
<div className="text-left">
<span className="block text-xs uppercase tracking-widest text-primary/70 mb-1">
{isEn ? "Standard Rate" : "Standart Fiyat"}
</span>
<span className="block font-bold text-xl md:text-2xl tracking-wide">
{isEn ? "8000 TL / Night (Breakfast Included)" : "8000 TL / Gecelik (Kahvaltı Dahil)"}
</span>
</div>
{/* Content */}
<div className={`p-10 md:p-14 flex flex-col justify-center ${room.reversed ? "md:order-1" : ""}`}>
<span className="text-muted text-xs tracking-[0.2em] uppercase mb-3 block">{room.sqm}</span>
<h2 className="font-serif text-3xl font-bold text-dark mb-4">{room.name}</h2>
<p className="text-gray-500 mb-8 leading-relaxed text-[15px]">{room.desc}</p>
<div className="grid grid-cols-2 gap-3 mb-10">
{room.features.map(({ icon: Icon, label }, j) => (
<div key={j} className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-full bg-primary/8 flex items-center justify-center flex-shrink-0">
<Icon size={13} className="text-primary" />
</div>
<span className="text-[13px] text-foreground/70">{label}</span>
</div>
))}
</div>
<div className="flex justify-between items-center pt-8 border-t border-sand">
<div>
<span className="text-muted text-xs block mb-1 uppercase tracking-wider">Gecelik</span>
<span className="font-serif text-3xl font-bold text-dark">{room.price}</span>
</div>
<Link
href="/iletisim"
className="btn-shimmer bg-accent text-white px-7 py-3.5 rounded-full text-[11px] font-bold tracking-[0.12em] uppercase hover:bg-accent/90 transition-colors shadow-lg"
>
Rezervasyon Yap
</Link>
</div>
</div>
</div>
</ScrollReveal>
))}
</section>
{/* Bento Grid Gallery */}
<section className="pb-24 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal>
<BentoGallery images={galleryImages} />
</ScrollReveal>
<div className="text-center mt-16">
<Link
href={`/${lang}/iletisim`}
className="btn-shimmer bg-accent text-white px-10 py-4 rounded-full text-sm font-bold tracking-[0.15em] uppercase hover:bg-accent/90 transition-colors shadow-xl"
>
{isEn ? "Contact For Reservation" : "Rezervasyon İçin İletişime Geçin"}
</Link>
</div>
</section>
{/* Info strip */}
<section className="py-16 bg-primary">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h3 className="font-serif text-3xl text-white font-bold mb-4">
Tüm odalarda dahil olanlar
{/* Info strip (Disabled)
<section className="py-20 bg-primary">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h3 className="font-serif text-3xl text-white font-bold mb-6">
{isEn ? "Included in your stay" : "Konaklamanıza dahil olanlar"}
</h3>
<div className="flex flex-wrap justify-center gap-4 mt-8">
{["Organik Serpme Kahvaltı", "Ücretsiz Otopark", "Plaj & Nehir Erişimi", "Şezlong & Havlu", "Wi-Fi", "24/7 Resepsiyon"].map((item) => (
<span key={item} className="inline-flex items-center gap-2 bg-white/10 text-white text-[13px] px-4 py-2 rounded-full border border-white/15">
<Check size={13} className="text-accent" />
<div className="flex flex-wrap justify-center gap-5 mt-8">
{displayAmenities.map((item) => (
<span key={item} className="inline-flex items-center gap-2.5 bg-white/10 text-white text-[14px] px-5 py-3 rounded-full border border-white/15 backdrop-blur-sm">
<Check size={16} className="text-accent" />
{item}
</span>
))}
</div>
</div>
</section>
*/}
</div>
);
}
+89 -558
View File
@@ -1,76 +1,87 @@
import Image from "next/image";
import Link from "next/link";
import * as Icons from "lucide-react";
import { ArrowRight, Star } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import { getDictionary, Locale } from "./dictionaries";
import prisma from "@/lib/prisma";
import { buildMeta } from "@/lib/metadata";
import type { Metadata } from "next";
import HeroVideo from "@/components/HeroVideo";
import HeroSection from "@/components/home/HeroSection";
import FeaturesStrip from "@/components/home/FeaturesStrip";
import AboutSection from "@/components/home/AboutSection";
import ActivitiesGrid from "@/components/home/ActivitiesGrid";
import EventsPreview from "@/components/home/EventsPreview";
import InstagramFeed from "@/components/home/InstagramFeed";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import CtaBand from "@/components/home/CtaBand";
import RoomsSection from "@/components/home/RoomsSection";
export const revalidate = 3600;
const defaultFeatures = [
{ icon: "Wind", label: "Kitesurf Okulu", sub: "IKO Sertifikalı" },
{ icon: "Anchor", label: "Yoga Platformu", sub: "Nehir Kenarı" },
{ icon: "Wind", label: "Kitesurf", sub: "Eğitimleri" },
{ icon: "Anchor", label: "Yoga Wellness", sub: "Platform" },
{ icon: "Utensils", label: "Restoran & Bar", sub: "Ege Mutfağı" },
{ icon: "BedDouble", label: "Butik Odalar", sub: "3 Tip Konaklama" },
{ icon: "Sun", label: "Gün Batımı Terası", sub: "Gökova Manzarası" },
];
const rooms = [
{
img: "https://images.unsplash.com/photo-1582719478250-c89404bb8a0e?q=80&w=2940&auto=format&fit=crop",
badge: "Standart",
badgeStyle: "bg-white/90 text-dark",
name: "Standart Oda",
desc: "Konforlu ve modern tasarımlı odamızda huzurlu bir konaklama.",
price: "₺4.500",
cta: "primary" as const,
},
{
img: "https://images.unsplash.com/photo-1566665797739-1674de7a421a?q=80&w=2874&auto=format&fit=crop",
badge: "Popüler",
badgeStyle: "bg-accent text-white",
name: "Nehir Manzaralı Oda",
desc: "Azmak nehrinin eşsiz manzarası eşliğinde güne uyanın.",
price: "₺5.500",
cta: "accent" as const,
},
{
img: "https://images.unsplash.com/photo-1499955085172-a104c9463ece?q=80&w=2940&auto=format&fit=crop",
badge: "Premium",
badgeStyle: "bg-white/90 text-dark",
name: "Ahşap Bungalow",
desc: "Doğayla tamamen iç içe, teraslı ve bağımsız girişli.",
price: "₺7.000",
cta: "primary" as const,
},
{ icon: "Sun", label: "Sunset DJ Party", sub: "Etkinlikler" },
];
const testimonials = [
{
text: "Akyaka'da pek çok yerde kaldım ama Kite Beach bambaşka bir deneyimdi. Nehrin sesi, kitesurf dersleri ve muhteşem kahvaltı — her şey mükemmeldi.",
name: "Ayşe K.",
from: "İstanbul",
text: "Çok atmosferik bir yer. Harika ortam, uçurtma ve rüzgar. Kahve fena değil ama biraz pahalı.",
name: "Elena Gerasimova",
from: "Google Yorumları",
},
{
text: "Yoga seansları ahşap platformda, sabah erken saatte, nehir manzarası eşliğinde. Kelimelerle anlatmak zor — mutlaka yaşanmalı.",
name: "Marco B.",
from: "İtalya",
text: "Ortama, plaja ve kitesurf yapanları izlemeye bayıldım. Takılacak bir sürü harika yer, iyi yemek ve iyi müzik var. Kitesurf'e uğramadan Akyaka'ya gelmiş sayılmazsınız.",
name: "Dhyani Love",
from: "Google Yorumları",
},
{
text: "Eğitmenler son derece ilgili. Kitesurf derslerimden sonra bağımlısı oldum. Önümüzdeki yaz mutlaka döneceğim.",
name: "Mehmet Ö.",
from: "Ankara",
text: "Harika bir kitesurf plajı. Öğleden sonraları istikrarlı termal rüzgarlar ve yumuşak kum. Çoğu sörfçü (Eylül-Ekim) dalış kıyafeti giyiyor ama ben şortla bile rahattım. Sadece plaja girmek için ödeme yapmanıza gerek yok, yalnızca sörf yapacaksanız ödüyorsunuz.",
name: "Shebastian Reyes",
from: "Google Yorumları",
},
];
import { getDictionary, Locale } from "./dictionaries";
import prisma from "@/lib/prisma";
const testimonialsEn = [
{
text: "Very atmospheric place. Cool man, kite and wind. Coffee not bad, but expensive.",
name: "Elena Gerasimova",
from: "Google Reviews",
},
{
text: "Loved the vibes love the beach and watching kite surfers. Lots of cool places to hang out, good food and good music. You cannot come to Akyaka without coming to kite surfing.",
name: "Dhyani Love",
from: "Google Reviews",
},
{
text: "Great kite beach. Steady thermal afternoon winds and soft sand. Most kiters wear a wetsuit (Sep - Oct) but I was fine with just board shorts. You don't have to pay just to go to the beach, only if you plan to kite.",
name: "Shebastian Reyes",
from: "Google Reviews",
},
];
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 +94,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 +110,17 @@ 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 displayTestimonials = lang === "en" ? testimonialsEn : testimonials;
const heroData = heroDb || {
location: dict.home.location,
welcome: dict.home.welcome,
@@ -107,11 +131,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",
@@ -128,511 +147,23 @@ export default async function Home({ params }: { params: Promise<{ lang: string
statsGuestLabel: lang === "en" ? "Happy Guests" : "Mutlu Misafir",
statsYearCount: "10+",
statsYearLabel: lang === "en" ? "Years Experience" : "Yıl Deneyim",
badgeIkoTitle: "IKO",
badgeIkoLabel: lang === "en" ? "Certified" : "Sertifikalı"
badgeIkoTitle: "Kitesurf",
badgeIkoLabel: lang === "en" ? "School" : "Okulu"
};
const displayFeatures = dbFeatures.length > 0 ? dbFeatures : defaultFeatures;
const instagramPosts = await getInstagramData();
return (
<div className="flex flex-col min-h-screen">
{/* ──────────────────────────────────────────
HERO
────────────────────────────────────────── */}
<section className="relative h-screen w-full flex items-center justify-center overflow-hidden">
<HeroVideo />
{/* Floating season badge
<div className="absolute top-32 right-6 md:right-16 z-20 animate-float hidden sm:block">
<div className="bg-white/10 backdrop-blur-md border border-white/20 rounded-2xl px-5 py-4 text-white">
<span className="block text-accent font-bold text-[10px] tracking-[0.25em] uppercase mb-1">Sezon</span>
<span className="font-serif text-xl font-medium">Nisan — Ekim</span>
</div>
</div>
*/}
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
<div className="animate-fade-up delay-100">
<span className="inline-block text-white/60 text-xs tracking-[0.35em] uppercase mb-8 font-light">
{heroData.location}
</span>
</div>
<div className="flex items-center justify-center gap-4 md:gap-6 mb-6 animate-fade-up delay-200">
<span
className="text-xs md:text-sm tracking-[0.4em] font-bold uppercase whitespace-nowrap text-white/90"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
>
{heroData.welcome}
</span>
<h1 className="font-sans font-black text-white leading-none tracking-tighter text-[clamp(4rem,10vw,10rem)] uppercase">
{heroData.title}
</h1>
</div>
<p className="animate-fade-up delay-500 text-white/75 text-base md:text-xl mb-10 font-light italic tracking-wide">
{heroData.subtitle}
</p>
<div className="animate-fade-up delay-600 flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href={`/${lang}/iletisim`}
className="btn-shimmer bg-accent text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase shadow-2xl hover:bg-accent/90 transition-colors w-full sm:w-auto text-center"
>
{heroData.bookNow}
</Link>
<Link
href={`/${lang}/aktiviteler`}
className="bg-white/10 backdrop-blur-sm border border-white/30 text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase hover:bg-white/20 transition-colors w-full sm:w-auto text-center"
>
{heroData.exploreActivities}
</Link>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-3">
<span className="text-white/40 text-[10px] tracking-[0.3em] uppercase">{heroData.scroll}</span>
<div className="w-px h-12 bg-gradient-to-b from-white/40 to-transparent animate-scroll-bounce" />
</div>
</section>
{/* ──────────────────────────────────────────
FEATURES STRIP
────────────────────────────────────────── */}
<section className="bg-dark text-white py-8">
<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;
return (
<div
key={i}
className={`flex flex-col items-center justify-center gap-2 py-6 px-4 group ${i >= 2 ? "hidden md:flex" : ""}`}
>
<IconComponent className="w-5 h-5 text-accent group-hover:scale-110 transition-transform duration-300" />
<span className="text-[13px] font-semibold text-center">{label}</span>
<span className="text-[11px] text-white/40 text-center">{sub}</span>
</div>
);
})}
</div>
</div>
</section>
{/* ──────────────────────────────────────────
ABOUT
────────────────────────────────────────── */}
<section className="py-28 bg-background overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal className="relative">
<div className="relative h-[540px] lg:h-[660px] rounded-3xl overflow-hidden shadow-2xl">
<Image
src={aboutData.image}
alt={aboutData.badge}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
</div>
{/* Stats card */}
<div className="absolute -bottom-5 -right-4 md:-right-8 bg-white rounded-2xl p-6 shadow-xl border border-sand">
<div className="grid grid-cols-2 gap-6 divide-x divide-sand">
<div className="text-center">
<span className="block font-serif text-3xl font-bold text-primary">{aboutData.statsGuestCount}</span>
<span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsGuestLabel}</span>
</div>
<div className="text-center pl-6">
<span className="block font-serif text-3xl font-bold text-primary">{aboutData.statsYearCount}</span>
<span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsYearLabel}</span>
</div>
</div>
</div>
{/* IKO badge */}
<div className="absolute -top-4 -left-4 bg-accent text-white rounded-2xl p-4 shadow-lg">
<Star className="w-4 h-4 mb-1" fill="white" />
<span className="block text-xs font-bold leading-none">{aboutData.badgeIkoTitle}</span>
<span className="block text-[10px] opacity-80 mt-0.5">{aboutData.badgeIkoLabel}</span>
</div>
</ScrollReveal>
<ScrollReveal delay={200}>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
{aboutData.badge}
</span>
<h2 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold text-dark leading-[1.05] mb-8">
{aboutData.titleLine1}<br />{aboutData.titleLine2}<br />
<span className="text-primary italic">{aboutData.titleHighlight}</span>
</h2>
<p className="text-gray-600 leading-relaxed mb-5">
{aboutData.description1}
</p>
<p className="text-gray-600 leading-relaxed mb-10">
{aboutData.description2}
</p>
<div className="section-line mb-8" />
<Link
href="/hakkimizda"
className="inline-flex items-center gap-2 text-dark font-bold text-sm tracking-wide group"
>
<span className="border-b-2 border-accent pb-0.5">Hikayemizi Okuyun</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1.5 transition-transform" />
</Link>
</ScrollReveal>
</div>
</div>
</section>
{/* ──────────────────────────────────────────
ROOMS
────────────────────────────────────────── */}
{/*
<section className="py-28 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Konaklama
</span>
<h2 className="font-serif text-4xl md:text-5xl font-bold text-dark mb-6">Kite Beach Akyaka Otel</h2>
<p className="text-muted max-w-3xl mx-auto text-sm leading-relaxed">
Akyakanın en güzel köşesinde, nehrin dinginliği ile denizin sonsuz maviliğinin kucaklaştığı o özel noktadayız. Kapınızı açtığınız anda sizi karşılayan nehir kıyısının sessizliği, güne başlamak için en huzurlu davetiniz olacak. Doğanın kalbinde, nehir manzarasına karşı yaptığınız bir yoga seansıyla ruhunuzu dengelerken; hemen ardından Akyakanın meşhur rüzgarlarında Kite Surf ile adrenalini zirveye taşıyabilirsiniz. Günün yorgunluğunu ise, restoranımızın mutfağından çıkan lezzetlerle, Egenin kendine has o büyüleyici gün batımını izleyerek geride bırakın. Burada zaman, sadece sizin istediğiniz hızda akıyor.
</p>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{rooms.map((room, i) => (
<div
key={i}
className={`card-lift group rounded-2xl overflow-hidden bg-sand border border-sand ${i === 1 ? "md:-mt-4 md:shadow-xl" : ""
}`}
>
<div className="relative h-64 overflow-hidden">
<Image
src={room.img}
alt={room.name}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<span className={`absolute top-4 left-4 text-[10px] font-bold px-3 py-1.5 rounded-full tracking-wider uppercase ${room.badgeStyle} backdrop-blur-sm`}>
{room.badge}
</span>
</div>
<div className="p-7">
<h3 className="font-serif text-xl font-bold text-dark mb-2">{room.name}</h3>
<p className="text-muted mb-6 text-[13px] leading-relaxed">{room.desc}</p>
<div className="flex justify-between items-center pt-5 border-t border-sand">
<div>
<span className="text-2xl font-bold text-dark">{room.price}</span>
<span className="text-muted text-xs ml-1">/gece</span>
</div>
<Link
href="/odalar"
className={`text-[11px] px-5 py-2.5 rounded-full font-bold tracking-wider uppercase transition-colors ${room.cta === "accent"
? "bg-accent text-white hover:bg-accent/90"
: "bg-primary text-white hover:bg-primary/90"
}`}
>
İncele
</Link>
</div>
</div>
</div>
))}
</ScrollReveal>
<div className="text-center mt-12">
<Link
href="/odalar"
className="inline-flex items-center gap-2 border-2 border-dark text-dark text-xs px-8 py-3.5 rounded-full font-bold uppercase tracking-widest hover:bg-dark hover:text-white transition-colors"
>
Tüm Odaları Gör
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
</section>
*/}
{/* ──────────────────────────────────────────
ACTIVITIES — BENTO GRID
────────────────────────────────────────── */}
<section className="py-28 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-10">
<div>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Ne Yapacaksınız?
</span>
<h2 className="font-serif text-5xl font-bold text-dark">Aktiviteler</h2>
</div>
<Link
href="/aktiviteler"
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-accent transition-colors"
>
Tümünü Gör <ArrowRight className="w-4 h-4" />
</Link>
</ScrollReveal>
{/* Bento rows */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{activities.length > 0 ? (
activities.map((act, i) => (
<ScrollReveal
key={act.id}
className={i === 0 || i === 3 ? "md:col-span-2" : ""}
delay={i * 50}
>
<Link href="/aktiviteler" className={`group relative block rounded-3xl overflow-hidden ${i < 2 ? "h-[400px]" : "h-[280px]"}`}>
<Image
src={act.image}
alt={act.title}
fill
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`} />
<div className={`absolute bottom-0 ${i === 0 || i === 3 ? "p-8" : "p-7"}`}>
{act.badge && (
<span className="block text-accent text-[10px] tracking-[0.25em] uppercase font-bold mb-2">{act.badge}</span>
)}
<h3 className={`font-serif text-white font-bold mb-1 ${i === 0 ? "text-4xl mb-2" : i === 1 || i === 3 ? "text-3xl" : "text-2xl"}`}>{act.title}</h3>
{i === 0 && (
<p className="text-white/60 text-sm mb-4 max-w-xs opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-400">
{act.description}
</p>
)}
{i !== 0 && (
<span className="text-white/60 text-xs">{act.price}</span>
)}
{i === 0 && (
<span className="inline-flex items-center gap-2 text-white text-sm font-semibold">
{act.price} <ArrowRight className="w-3.5 h-3.5" />
</span>
)}
</div>
</Link>
</ScrollReveal>
))
) : (
<div className="md:col-span-3 py-10 text-center text-gray-500">Henüz aktivite eklenmedi.</div>
)}
</div>
</div>
</section>
{/* ──────────────────────────────────────────
EVENTS (PREVIEW)
────────────────────────────────────────── */}
<section className="py-28 bg-dark text-white relative overflow-hidden">
{/* Background glow */}
<div className="absolute top-0 right-0 w-96 h-96 bg-accent/20 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-primary/20 rounded-full blur-[150px] pointer-events-none" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<ScrollReveal className="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-16">
<div>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Takvim
</span>
<h2 className="font-serif text-5xl font-bold">Etkinlikler</h2>
</div>
<Link
href="/etkinlikler"
className="inline-flex items-center gap-2 text-sm font-semibold text-white/70 hover:text-white transition-colors border border-white/20 hover:border-white px-6 py-2.5 rounded-full"
>
Tüm Etkinlikler <ArrowRight className="w-4 h-4" />
</Link>
</ScrollReveal>
<ScrollReveal delay={100}>
{events.length > 0 ? (
events.map((ev) => (
<div key={ev.id} className="group relative flex flex-col md:flex-row gap-8 bg-white/5 border border-white/10 rounded-3xl p-6 md:p-10 hover:bg-white/10 transition-colors backdrop-blur-sm mb-6">
<div className="relative w-full md:w-1/3 h-[250px] md:h-auto rounded-2xl overflow-hidden shrink-0">
<Image
src={ev.image}
alt={ev.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
<div className="flex flex-col justify-center w-full md:w-2/3">
{ev.badge && (
<div className="inline-block bg-accent/20 text-accent text-[10px] font-bold px-3 py-1.5 rounded-full tracking-widest uppercase mb-4 w-max">
{ev.badge}
</div>
)}
<h3 className="font-serif text-3xl md:text-4xl font-bold mb-4">{ev.title}</h3>
<p className="text-white/60 leading-relaxed mb-6 max-w-xl">
{ev.description}
</p>
<div className="flex flex-wrap items-center gap-6 mb-8 text-sm text-white/80">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.date}
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.time}
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.location}
</span>
</div>
<Link
href="/etkinlikler"
className="inline-flex items-center gap-2 text-xs font-bold tracking-widest uppercase text-accent hover:text-white transition-colors w-max"
>
Detayları İncele <ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
))
) : (
<div className="py-10 text-center text-white/50">Henüz etkinlik eklenmedi.</div>
)}
</ScrollReveal>
</div>
</section>
{/* ──────────────────────────────────────────
INSTAGRAM FEED
────────────────────────────────────────── */}
{instagramPosts.length > 0 && (
<section className="py-28 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
@kitebeachakyaka
</span>
<h2 className="font-serif text-5xl font-bold text-dark">Instagram</h2>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{instagramPosts.map((post: any) => (
<a
key={post.id}
href={`https://instagram.com/p/${post.code}`}
target="_blank"
rel="noreferrer"
className="group block relative rounded-2xl overflow-hidden aspect-[4/5] border border-sand"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.image_versions2?.candidates[0]?.url}
alt={post.caption?.text?.substring(0, 100) || "Instagram post"}
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" />
<p className="text-xs line-clamp-3">
{post.caption?.text}
</p>
</div>
</a>
))}
</ScrollReveal>
<div className="text-center mt-12">
<a
href="https://instagram.com/kitebeachakyaka"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 border-2 border-dark text-dark text-xs px-8 py-3.5 rounded-full font-bold uppercase tracking-widest hover:bg-dark hover:text-white transition-colors"
>
Bizi Takip Edin
<ArrowRight className="w-3.5 h-3.5" />
</a>
</div>
</div>
</section>
)}
{/* ──────────────────────────────────────────
TESTIMONIALS
────────────────────────────────────────── */}
<section className="py-28 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
Misafir Yorumları
</span>
<h2 className="font-serif text-5xl font-bold text-dark">Deneyimler</h2>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 md:grid-cols-3 gap-6">
{testimonials.map((t, i) => (
<div key={i} className="relative bg-sand rounded-2xl p-8 border border-sand overflow-hidden">
{/* Large decorative quote */}
<span className="absolute top-3 right-5 font-serif text-[8rem] leading-none text-primary/8 select-none">
"
</span>
{/* Stars */}
<div className="flex gap-1 mb-5 relative z-10">
{Array.from({ length: 5 }).map((_, j) => (
<Star key={j} className="w-3.5 h-3.5 text-accent" fill="#E87040" />
))}
</div>
<p className="text-foreground/75 text-[13px] leading-relaxed mb-6 relative z-10 italic">
"{t.text}"
</p>
<div className="flex items-center gap-3 relative z-10">
<div className="w-9 h-9 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-primary font-bold text-sm">{t.name[0]}</span>
</div>
<div>
<span className="block font-semibold text-dark text-sm">{t.name}</span>
<span className="text-muted text-xs">{t.from}</span>
</div>
</div>
</div>
))}
</ScrollReveal>
</div>
</section>
{/* ──────────────────────────────────────────
CTA BAND
────────────────────────────────────────── */}
<section className="relative py-28 overflow-hidden">
<div
className="absolute inset-0 animate-gradient-pan"
style={{
background:
"linear-gradient(135deg, #0D3D50 0%, #1A6B8A 35%, #0E1A24 60%, #1A4A60 100%)",
}}
/>
<div className="absolute inset-0 opacity-[0.04]">
<Image
src="https://images.unsplash.com/photo-1498623116890-37e912163d5d?q=80&w=2940&auto=format&fit=crop"
alt=""
fill
className="object-cover"
/>
</div>
<div className="relative z-10 max-w-3xl mx-auto px-4 text-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-6">
Hemen Başlayın
</span>
<h2 className="font-serif text-5xl md:text-6xl font-bold text-white leading-tight mb-6">
Hayalindeki tatili<br />planlamaya hazır mısın?
</h2>
<p className="text-white/50 mb-10 text-base leading-relaxed">
Rüzgar seni çağırıyor. Hemen yerini ayırt ve bu eşsiz deneyimin bir parçası ol.
</p>
<Link
href="/iletisim"
className="btn-shimmer inline-block bg-accent text-white px-12 py-4 rounded-full text-xs font-bold tracking-[0.2em] uppercase shadow-2xl hover:bg-white hover:text-dark transition-colors"
>
Hemen Rezervasyon Yap
</Link>
</ScrollReveal>
</div>
</section>
<HeroSection heroData={heroData} lang={lang} />
<FeaturesStrip features={displayFeatures} />
<AboutSection aboutData={aboutData} lang={lang} />
<RoomsSection lang={lang} />
<ActivitiesGrid activities={activities} lang={lang} />
<EventsPreview events={events} lang={lang} />
<InstagramFeed instagramPosts={instagramPosts} lang={lang} />
<TestimonialsSection testimonials={displayTestimonials} lang={lang} />
<CtaBand lang={lang} />
</div>
);
}
+75 -37
View File
@@ -2,8 +2,23 @@ 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";
const menuCategories = [
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 menuCategoriesTr = [
{
icon: Coffee,
name: "Serpme Kahvaltı",
@@ -27,48 +42,66 @@ const menuCategories = [
},
];
export default function RestoranPage() {
return (
<div className="min-h-screen bg-background">
const menuCategoriesEn = [
{
icon: Coffee,
name: "Breakfast Spread",
desc: "Start the day energized with local cheeses, homemade jams, organic olive oil, and freshly baked village bread.",
hours: "08:30 11:30",
highlight: "Local Products",
},
{
icon: UtensilsCrossed,
name: "Lunch & Dinner",
desc: "Daily fresh seafood, Aegean olive oil appetizers (meze), and select pasta from Italian cuisine for dinner.",
hours: "12:00 23:00",
highlight: "Fresh Aegean Cuisine",
},
{
icon: Wine,
name: "Lounge & Bar",
desc: "Delightful moments against the sunset with signature cocktails prepared by our mixologists, local wines, and Turkish raki.",
hours: "16:00 01:00",
highlight: "Sunset View",
},
];
export default async function RestoranPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const isEn = lang === "en";
const displayMenuCategories = isEn ? menuCategoriesEn : menuCategoriesTr;
return (
<div className="min-h-screen bg-background pt-40 pb-10">
{/* Page Header */}
<section className="relative h-[55vh] w-full flex items-end overflow-hidden">
<div className="absolute inset-0">
<Image
src="https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?q=80&w=2940&auto=format&fit=crop"
alt="Restoran & Bar"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/85 via-dark/30 to-black/20" />
</div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-14 w-full">
<span className="block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-3 animate-fade-up">
Lezzetler
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4 animate-fade-up">
{isEn ? "Flavors" : "Lezzetler"}
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
Restoran & Bar
<h1 className="font-serif text-5xl md:text-6xl text-dark font-bold animate-fade-up delay-100 mb-6">
{isEn ? "Restaurant & Bar" : "Restoran & Bar"}
</h1>
<p className="text-white/60 text-lg mt-3 font-light animate-fade-up delay-200">
Ege lezzetleri ve nehir manzarası.
<p className="text-muted text-lg max-w-2xl mx-auto animate-fade-up delay-200">
{isEn ? "Aegean flavors and river view." : "Ege lezzetleri ve nehir manzarası."}
</p>
</div>
</section>
{/* Intro */}
<section className="py-20 bg-white">
<div className="max-w-3xl mx-auto px-4 sm:px-6 text-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
Mutfağımız
{isEn ? "Our Kitchen" : "Mutfağımız"}
</span>
<h2 className="font-serif text-4xl font-bold text-dark mb-6">
Tazeden, Yerelden, Sevgiyle
{isEn ? "Fresh, Local, With Love" : "Tazeden, Yerelden, Sevgiyle"}
</h2>
<p className="text-gray-500 leading-relaxed text-[15px]">
Şeflerimizin özenle hazırladığı taze Ege mutfağı lezzetleri, organik kahvaltılıklar ve
imza kokteyllerimizle Kite Beach Restoran'da günün her saati kendinizi şımartın.
Azmak nehrinin hemen kenarında, ahşap terasımızda yemeğinizin tadını çıkarın.
{isEn ? (
"Pamper yourself at any time of the day at Kite Beach Restaurant with fresh Aegean cuisine flavors carefully prepared by our chefs, organic breakfast items, and our signature cocktails. Enjoy your meal on our wooden terrace, right by the Azmak River."
) : (
"Şeflerimizin özenle hazırladığı taze Ege mutfağı lezzetleri, organik kahvaltılıklar ve imza kokteyllerimizle Kite Beach Restoran'da günün her saati kendinizi şımartın. Azmak nehrinin hemen kenarında, ahşap terasımızda yemeğinizin tadını çıkarın."
)}
</p>
</ScrollReveal>
</div>
@@ -78,7 +111,7 @@ export default function RestoranPage() {
<section className="pb-20 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal stagger className="grid grid-cols-1 md:grid-cols-3 gap-6">
{menuCategories.map((cat, i) => {
{displayMenuCategories.map((cat, i) => {
const Icon = cat.icon;
return (
<div
@@ -108,23 +141,25 @@ export default function RestoranPage() {
<section className="py-20 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-12">
<h2 className="font-serif text-4xl font-bold text-dark">Atmosfer</h2>
<h2 className="font-serif text-4xl font-bold text-dark">{isEn ? "Atmosphere" : "Atmosfer"}</h2>
</ScrollReveal>
<ScrollReveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative h-80 rounded-2xl overflow-hidden md:col-span-2 group">
<Image
src="https://images.unsplash.com/photo-1544148103-0773bf10d330?q=80&w=2940&auto=format&fit=crop"
alt="Restoran atmosferi"
alt={isEn ? "Restaurant atmosphere" : "Restoran atmosferi"}
fill
sizes="(max-width: 768px) 100vw, 66vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
<div className="relative h-80 rounded-2xl overflow-hidden group">
<Image
src="https://images.unsplash.com/photo-1551504734-5ee1c4a1479b?q=80&w=2940&auto=format&fit=crop"
alt="Kahvaltı"
alt={isEn ? "Breakfast" : "Kahvaltı"}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
@@ -138,20 +173,23 @@ export default function RestoranPage() {
<div className="max-w-3xl mx-auto px-4 text-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
Özel Günler
{isEn ? "Special Days" : "Özel Günler"}
</span>
<h3 className="font-serif text-4xl font-bold mb-5">
Özel Etkinlikler & Rezervasyonlar
{isEn ? "Special Events & Contact" : "Özel Etkinlikler & İletişim"}
</h3>
<p className="text-white/50 mb-8 leading-relaxed">
Doğum günü, yıl dönümü veya özel grup yemekleriniz için nehir manzaralı
terasımızı rezerve edebilirsiniz. Ekibimiz organizasyonunuzu özel kılmak için burada.
{isEn ? (
"You can contact us about our terrace with a river view for your birthday, anniversary, or special group dinners. Our team is here to make your organization special."
) : (
"Doğum günü, yıl dönümü veya özel grup yemekleriniz için nehir manzaralı terasımızla ilgili bizimle iletişime geçebilirsiniz. Ekibimiz organizasyonunuzu özel kılmak için burada."
)}
</p>
<Link
href="/iletisim"
href={`/${lang}/iletisim`}
className="btn-shimmer inline-block bg-accent text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase"
>
Masa Rezervasyonu Yap
{isEn ? "Contact Us" : "Bize Ulaşın"}
</Link>
</ScrollReveal>
</div>
+4 -4
View File
@@ -47,7 +47,7 @@ export async function loadMockActivities() {
title: "Stand-Up Paddle",
description: "Azmak nehrinin berrak sularında eşsiz bir keşif.",
price: "₺300/Saat",
image: "https://images.unsplash.com/photo-1522883392419-7d8cb2435728?q=80&w=2924&auto=format&fit=crop",
image: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 3
},
@@ -56,7 +56,7 @@ export async function loadMockActivities() {
title: "Stand-Up Paddle",
description: "A unique discovery in the crystal clear waters of Azmak River.",
price: "₺300/Hour",
image: "https://images.unsplash.com/photo-1522883392419-7d8cb2435728?q=80&w=2924&auto=format&fit=crop",
image: "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 3
},
@@ -93,7 +93,7 @@ export async function addActivity(prevState: any, formData: FormData) {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") as string,
price: formData.get("price") as string,
price: (formData.get("price") as string) || "",
image: formData.get("image") as string,
badge: formData.get("badge") as string || null,
order: parseInt(formData.get("order") as string || "0", 10),
@@ -118,7 +118,7 @@ export async function updateActivity(prevState: any, formData: FormData) {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") as string,
price: formData.get("price") as string,
price: (formData.get("price") as string) || "",
image: formData.get("image") as string,
badge: formData.get("badge") as string || null,
order: parseInt(formData.get("order") as string || "0", 10),
+43 -7
View File
@@ -2,17 +2,58 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
export type ActionState = { error: string } | null;
export async function login(prevState: ActionState, formData: FormData): Promise<ActionState> {
const username = formData.get("username");
const password = formData.get("password");
const username = formData.get("username") as string;
const password = formData.get("password") as string;
if (!username || !password) {
return { error: "Kullanıcı adı ve şifre gereklidir" };
}
// Fallback to initial .env credentials if no users exist
const userCount = await prisma.user.count();
if (userCount === 0) {
if (
username === process.env.ADMIN_USERNAME &&
password === process.env.ADMIN_PASSWORD
) {
// Create the first admin user
const hashedPassword = await bcrypt.hash(password, 10);
await prisma.user.create({
data: {
username,
password: hashedPassword,
},
});
await setSessionCookie();
redirect("/admin");
} else {
return { error: "Geçersiz kullanıcı adı veya şifre" };
}
} else {
// Normal DB login
const user = await prisma.user.findUnique({
where: { username },
});
if (user && await bcrypt.compare(password, user.password)) {
await setSessionCookie();
redirect("/admin");
} else {
return { error: "Geçersiz kullanıcı adı veya şifre" };
}
}
// This redirect is inside if blocks but typescript requires a return here.
return { error: "Bilinmeyen bir hata oluştu" };
}
async function setSessionCookie() {
const cookieStore = await cookies();
cookieStore.set("admin_session", "true", {
httpOnly: true,
@@ -20,11 +61,6 @@ export async function login(prevState: ActionState, formData: FormData): Promise
maxAge: 60 * 60 * 24 * 7, // 1 week
path: "/",
});
} else {
return { error: "Geçersiz kullanıcı adı veya şifre" };
}
redirect("/admin");
}
export async function logout() {
+2 -2
View File
@@ -12,8 +12,8 @@ export async function loadMockFeatures() {
{ lang: "en", icon: "Anchor", label: "Yoga Platform", sub: "Riverside", order: 2 },
{ lang: "tr", icon: "Utensils", label: "Restoran & Bar", sub: "Ege Mutfağı", order: 3 },
{ lang: "en", icon: "Utensils", label: "Restaurant & Bar", sub: "Aegean Cuisine", order: 3 },
{ lang: "tr", icon: "BedDouble", label: "Butik Odalar", sub: "3 Tip Konaklama", order: 4 },
{ lang: "en", icon: "BedDouble", label: "Boutique Rooms", sub: "3 Types of Stay", order: 4 },
// { lang: "tr", icon: "BedDouble", label: "Butik Odalar", sub: "3 Tip Konaklama", order: 4 },
// { lang: "en", icon: "BedDouble", label: "Boutique Rooms", sub: "3 Types of Stay", order: 4 },
{ lang: "tr", icon: "Sun", label: "Gün Batımı Terası", sub: "Gökova Manzarası", order: 5 },
{ lang: "en", icon: "Sun", label: "Sunset Terrace", sub: "Gokova View", order: 5 },
];
+114
View File
@@ -0,0 +1,114 @@
"use server";
import prisma from "@/lib/prisma";
import bcrypt from "bcryptjs";
import { revalidatePath } from "next/cache";
export type UserActionState = {
success?: boolean;
message?: string;
} | null;
export async function getUsers() {
return await prisma.user.findMany({
select: {
id: true,
username: true,
createdAt: true,
updatedAt: true,
},
orderBy: {
createdAt: 'desc'
}
});
}
export async function createUser(prevState: UserActionState, formData: FormData): Promise<UserActionState> {
const username = formData.get("username") as string;
const password = formData.get("password") as string;
if (!username || !password) {
return { success: false, message: "Kullanıcı adı ve şifre gereklidir." };
}
try {
const existingUser = await prisma.user.findUnique({
where: { username }
});
if (existingUser) {
return { success: false, message: "Bu kullanıcı adı zaten kullanılıyor." };
}
const hashedPassword = await bcrypt.hash(password, 10);
await prisma.user.create({
data: {
username,
password: hashedPassword,
}
});
revalidatePath("/admin/users");
return { success: true, message: "Kullanıcı başarıyla oluşturuldu." };
} catch (error) {
return { success: false, message: "Kullanıcı oluşturulurken bir hata oluştu." };
}
}
export async function updateUser(prevState: UserActionState, formData: FormData): Promise<UserActionState> {
const idStr = formData.get("id") as string;
const username = formData.get("username") as string;
const password = formData.get("password") as string; // Optional during update
if (!idStr || !username) {
return { success: false, message: "Kullanıcı adı gereklidir." };
}
const id = parseInt(idStr, 10);
try {
const existingUser = await prisma.user.findUnique({
where: { username }
});
if (existingUser && existingUser.id !== id) {
return { success: false, message: "Bu kullanıcı adı zaten kullanılıyor." };
}
const dataToUpdate: any = { username };
if (password) {
dataToUpdate.password = await bcrypt.hash(password, 10);
}
await prisma.user.update({
where: { id },
data: dataToUpdate,
});
revalidatePath("/admin/users");
return { success: true, message: "Kullanıcı başarıyla güncellendi." };
} catch (error) {
return { success: false, message: "Kullanıcı güncellenirken bir hata oluştu." };
}
}
export async function deleteUser(id: number) {
try {
const userCount = await prisma.user.count();
if (userCount <= 1) {
return { success: false, message: "Sistemde en az bir yönetici bulunmalıdır. Bu kullanıcıyı silemezsiniz." };
}
await prisma.user.delete({
where: { id }
});
revalidatePath("/admin/users");
return { success: true, message: "Kullanıcı başarıyla silindi." };
} catch (error) {
return { success: false, message: "Kullanıcı silinirken bir hata oluştu." };
}
}
+5 -17
View File
@@ -92,7 +92,7 @@ export default function ActivitiesClient({ activities }: { activities: Activity[
<tr>
<th className="px-8 py-5">Başlık</th>
<th className="px-8 py-5">Dil</th>
<th className="px-8 py-5">Fiyat</th>
<th className="px-8 py-5 text-right">İşlemler</th>
</tr>
</thead>
@@ -103,7 +103,7 @@ export default function ActivitiesClient({ activities }: { activities: Activity[
<td className="px-8 py-5">
<span className="bg-gray-100 text-gray-600 px-3 py-1 rounded-full text-[10px] uppercase font-black tracking-widest border border-gray-200">{act.lang}</span>
</td>
<td className="px-8 py-5 font-medium">{act.price}</td>
<td className="px-8 py-5 text-right">
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@@ -132,10 +132,10 @@ export default function ActivitiesClient({ activities }: { activities: Activity[
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-10 md:pt-16">
<div className="absolute inset-0 bg-dark/60 backdrop-blur-sm" onClick={closeModal} />
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl relative z-10 animate-fade-up overflow-hidden max-h-[90vh] flex flex-col">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl relative z-10 animate-fade-up overflow-hidden max-h-[85vh] flex flex-col">
<div className="flex justify-between items-center p-6 md:p-8 border-b border-gray-100 bg-gray-50/50">
<h2 className="text-2xl font-bold font-serif text-dark">
{editingActivity ? "Aktivite Düzenle" : "Yeni Aktivite Ekle"}
@@ -145,7 +145,7 @@ export default function ActivitiesClient({ activities }: { activities: Activity[
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<div className="p-6 md:p-8 overflow-y-auto flex-1 min-h-0">
<form action={handleSubmit} className="space-y-6">
{error && (
@@ -189,18 +189,6 @@ export default function ActivitiesClient({ activities }: { activities: Activity[
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Fiyat (Metin)</label>
<input
type="text"
name="price"
defaultValue={editingActivity?.price || ""}
required
className="w-full px-5 py-3.5 bg-gray-50 border border-gray-200 text-dark rounded-xl focus:ring-4 focus:ring-primary/20 focus:border-primary outline-none transition-all hover:border-gray-300"
placeholder="Örn: ₺2.000/Saat"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Rozet (Opsiyonel)</label>
<input
+2 -1
View File
@@ -2,7 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LayoutDashboard, Settings, Image as ImageIcon, Compass, CalendarDays, Star, Info } from "lucide-react";
import { LayoutDashboard, Settings, Image as ImageIcon, Compass, CalendarDays, Star, Info, Users } from "lucide-react";
export default function SidebarNav() {
const pathname = usePathname();
@@ -15,6 +15,7 @@ export default function SidebarNav() {
{ name: "Özellikler", href: "/admin/features", icon: Star },
{ name: "Aktiviteler", href: "/admin/activities", icon: Compass },
{ name: "Etkinlikler", href: "/admin/events", icon: CalendarDays },
{ name: "Kullanıcılar", href: "/admin/users", icon: Users },
];
return (
+3 -3
View File
@@ -132,10 +132,10 @@ export default function EventsClient({ events }: { events: Event[] }) {
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 pt-10 md:pt-16">
<div className="absolute inset-0 bg-dark/60 backdrop-blur-sm" onClick={closeModal} />
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl relative z-10 animate-fade-up overflow-hidden max-h-[90vh] flex flex-col">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-2xl relative z-10 animate-fade-up overflow-hidden max-h-[85vh] flex flex-col">
<div className="flex justify-between items-center p-6 md:p-8 border-b border-gray-100 bg-gray-50/50">
<h2 className="text-2xl font-bold font-serif text-dark">
{editingEvent ? "Etkinlik Düzenle" : "Yeni Etkinlik Ekle"}
@@ -145,7 +145,7 @@ export default function EventsClient({ events }: { events: Event[] }) {
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<div className="p-6 md:p-8 overflow-y-auto flex-1 min-h-0">
<form action={handleSubmit} className="space-y-6">
{error && (
+3 -3
View File
@@ -16,7 +16,7 @@ export default function HeroForm({ trData, enData }: { trData: HeroSection | nul
welcome: "Kite Beach'e Hoş Geldiniz",
title: "Rüzgarı Hisset, Özgürlüğe Uç",
subtitle: "Türkiye'nin en iyi kitesurf noktasında unutulmaz bir deneyim yaşayın. Profesyonel eğitmenler, mükemmel rüzgar ve eşsiz doğa sizi bekliyor.",
bookNow: "Hemen Rezervasyon Yap",
bookNow: "Bize Ulaşın",
exploreActivities: "Aktiviteleri Keşfet",
scroll: "Aşağı Kaydır"
},
@@ -26,7 +26,7 @@ export default function HeroForm({ trData, enData }: { trData: HeroSection | nul
welcome: "Welcome to Kite Beach",
title: "Feel the Wind, Fly to Freedom",
subtitle: "Experience an unforgettable adventure at Turkey's best kitesurfing spot. Professional instructors, perfect wind, and unique nature await you.",
bookNow: "Book Now",
bookNow: "Contact Us",
exploreActivities: "Explore Activities",
scroll: "Scroll Down"
}
@@ -112,7 +112,7 @@ export default function HeroForm({ trData, enData }: { trData: HeroSection | nul
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rezervasyon Butonu (Book Now)</label>
<label className="block text-sm font-medium text-gray-700 mb-1">İletişim Butonu (Contact Us)</label>
<input
type="text"
name="bookNow"
+201
View File
@@ -0,0 +1,201 @@
"use client";
import { useState, useActionState, useEffect } from "react";
import { createUser, updateUser, deleteUser, UserActionState } from "@/app/actions/users";
import { User, UserPlus, Edit2, Trash2, X, Save, ShieldAlert } from "lucide-react";
type UserType = {
id: number;
username: string;
createdAt: Date;
updatedAt: Date;
};
export default function UsersClient({ initialUsers }: { initialUsers: UserType[] }) {
const [users, setUsers] = useState<UserType[]>(initialUsers);
const [isEditing, setIsEditing] = useState(false);
const [editingUser, setEditingUser] = useState<UserType | null>(null);
// Update state when initialUsers change
useEffect(() => {
setUsers(initialUsers);
}, [initialUsers]);
const [createState, createAction, isCreating] = useActionState<UserActionState, FormData>(
createUser,
null
);
const [updateState, updateAction, isUpdating] = useActionState<UserActionState, FormData>(
updateUser,
null
);
const handleDelete = async (id: number) => {
if (confirm("Bu kullanıcıyı silmek istediğinize emin misiniz?")) {
const result = await deleteUser(id);
if (result?.success) {
setUsers(users.filter(u => u.id !== id));
} else {
alert(result?.message || "Bir hata oluştu.");
}
}
};
const handleEditClick = (user: UserType) => {
setEditingUser(user);
setIsEditing(true);
};
const handleAddNew = () => {
setEditingUser(null);
setIsEditing(true);
};
const handleCancel = () => {
setIsEditing(false);
setEditingUser(null);
};
// Check state to close modal on success
useEffect(() => {
if (createState?.success || updateState?.success) {
setIsEditing(false);
setEditingUser(null);
}
}, [createState, updateState]);
return (
<div className="space-y-6">
{/* List Section */}
{!isEditing && (
<div className="bg-white rounded-2xl p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-primary/10 relative overflow-hidden group">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-dark flex items-center gap-2">
<User className="w-5 h-5 text-accent" /> Yönetici Hesapları
</h2>
<button
onClick={handleAddNew}
className="flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent/90 text-white rounded-xl transition-all font-medium text-sm shadow-md"
>
<UserPlus className="w-4 h-4" /> Yeni Kullanıcı
</button>
</div>
{(createState?.message || updateState?.message) && (
<div className={`p-4 mb-6 rounded-xl text-sm ${createState?.success || updateState?.success ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
{createState?.message || updateState?.message}
</div>
)}
<div className="space-y-4">
{users.map((user) => (
<div key={user.id} className="flex items-center justify-between p-4 rounded-xl border border-primary/5 hover:border-primary/20 transition-all bg-gray-50/50">
<div className="flex flex-col">
<span className="font-semibold text-dark">{user.username}</span>
<span className="text-xs text-gray-500">
Oluşturulma: {new Date(user.createdAt).toLocaleDateString('tr-TR')}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleEditClick(user)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Düzenle"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(user.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
disabled={users.length <= 1}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
{users.length === 0 && (
<p className="text-center text-gray-500 py-4">Kullanıcı bulunamadı.</p>
)}
</div>
</div>
)}
{/* Form Section */}
{isEditing && (
<div className="bg-white rounded-2xl p-6 shadow-[0_8px_30px_rgb(0,0,0,0.04)] border border-primary/10 relative overflow-hidden">
<div className="flex justify-between items-center mb-6 border-b border-primary/10 pb-4">
<h2 className="text-xl font-bold text-dark flex items-center gap-2">
{editingUser ? <Edit2 className="w-5 h-5 text-accent" /> : <UserPlus className="w-5 h-5 text-accent" />}
{editingUser ? "Kullanıcıyı Düzenle" : "Yeni Kullanıcı Ekle"}
</h2>
<button
onClick={handleCancel}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<form action={editingUser ? updateAction : createAction} className="space-y-4">
{editingUser && <input type="hidden" name="id" value={editingUser.id} />}
<div className="space-y-2">
<label className="block text-sm font-medium text-dark">Kullanıcı Adı</label>
<input
type="text"
name="username"
defaultValue={editingUser?.username || ""}
required
className="w-full px-4 py-2 bg-gray-50 border border-primary/10 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/50 text-dark"
placeholder="Örn: admin"
/>
</div>
<div className="space-y-2">
<label className="block text-sm font-medium text-dark">Şifre</label>
<input
type="password"
name="password"
required={!editingUser}
className="w-full px-4 py-2 bg-gray-50 border border-primary/10 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent/50 text-dark"
placeholder={editingUser ? "Değiştirmek için yeni şifre girin (İsteğe bağlı)" : "Güçlü bir şifre girin"}
/>
{editingUser && (
<p className="text-xs text-gray-500 mt-1 flex items-center gap-1">
<ShieldAlert className="w-3 h-3" /> Şifreyi değiştirmek istemiyorsanız boş bırakın.
</p>
)}
</div>
{(createState?.message || updateState?.message) && (
<div className={`p-4 rounded-xl text-sm ${createState?.success || updateState?.success ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>
{createState?.message || updateState?.message}
</div>
)}
<div className="flex gap-3 pt-4 border-t border-primary/10">
<button
type="button"
onClick={handleCancel}
className="px-6 py-2 border border-primary/20 text-dark hover:bg-gray-50 rounded-xl transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isCreating || isUpdating}
className="flex-1 flex items-center justify-center gap-2 px-6 py-2 bg-accent hover:bg-accent/90 text-white rounded-xl transition-all font-medium disabled:opacity-50"
>
<Save className="w-4 h-4" />
{isCreating || isUpdating ? "Kaydediliyor..." : "Kaydet"}
</button>
</div>
</form>
</div>
)}
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { getUsers } from "@/app/actions/users";
import UsersClient from "./UsersClient";
export default async function UsersPage() {
const users = await getUsers();
return (
<div className="max-w-4xl mx-auto w-full">
<h1 className="text-3xl font-bold font-serif mb-6 text-dark">Kullanıcı Yönetimi</h1>
<UsersClient initialUsers={users} />
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
export const dynamic = 'force-static';
export function GET() {
const body = `# Kite Beach Akyaka
> Kite Beach Akyaka is a boutique kitesurf hotel and beach club located on the banks of the Azmak River in Akyaka, Muğla, Turkey. It offers kitesurf lessons (IKO-certified), accommodation, an Aegean restaurant & bar, and various outdoor activities.
- Turkish site: https://kitebeachakyaka.com.tr/tr
- English site: https://kitebeachakyaka.com.tr/en
## Key Information
- **Location**: Akyaka Mahallesi, Akçapınar Mevkii, Akyaka, Muğla, Turkey
- **Coordinates**: 37.0543° N, 28.3286° E
- **Phone / WhatsApp**: +90 544 504 03 10
- **Email**: info@kitebeachakyaka.com.tr
- **Reception hours**: 08:00 22:00 daily
- **Type**: Boutique hotel, kitesurf school, beach club, restaurant & bar
## Site Sections
- Home / Ana Sayfa: https://kitebeachakyaka.com.tr/tr
- Activities / Aktiviteler: https://kitebeachakyaka.com.tr/tr/aktiviteler
- Events / Etkinlikler: https://kitebeachakyaka.com.tr/tr/etkinlikler
- Restaurant & Bar / Restoran: https://kitebeachakyaka.com.tr/tr/restoran
- About / Hakkımızda: https://kitebeachakyaka.com.tr/tr/hakkimizda
- Gallery / Galeri: https://kitebeachakyaka.com.tr/tr/galeri
- Contact / İletişim: https://kitebeachakyaka.com.tr/tr/iletisim
## Services
- **Kitesurf school**: IKO-certified instructors, beginner to advanced lessons, equipment rental
- **Accommodation**: Boutique rooms with river and sea views
- **Restaurant**: Aegean cuisine, breakfast spreads, mezze, fresh seafood, sunset cocktails
- **Activities**: Stand-up paddleboarding, cycling, boat tours, photography tours
- **Events**: Concerts (Mahmut Orhan), kitesurf festivals, sunset parties, private events
## About
Kite Beach Akyaka was founded with the mission of combining kitesurf passion with nature, comfort, and local flavors on the banks of the Azmak River — where the river meets the Gökova Bay. The property emphasises sustainability, zero-waste practices, and sourcing ingredients from local Aegean producers.
- 10+ years of experience
- 500+ happy guests
- 3 IKO-certified instructors
- Supports sustainable tourism in Akyaka
## Languages
This site is fully bilingual: Turkish (default) and English.
`;
return new Response(body, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, max-age=86400',
},
});
}
+16
View File
@@ -0,0 +1,16 @@
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Kite Beach Akyaka",
short_name: "Kite Beach",
description: "Nehrin huzuru, denizin enerjisi — tek bir noktada.",
start_url: "/tr",
display: "standalone",
background_color: "#0E1A24",
theme_color: "#0D3D50",
icons: [
{ src: "/logo.png", sizes: "any", type: "image/png" },
],
};
}
+30
View File
@@ -0,0 +1,30 @@
import type { MetadataRoute } from 'next';
const PROTECTED = ['/admin/', '/api/'];
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: PROTECTED,
},
// AI arama / citation botları
{ userAgent: 'OAI-SearchBot', allow: '/', disallow: PROTECTED },
{ userAgent: 'ChatGPT-User', allow: '/', disallow: PROTECTED },
{ userAgent: 'PerplexityBot', allow: '/', disallow: PROTECTED },
{ userAgent: 'Perplexity-User', allow: '/', disallow: PROTECTED },
{ userAgent: 'Claude-SearchBot', allow: '/', disallow: PROTECTED },
{ userAgent: 'Claude-User', allow: '/', disallow: PROTECTED },
// Training crawler'ları engelle
{ userAgent: 'GPTBot', disallow: '/' },
{ userAgent: 'ClaudeBot', disallow: '/' },
{ userAgent: 'Google-Extended', disallow: '/' },
{ userAgent: 'CCBot', disallow: '/' },
{ userAgent: 'Bytespider', disallow: '/' }, // ByteDance/TikTok
{ userAgent: 'Meta-ExternalAgent', disallow: '/' }, // Meta training
],
sitemap: 'https://kitebeachakyaka.com.tr/sitemap.xml',
};
}
+34
View File
@@ -0,0 +1,34 @@
import { MetadataRoute } from 'next';
const baseUrl = 'https://kitebeachakyaka.com.tr';
const langs = ['tr', 'en'];
const routes = [
{ path: '', changeFrequency: 'daily' as const, priority: 1.0 },
{ path: '/odalar', changeFrequency: 'weekly' as const, priority: 0.9 },
{ path: '/aktiviteler', changeFrequency: 'weekly' as const, priority: 0.9 },
{ path: '/etkinlikler', changeFrequency: 'weekly' as const, priority: 0.9 },
{ path: '/restoran', changeFrequency: 'weekly' as const, priority: 0.8 },
{ path: '/iletisim', changeFrequency: 'monthly' as const, priority: 0.8 },
{ path: '/hakkimizda', changeFrequency: 'monthly' as const, priority: 0.7 },
{ path: '/galeri', changeFrequency: 'monthly' as const, priority: 0.7 },
{ path: '/gizlilik', changeFrequency: 'monthly' as const, priority: 0.3 },
{ path: '/kvkk', changeFrequency: 'monthly' as const, priority: 0.3 },
{ path: '/cerez', changeFrequency: 'monthly' as const, priority: 0.3 },
];
export default function sitemap(): MetadataRoute.Sitemap {
return langs.flatMap((lang) =>
routes.map(({ path, changeFrequency, priority }) => ({
url: `${baseUrl}/${lang}${path}`,
lastModified: new Date(),
changeFrequency,
priority,
alternates: {
languages: {
'x-default': `${baseUrl}/tr${path}`,
...Object.fromEntries(langs.map((l) => [l, `${baseUrl}/${l}${path}`])),
},
},
}))
);
}
+42
View File
@@ -0,0 +1,42 @@
import 'dotenv/config';
import prisma from './lib/prisma';
import bcrypt from 'bcryptjs';
async function main() {
const users = await prisma.user.findMany();
console.log("Users in DB:", users.length);
for (const u of users) {
console.log(`- ID: ${u.id}, Username: ${u.username}`);
}
if (users.length > 0) {
// Force reset the password of the first user to what's in .env just in case
const username = process.env.ADMIN_USERNAME || "admin";
const password = process.env.ADMIN_PASSWORD || "secretpassword";
console.log(`Updating password for user ${username} to ${password}...`);
const hashedPassword = await bcrypt.hash(password, 10);
// Check if the exact user exists
const exactUser = await prisma.user.findUnique({ where: { username } });
if (exactUser) {
await prisma.user.update({
where: { username },
data: { password: hashedPassword }
});
console.log(`Password reset successfully for ${username}.`);
} else {
console.log(`No exact user found with username ${username}. Updating first user.`);
await prisma.user.update({
where: { id: users[0].id },
data: { username: username, password: hashedPassword }
});
console.log(`Updated first user to be ${username} with new password.`);
}
} else {
console.log("No users exist in DB. The auth code will create one automatically.");
}
}
main()
.catch(e => console.error(e))
.finally(() => prisma.$disconnect());
+144
View File
@@ -0,0 +1,144 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { X } from "lucide-react";
interface BentoGalleryProps {
images: string[];
}
export default function BentoGallery({ images }: BentoGalleryProps) {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
const openLightbox = (index: number) => {
setLightboxIndex(index);
};
const closeLightbox = () => {
setLightboxIndex(null);
};
return (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 auto-rows-[200px] md:auto-rows-[250px]">
{/* Big Feature Image */}
{images.length > 0 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-2 row-span-2 cursor-pointer h-full"
onClick={() => openLightbox(0)}
>
<Image
src={images[0]}
alt="Gallery Image 1"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
{/* Top Right */}
{images.length > 1 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-2 row-span-1 cursor-pointer h-full"
onClick={() => openLightbox(1)}
>
<Image
src={images[1]}
alt="Gallery Image 2"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
{/* Middle Right 1 */}
{images.length > 2 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-1 row-span-1 cursor-pointer h-full"
onClick={() => openLightbox(2)}
>
<Image
src={images[2]}
alt="Gallery Image 3"
fill
sizes="(max-width: 768px) 50vw, 25vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
{/* Middle Right 2 */}
{images.length > 3 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-1 row-span-1 cursor-pointer h-full"
onClick={() => openLightbox(3)}
>
<Image
src={images[3]}
alt="Gallery Image 4"
fill
sizes="(max-width: 768px) 50vw, 25vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
{/* Bottom Row 1 */}
{images.length > 4 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-2 md:col-span-3 row-span-1 cursor-pointer h-full"
onClick={() => openLightbox(4)}
>
<Image
src={images[4]}
alt="Gallery Image 5"
fill
sizes="(max-width: 768px) 100vw, 75vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
{/* Bottom Row 2 */}
{images.length > 5 && (
<div
className="relative rounded-3xl overflow-hidden group col-span-2 md:col-span-1 row-span-1 cursor-pointer h-full"
onClick={() => openLightbox(5)}
>
<Image
src={images[5]}
alt="Gallery Image 6"
fill
sizes="(max-width: 768px) 100vw, 25vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
</div>
)}
</div>
{/* Lightbox */}
{lightboxIndex !== null && (
<div
className="fixed inset-0 z-[100] bg-dark/95 backdrop-blur-sm flex items-center justify-center p-4 cursor-zoom-out opacity-0 animate-fade-in"
style={{ animationFillMode: "forwards" }}
onClick={closeLightbox}
>
<div className="relative max-w-6xl w-full h-[85vh] aspect-auto" onClick={(e) => e.stopPropagation()}>
<Image
src={images[lightboxIndex]}
alt="Lightbox Image"
fill
sizes="100vw"
className="object-contain rounded-xl shadow-2xl"
/>
</div>
<button
className="absolute top-6 right-6 text-white/60 hover:text-white transition-colors bg-white/10 hover:bg-white/20 p-2 rounded-full"
onClick={closeLightbox}
>
<X size={28} />
</button>
</div>
)}
</>
);
}
+68 -32
View File
@@ -2,22 +2,12 @@ import Link from "next/link";
import Image from "next/image";
import { MapPin, Phone, Mail } from "lucide-react";
const quickLinks = [
{ name: "Ana Sayfa", href: "/" },
{ name: "Odalar", href: "/odalar" },
{ name: "Aktiviteler", href: "/aktiviteler"},
{ name: "Restoran", href: "/restoran" },
{ name: "Etkinlikler", href: "/etkinlikler"},
{ name: "Galeri", href: "/galeri" },
{ name: "Hakkımızda", href: "/hakkimizda" },
];
function InstagramIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="20" height="20" x="2" y="2" rx="5" ry="5"/>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/>
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/>
<rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
</svg>
);
}
@@ -25,7 +15,7 @@ function InstagramIcon() {
function FacebookIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
</svg>
);
}
@@ -33,13 +23,32 @@ function FacebookIcon() {
function YoutubeIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"/>
<path d="m10 15 5-3-5-3z"/>
<path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17" />
<path d="m10 15 5-3-5-3z" />
</svg>
);
}
export default function Footer() {
export default function Footer({ lang = "tr" }: { lang?: string }) {
const isEn = lang === "en";
const quickLinks = isEn ? [
{ name: "Home", href: "/en" },
{ name: "Rooms", href: "/en/odalar" },
{ name: "Activities", href: "/en/aktiviteler" },
{ name: "Restaurant", href: "/en/restoran" },
{ name: "Events", href: "/en/etkinlikler" },
{ name: "Gallery", href: "/en/galeri" },
{ name: "About Us", href: "/en/hakkimizda" },
] : [
{ name: "Ana Sayfa", href: "/tr" },
{ name: "Odalar", href: "/tr/odalar" },
{ name: "Aktiviteler", href: "/tr/aktiviteler" },
{ name: "Restoran", href: "/tr/restoran" },
{ name: "Etkinlikler", href: "/tr/etkinlikler" },
{ name: "Galeri", href: "/tr/galeri" },
{ name: "Hakkımızda", href: "/tr/hakkimizda" },
];
return (
<footer className="bg-dark text-white">
{/* Top accent line */}
@@ -50,7 +59,7 @@ export default function Footer() {
{/* Brand */}
<div className="md:col-span-4">
<Link href="/" className="inline-block mb-5">
<Link href={isEn ? "/en" : "/tr"} className="inline-block mb-5">
<Image
src="/logo.png"
alt="Kite Beach Logo"
@@ -60,7 +69,9 @@ export default function Footer() {
/>
</Link>
<p className="text-white/40 text-[13px] leading-relaxed mb-7 max-w-xs">
Nehrin huzuru, denizin enerjisi tek bir noktada. Akyaka'nın kalbinde butik bir kaçış noktası.
{isEn
? "The tranquility of the river, the energy of the sea — in one place. A boutique escape in the heart of Akyaka."
: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Akyaka'nın kalbinde butik bir kaçış noktası."}
</p>
{/* Social icons */}
<div className="flex gap-3">
@@ -84,7 +95,7 @@ export default function Footer() {
{/* Quick Links */}
<div className="md:col-span-3">
<h3 className="text-[11px] font-bold tracking-[0.25em] text-white/30 uppercase mb-6">
Sayfalar
{isEn ? "Pages" : "Sayfalar"}
</h3>
<ul className="space-y-3">
{quickLinks.map((link) => (
@@ -103,7 +114,7 @@ export default function Footer() {
{/* Contact */}
<div className="md:col-span-5">
<h3 className="text-[11px] font-bold tracking-[0.25em] text-white/30 uppercase mb-6">
İletişim
{isEn ? "Contact" : letişim"}
</h3>
<ul className="space-y-5">
<li className="flex items-start gap-3">
@@ -111,7 +122,11 @@ export default function Footer() {
<MapPin size={13} className="text-accent" />
</div>
<span className="text-white/50 text-[13px] leading-relaxed">
Akyaka Mahallesi, Akçapınar Mevkii<br />Muğla, Türkiye
{isEn ? (
<>Akyaka District, Akcapinar Location<br />Mugla, Turkey</>
) : (
<>Akyaka Mahallesi, Akçapınar Mevkii<br />Muğla, Türkiye</>
)}
</span>
</li>
<li className="flex items-center gap-3">
@@ -119,10 +134,10 @@ export default function Footer() {
<Phone size={13} className="text-accent" />
</div>
<a
href="tel:+905330816181"
href="tel:+905445040310"
className="text-white/50 hover:text-white transition-colors text-[13px]"
>
+90 533 081 6181
+90 544 504 03 10
</a>
</li>
<li className="flex items-center gap-3">
@@ -142,25 +157,46 @@ export default function Footer() {
</div>
{/* Bottom bar */}
<div className="border-t border-white/8 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-white/25 text-xs">
© 2026 Kite Beach Akyaka. Tüm hakları saklıdır.
<div className="border-t border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-white/40">
<p>
{isEn ? `© ${new Date().getFullYear()} Kite Beach Akyaka. All rights reserved.` : `© ${new Date().getFullYear()} Kite Beach Akyaka. Tüm hakları saklıdır.`}
</p>
<div className="flex gap-6">
<div className="flex flex-wrap items-center justify-center gap-6">
{[
{ name: "KVKK", href: "/kvkk" },
{ name: "Gizlilik Politikası", href: "/gizlilik" },
{ name: "Çerez Politikası", href: "/cerez" }
{ name: isEn ? "Privacy Policy" : "Gizlilik Politikası", href: "/gizlilik" },
{ name: isEn ? "Cookie Policy" : "Çerez Politikası", href: "/cerez" }
].map((item) => (
<Link
key={item.name}
href={item.href}
className="text-white/25 hover:text-white/60 transition-colors text-xs"
href={isEn ? `/en${item.href}` : `/tr${item.href}`}
className="hover:text-white/80 transition-colors"
>
{item.name}
</Link>
))}
</div>
{/* Görünür Ayris Tech İmzası & Google Bot İndeksleme Katmanı */}
<div className="flex items-center gap-1.5">
<span className="opacity-75">created by</span>
<a
href="https://ayris.tech"
target="_blank"
rel="noopener"
className="font-medium text-white/70 hover:text-white transition-colors duration-200 hover:underline underline-offset-4"
>
ayristech
</a>
{/* Google Bot & Arama Motoru İndeksleme Katmanı (Görünmez / Screen-Reader Safe) */}
<span className="sr-only">
Legal AI & Solutions:
<a href="https://ayris.legal" rel="noopener">AyrisLegal</a>,
<a href="https://ayrislegal.com" rel="noopener">Ayris Legal AI</a>
</span>
</div>
</div>
</div>
</footer>
+1
View File
@@ -12,6 +12,7 @@ export default function HeroVideo() {
src="https://img.youtube.com/vi/V9AhK54xsxY/maxresdefault.jpg"
alt="Kite Beach Akyaka Video Thumbnail"
fill
sizes="100vw"
className={`object-cover transition-opacity duration-1000 ease-in-out ${
isVideoLoaded ? "opacity-0" : "opacity-80"
}`}
+64 -27
View File
@@ -5,19 +5,27 @@ import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Menu, X, Globe } from "lucide-react";
const navLinks = [
// { name: "Odalar", href: "/odalar" },
{ name: "Aktiviteler",href: "/aktiviteler"},
{ name: "Restoran", href: "/restoran" },
{ name: "Etkinlikler",href: "/etkinlikler"},
{ name: "Galeri", href: "/galeri" },
{ name: "Hakkımızda", href: "/hakkimizda" },
{ name: "İletişim", href: "/iletisim" },
const navLinksEn = [
{ name: "Rooms", href: "/en/odalar" },
{ name: "Activities", href: "/en/aktiviteler" },
{ name: "Restaurant", href: "/en/restoran" },
{ name: "Events", href: "/en/etkinlikler" },
{ name: "About Us", href: "/en/hakkimizda" },
{ name: "Contact", href: "/en/iletisim" },
];
export default function Navbar() {
const navLinksTr = [
{ name: "Odalar", href: "/tr/odalar" },
{ name: "Aktiviteler", href: "/tr/aktiviteler" },
{ name: "Restoran", href: "/tr/restoran" },
{ name: "Etkinlikler", href: "/tr/etkinlikler" },
{ name: "Hakkımızda", href: "/tr/hakkimizda" },
{ name: "İletişim", href: "/tr/iletisim" },
];
export default function Navbar({ lang }: { lang?: string }) {
const [isOpen, setIsOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const pathname = usePathname();
@@ -30,12 +38,24 @@ export default function Navbar() {
useEffect(() => { setIsOpen(false); }, [pathname]);
const isHome = pathname === "/";
const currentLang = lang || (pathname.startsWith("/en") ? "en" : "tr");
const toggleLang = currentLang === "en" ? "tr" : "en";
const navLinks = currentLang === "en" ? navLinksEn : navLinksTr;
const isHome = pathname === "/" || pathname === "/tr" || pathname === "/en";
const getToggleHref = () => {
if (pathname === "/") return `/${toggleLang}`;
if (pathname.startsWith("/tr/")) return pathname.replace("/tr/", `/${toggleLang}/`);
if (pathname === "/tr") return `/${toggleLang}`;
if (pathname.startsWith("/en/")) return pathname.replace("/en/", `/${toggleLang}/`);
if (pathname === "/en") return `/${toggleLang}`;
return `/${toggleLang}${pathname}`;
};
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
scrolled
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${scrolled
? "bg-white/95 backdrop-blur-md shadow-sm py-4"
: "bg-transparent py-6"
}`}
@@ -44,13 +64,13 @@ export default function Navbar() {
<div className="flex justify-between items-center">
{/* Logo */}
<Link href="/" className="flex-shrink-0 flex items-center group">
<Link href={`/${currentLang}`} className="flex-shrink-0 flex items-center group">
<Image
src="/logo.png"
alt="Kite Beach Logo"
width={140}
height={48}
className={`object-contain transition-all duration-300 ${!scrolled && isHome ? 'brightness-0 invert' : ''}`}
className={`w-auto h-auto object-contain transition-all duration-300 ${!scrolled && isHome ? 'brightness-0 invert' : ''}`}
priority
/>
</Link>
@@ -63,8 +83,7 @@ export default function Navbar() {
<Link
key={link.name}
href={link.href}
className={`text-[13px] font-medium transition-colors relative group ${
scrolled
className={`text-[13px] font-medium transition-colors relative group ${scrolled
? active ? "text-primary" : "text-foreground hover:text-primary"
: isHome
? active ? "text-accent" : "text-white/85 hover:text-white"
@@ -73,8 +92,7 @@ export default function Navbar() {
>
{link.name}
<span
className={`absolute -bottom-0.5 left-0 h-px bg-current transition-all duration-300 ${
active ? "w-full" : "w-0 group-hover:w-full"
className={`absolute -bottom-0.5 left-0 h-px bg-current transition-all duration-300 ${active ? "w-full" : "w-0 group-hover:w-full"
}`}
/>
</Link>
@@ -82,18 +100,31 @@ export default function Navbar() {
})}
<Link
href="/iletisim"
href={`/${currentLang}/iletisim`}
className="btn-shimmer bg-accent text-white px-6 py-2.5 rounded-full text-[11px] font-bold tracking-[0.12em] uppercase hover:bg-accent/90 transition-colors shadow-md"
>
Rezervasyon
{currentLang === "en" ? "Contact Us" : "Bize Ulaşın"}
</Link>
{/* Lang Toggle */}
<Link
href={getToggleHref()}
className={`flex items-center gap-1.5 text-[11px] font-bold tracking-[0.12em] uppercase transition-colors hover:text-accent ${scrolled
? "text-dark"
: isHome
? "text-white"
: "text-dark"
}`}
>
<Globe size={14} />
<span>{currentLang === "tr" ? "EN" : "TR"}</span>
</Link>
</div>
{/* Mobile toggle */}
<button
onClick={() => setIsOpen(!isOpen)}
className={`flex md:hidden p-1 transition-colors ${
scrolled ? "text-dark" : isHome ? "text-white" : "text-dark"
className={`flex md:hidden p-1 transition-colors ${scrolled ? "text-dark" : isHome ? "text-white" : "text-dark"
}`}
aria-label="Menü"
>
@@ -123,8 +154,7 @@ export default function Navbar() {
>
<Link
href={link.href}
className={`block py-3 text-base font-medium border-b border-sand/60 transition-colors ${
pathname === link.href ? "text-primary" : "text-dark hover:text-primary"
className={`block py-3 text-base font-medium border-b border-sand/60 transition-colors ${pathname === link.href ? "text-primary" : "text-dark hover:text-primary"
}`}
>
{link.name}
@@ -132,10 +162,17 @@ export default function Navbar() {
</motion.div>
))}
<Link
href="/iletisim"
href={getToggleHref()}
className="mt-2 flex items-center justify-center gap-2 text-sm font-bold uppercase text-dark border border-sand py-3 rounded-full hover:bg-sand/20 transition-colors"
>
<Globe size={16} />
{currentLang === "tr" ? "English" : "Türkçe"}
</Link>
<Link
href={`/${currentLang}/iletisim`}
className="mt-4 block w-full text-center bg-accent text-white px-6 py-3.5 rounded-full text-sm font-bold uppercase tracking-wider"
>
Rezervasyon Yap
{currentLang === "en" ? "Contact Us" : "Bize Ulaşın"}
</Link>
</div>
</motion.div>
+1 -1
View File
@@ -6,7 +6,7 @@ import { motion } from "framer-motion";
export default function WhatsAppButton() {
return (
<motion.a
href="https://wa.me/905330816181"
href="https://wa.me/905445040310"
target="_blank"
rel="noopener noreferrer"
className="fixed bottom-6 right-6 z-50 flex items-center justify-center w-14 h-14 bg-green-500 text-white rounded-full shadow-lg hover:shadow-2xl hover:bg-green-600 transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
+90
View File
@@ -0,0 +1,90 @@
import Image from "next/image";
import Link from "next/link";
import { Star, ArrowRight } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
interface AboutSectionProps {
aboutData: {
image: string;
badge: string;
statsGuestCount: string;
statsGuestLabel: string;
statsYearCount: string;
statsYearLabel: string;
badgeIkoTitle: string;
badgeIkoLabel: string;
titleLine1: string;
titleLine2: string;
titleHighlight: string;
description1: string;
description2: string;
};
lang: string;
}
export default function AboutSection({ aboutData, lang }: AboutSectionProps) {
return (
<section className="py-28 bg-background overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal className="relative">
<div className="relative h-[540px] lg:h-[660px] rounded-3xl overflow-hidden shadow-2xl">
<Image
src="/kitesurf.png"
alt={aboutData.badge}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
</div>
{/* Stats card */}
<div className="absolute -bottom-5 -right-4 md:-right-8 bg-white rounded-2xl p-6 shadow-xl border border-sand">
<div className="grid grid-cols-2 gap-6 divide-x divide-sand">
<div className="text-center">
<span className="block font-serif text-3xl font-bold text-primary">{aboutData.statsGuestCount}</span>
<span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsGuestLabel}</span>
</div>
<div className="text-center pl-6">
<span className="block font-serif text-3xl font-bold text-primary">{aboutData.statsYearCount}</span>
<span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsYearLabel}</span>
</div>
</div>
</div>
{/* IKO badge */}
<div className="absolute -top-4 -left-4 bg-accent text-white rounded-2xl p-4 shadow-lg">
<Star className="w-4 h-4 mb-1" fill="white" />
<span className="block text-xs font-bold leading-none">{aboutData.badgeIkoTitle}</span>
<span className="block text-[10px] opacity-80 mt-0.5">{aboutData.badgeIkoLabel}</span>
</div>
</ScrollReveal>
<ScrollReveal delay={200}>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
{aboutData.badge}
</span>
<h2 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold text-dark leading-[1.05] mb-8">
{aboutData.titleLine1}<br />{aboutData.titleLine2}<br />
<span className="text-primary italic">{aboutData.titleHighlight}</span>
</h2>
<p className="text-gray-600 leading-relaxed mb-5">
{aboutData.description1}
</p>
<p className="text-gray-600 leading-relaxed mb-10">
{aboutData.description2}
</p>
<div className="section-line mb-8" />
<Link
href={`/${lang}/hakkimizda`}
className="inline-flex items-center gap-2 text-dark font-bold text-sm tracking-wide group"
>
<span className="border-b-2 border-accent pb-0.5">
{lang === "en" ? "Read Our Story" : "Hikayemizi Okuyun"}
</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1.5 transition-transform" />
</Link>
</ScrollReveal>
</div>
</div>
</section>
);
}
+81
View File
@@ -0,0 +1,81 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
interface Activity {
id: number | string;
image: string;
title: string;
description: string;
badge?: string | null;
}
interface ActivitiesGridProps {
activities: Activity[];
lang: string;
}
export default function ActivitiesGrid({ activities, lang }: ActivitiesGridProps) {
return (
<section className="py-28 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-10">
<div>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
{lang === "en" ? "What Will You Do?" : "Ne Yapacaksınız?"}
</span>
<h2 className="font-serif text-5xl font-bold text-dark">
{lang === "en" ? "Activities" : "Aktiviteler"}
</h2>
</div>
<Link
href={`/${lang}/aktiviteler`}
className="inline-flex items-center gap-2 text-sm font-semibold text-primary hover:text-accent transition-colors"
>
{lang === "en" ? "See All" : "Tümünü Gör"} <ArrowRight className="w-4 h-4" />
</Link>
</ScrollReveal>
{/* Single Row 3 Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{activities.length > 0 ? (
activities.map((act, i) => (
<ScrollReveal
key={act.id}
delay={i * 50}
>
<Link href={`/${lang}/aktiviteler`} className={`group relative block rounded-3xl overflow-hidden h-[400px]`}>
<Image
src={act.image}
alt={act.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className={`absolute inset-0 bg-gradient-to-t from-dark/90 via-dark/30 to-transparent`} />
<div className={`absolute bottom-0 p-8 flex flex-col justify-end h-full`}>
{act.badge && (
<span className="block text-accent text-[10px] tracking-[0.25em] uppercase font-bold mb-3">{act.badge}</span>
)}
<h3 className={`font-serif text-white font-bold mb-3 text-3xl`}>{act.title}</h3>
<p className="text-white/70 text-sm mb-4 max-w-xs line-clamp-3">
{act.description}
</p>
<span className="inline-flex items-center gap-2 text-white text-sm font-semibold hover:text-accent transition-colors mt-auto opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 duration-400">
<ArrowRight className="w-4 h-4" />
</span>
</div>
</Link>
</ScrollReveal>
))
) : (
<div className="md:col-span-3 py-10 text-center text-gray-500">
{lang === "en" ? "No activities added yet." : "Henüz aktivite eklenmedi."}
</div>
)}
</div>
</div>
</section>
);
}
+50
View File
@@ -0,0 +1,50 @@
import Image from "next/image";
import ScrollReveal from "@/components/ScrollReveal";
interface CtaBandProps {
lang: string;
}
export default function CtaBand({ lang }: CtaBandProps) {
return (
<section className="relative py-28 overflow-hidden">
<div
className="absolute inset-0 animate-gradient-pan"
style={{
background:
"linear-gradient(135deg, #0D3D50 0%, #1A6B8A 35%, #0E1A24 60%, #1A4A60 100%)",
}}
/>
<div className="absolute inset-0 opacity-[0.04]">
<Image
src="https://images.unsplash.com/photo-1498623116890-37e912163d5d?q=80&w=2940&auto=format&fit=crop"
alt=""
fill
sizes="100vw"
className="object-cover"
/>
</div>
<div className="relative z-10 max-w-3xl mx-auto px-4 text-center">
<ScrollReveal>
<span className="inline-block text-accent text-xs tracking-[0.3em] uppercase font-semibold mb-6">
{lang === "en" ? "Get Started" : "Hemen Başlayın"}
</span>
<h2 className="font-serif text-5xl md:text-6xl font-bold text-white leading-tight mb-6">
{lang === "en" ? (
<>Are you ready to plan<br />your dream vacation?</>
) : (
<>Hayalindeki tatili<br />planlamaya hazır mısın?</>
)}
</h2>
<p className="text-white/50 mb-10 text-base leading-relaxed">
{lang === "en"
? "The wind is calling you. Book your spot now and become a part of this unique experience."
: "Rüzgar seni çağırıyor. Hemen yerini ayırt ve bu eşsiz deneyimin bir parçası ol."}
</p>
</ScrollReveal>
</div>
</section>
);
}
+99
View File
@@ -0,0 +1,99 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
interface Event {
id: number | string;
image: string;
title: string;
description: string;
badge?: string | null;
date: string;
time: string;
location: string;
}
interface EventsPreviewProps {
events: Event[];
lang: string;
}
export default function EventsPreview({ events, lang }: EventsPreviewProps) {
return (
<section className="py-28 bg-dark text-white relative overflow-hidden">
{/* Background glow */}
<div className="absolute top-0 right-0 w-96 h-96 bg-accent/20 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-primary/20 rounded-full blur-[150px] pointer-events-none" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<ScrollReveal className="flex flex-col md:flex-row md:items-end md:justify-between gap-6 mb-16">
<div>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
{lang === "en" ? "Calendar" : "Takvim"}
</span>
<h2 className="font-serif text-5xl font-bold">
{lang === "en" ? "Events" : "Etkinlikler"}
</h2>
</div>
<Link
href={`/${lang}/etkinlikler`}
className="inline-flex items-center gap-2 text-sm font-semibold text-white/70 hover:text-white transition-colors border border-white/20 hover:border-white px-6 py-2.5 rounded-full"
>
{lang === "en" ? "All Events" : "Tüm Etkinlikler"} <ArrowRight className="w-4 h-4" />
</Link>
</ScrollReveal>
<ScrollReveal delay={100}>
{events.length > 0 ? (
events.map((ev) => (
<div key={ev.id} className="group relative flex flex-col md:flex-row gap-8 bg-white/5 border border-white/10 rounded-3xl p-6 md:p-10 hover:bg-white/10 transition-colors backdrop-blur-sm mb-6">
<div className="relative w-full md:w-1/3 h-[250px] md:h-auto rounded-2xl overflow-hidden shrink-0">
<Image
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>
<div className="flex flex-col justify-center w-full md:w-2/3">
{ev.badge && (
<div className="inline-block bg-accent/20 text-accent text-[10px] font-bold px-3 py-1.5 rounded-full tracking-widest uppercase mb-4 w-max">
{ev.badge}
</div>
)}
<h3 className="font-serif text-3xl md:text-4xl font-bold mb-4">{ev.title}</h3>
<p className="text-white/60 leading-relaxed mb-6 max-w-xl">
{ev.description}
</p>
<div className="flex flex-wrap items-center gap-6 mb-8 text-sm text-white/80">
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.date}
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.time}
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> {ev.location}
</span>
</div>
<Link
href={`/${lang}/etkinlikler`}
className="inline-flex items-center gap-2 text-xs font-bold tracking-widest uppercase text-accent hover:text-white transition-colors w-max"
>
{lang === "en" ? "View Details" : "Detayları İncele"} <ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
))
) : (
<div className="py-10 text-center text-white/50">
{lang === "en" ? "No events added yet." : "Henüz etkinlik eklenmedi."}
</div>
)}
</ScrollReveal>
</div>
</section>
);
}
+46
View File
@@ -0,0 +1,46 @@
import {
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";
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,
};
interface Feature {
icon: string | null;
label: string;
sub: string;
}
interface FeaturesStripProps {
features: Feature[];
}
export default function FeaturesStrip({ features }: FeaturesStripProps) {
return (
<section className="bg-dark text-white py-8">
<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">
{features.map(({ icon, label, sub }, i) => {
const IconComponent = ICON_MAP[icon as string] || Star;
return (
<div
key={i}
className={`flex flex-col items-center justify-center gap-2 py-6 px-4 group ${i >= 2 ? "hidden md:flex" : ""}`}
>
<IconComponent className="w-5 h-5 text-accent group-hover:scale-110 transition-transform duration-300" />
<span className="text-[13px] font-semibold text-center">{label}</span>
<span className="text-[11px] text-white/40 text-center">{sub}</span>
</div>
);
})}
</div>
</div>
</section>
);
}
+61
View File
@@ -0,0 +1,61 @@
import Link from "next/link";
import HeroVideo from "@/components/HeroVideo";
interface HeroSectionProps {
heroData: {
location: string;
welcome: string;
title: string;
subtitle: string;
exploreActivities: string;
scroll: string;
};
lang: string;
}
export default function HeroSection({ heroData, lang }: HeroSectionProps) {
return (
<section className="relative h-screen w-full flex items-center justify-center overflow-hidden">
<HeroVideo />
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
<div className="animate-fade-up delay-100">
<span className="inline-block text-white/60 text-xs tracking-[0.35em] uppercase mb-8 font-light">
{heroData.location}
</span>
</div>
<div className="flex items-center justify-center gap-4 md:gap-6 mb-6 animate-fade-up delay-200">
<span
className="text-xs md:text-sm tracking-[0.4em] font-bold uppercase whitespace-nowrap text-white/90"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
>
{heroData.welcome}
</span>
<h1 className="font-sans font-black text-white leading-none tracking-tighter text-[clamp(4rem,10vw,10rem)] uppercase">
{heroData.title}
</h1>
</div>
<p className="animate-fade-up delay-500 text-white/75 text-base md:text-xl mb-10 font-light italic tracking-wide">
{heroData.subtitle}
</p>
<div className="animate-fade-up delay-600 flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href={`/${lang}/aktiviteler`}
className="bg-white/10 backdrop-blur-sm border border-white/30 text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase hover:bg-white/20 transition-colors w-full sm:w-auto text-center"
>
{heroData.exploreActivities}
</Link>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-3">
<span className="text-white/40 text-[10px] tracking-[0.3em] uppercase">{heroData.scroll}</span>
<div className="w-px h-12 bg-gradient-to-b from-white/40 to-transparent animate-scroll-bounce" />
</div>
</section>
);
}
+63
View File
@@ -0,0 +1,63 @@
import { ArrowRight, Camera } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
interface InstagramFeedProps {
instagramPosts: any[];
lang: string;
}
export default function InstagramFeed({ instagramPosts, lang }: InstagramFeedProps) {
if (!instagramPosts || instagramPosts.length === 0) {
return null;
}
return (
<section className="py-28 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
@kitebeachakyaka
</span>
<h2 className="font-serif text-5xl font-bold text-dark">Instagram</h2>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{instagramPosts.map((post: any) => (
<a
key={post.id}
href={`https://instagram.com/p/${post.code}`}
target="_blank"
rel="noreferrer"
className="group block relative rounded-2xl overflow-hidden aspect-[4/5] border border-sand"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={post.image_versions2?.candidates[0]?.url}
alt={post.caption?.text?.substring(0, 100) || "Instagram post"}
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">
<Camera className="w-8 h-8 mb-3" />
<p className="text-xs line-clamp-3">
{post.caption?.text}
</p>
</div>
</a>
))}
</ScrollReveal>
<div className="text-center mt-12">
<a
href="https://instagram.com/kitebeachakyaka"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 border-2 border-dark text-dark text-xs px-8 py-3.5 rounded-full font-bold uppercase tracking-widest hover:bg-dark hover:text-white transition-colors"
>
{lang === "en" ? "Follow Us" : "Bizi Takip Edin"}
<ArrowRight className="w-3.5 h-3.5" />
</a>
</div>
</div>
</section>
);
}
+57
View File
@@ -0,0 +1,57 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRight, Info } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import BentoGallery from "@/components/BentoGallery";
export default function RoomsSection({ lang }: { lang: string }) {
const isEn = lang === "en";
const galleryImages = [
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.15.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.18.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.19.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.20%20(1).jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.20.jpeg",
"https://media.ayris.tech/t/kitebeachakyaka/rooms/WhatsApp%20Image%202026-07-13%20at%2017.02.21.jpeg",
];
return (
<section className="py-28 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
{isEn ? "Accommodation" : "Konaklama"}
</span>
<h2 className="font-serif text-4xl md:text-5xl font-bold text-dark mb-6">Kite Beach Akyaka Otel</h2>
<p className="text-muted max-w-3xl mx-auto text-sm leading-relaxed mb-8">
{isEn
? "We are at that special point in Akyaka's most beautiful corner, where the tranquility of the river embraces the endless blue of the sea. The silence of the riverbank that greets you as soon as you open your door will be your most peaceful invitation to start the day. You can balance your soul with a yoga session against the river view in the heart of nature, and then push the adrenaline to the top with Kite Surfing in Akyaka's famous winds. Leave the tiredness of the day behind by tasting the delicacies from our restaurant's kitchen and watching the enchanting sunset of the Aegean. Here, time flows only at the pace you desire."
: "Akyakanın en güzel köşesinde, nehrin dinginliği ile denizin sonsuz maviliğinin kucaklaştığı o özel noktadayız. Kapınızı açtığınız anda sizi karşılayan nehir kıyısının sessizliği, güne başlamak için en huzurlu davetiniz olacak. Doğanın kalbinde, nehir manzarasına karşı yaptığınız bir yoga seansıyla ruhunuzu dengelerken; hemen ardından Akyakanın meşhur rüzgarlarında Kite Surf ile adrenalini zirveye taşıyabilirsiniz. Günün yorgunluğunu ise, restoranımızın mutfağından çıkan lezzetlerle, Egenin kendine has o büyüleyici gün batımını izleyerek geride bırakın. Burada zaman, sadece sizin istediğiniz hızda akıyor."}
</p>
<div className="inline-flex items-center gap-3 bg-primary/10 text-primary border border-primary/20 px-6 py-3 rounded-2xl shadow-sm">
<Info size={18} />
<span className="font-bold text-[15px] tracking-wide">
{isEn ? "Price: 8000 TL / Night (Breakfast Included)" : "Fiyat Bilgisi: 8000 TL / Gecelik (Kahvaltı Dahil)"}
</span>
</div>
</ScrollReveal>
<ScrollReveal>
<BentoGallery images={galleryImages} />
</ScrollReveal>
<div className="text-center mt-12">
<Link
href={isEn ? "/en/odalar" : "/tr/odalar"}
className="inline-flex items-center gap-2 border-2 border-dark text-dark text-xs px-8 py-3.5 rounded-full font-bold uppercase tracking-widest hover:bg-dark hover:text-white transition-colors"
>
{isEn ? "View Details" : "Detayları Gör"}
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
</section>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { Star } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
interface Testimonial {
text: string;
name: string;
from: string;
}
interface TestimonialsSectionProps {
testimonials: Testimonial[];
lang: string;
}
export default function TestimonialsSection({ testimonials, lang }: TestimonialsSectionProps) {
return (
<section className="py-28 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal className="text-center mb-16">
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-4">
{lang === "en" ? "Guest Reviews" : "Misafir Yorumları"}
</span>
<h2 className="font-serif text-5xl font-bold text-dark">
{lang === "en" ? "Experiences" : "Deneyimler"}
</h2>
</ScrollReveal>
<ScrollReveal stagger className="grid grid-cols-1 md:grid-cols-3 gap-6">
{testimonials.map((t, i) => (
<div key={i} className="relative bg-sand rounded-2xl p-8 border border-sand overflow-hidden">
{/* Large decorative quote */}
<span className="absolute top-3 right-5 font-serif text-[8rem] leading-none text-primary/8 select-none">
"
</span>
{/* Stars */}
<div className="flex gap-1 mb-5 relative z-10">
{Array.from({ length: 5 }).map((_, j) => (
<Star key={j} className="w-3.5 h-3.5 text-accent" fill="#E87040" />
))}
</div>
<p className="text-foreground/75 text-[13px] leading-relaxed mb-6 relative z-10 italic">
"{t.text}"
</p>
<div className="flex items-center gap-3 relative z-10">
<div className="w-9 h-9 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-primary font-bold text-sm">{t.name[0]}</span>
</div>
<div>
<span className="block font-semibold text-dark text-sm">{t.name}</span>
<span className="text-muted text-xs">{t.from}</span>
</div>
</div>
</div>
))}
</ScrollReveal>
</div>
</section>
);
}
+1 -1
View File
@@ -4,7 +4,7 @@
"welcome": "WELCOME TO",
"title": "KITE BEACH",
"subtitle": "\"The tranquility of the river, the energy of the sea — in one place.\"",
"bookNow": "Book Now",
"bookNow": "Contact Us",
"exploreActivities": "Explore Activities",
"scroll": "Discover"
}
+1 -1
View File
@@ -4,7 +4,7 @@
"welcome": "WELCOME TO",
"title": "KITE BEACH",
"subtitle": "\"Nehrin huzuru, denizin enerjisi — tek bir noktada.\"",
"bookNow": "Rezervasyon Yap",
"bookNow": "Bize Ulaşın",
"exploreActivities": "Aktiviteleri Keşfet",
"scroll": "Keşfet"
}
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
const BASE_URL = "https://kitebeachakyaka.com.tr";
export function buildMeta(
lang: string,
path: string,
tr: { title: string; description: string },
en: { title: string; description: string }
): Metadata {
const isEn = lang === "en";
return {
title: isEn ? en.title : tr.title,
description: isEn ? en.description : tr.description,
alternates: {
canonical: `${BASE_URL}/${lang}${path}`,
languages: {
"x-default": `${BASE_URL}/tr${path}`,
tr: `${BASE_URL}/tr${path}`,
en: `${BASE_URL}/en${path}`,
},
},
};
}
+39
View File
@@ -0,0 +1,39 @@
'use client'
export default function openinaryLoader({ src, width, quality }: { src: string, width: number, quality?: number }) {
// Handle already absolute openinary URLs
let path = src;
if (src.startsWith('https://media.ayris.tech/t/')) {
// format: https://media.ayris.tech/t/w_800,h_800/moyqr/img.jpg
const parts = src.split('/');
path = parts.slice(5).join('/');
} else if (src.startsWith('https://media.ayris.tech/upload/')) {
// format: https://media.ayris.tech/upload/moyqr/img.jpg
const parts = src.split('/');
path = parts.slice(4).join('/');
} else if (src.includes('res.cloudinary.com')) {
// Correctly apply width & quality for unmigrated Cloudinary URLs
// e.g. https://res.cloudinary.com/domain/image/upload/v1234/path.jpg
// becomes: https://res.cloudinary.com/domain/image/upload/w_800,f_webp,q_75/v1234/path.jpg
const parts = src.split('/upload/');
if (parts.length === 2) {
return `${parts[0]}/upload/w_${width},f_webp,q_${quality || 75}/${parts[1]}`;
}
return src;
} else if (src.startsWith('http')) {
// For other external URLs, we append the width as a query parameter to satisfy Next.js.
const url = new URL(src);
url.searchParams.set('w', width.toString());
if (quality) {
url.searchParams.set('q', quality.toString());
}
return url.toString();
}
// Clean up any leading slash
if (path.startsWith('/')) {
path = path.substring(1);
}
return `https://media.ayris.tech/t/w_${width},f_webp,q_${quality || 75}/${path}`
}
+5
View File
@@ -0,0 +1,5 @@
const BASE = process.env.NEXT_PUBLIC_OPENINARY_URL;
export function optimizedImage(path: string, params: string) {
return `${BASE}/t/${params}/${path}`;
}
+19
View File
@@ -0,0 +1,19 @@
export async function uploadToOpeninary(file: File, folder: string) {
const formData = new FormData();
formData.append("files", file);
formData.append("folder", folder);
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
body: formData,
});
if (!res.ok) {
const errorText = await res.text();
console.error("Openinary upload error:", errorText);
throw new Error("Upload başarısız: " + errorText);
}
const data = await res.json();
return data.files[0]; // { path, url, size, ... }
}
+29
View File
@@ -2,8 +2,17 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
poweredByHeader: false,
images: {
formats: ["image/avif", "image/webp"],
minimumCacheTTL: 2592000,
remotePatterns: [
{
protocol: 'https',
hostname: 'media.ayris.tech',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
@@ -16,8 +25,28 @@ const nextConfig: NextConfig = {
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: '**.fbcdn.net',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: '**.cdninstagram.com',
port: '',
pathname: '/**',
},
],
},
async rewrites() {
return [
{
source: '/api/st',
destination: 'https://panel.ayris.tech/api/analytics/collect',
},
];
},
};
export default nextConfig;
+19 -115
View File
@@ -11,6 +11,7 @@
"@formatjs/intl-localematcher": "^0.8.10",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcryptjs": "^3.0.3",
"framer-motion": "^12.40.0",
"lucide-react": "^1.17.0",
"negotiator": "^1.0.0",
@@ -21,6 +22,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/pg": "^8.20.0",
@@ -681,9 +683,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -700,9 +699,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -719,9 +715,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -738,9 +731,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -757,9 +747,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -776,9 +763,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -795,9 +779,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -814,9 +795,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -833,9 +811,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -858,9 +833,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -883,9 +855,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -908,9 +877,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -933,9 +899,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -958,9 +921,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -983,9 +943,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1008,9 +965,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1233,9 +1187,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1252,9 +1203,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1271,9 +1219,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1290,9 +1235,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1919,9 +1861,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1939,9 +1878,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1959,9 +1895,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1979,9 +1912,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2080,6 +2010,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -2142,7 +2079,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -2549,9 +2486,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2566,9 +2500,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2583,9 +2514,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2600,9 +2528,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2617,9 +2542,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2634,9 +2556,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2651,9 +2570,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2668,9 +2584,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2685,9 +2598,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2702,9 +2612,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3101,6 +3008,15 @@
"node": ">=6.0.0"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/better-result": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz",
@@ -5673,9 +5589,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5697,9 +5610,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5721,9 +5631,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5745,9 +5652,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
+2
View File
@@ -12,6 +12,7 @@
"@formatjs/intl-localematcher": "^0.8.10",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcryptjs": "^3.0.3",
"framer-motion": "^12.40.0",
"lucide-react": "^1.17.0",
"negotiator": "^1.0.0",
@@ -22,6 +23,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/negotiator": "^0.6.4",
"@types/node": "^20",
"@types/pg": "^8.20.0",
+15 -7
View File
@@ -17,7 +17,7 @@ model SiteSettings {
}
model HeroSection {
lang String @id // "tr" or "en"
lang String @id
location String
welcome String
title String
@@ -30,12 +30,12 @@ model HeroSection {
model Activity {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
lang String
title String
description String
price String
image String
badge String? // e.g. "IKO Sertifikalı"
badge String?
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -43,21 +43,21 @@ model Activity {
model Event {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
lang String
title String
description String
date String
time String
location String
image String
badge String? // e.g. "Yaklaşan Etkinlik"
badge String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Feature {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
lang String
icon String
label String
sub String
@@ -67,7 +67,7 @@ model Feature {
}
model AboutSection {
lang String @id // "tr" or "en"
lang String @id
badge String
titleLine1 String
titleLine2 String
@@ -83,3 +83,11 @@ model AboutSection {
badgeIkoLabel String
updatedAt DateTime @updatedAt
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+29
View File
@@ -0,0 +1,29 @@
(function() {
var d = document.currentScript && document.currentScript.getAttribute('data-domain');
if (!d) d = location.hostname;
function send(path, ref) {
var payload = {
domain: d,
path: path,
referrer: ref || document.referrer || ''
};
var url = '/api/st';
if (navigator.sendBeacon) {
navigator.sendBeacon(url, JSON.stringify(payload));
} else {
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), keepalive: true });
}
}
send(location.pathname, document.referrer);
var _push = history.pushState;
history.pushState = function() {
_push.apply(history, arguments);
send(location.pathname, '');
};
window.addEventListener('popstate', function() {
send(location.pathname, '');
});
})();
+16
View File
@@ -0,0 +1,16 @@
import 'dotenv/config';
import prisma from './lib/prisma';
async function main() {
const imageUrl = "https://media.ayris.tech/t/kitebeachakyaka/kitesurf.png";
await prisma.aboutSection.updateMany({
data: {
image: imageUrl
}
});
console.log("About section image updated to the kitesurf one successfully!");
}
main().catch(console.error).finally(() => prisma.$disconnect());
+24
View File
@@ -0,0 +1,24 @@
import 'dotenv/config';
import prisma from './lib/prisma';
async function main() {
await prisma.aboutSection.updateMany({
where: { lang: 'tr' },
data: {
badgeIkoTitle: 'Kitesurf',
badgeIkoLabel: 'Okulu'
}
});
await prisma.aboutSection.updateMany({
where: { lang: 'en' },
data: {
badgeIkoTitle: 'Kitesurf',
badgeIkoLabel: 'School'
}
});
console.log('AboutSection DB updated successfully');
}
main().catch(console.error).finally(() => prisma.$disconnect());
+69
View File
@@ -0,0 +1,69 @@
import 'dotenv/config';
import prisma from './lib/prisma';
async function main() {
await prisma.activity.deleteMany({});
await prisma.activity.createMany({
data: [
{
lang: 'tr',
title: 'Kitesurf',
description: 'Akyaka\'nın muhteşem rüzgarında profesyonel eğitmenlerle kitesurf deneyimi yaşayın.',
price: 'Fiyat bilgisi için ulaşın',
image: 'https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop',
badge: 'Popüler',
order: 1
},
{
lang: 'tr',
title: 'Yoga Wellness',
description: 'Nehir kenarında güne taze bir başlangıç veya gün batımında ruhunuzu dinlendiren yoga seansları.',
price: 'Fiyat bilgisi için ulaşın',
image: 'https://images.unsplash.com/photo-1545205597-3d9d02c29597?q=80&w=2940&auto=format&fit=crop',
badge: 'Rahatlatıcı',
order: 2
},
{
lang: 'tr',
title: 'Sunset DJ Party',
description: 'Gün batımını Ege\'nin muhteşem manzarası ve harika müzikler eşliğinde kutlayın.',
price: 'Ücretsiz Katılım',
image: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?q=80&w=2874&auto=format&fit=crop',
badge: 'Eğlence',
order: 3
},
{
lang: 'en',
title: 'Kitesurf',
description: 'Experience kitesurfing with professional instructors in Akyaka\'s amazing wind.',
price: 'Contact for price',
image: 'https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop',
badge: 'Popular',
order: 1
},
{
lang: 'en',
title: 'Yoga Wellness',
description: 'A fresh start to the day by the river or relaxing yoga sessions at sunset.',
price: 'Contact for price',
image: 'https://images.unsplash.com/photo-1545205597-3d9d02c29597?q=80&w=2940&auto=format&fit=crop',
badge: 'Relaxing',
order: 2
},
{
lang: 'en',
title: 'Sunset DJ Party',
description: 'Celebrate the sunset with the amazing view of the Aegean and great music.',
price: 'Free Entry',
image: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?q=80&w=2874&auto=format&fit=crop',
badge: 'Entertainment',
order: 3
}
]
});
console.log('Activities updated successfully');
}
main().catch(console.error).finally(() => prisma.$disconnect());
+19
View File
@@ -0,0 +1,19 @@
import 'dotenv/config';
import prisma from './lib/prisma';
async function main() {
const imageUrl = "https://media.ayris.tech/t/kitebeachakyaka/kitesurf.png";
await prisma.activity.updateMany({
where: {
title: "Kitesurf"
},
data: {
image: imageUrl
}
});
console.log("Kitesurf activity image updated to the local one successfully!");
}
main().catch(console.error).finally(() => prisma.$disconnect());
+41
View File
@@ -0,0 +1,41 @@
import fs from 'fs';
import path from 'path';
import 'dotenv/config';
async function uploadFile(filePath, folder) {
const fileName = path.basename(filePath);
const fileBuffer = fs.readFileSync(filePath);
const blob = new Blob([fileBuffer], { type: 'image/png' });
const formData = new FormData();
formData.append('files', blob, fileName);
formData.append('folder', folder);
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
body: formData,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Upload failed for ${fileName}: ${text}`);
}
const data = await res.json();
return data.files[0].url;
}
async function main() {
const filePath = './public/kitesurf.png';
console.log(`Uploading ${filePath}...`);
try {
const url = await uploadFile(filePath, 'kitebeachakyaka');
console.log(`Uploaded! URL: ${url}`);
} catch (e) {
console.error(e);
}
}
main().catch(console.error);
+53
View File
@@ -0,0 +1,53 @@
import fs from 'fs';
import path from 'path';
import 'dotenv/config';
async function uploadFile(filePath, folder) {
const fileName = path.basename(filePath);
const fileBuffer = fs.readFileSync(filePath);
const blob = new Blob([fileBuffer], { type: 'image/jpeg' });
const formData = new FormData();
// We can pass a file name to the formData in Node by passing an object with name or using File (if available)
// But FormData append takes (name, value, filename)
formData.append('files', blob, fileName);
formData.append('folder', folder);
const res = await fetch(`${process.env.OPENINARY_API_URL}/api/upload`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENINARY_API_KEY}` },
body: formData,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Upload failed for ${fileName}: ${text}`);
}
const data = await res.json();
return data.files[0].url;
}
async function main() {
const dirPath = './public/rooms';
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.jpeg') || f.endsWith('.jpg'));
const uploadedUrls = [];
for (const file of files) {
console.log(`Uploading ${file}...`);
try {
const url = await uploadFile(path.join(dirPath, file), 'kitebeachakyaka/rooms');
console.log(`Uploaded! URL: ${url}`);
uploadedUrls.push(url);
} catch (e) {
console.error(e);
}
}
console.log('\n--- UPLOADED URLS ---');
console.log(JSON.stringify(uploadedUrls, null, 2));
}
main().catch(console.error);