This commit is contained in:
2026-06-09 15:15:40 +03:00
parent f6b4acb760
commit d908d83ca5
58 changed files with 4320 additions and 419 deletions
+2
View File
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
/app/generated/prisma
+54
View File
@@ -0,0 +1,54 @@
# 1. Base image
FROM node:20-alpine AS base
# 2. Dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci --legacy-peer-deps
# 3. Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Environment variables must be present at build time for Next.js
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npm run build
# 4. Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Start the server
CMD ["node", "server.js"]
+103
View File
@@ -0,0 +1,103 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRight, Clock, Users } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
import prisma from "@/lib/prisma";
export default async function AktivitelerPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const activities = await prisma.activity.findMany({ where: { lang }, orderBy: { order: "asc" } });
return (
<div className="min-h-screen bg-background">
{/* 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
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
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>
</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">
{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">
<div className="relative h-64 overflow-hidden">
<Image
src={act.image}
alt={act.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
{act.badge && (
<span className="absolute top-4 left-4 text-[10px] font-bold px-3 py-1.5 rounded-full tracking-widest uppercase bg-accent text-white shadow">
{act.badge}
</span>
)}
</div>
<div className="p-8 flex-grow flex flex-col">
<h2 className="font-serif text-2xl font-bold text-dark mb-3">{act.title}</h2>
<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>
<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"
>
Bilgi Al
<ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</div>
</ScrollReveal>
)) : (
<div className="col-span-2 text-center py-20 text-gray-500">Henüz aktivite eklenmedi.</div>
)}
</div>
</section>
{/* CTA */}
<section className="py-20 bg-primary">
<div className="max-w-3xl mx-auto px-4 text-center">
<h3 className="font-serif text-4xl text-white font-bold mb-4">
Özel Paket Teklifi Al
</h3>
<p className="text-white/60 mb-8">
Kitesurf + konaklama veya yoga + SUP gibi özel aktivite paketleri için bizimle iletişime geçin.
</p>
<Link
href="/iletisim"
className="btn-shimmer inline-block bg-accent text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase"
>
İletişime Geç
</Link>
</div>
</section>
</div>
);
}
+61
View File
@@ -0,0 +1,61 @@
export default async function CookiesPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const content = {
tr: {
title: "Çerez Politikası",
subtitle: "Web sitemizdeki çerez kullanımı hakkında bilgiler.",
text: `Kite Beach Akyaka olarak, web sitemizden en verimli şekilde faydalanabilmeniz ve kullanıcı deneyiminizi geliştirebilmek amacıyla Çerezler (Cookies) kullanmaktayız. Çerez kullanılmasını tercih etmezseniz tarayıcınızın ayarlarından çerezleri silebilir ya da engelleyebilirsiniz.
1. Çerez Nedir?
Çerezler, bir web sitesini ziyaret ettiğinizde bilgisayarınıza veya mobil cihazınıza kaydedilen küçük metin dosyalarıdır. Bu dosyalar, web sitesinin daha verimli çalışmasını sağlamak ve ziyaretçilere kişiselleştirilmiş bir deneyim sunmak için kullanılır.
2. Hangi Çerezleri Kullanıyoruz?
- **Zorunlu Çerezler:** Sitemizin temel işlevlerini yerine getirebilmesi için kesinlikle gerekli olan çerezlerdir. (Örn: Oturum yönetimi, dil tercihi)
- **Performans ve Analitik Çerezleri:** Sitemizin ziyaretçiler tarafından nasıl kullanıldığını analiz etmek, en çok ziyaret edilen sayfaları belirlemek ve sitemizin performansını artırmak için kullanılır. (Örn: Google Analytics çerezleri)
- **İşlevsellik Çerezleri:** Ziyaretleriniz sırasındaki tercihlerinizi (seçtiğiniz dil veya bölge vb.) hatırlamamızı sağlayan çerezlerdir.
3. Çerezlerin Yönetimi
Tarayıcınızın ayarlarını değiştirerek çerezlere ilişkin tercihlerinizi kişiselleştirme imkanına sahipsiniz. Çoğu tarayıcı, çerezleri otomatik olarak kabul eder ancak çerezleri reddetmek veya çerez gönderildiğinde uyarı almak için ayarlarınızı değiştirebilirsiniz.
Çerez ayarlarınızı nasıl yöneteceğinizi öğrenmek için tarayıcınızın (Chrome, Safari, Firefox, Edge vb.) "Yardım" veya "Ayarlar" menüsünü kullanabilirsiniz.
`,
},
en: {
title: "Cookie Policy",
subtitle: "Information about the use of cookies on our website.",
text: `As Kite Beach Akyaka, we use Cookies in order for you to benefit from our website in the most efficient way and to improve your user experience. If you prefer not to use cookies, you can delete or block cookies from your browser settings.
1. What is a Cookie?
Cookies are small text files that are saved on your computer or mobile device when you visit a website. These files are used to make the website work more efficiently and to offer a personalized experience to visitors.
2. Which Cookies Do We Use?
- **Strictly Necessary Cookies:** These are cookies that are absolutely necessary for our site to fulfill its basic functions. (E.g. Session management, language preference)
- **Performance and Analytical Cookies:** Used to analyze how our site is used by visitors, identify the most visited pages, and improve the performance of our site. (E.g. Google Analytics cookies)
- **Functionality Cookies:** These are cookies that allow us to remember your preferences (language or region you have chosen, etc.) during your visits.
3. Management of Cookies
You have the opportunity to personalize your preferences regarding cookies by changing your browser settings. Most browsers automatically accept cookies, but you can change your settings to reject cookies or to receive a warning when a cookie is sent.
You can use the "Help" or "Settings" menu of your browser (Chrome, Safari, Firefox, Edge, etc.) to learn how to manage your cookie settings.`
}
};
const t = content[lang as keyof typeof content] || content.tr;
return (
<div className="pt-32 pb-20 bg-background min-h-screen">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="font-serif text-4xl md:text-5xl font-bold text-dark mb-4">{t.title}</h1>
<h2 className="text-xl text-primary font-medium mb-10">{t.subtitle}</h2>
<div className="prose prose-lg text-gray-600">
{t.text.split('\n\n').map((paragraph, index) => (
<p key={index} className="mb-6 whitespace-pre-line leading-relaxed">
{paragraph}
</p>
))}
</div>
</div>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import "server-only";
const dictionaries = {
en: () => import("../../dictionaries/en.json").then((module) => module.default),
tr: () => import("../../dictionaries/tr.json").then((module) => module.default),
};
export type Locale = keyof typeof dictionaries;
export const hasLocale = (locale: string): locale is Locale =>
locale in dictionaries;
export const getDictionary = async (locale: Locale) => {
if (!hasLocale(locale)) return dictionaries.tr();
return dictionaries[locale]();
};
@@ -3,51 +3,17 @@ import Link from "next/link";
import { ArrowRight, Calendar, Clock, MapPin, Music } from "lucide-react"; import { ArrowRight, Calendar, Clock, MapPin, Music } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal"; import ScrollReveal from "@/components/ScrollReveal";
const events = [ import prisma from "@/lib/prisma";
{
id: "mahmut-orhan",
title: "Mahmut Orhan - Sunset Session",
date: "15 Ağustos 2026",
time: "18:00 - 23:30",
location: "Kite Beach Akyaka, Beach Club",
category: "Elektronik Müzik / DJ Performans",
price: "Biletler Yakında",
image: "https://images.unsplash.com/photo-1540039155732-680c74f07599?q=80&w=2832&auto=format&fit=crop",
description: "Dünyaca ünlü DJ Mahmut Orhan, unutulmaz bir gün batımı performansı için Kite Beach Akyaka'da! Ege'nin muhteşem manzarasına karşı elektronik müziğin ritmini hissedeceğiniz bu özel geceyi kaçırmayın. Bilet detayları çok yakında açıklanacaktır.",
featured: true,
},
{
id: "kite-festival",
title: "Akyaka Kitesurf Festivali '26",
date: "20-22 Eylül 2026",
time: "Tüm Gün",
location: "Kite Beach Sahili",
category: "Spor & Festival",
price: "Ücretsiz Katılım",
image: "https://images.unsplash.com/photo-1533000756353-909d73d2a3c7?q=80&w=2940&auto=format&fit=crop",
description: "Geleneksel Kitesurf festivalimiz bu yıl daha da büyük! Profesyonel yarışçılar, amatör denemeler, gün boyu müzik ve sahil etkinlikleri ile rüzgarın tadını çıkarın.",
featured: false,
},
{
id: "yoga-retreat",
title: "Dolunay Yoga & Meditasyon",
date: "28 Ekim 2026",
time: "19:00 - 21:00",
location: "Nehir Platformu",
category: "Wellness",
price: "₺450",
image: "https://images.unsplash.com/photo-1599901860904-17e08627c271?q=80&w=2940&auto=format&fit=crop",
description: "Dolunayın büyüleyici enerjisinde, Azmak Nehri'nin sessizliğinde özel bir yoga ve ses banyosu meditasyonu. Tüm seviyeler için uygundur. Matlar tarafımızdan temin edilir.",
featured: false,
}
];
export const metadata = { export const metadata = {
title: "Etkinlikler | Kite Beach Akyaka", title: "Etkinlikler | Kite Beach Akyaka",
description: "Mahmut Orhan konserinden Kitesurf festivallerine, Akyaka'daki en özel etkinlikler ve gün batımı partileri.", description: "Mahmut Orhan konserinden Kitesurf festivallerine, Akyaka'daki en özel etkinlikler ve gün batımı partileri.",
}; };
export default function EventsPage() { export default async function EventsPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const events = await prisma.event.findMany({ where: { lang }, orderBy: { date: "asc" } });
return ( return (
<div className="flex flex-col min-h-screen bg-background"> <div className="flex flex-col min-h-screen bg-background">
@@ -88,7 +54,7 @@ export default function EventsPage() {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="space-y-16"> <div className="space-y-16">
{events.map((event, index) => ( {events.length > 0 ? events.map((event, index) => (
<ScrollReveal key={event.id} delay={index * 100}> <ScrollReveal key={event.id} delay={index * 100}>
<div className={`flex flex-col ${index % 2 === 1 ? 'md:flex-row-reverse' : 'md:flex-row'} gap-8 lg:gap-12 bg-white rounded-3xl overflow-hidden shadow-sm border border-sand transition-all hover:shadow-xl`}> <div className={`flex flex-col ${index % 2 === 1 ? 'md:flex-row-reverse' : 'md:flex-row'} gap-8 lg:gap-12 bg-white rounded-3xl overflow-hidden shadow-sm border border-sand transition-all hover:shadow-xl`}>
@@ -100,20 +66,17 @@ export default function EventsPage() {
fill fill
className="object-cover" className="object-cover"
/> />
{event.featured && ( {event.badge && (
<div className="absolute top-4 left-4 bg-accent text-white px-4 py-1.5 rounded-full text-xs font-bold tracking-widest uppercase flex items-center gap-2 shadow-lg backdrop-blur-md"> <div className="absolute top-4 left-4 bg-accent text-white px-4 py-1.5 rounded-full text-xs font-bold tracking-widest uppercase flex items-center gap-2 shadow-lg backdrop-blur-md">
<Music className="w-3.5 h-3.5" /> <Music className="w-3.5 h-3.5" />
Öne Çıkan {event.badge}
</div> </div>
)} )}
</div> </div>
{/* Content Container */} {/* Content Container */}
<div className="w-full md:w-1/2 lg:w-[45%] p-8 md:p-10 flex flex-col justify-center"> <div className="w-full md:w-1/2 lg:w-[45%] p-8 md:p-10 flex flex-col justify-center">
<span className="text-primary text-[10px] tracking-[0.2em] font-bold uppercase mb-3 block"> <h2 className="font-serif text-3xl md:text-4xl font-bold text-dark mb-6 leading-tight mt-4">
{event.category}
</span>
<h2 className="font-serif text-3xl md:text-4xl font-bold text-dark mb-6 leading-tight">
{event.title} {event.title}
</h2> </h2>
@@ -137,21 +100,20 @@ export default function EventsPage() {
</p> </p>
<div className="flex items-center justify-between mt-auto pt-6 border-t border-sand"> <div className="flex items-center justify-between mt-auto pt-6 border-t border-sand">
<span className="font-bold text-dark text-lg">{event.price}</span>
<Link <Link
href="/iletisim" href={`/${lang}/iletisim`}
className={`inline-flex items-center gap-2 text-xs font-bold tracking-widest uppercase transition-colors ${ 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"
event.featured ? 'bg-accent text-white px-6 py-3 rounded-full hover:bg-accent/90' : 'text-primary hover:text-accent'
}`}
> >
Bilet / Bilgi <ArrowRight className="w-4 h-4" /> Bize Ulaşın <ArrowRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
</ScrollReveal> </ScrollReveal>
))} )) : (
<div className="text-center py-20 text-gray-500">Henüz etkinlik eklenmedi.</div>
)}
</div> </div>
</div> </div>
@@ -8,13 +8,13 @@ type Filter = (typeof filters)[number];
const gallery: { src: string; alt: string; category: Exclude<Filter, "Tümü"> }[] = [ 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" }, { src: "https://images.unsplash.com/photo-1582719478250-c89404bb8a0e?q=80&w=2940&auto=format&fit=crop", alt: "Standart Oda", category: "Odalar" },
{ src: "https://images.unsplash.com/photo-1533000756353-909d73d2a3c7?q=80&w=2940&auto=format&fit=crop", alt: "Kitesurf", category: "Aktiviteler" }, { 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-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-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-1517248135467-4c7edcad34c4?q=80&w=2940&auto=format&fit=crop", alt: "Restoran", category: "Restoran" },
{ src: "https://images.unsplash.com/photo-1599901860904-17e08627c271?q=80&w=2940&auto=format&fit=crop", alt: "Yoga", category: "Aktiviteler" }, { 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-1499955085172-a104c9463ece?q=80&w=2940&auto=format&fit=crop", alt: "Bungalow", category: "Odalar" },
{ src: "https://images.unsplash.com/photo-1544606771-46abcf9dbb83?q=80&w=2924&auto=format&fit=crop", alt: "SUP", category: "Aktiviteler" }, { 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-1551504734-5ee1c4a1479b?q=80&w=2940&auto=format&fit=crop", alt: "Kahvaltı", category: "Restoran" }, { 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-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-1544148103-0773bf10d330?q=80&w=2940&auto=format&fit=crop", alt: "Restoran Ambiyans", category: "Restoran" },
+75
View File
@@ -0,0 +1,75 @@
export default async function PrivacyPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const content = {
tr: {
title: "Gizlilik Politikası",
subtitle: "Verilerinizin güvenliği bizim için önemlidir.",
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.
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,
- 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),
- Yasal yükümlülüklerimizi yerine getirmek.
3. Bilgi Güvenliği
Kişisel bilgilerinizin yetkisiz erişime, değişime, ifşaya veya imhaya karşı korunması için gerekli teknik ve idari güvenlik önlemlerini almaktayız. Web sitemizdeki tüm veri akışları şifrelenmiş bağlantılar (SSL) üzerinden yapılmaktadır.
4. Üçüncü Taraflarla Paylaşım
Bilgilerinizi izniniz olmadan üçüncü taraflarla satmıyor, kiralamıyor veya paylaşmıyoruz. Sadece hizmet sağlamak için işbirliği yaptığımız ödeme sağlayıcıları gibi güvenilir partnerlerle, gizlilik sözleşmeleri çerçevesinde veri paylaşımı yapılabilmektedir.
5. Değişiklikler
Bu Gizlilik Politikası, zaman zaman güncellenebilir. Herhangi bir değişiklik durumunda yeni politika sitemizde yayınlanarak yürürlüğe girecektir.
`,
},
en: {
title: "Privacy Policy",
subtitle: "The security of your data is important to us.",
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.
2. How Do We Use Your Information?
The information we collect is used for the following purposes:
- Processing and confirming your reservations,
- 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),
- Fulfilling our legal obligations.
3. Information Security
We take the necessary technical and administrative security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. All data flows on our website are carried out over encrypted connections (SSL).
4. Sharing with Third Parties
We do not sell, rent, or share your information with third parties without your permission. Data sharing can only be done with reliable partners, such as payment providers with whom we cooperate to provide services, within the framework of confidentiality agreements.
5. Changes
This Privacy Policy may be updated from time to time. In case of any changes, the new policy will come into effect by being published on our site.`
}
};
const t = content[lang as keyof typeof content] || content.tr;
return (
<div className="pt-32 pb-20 bg-background min-h-screen">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="font-serif text-4xl md:text-5xl font-bold text-dark mb-4">{t.title}</h1>
<h2 className="text-xl text-primary font-medium mb-10">{t.subtitle}</h2>
<div className="prose prose-lg text-gray-600">
{t.text.split('\n\n').map((paragraph, index) => (
<p key={index} className="mb-6 whitespace-pre-line leading-relaxed">
{paragraph}
</p>
))}
</div>
</div>
</div>
);
}
@@ -84,7 +84,7 @@ export default function HakkimizdaPage() {
<ScrollReveal delay={150}> <ScrollReveal delay={150}>
<div className="relative h-[460px] rounded-3xl overflow-hidden"> <div className="relative h-[460px] rounded-3xl overflow-hidden">
<Image <Image
src="https://images.unsplash.com/photo-1533000756353-909d73d2a3c7?q=80&w=2940&auto=format&fit=crop" src="https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop"
alt="Kitesurf" alt="Kitesurf"
fill fill
className="object-cover" className="object-cover"
+80
View File
@@ -0,0 +1,80 @@
export default async function KVKKPage({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const content = {
tr: {
title: "Kişisel Verilerin Korunması",
subtitle: "Kişisel Verilerin Korunması Kanunu (KVKK) Aydınlatma Metni",
text: `Kite Beach Akyaka olarak, kişisel verilerinizin güvenliğine büyük önem veriyoruz. 6698 sayılı Kişisel Verilerin Korunması Kanunu (“Kanun”) uyarınca, elde edilen kişisel verileriniz veri sorumlusu sıfatıyla Kite Beach Akyaka tarafından aşağıda açıklanan kapsamda işlenebilecektir.
1. Kişisel Verilerin İşlenme Amacı
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,
- İ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.
4. Veri Sahibinin Hakları
KVKKnın 11. maddesi uyarınca;
- Kişisel veri işlenip işlenmediğini öğrenme,
- Kişisel veriler işlenmişse buna ilişkin bilgi talep etme,
- Amacına uygun kullanılıp kullanılmadığını öğrenme,
- Yurt içinde veya yurt dışında aktarıldığı üçüncü kişileri bilme,
- Eksik veya yanlış işlenmişse düzeltilmesini isteme haklarına sahipsiniz.
Detaylı bilgi ve talepleriniz için info@kitebeachakyaka.com.tr adresi üzerinden bizimle iletişime geçebilirsiniz.`,
},
en: {
title: "Personal Data Protection",
subtitle: "Information Text on the Protection of Personal Data",
text: `As Kite Beach Akyaka, we attach great importance to the security of your personal data. In accordance with the Personal Data Protection Law No. 6698 ("Law"), your personal data may be processed by Kite Beach Akyaka as the data controller within the scope described below.
1. Purpose of Processing Personal Data
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,
- 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.
4. Rights of the Data Subject
In accordance with Article 11 of the KVKK, you have the right to;
- Learn whether personal data is processed,
- Request information if personal data has been processed,
- Learn the purpose of processing personal data and whether they are used appropriately,
- Know the third parties to whom personal data is transferred domestically or abroad,
- Request the correction of personal data if it is incomplete or improperly processed.
For detailed information and requests, you can contact us at info@kitebeachakyaka.com.tr.`
}
};
const t = content[lang as keyof typeof content] || content.tr;
return (
<div className="pt-32 pb-20 bg-background min-h-screen">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="font-serif text-4xl md:text-5xl font-bold text-dark mb-4">{t.title}</h1>
<h2 className="text-xl text-primary font-medium mb-10">{t.subtitle}</h2>
<div className="prose prose-lg text-gray-600">
{t.text.split('\n\n').map((paragraph, index) => (
<p key={index} className="mb-6 whitespace-pre-line leading-relaxed">
{paragraph}
</p>
))}
</div>
</div>
</div>
);
}
+12 -4
View File
@@ -1,6 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter, Playfair_Display } from "next/font/google"; import { Inter, Playfair_Display } from "next/font/google";
import "./globals.css"; import "../globals.css";
import Navbar from "@/components/Navbar"; import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import WhatsAppButton from "@/components/WhatsAppButton"; import WhatsAppButton from "@/components/WhatsAppButton";
@@ -20,17 +20,25 @@ export const metadata: Metadata = {
description: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Kite Beach Akyaka butik otel ve kitesurf merkezine hoş geldiniz.", description: "Nehrin huzuru, denizin enerjisi — tek bir noktada. Kite Beach Akyaka butik otel ve kitesurf merkezine hoş geldiniz.",
}; };
export default function RootLayout({ export async function generateStaticParams() {
return [{ lang: 'tr' }, { lang: 'en' }]
}
export default async function RootLayout({
children, children,
params,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
params: Promise<{ lang: string }>;
}>) { }>) {
const { lang } = await params;
return ( return (
<html <html
lang="tr" lang={lang}
className={`${inter.variable} ${playfair.variable} h-full antialiased scroll-smooth`} className={`${inter.variable} ${playfair.variable} h-full antialiased scroll-smooth`}
suppressHydrationWarning
> >
<body className="min-h-full flex flex-col bg-background text-foreground"> <body className="min-h-full flex flex-col bg-background text-foreground" suppressHydrationWarning>
<Navbar /> <Navbar />
<main className="flex-grow">{children}</main> <main className="flex-grow">{children}</main>
<Footer /> <Footer />
+172 -148
View File
@@ -1,16 +1,17 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ArrowRight, Wind, Anchor, Utensils, BedDouble, Sun, Star } from "lucide-react"; import * as Icons from "lucide-react";
import { ArrowRight, Star } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal"; import ScrollReveal from "@/components/ScrollReveal";
import HeroVideo from "@/components/HeroVideo"; import HeroVideo from "@/components/HeroVideo";
const features = [ const defaultFeatures = [
{ icon: Wind, label: "Kitesurf Okulu", sub: "IKO Sertifikalı" }, { icon: "Wind", label: "Kitesurf Okulu", sub: "IKO Sertifikalı" },
{ icon: Anchor, label: "Yoga Platformu", sub: "Nehir Kenarı" }, { icon: "Anchor", label: "Yoga Platformu", sub: "Nehir Kenarı" },
{ icon: Utensils, label: "Restoran & Bar", sub: "Ege Mutfağı" }, { icon: "Utensils", label: "Restoran & Bar", sub: "Ege Mutfağı" },
{ icon: BedDouble, label: "Butik Odalar", sub: "3 Tip Konaklama" }, { icon: "BedDouble", label: "Butik Odalar", sub: "3 Tip Konaklama" },
{ icon: Sun, label: "Gün Batımı Terası", sub: "Gökova Manzarası" }, { icon: "Sun", label: "Gün Batımı Terası", sub: "Gökova Manzarası" },
]; ];
const rooms = [ const rooms = [
@@ -61,7 +62,51 @@ const testimonials = [
}, },
]; ];
export default function Home() { import { getDictionary, Locale } from "./dictionaries";
import prisma from "@/lib/prisma";
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 heroData = heroDb || {
location: dict.home.location,
welcome: dict.home.welcome,
title: dict.home.title,
subtitle: dict.home.subtitle,
bookNow: dict.home.bookNow,
exploreActivities: dict.home.exploreActivities,
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",
titleLine2: lang === "en" ? "Heart of Nature" : "Kalbinde",
titleHighlight: lang === "en" ? "A Special Escape" : "Özel Bir Kaçış",
description1: lang === "en"
? "Located right on the banks of the Azmak River flowing into the crystal waters of the Gulf of Gokova, we are in a unique location where the wind never ceases."
: "Gökova Körfezi'nin kristal sularına dökülen Azmak Nehri'nin hemen kıyısında, rüzgarın hiç eksik olmadığı eşsiz bir konumda yer alıyoruz.",
description2: lang === "en"
? "Kite Beach Akyaka offers a unique living space that integrates the passion for kitesurfing with nature, comfort, and local flavors."
: "Kite Beach Akyaka; kitesurf tutkusunu doğayla, konforla ve yerel lezzetlerle bütünleştiren benzersiz bir yaşam alanı sunar.",
image: "https://images.unsplash.com/photo-1544551763-46a013bb70d5?q=80&w=2940&auto=format&fit=crop",
statsGuestCount: "500+",
statsGuestLabel: lang === "en" ? "Happy Guests" : "Mutlu Misafir",
statsYearCount: "10+",
statsYearLabel: lang === "en" ? "Years Experience" : "Yıl Deneyim",
badgeIkoTitle: "IKO",
badgeIkoLabel: lang === "en" ? "Certified" : "Sertifikalı"
};
const displayFeatures = dbFeatures.length > 0 ? dbFeatures : defaultFeatures;
return ( return (
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
@@ -82,7 +127,7 @@ export default function Home() {
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto"> <div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
<div className="animate-fade-up delay-100"> <div className="animate-fade-up delay-100">
<span className="inline-block text-white/60 text-xs tracking-[0.35em] uppercase mb-8 font-light"> <span className="inline-block text-white/60 text-xs tracking-[0.35em] uppercase mb-8 font-light">
Akyaka · Muğla · Türkiye {heroData.location}
</span> </span>
</div> </div>
@@ -91,36 +136,36 @@ export default function Home() {
className="text-xs md:text-sm tracking-[0.4em] font-bold uppercase whitespace-nowrap text-white/90" className="text-xs md:text-sm tracking-[0.4em] font-bold uppercase whitespace-nowrap text-white/90"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }} style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
> >
WELCOME TO {heroData.welcome}
</span> </span>
<h1 className="font-sans font-black text-white leading-none tracking-tighter text-[clamp(4rem,10vw,10rem)] uppercase"> <h1 className="font-sans font-black text-white leading-none tracking-tighter text-[clamp(4rem,10vw,10rem)] uppercase">
KITE BEACH {heroData.title}
</h1> </h1>
</div> </div>
<p className="animate-fade-up delay-500 text-white/75 text-base md:text-xl mb-10 font-light italic tracking-wide"> <p className="animate-fade-up delay-500 text-white/75 text-base md:text-xl mb-10 font-light italic tracking-wide">
"Nehrin huzuru, denizin enerjisi — tek bir noktada." {heroData.subtitle}
</p> </p>
<div className="animate-fade-up delay-600 flex flex-col sm:flex-row items-center justify-center gap-4"> <div className="animate-fade-up delay-600 flex flex-col sm:flex-row items-center justify-center gap-4">
<Link <Link
href="/iletisim" 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" 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"
> >
Rezervasyon Yap {heroData.bookNow}
</Link> </Link>
<Link <Link
href="/aktiviteler" 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" 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"
> >
Aktiviteleri Keşfet {heroData.exploreActivities}
</Link> </Link>
</div> </div>
</div> </div>
{/* Scroll indicator */} {/* Scroll indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-3"> <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">Keşfet</span> <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 className="w-px h-12 bg-gradient-to-b from-white/40 to-transparent animate-scroll-bounce" />
</div> </div>
</section> </section>
@@ -131,16 +176,19 @@ export default function Home() {
<section className="bg-dark text-white py-8"> <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="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"> <div className="grid grid-cols-2 md:grid-cols-5 divide-x divide-white/8">
{features.map(({ icon: Icon, label, sub }, i) => ( {displayFeatures.map(({ icon, label, sub }, i) => {
<div const IconComponent = (Icons as any)[icon as string] || Icons.Star;
key={i} return (
className={`flex flex-col items-center justify-center gap-2 py-6 px-4 group ${i >= 2 ? "hidden md:flex" : ""}`} <div
> key={i}
<Icon className="w-5 h-5 text-accent group-hover:scale-110 transition-transform duration-300" /> className={`flex flex-col items-center justify-center gap-2 py-6 px-4 group ${i >= 2 ? "hidden md:flex" : ""}`}
<span className="text-[13px] font-semibold text-center">{label}</span> >
<span className="text-[11px] text-white/40 text-center">{sub}</span> <IconComponent className="w-5 h-5 text-accent group-hover:scale-110 transition-transform duration-300" />
</div> <span className="text-[13px] font-semibold text-center">{label}</span>
))} <span className="text-[11px] text-white/40 text-center">{sub}</span>
</div>
);
})}
</div> </div>
</div> </div>
</section> </section>
@@ -155,9 +203,10 @@ export default function Home() {
<ScrollReveal className="relative"> <ScrollReveal className="relative">
<div className="relative h-[540px] lg:h-[660px] rounded-3xl overflow-hidden shadow-2xl"> <div className="relative h-[540px] lg:h-[660px] rounded-3xl overflow-hidden shadow-2xl">
<Image <Image
src="https://images.unsplash.com/photo-1544551763-46a013bb70d5?q=80&w=2940&auto=format&fit=crop" src={aboutData.image}
alt="Akyaka Doğa" alt={aboutData.badge}
fill fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover" className="object-cover"
/> />
</div> </div>
@@ -165,38 +214,36 @@ export default function Home() {
<div className="absolute -bottom-5 -right-4 md:-right-8 bg-white rounded-2xl p-6 shadow-xl border border-sand"> <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="grid grid-cols-2 gap-6 divide-x divide-sand">
<div className="text-center"> <div className="text-center">
<span className="block font-serif text-3xl font-bold text-primary">500+</span> <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">Mutlu Misafir</span> <span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsGuestLabel}</span>
</div> </div>
<div className="text-center pl-6"> <div className="text-center pl-6">
<span className="block font-serif text-3xl font-bold text-primary">10+</span> <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">Yıl Deneyim</span> <span className="block text-[10px] text-muted uppercase tracking-wider mt-1">{aboutData.statsYearLabel}</span>
</div> </div>
</div> </div>
</div> </div>
{/* IKO badge */} {/* IKO badge */}
<div className="absolute -top-4 -left-4 bg-accent text-white rounded-2xl p-4 shadow-lg"> <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" /> <Star className="w-4 h-4 mb-1" fill="white" />
<span className="block text-xs font-bold leading-none">IKO</span> <span className="block text-xs font-bold leading-none">{aboutData.badgeIkoTitle}</span>
<span className="block text-[10px] opacity-80 mt-0.5">Sertifikalı</span> <span className="block text-[10px] opacity-80 mt-0.5">{aboutData.badgeIkoLabel}</span>
</div> </div>
</ScrollReveal> </ScrollReveal>
<ScrollReveal delay={200}> <ScrollReveal delay={200}>
<span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5"> <span className="inline-block text-accent text-xs tracking-[0.25em] uppercase font-semibold mb-5">
Hikayemiz {aboutData.badge}
</span> </span>
<h2 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold text-dark leading-[1.05] mb-8"> <h2 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold text-dark leading-[1.05] mb-8">
Doğanın<br />Kalbinde<br /> {aboutData.titleLine1}<br />{aboutData.titleLine2}<br />
<span className="text-primary italic">Özel Bir Kaçış</span> <span className="text-primary italic">{aboutData.titleHighlight}</span>
</h2> </h2>
<p className="text-gray-600 leading-relaxed mb-5"> <p className="text-gray-600 leading-relaxed mb-5">
Gökova Körfezi'nin kristal sularına dökülen Azmak Nehri'nin hemen kıyısında, {aboutData.description1}
rüzgarın hiç eksik olmadığı eşsiz bir konumda yer alıyoruz.
</p> </p>
<p className="text-gray-600 leading-relaxed mb-10"> <p className="text-gray-600 leading-relaxed mb-10">
Kite Beach Akyaka; kitesurf tutkusunu doğayla, konforla ve yerel lezzetlerle {aboutData.description2}
bütünleştiren benzersiz bir yaşam alanı sunar.
</p> </p>
<div className="section-line mb-8" /> <div className="section-line mb-8" />
<Link <Link
@@ -304,77 +351,46 @@ export default function Home() {
{/* Bento rows */} {/* Bento rows */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Row 1 */} {activities.length > 0 ? (
<ScrollReveal className="md:col-span-2"> activities.map((act, i) => (
<Link href="/aktiviteler" className="group relative block h-[400px] rounded-3xl overflow-hidden"> <ScrollReveal
<Image key={act.id}
src="https://images.unsplash.com/photo-1533000756353-909d73d2a3c7?q=80&w=2940&auto=format&fit=crop" className={i === 0 || i === 3 ? "md:col-span-2" : ""}
alt="Kitesurf" delay={i * 50}
fill >
className="object-cover group-hover:scale-105 transition-transform duration-700" <Link href="/aktiviteler" className={`group relative block rounded-3xl overflow-hidden ${i < 2 ? "h-[400px]" : "h-[280px]"}`}>
/> <Image
<div className="absolute inset-0 bg-gradient-to-t from-dark/90 via-dark/20 to-transparent" /> src={act.image}
<div className="absolute bottom-0 p-8"> alt={act.title}
<span className="block text-accent text-[10px] tracking-[0.25em] uppercase font-bold mb-2">IKO Sertifikalı</span> fill
<h3 className="font-serif text-4xl text-white font-bold mb-2">Kitesurf</h3> className="object-cover group-hover:scale-105 transition-transform duration-700"
<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"> />
Başlangıçtan ileri seviyeye. Akyaka'nın ünlü termik rüzgarında uçun. <div className={`absolute inset-0 bg-gradient-to-t ${i === 0 ? "from-dark/90 via-dark/20" : "from-dark/80"} to-transparent`} />
</p> <div className={`absolute bottom-0 ${i === 0 || i === 3 ? "p-8" : "p-7"}`}>
<span className="inline-flex items-center gap-2 text-white text-sm font-semibold"> {act.badge && (
2.000/Saat <ArrowRight className="w-3.5 h-3.5" /> <span className="block text-accent text-[10px] tracking-[0.25em] uppercase font-bold mb-2">{act.badge}</span>
</span> )}
</div> <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>
</Link> {i === 0 && (
</ScrollReveal> <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}
<ScrollReveal delay={100}> </p>
<Link href="/aktiviteler" className="group relative block h-[400px] rounded-3xl overflow-hidden"> )}
<Image {i !== 0 && (
src="https://images.unsplash.com/photo-1599901860904-17e08627c271?q=80&w=2940&auto=format&fit=crop" <span className="text-white/60 text-xs">{act.price}</span>
alt="Yoga" )}
fill {i === 0 && (
className="object-cover group-hover:scale-105 transition-transform duration-700" <span className="inline-flex items-center gap-2 text-white text-sm font-semibold">
/> {act.price} <ArrowRight className="w-3.5 h-3.5" />
<div className="absolute inset-0 bg-gradient-to-t from-dark/80 to-transparent" /> </span>
<div className="absolute bottom-0 p-7"> )}
<h3 className="font-serif text-3xl text-white font-bold mb-1">Yoga</h3> </div>
<span className="text-white/60 text-xs">400/Seans</span> </Link>
</div> </ScrollReveal>
</Link> ))
</ScrollReveal> ) : (
<div className="md:col-span-3 py-10 text-center text-gray-500">Henüz aktivite eklenmedi.</div>
{/* Row 2 */} )}
<ScrollReveal delay={150}>
<Link href="/aktiviteler" className="group relative block h-[280px] rounded-3xl overflow-hidden">
<Image
src="https://images.unsplash.com/photo-1544606771-46abcf9dbb83?q=80&w=2924&auto=format&fit=crop"
alt="SUP"
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/80 to-transparent" />
<div className="absolute bottom-0 p-7">
<h3 className="font-serif text-2xl text-white font-bold mb-1">Stand-Up Paddle</h3>
<span className="text-white/60 text-xs">300/Saat</span>
</div>
</Link>
</ScrollReveal>
<ScrollReveal className="md:col-span-2" delay={200}>
<Link href="/aktiviteler" className="group relative block h-[280px] rounded-3xl overflow-hidden">
<Image
src="https://images.unsplash.com/photo-1511994298241-608e28f14fde?q=80&w=2940&auto=format&fit=crop"
alt="Bisiklet"
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-dark/80 to-transparent" />
<div className="absolute bottom-0 p-8">
<h3 className="font-serif text-3xl text-white font-bold mb-1">Bisiklet Kiralama</h3>
<span className="text-white/60 text-xs">250/Gün · Akyaka Köy Turu</span>
</div>
</Link>
</ScrollReveal>
</div> </div>
</div> </div>
</section> </section>
@@ -404,42 +420,50 @@ export default function Home() {
</ScrollReveal> </ScrollReveal>
<ScrollReveal delay={100}> <ScrollReveal delay={100}>
<div 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"> {events.length > 0 ? (
<div className="relative w-full md:w-1/3 h-[250px] md:h-auto rounded-2xl overflow-hidden shrink-0"> events.map((ev) => (
<Image <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">
src="https://images.unsplash.com/photo-1540039155732-680c74f07599?q=80&w=2832&auto=format&fit=crop" <div className="relative w-full md:w-1/3 h-[250px] md:h-auto rounded-2xl overflow-hidden shrink-0">
alt="Mahmut Orhan Konseri" <Image
fill src={ev.image}
className="object-cover group-hover:scale-105 transition-transform duration-700" alt={ev.title}
/> fill
</div> className="object-cover group-hover:scale-105 transition-transform duration-700"
<div className="flex flex-col justify-center w-full md:w-2/3"> />
<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"> </div>
Yaklaşan Etkinlik <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>
<h3 className="font-serif text-3xl md:text-4xl font-bold mb-4">Mahmut Orhan - Sunset Session</h3> ))
<p className="text-white/60 leading-relaxed mb-6 max-w-xl"> ) : (
Dünyaca ünlü DJ Mahmut Orhan, unutulmaz bir gün batımı performansı için Kite Beach Akyaka'da! Ege'nin muhteşem manzarasına karşı elektronik müziğin ritmini hissedeceğiniz bu özel geceyi kaçırmayın. <div className="py-10 text-center text-white/50">Henüz etkinlik eklenmedi.</div>
</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" /> 15 Ağustos 2026
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> 18:00
</span>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent" /> Beach Club
</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>
</ScrollReveal> </ScrollReveal>
</div> </div>
</section> </section>
+65
View File
@@ -0,0 +1,65 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function updateAboutSection(prevState: any, formData: FormData) {
try {
const lang = formData.get("lang") as string;
const badge = formData.get("badge") as string;
const titleLine1 = formData.get("titleLine1") as string;
const titleLine2 = formData.get("titleLine2") as string;
const titleHighlight = formData.get("titleHighlight") as string;
const description1 = formData.get("description1") as string;
const description2 = formData.get("description2") as string;
const image = formData.get("image") as string;
const statsGuestCount = formData.get("statsGuestCount") as string;
const statsGuestLabel = formData.get("statsGuestLabel") as string;
const statsYearCount = formData.get("statsYearCount") as string;
const statsYearLabel = formData.get("statsYearLabel") as string;
const badgeIkoTitle = formData.get("badgeIkoTitle") as string;
const badgeIkoLabel = formData.get("badgeIkoLabel") as string;
await prisma.aboutSection.upsert({
where: { lang },
update: {
badge,
titleLine1,
titleLine2,
titleHighlight,
description1,
description2,
image,
statsGuestCount,
statsGuestLabel,
statsYearCount,
statsYearLabel,
badgeIkoTitle,
badgeIkoLabel
},
create: {
lang,
badge,
titleLine1,
titleLine2,
titleHighlight,
description1,
description2,
image,
statsGuestCount,
statsGuestLabel,
statsYearCount,
statsYearLabel,
badgeIkoTitle,
badgeIkoLabel
}
});
revalidatePath('/admin/about');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Hakkımızda bölümü başarıyla güncellendi." };
} catch (error) {
return { error: "Güncellenirken bir hata oluştu." };
}
}
+152
View File
@@ -0,0 +1,152 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function loadMockActivities() {
await prisma.activity.deleteMany({});
const mockActivities = [
{
lang: "tr",
title: "Kitesurf",
description: "Başlangıçtan ileri seviyeye. Akyaka'nın ünlü termik rüzgarında uçun.",
price: "₺2.000/Saat",
image: "https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop",
badge: "IKO Sertifikalı",
order: 1
},
{
lang: "en",
title: "Kitesurf",
description: "From beginner to advanced. Fly in Akyaka's famous thermal wind.",
price: "₺2.000/Hour",
image: "https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop",
badge: "IKO Certified",
order: 1
},
{
lang: "tr",
title: "Yoga",
description: "Güne nehir kenarında huzur dolu bir yoga seansıyla başlayın.",
price: "₺400/Seans",
image: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 2
},
{
lang: "en",
title: "Yoga",
description: "Start the day with a peaceful yoga session by the river.",
price: "₺400/Session",
image: "https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 2
},
{
lang: "tr",
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",
badge: null,
order: 3
},
{
lang: "en",
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",
badge: null,
order: 3
},
{
lang: "tr",
title: "Bisiklet Kiralama",
description: "Akyaka Köy Turu ile doğanın tadını çıkarın.",
price: "₺250/Gün",
image: "https://images.unsplash.com/photo-1511994298241-608e28f14fde?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 4
},
{
lang: "en",
title: "Bicycle Rental",
description: "Enjoy nature with an Akyaka Village Tour.",
price: "₺250/Day",
image: "https://images.unsplash.com/photo-1511994298241-608e28f14fde?q=80&w=2940&auto=format&fit=crop",
badge: null,
order: 4
}
];
await prisma.activity.createMany({ data: mockActivities });
revalidatePath('/admin/activities');
revalidatePath('/tr');
revalidatePath('/en');
}
export async function addActivity(prevState: any, formData: FormData) {
try {
const data = {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") 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),
};
await prisma.activity.create({ data });
revalidatePath('/admin/activities');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Aktivite başarıyla eklendi." };
} catch (error) {
return { error: "Aktivite eklenirken bir hata oluştu." };
}
}
export async function updateActivity(prevState: any, formData: FormData) {
try {
const id = parseInt(formData.get("id") as string, 10);
const data = {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") 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),
};
await prisma.activity.update({
where: { id },
data
});
revalidatePath('/admin/activities');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Aktivite başarıyla güncellendi." };
} catch (error) {
return { error: "Aktivite güncellenirken bir hata oluştu." };
}
}
export async function deleteActivity(id: number) {
try {
await prisma.activity.delete({ where: { id } });
revalidatePath('/admin/activities');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Aktivite silindi." };
} catch (error) {
return { error: "Silme işlemi başarısız." };
}
}
+34
View File
@@ -0,0 +1,34 @@
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
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");
if (
username === process.env.ADMIN_USERNAME &&
password === process.env.ADMIN_PASSWORD
) {
const cookieStore = await cookies();
cookieStore.set("admin_session", "true", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7, // 1 week
path: "/",
});
} else {
return { error: "Geçersiz kullanıcı adı veya şifre" };
}
redirect("/admin");
}
export async function logout() {
const cookieStore = await cookies();
cookieStore.delete("admin_session");
redirect("/admin/login");
}
+102
View File
@@ -0,0 +1,102 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function loadMockEvents() {
await prisma.event.deleteMany({});
const mockEvents = [
{
lang: "tr",
title: "Mahmut Orhan - Sunset Session",
description: "Dünyaca ünlü DJ Mahmut Orhan, unutulmaz bir gün batımı performansı için Kite Beach Akyaka'da! Ege'nin muhteşem manzarasına karşı elektronik müziğin ritmini hissedeceğiniz bu özel geceyi kaçırmayın.",
date: "15 Ağustos 2026",
time: "18:00",
location: "Beach Club",
image: "https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?q=80&w=2832&auto=format&fit=crop",
badge: "Yaklaşan Etkinlik"
},
{
lang: "en",
title: "Mahmut Orhan - Sunset Session",
description: "World famous DJ Mahmut Orhan is at Kite Beach Akyaka for an unforgettable sunset performance! Do not miss this special night where you will feel the rhythm of electronic music against the magnificent view of the Aegean.",
date: "15 August 2026",
time: "18:00",
location: "Beach Club",
image: "https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?q=80&w=2832&auto=format&fit=crop",
badge: "Upcoming Event"
}
];
await prisma.event.createMany({ data: mockEvents });
revalidatePath('/admin/events');
revalidatePath('/tr');
revalidatePath('/en');
}
export async function addEvent(prevState: any, formData: FormData) {
try {
const data = {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") as string,
date: formData.get("date") as string,
time: formData.get("time") as string,
location: formData.get("location") as string,
image: formData.get("image") as string,
badge: formData.get("badge") as string || null,
};
await prisma.event.create({ data });
revalidatePath('/admin/events');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Etkinlik başarıyla eklendi." };
} catch (error) {
return { error: "Etkinlik eklenirken bir hata oluştu." };
}
}
export async function updateEvent(prevState: any, formData: FormData) {
try {
const id = parseInt(formData.get("id") as string, 10);
const data = {
lang: formData.get("lang") as string,
title: formData.get("title") as string,
description: formData.get("description") as string,
date: formData.get("date") as string,
time: formData.get("time") as string,
location: formData.get("location") as string,
image: formData.get("image") as string,
badge: formData.get("badge") as string || null,
};
await prisma.event.update({
where: { id },
data
});
revalidatePath('/admin/events');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Etkinlik başarıyla güncellendi." };
} catch (error) {
return { error: "Etkinlik güncellenirken bir hata oluştu." };
}
}
export async function deleteEvent(id: number) {
try {
await prisma.event.delete({ where: { id } });
revalidatePath('/admin/events');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Etkinlik silindi." };
} catch (error) {
return { error: "Silme işlemi başarısız." };
}
}
+86
View File
@@ -0,0 +1,86 @@
"use server";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function loadMockFeatures() {
await prisma.feature.deleteMany({});
const mockFeatures = [
{ lang: "tr", icon: "Wind", label: "Kitesurf Okulu", sub: "IKO Sertifikalı", order: 1 },
{ lang: "en", icon: "Wind", label: "Kitesurf School", sub: "IKO Certified", order: 1 },
{ lang: "tr", icon: "Anchor", label: "Yoga Platformu", sub: "Nehir Kenarı", order: 2 },
{ 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: "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 },
];
await prisma.feature.createMany({ data: mockFeatures });
revalidatePath('/admin/features');
revalidatePath('/tr');
revalidatePath('/en');
}
export async function addFeature(prevState: any, formData: FormData) {
try {
const data = {
lang: formData.get("lang") as string,
icon: formData.get("icon") as string,
label: formData.get("label") as string,
sub: formData.get("sub") as string,
order: parseInt(formData.get("order") as string || "0", 10),
};
await prisma.feature.create({ data });
revalidatePath('/admin/features');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Özellik başarıyla eklendi." };
} catch (error) {
return { error: "Özellik eklenirken bir hata oluştu." };
}
}
export async function updateFeature(prevState: any, formData: FormData) {
try {
const id = parseInt(formData.get("id") as string, 10);
const data = {
lang: formData.get("lang") as string,
icon: formData.get("icon") as string,
label: formData.get("label") as string,
sub: formData.get("sub") as string,
order: parseInt(formData.get("order") as string || "0", 10),
};
await prisma.feature.update({
where: { id },
data
});
revalidatePath('/admin/features');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Özellik başarıyla güncellendi." };
} catch (error) {
return { error: "Özellik güncellenirken bir hata oluştu." };
}
}
export async function deleteFeature(id: number) {
try {
await prisma.feature.delete({ where: { id } });
revalidatePath('/admin/features');
revalidatePath('/tr');
revalidatePath('/en');
return { success: true, message: "Özellik silindi." };
} catch (error) {
return { error: "Silme işlemi başarısız." };
}
}
+48
View File
@@ -0,0 +1,48 @@
"use server";
import { revalidatePath } from "next/cache";
import prisma from "@/lib/prisma";
export type HeroActionState = {
success?: boolean;
message?: string;
} | null;
export async function updateHero(prevState: HeroActionState, formData: FormData): Promise<HeroActionState> {
const lang = formData.get("lang") as string;
const location = formData.get("location") as string;
const welcome = formData.get("welcome") as string;
const title = formData.get("title") as string;
const subtitle = formData.get("subtitle") as string;
const bookNow = formData.get("bookNow") as string;
const exploreActivities = formData.get("exploreActivities") as string;
const scroll = formData.get("scroll") as string;
if (!lang) {
return { success: false, message: "Dil seçimi eksik." };
}
await prisma.heroSection.upsert({
where: { lang },
update: { location, welcome, title, subtitle, bookNow, exploreActivities, scroll },
create: {
lang,
location,
welcome,
title,
subtitle,
bookNow,
exploreActivities,
scroll
}
});
revalidatePath('/admin/hero');
revalidatePath(`/${lang}`);
revalidatePath('/');
return {
success: true,
message: `${lang.toUpperCase()} Hero ayarları başarıyla kaydedildi!`
};
}
+38
View File
@@ -0,0 +1,38 @@
"use server";
import { revalidatePath } from "next/cache";
import prisma from "@/lib/prisma";
export type SettingsActionState = {
success?: boolean;
message?: string;
} | null;
export async function updateSettings(prevState: SettingsActionState, formData: FormData): Promise<SettingsActionState> {
const title = formData.get("title") as string;
const email = formData.get("email") as string;
const phone = formData.get("phone") as string;
const address = formData.get("address") as string;
const instagram = formData.get("instagram") as string;
await prisma.siteSettings.upsert({
where: { id: 1 },
update: { title, email, phone, address, instagram },
create: {
id: 1,
title,
email,
phone,
address,
instagram
}
});
revalidatePath('/admin/settings');
revalidatePath('/');
return {
success: true,
message: "Ayarlar başarıyla veritabanına kaydedildi!"
};
}
+219
View File
@@ -0,0 +1,219 @@
"use client";
import { useState, useTransition } from "react";
import { AboutSection } from "@prisma/client";
import { Save } from "lucide-react";
import { updateAboutSection } from "@/app/actions/about";
export default function AboutForm({ trData, enData }: { trData: AboutSection | null, enData: AboutSection | null }) {
const [activeTab, setActiveTab] = useState<"tr" | "en">("tr");
const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const handleSubmit = async (formData: FormData) => {
setMessage(null);
formData.append("lang", activeTab);
startTransition(async () => {
const result = await updateAboutSection(null, formData);
if (result?.error) {
setMessage({ type: 'error', text: result.error });
} else if (result?.success) {
setMessage({ type: 'success', text: result.message });
}
});
};
const activeData = activeTab === "tr" ? trData : enData;
return (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden animate-fade-up">
{/* Tabs */}
<div className="flex border-b border-gray-100 bg-gray-50/50 p-2">
<button
onClick={() => setActiveTab("tr")}
className={`flex-1 py-3 px-6 rounded-2xl text-sm font-bold tracking-widest uppercase transition-all ${
activeTab === "tr"
? "bg-white text-primary shadow-sm border border-gray-200"
: "text-gray-400 hover:text-dark hover:bg-white/50"
}`}
>
🇹🇷 Türkçe
</button>
<button
onClick={() => setActiveTab("en")}
className={`flex-1 py-3 px-6 rounded-2xl text-sm font-bold tracking-widest uppercase transition-all ${
activeTab === "en"
? "bg-white text-primary shadow-sm border border-gray-200"
: "text-gray-400 hover:text-dark hover:bg-white/50"
}`}
>
🇬🇧 English
</button>
</div>
<div className="p-8 md:p-12">
<form action={handleSubmit} className="space-y-8">
{message && (
<div className={`p-4 rounded-xl text-sm font-medium ${message.type === 'success' ? 'bg-green-50 text-green-600 border border-green-200' : 'bg-red-50 text-red-600 border border-red-200'}`}>
{message.text}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<h3 className="text-lg font-serif font-bold text-dark border-b border-gray-100 pb-3">Ana Başlıklar</h3>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Üst Başlık (Rozet)</label>
<input
type="text"
name="badge"
defaultValue={activeData?.badge || "Hikayemiz"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Başlık 1. Satır</label>
<input
type="text"
name="titleLine1"
defaultValue={activeData?.titleLine1 || "Doğanın"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Başlık 2. Satır</label>
<input
type="text"
name="titleLine2"
defaultValue={activeData?.titleLine2 || "Kalbinde"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Vurgulu Başlık</label>
<input
type="text"
name="titleHighlight"
defaultValue={activeData?.titleHighlight || "Özel Bir Kaçış"}
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"
/>
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-serif font-bold text-dark border-b border-gray-100 pb-3">İçerik ve Görsel</h3>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Açıklama Paragraf 1</label>
<textarea
name="description1"
defaultValue={activeData?.description1 || "Gökova Körfezi'nin kristal sularına dökülen Azmak Nehri'nin hemen kıyısında, rüzgarın hiç eksik olmadığı eşsiz bir konumda yer alıyoruz."}
rows={3}
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 resize-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Açıklama Paragraf 2</label>
<textarea
name="description2"
defaultValue={activeData?.description2 || "Kite Beach Akyaka; kitesurf tutkusunu doğayla, konforla ve yerel lezzetlerle bütünleştiren benzersiz bir yaşam alanı sunar."}
rows={3}
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 resize-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Görsel URL</label>
<input
type="url"
name="image"
defaultValue={activeData?.image || "https://images.unsplash.com/photo-1544551763-46a013bb70d5?q=80&w=2940&auto=format&fit=crop"}
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"
/>
</div>
</div>
<div className="space-y-6 md:col-span-2">
<h3 className="text-lg font-serif font-bold text-dark border-b border-gray-100 pb-3">İstatistikler ve Rozetler</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">İstatistik 1 (Sayı)</label>
<input
type="text"
name="statsGuestCount"
defaultValue={activeData?.statsGuestCount || "500+"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">İstatistik 1 (Metin)</label>
<input
type="text"
name="statsGuestLabel"
defaultValue={activeData?.statsGuestLabel || "Mutlu Misafir"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">İstatistik 2 (Sayı)</label>
<input
type="text"
name="statsYearCount"
defaultValue={activeData?.statsYearCount || "10+"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">İstatistik 2 (Metin)</label>
<input
type="text"
name="statsYearLabel"
defaultValue={activeData?.statsYearLabel || "Yıl Deneyim"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Rozet Başlık (Sol Üst)</label>
<input
type="text"
name="badgeIkoTitle"
defaultValue={activeData?.badgeIkoTitle || "IKO"}
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"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Rozet Alt Metin</label>
<input
type="text"
name="badgeIkoLabel"
defaultValue={activeData?.badgeIkoLabel || "Sertifikalı"}
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"
/>
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-8 mt-8 border-t border-gray-100">
<button
type="submit"
disabled={isPending}
className="flex items-center gap-2 bg-accent text-white font-bold tracking-widest uppercase py-4 px-10 rounded-xl hover:bg-accent/90 transition-all shadow-lg shadow-accent/20 text-sm disabled:opacity-50"
>
<Save className="w-5 h-5" />
{isPending ? "Kaydediliyor..." : "Kaydet"}
</button>
</div>
</form>
</div>
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import prisma from "@/lib/prisma";
import AboutForm from "./AboutForm";
export default async function AboutPage() {
const [trData, enData] = await Promise.all([
prisma.aboutSection.findUnique({ where: { lang: "tr" } }),
prisma.aboutSection.findUnique({ where: { lang: "en" } })
]);
return (
<div className="max-w-4xl mx-auto w-full">
<div className="mb-10">
<h1 className="text-4xl font-black font-serif text-dark tracking-tight">Hakkımızda Bölümü</h1>
<p className="text-gray-500 mt-2 font-medium">
Anasayfadaki "Hikayemiz" bölümünü ve ilgili istatistikleri buradan düzenleyebilirsiniz.
</p>
</div>
<AboutForm trData={trData} enData={enData} />
</div>
);
}
+255
View File
@@ -0,0 +1,255 @@
"use client";
import { useState, useTransition } from "react";
import { Activity } from "@prisma/client";
import { Plus, Edit2, Trash2, X, Save } from "lucide-react";
import { loadMockActivities, addActivity, updateActivity, deleteActivity } from "@/app/actions/activities";
export default function ActivitiesClient({ activities }: { activities: Activity[] }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingActivity, setEditingActivity] = useState<Activity | null>(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const openAddModal = () => {
setEditingActivity(null);
setError(null);
setIsModalOpen(true);
};
const openEditModal = (activity: Activity) => {
setEditingActivity(activity);
setError(null);
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingActivity(null);
setError(null);
};
const handleDelete = (id: number) => {
if (confirm("Bu aktiviteyi silmek istediğinize emin misiniz?")) {
startTransition(async () => {
const result = await deleteActivity(id);
if (result.error) {
alert(result.error);
}
});
}
};
const handleSubmit = async (formData: FormData) => {
setError(null);
if (editingActivity) {
formData.append("id", editingActivity.id.toString());
const result = await updateActivity(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
} else {
const result = await addActivity(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
}
};
return (
<>
<div className="flex justify-between items-center mb-10">
<h1 className="text-4xl font-black font-serif text-dark tracking-tight">Aktiviteler</h1>
<div className="flex items-center gap-3">
<form action={loadMockActivities}>
<button type="submit" className="bg-white text-dark border border-gray-200 px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-gray-50 hover:shadow-lg transition-all text-sm flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
Örnek Verileri Yükle
</button>
</form>
<button
onClick={openAddModal}
className="bg-dark text-white px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-dark/90 hover:shadow-lg transition-all text-sm flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Yeni Ekle
</button>
</div>
</div>
{activities.length === 0 ? (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 p-12 text-center text-gray-500 font-medium">
Henüz aktivite eklenmemiş. Sağ üstten yeni ekleyebilir veya örnek verileri yükleyebilirsiniz.
</div>
) : (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden animate-fade-up">
<table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-50/50 border-b border-gray-100 text-gray-500 uppercase tracking-widest text-[10px] font-bold">
<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>
<tbody className="divide-y divide-gray-50">
{activities.map(act => (
<tr key={act.id} className="hover:bg-gray-50/80 transition-colors group">
<td className="px-8 py-5 font-bold text-dark">{act.title}</td>
<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
onClick={() => openEditModal(act)}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
title="Düzenle"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(act.id)}
disabled={isPending}
className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500/20 transition-colors disabled:opacity-50"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<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="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"}
</h2>
<button onClick={closeModal} className="p-2 text-gray-400 hover:text-dark hover:bg-gray-200 rounded-full transition-colors">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<form action={handleSubmit} className="space-y-6">
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Dil</label>
<select
name="lang"
defaultValue={editingActivity?.lang || "tr"}
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"
>
<option value="tr">Türkçe</option>
<option value="en">English</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Başlık</label>
<input
type="text"
name="title"
defaultValue={editingActivity?.title || ""}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Açıklama</label>
<textarea
name="description"
defaultValue={editingActivity?.description || ""}
required
rows={3}
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 resize-none"
/>
</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
type="text"
name="badge"
defaultValue={editingActivity?.badge || ""}
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: Çok Satan"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Görsel URL</label>
<input
type="url"
name="image"
defaultValue={editingActivity?.image || ""}
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="https://..."
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Sıra (Görünüm Sırası)</label>
<input
type="number"
name="order"
defaultValue={editingActivity?.order || 0}
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"
/>
</div>
</div>
<div className="flex justify-end pt-6 mt-6 border-t border-gray-100">
<button
type="submit"
className="flex items-center gap-2 bg-accent text-white font-bold tracking-widest uppercase py-4 px-8 rounded-xl hover:bg-accent/90 transition-all shadow-lg shadow-accent/20 text-sm"
>
<Save className="w-4 h-4" />
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
)}
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import prisma from "@/lib/prisma";
import ActivitiesClient from "./ActivitiesClient";
export default async function ActivitiesPage() {
const activities = await prisma.activity.findMany({
orderBy: { order: 'asc' }
});
return (
<div className="max-w-5xl mx-auto w-full">
<ActivitiesClient activities={activities} />
</div>
);
}
+46
View File
@@ -0,0 +1,46 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { LayoutDashboard, Settings, Image as ImageIcon, Compass, CalendarDays, Star, Info } from "lucide-react";
export default function SidebarNav() {
const pathname = usePathname();
const navLinks = [
{ name: "Gösterge Paneli", href: "/admin", icon: LayoutDashboard },
{ name: "Site Ayarları", href: "/admin/settings", icon: Settings },
{ name: "Hero Bölümü", href: "/admin/hero", icon: ImageIcon },
{ name: "Hakkımızda", href: "/admin/about", icon: Info },
{ name: "Özellikler", href: "/admin/features", icon: Star },
{ name: "Aktiviteler", href: "/admin/activities", icon: Compass },
{ name: "Etkinlikler", href: "/admin/events", icon: CalendarDays },
];
return (
<nav className="flex-1 px-4 py-6 space-y-2 overflow-y-auto">
{navLinks.map((link) => {
const isActive = pathname === link.href || (link.href !== "/admin" && pathname?.startsWith(link.href));
const Icon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 relative group ${
isActive
? "bg-white/10 text-white shadow-lg backdrop-blur-md"
: "text-white/60 hover:bg-white/5 hover:text-white"
}`}
>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1.5 h-8 bg-accent rounded-r-full shadow-[0_0_10px_rgba(var(--accent),0.5)]" />
)}
<Icon className={`w-5 h-5 transition-transform duration-300 ${isActive ? "scale-110" : "group-hover:scale-110"}`} />
<span className="font-medium text-sm tracking-wide">{link.name}</span>
</Link>
);
})}
</nav>
);
}
+269
View File
@@ -0,0 +1,269 @@
"use client";
import { useState, useTransition } from "react";
import { Event } from "@prisma/client";
import { Plus, Edit2, Trash2, X, Save } from "lucide-react";
import { loadMockEvents, addEvent, updateEvent, deleteEvent } from "@/app/actions/events";
export default function EventsClient({ events }: { events: Event[] }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const openAddModal = () => {
setEditingEvent(null);
setError(null);
setIsModalOpen(true);
};
const openEditModal = (event: Event) => {
setEditingEvent(event);
setError(null);
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingEvent(null);
setError(null);
};
const handleDelete = (id: number) => {
if (confirm("Bu etkinliği silmek istediğinize emin misiniz?")) {
startTransition(async () => {
const result = await deleteEvent(id);
if (result.error) {
alert(result.error);
}
});
}
};
const handleSubmit = async (formData: FormData) => {
setError(null);
if (editingEvent) {
formData.append("id", editingEvent.id.toString());
const result = await updateEvent(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
} else {
const result = await addEvent(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
}
};
return (
<>
<div className="flex justify-between items-center mb-10">
<h1 className="text-4xl font-black font-serif text-dark tracking-tight">Etkinlikler</h1>
<div className="flex items-center gap-3">
<form action={loadMockEvents}>
<button type="submit" className="bg-white text-dark border border-gray-200 px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-gray-50 hover:shadow-lg transition-all text-sm flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
Örnek Verileri Yükle
</button>
</form>
<button
onClick={openAddModal}
className="bg-dark text-white px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-dark/90 hover:shadow-lg transition-all text-sm flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Yeni Ekle
</button>
</div>
</div>
{events.length === 0 ? (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 p-12 text-center text-gray-500 font-medium">
Henüz etkinlik eklenmemiş. Sağ üstten yeni ekleyebilir veya örnek verileri yükleyebilirsiniz.
</div>
) : (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden animate-fade-up">
<table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-50/50 border-b border-gray-100 text-gray-500 uppercase tracking-widest text-[10px] font-bold">
<tr>
<th className="px-8 py-5">Etkinlik Adı</th>
<th className="px-8 py-5">Dil</th>
<th className="px-8 py-5">Tarih</th>
<th className="px-8 py-5 text-right">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{events.map(ev => (
<tr key={ev.id} className="hover:bg-gray-50/80 transition-colors group">
<td className="px-8 py-5 font-bold text-dark">{ev.title}</td>
<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">{ev.lang}</span>
</td>
<td className="px-8 py-5 font-medium">{ev.date} {ev.time}</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
onClick={() => openEditModal(ev)}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
title="Düzenle"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(ev.id)}
disabled={isPending}
className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500/20 transition-colors disabled:opacity-50"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<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="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"}
</h2>
<button onClick={closeModal} className="p-2 text-gray-400 hover:text-dark hover:bg-gray-200 rounded-full transition-colors">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<form action={handleSubmit} className="space-y-6">
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Dil</label>
<select
name="lang"
defaultValue={editingEvent?.lang || "tr"}
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"
>
<option value="tr">Türkçe</option>
<option value="en">English</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Etkinlik Adı</label>
<input
type="text"
name="title"
defaultValue={editingEvent?.title || ""}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Açıklama</label>
<textarea
name="description"
defaultValue={editingEvent?.description || ""}
required
rows={3}
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 resize-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Tarih</label>
<input
type="text"
name="date"
defaultValue={editingEvent?.date || ""}
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: 15 Ağustos 2026"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Saat</label>
<input
type="text"
name="time"
defaultValue={editingEvent?.time || ""}
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: 18:00"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Lokasyon</label>
<input
type="text"
name="location"
defaultValue={editingEvent?.location || ""}
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: Beach Club"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Rozet (Opsiyonel)</label>
<input
type="text"
name="badge"
defaultValue={editingEvent?.badge || ""}
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: Yaklaşan Etkinlik"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Görsel URL</label>
<input
type="url"
name="image"
defaultValue={editingEvent?.image || ""}
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="https://..."
/>
</div>
</div>
<div className="flex justify-end pt-6 mt-6 border-t border-gray-100">
<button
type="submit"
className="flex items-center gap-2 bg-accent text-white font-bold tracking-widest uppercase py-4 px-8 rounded-xl hover:bg-accent/90 transition-all shadow-lg shadow-accent/20 text-sm"
>
<Save className="w-4 h-4" />
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
)}
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import prisma from "@/lib/prisma";
import EventsClient from "./EventsClient";
export default async function EventsPage() {
const events = await prisma.event.findMany({
orderBy: { date: 'asc' }
});
return (
<div className="max-w-5xl mx-auto w-full">
<EventsClient events={events} />
</div>
);
}
+250
View File
@@ -0,0 +1,250 @@
"use client";
import { useState, useTransition } from "react";
import { Feature } from "@prisma/client";
import { Plus, Edit2, Trash2, X, Save } from "lucide-react";
import * as Icons from "lucide-react";
import { loadMockFeatures, addFeature, updateFeature, deleteFeature } from "@/app/actions/features";
export default function FeaturesClient({ features }: { features: Feature[] }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const openAddModal = () => {
setEditingFeature(null);
setError(null);
setIsModalOpen(true);
};
const openEditModal = (feature: Feature) => {
setEditingFeature(feature);
setError(null);
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingFeature(null);
setError(null);
};
const handleDelete = (id: number) => {
if (confirm("Bu özelliği silmek istediğinize emin misiniz?")) {
startTransition(async () => {
const result = await deleteFeature(id);
if (result.error) {
alert(result.error);
}
});
}
};
const handleSubmit = async (formData: FormData) => {
setError(null);
if (editingFeature) {
formData.append("id", editingFeature.id.toString());
const result = await updateFeature(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
} else {
const result = await addFeature(null, formData);
if (result.error) {
setError(result.error);
} else {
closeModal();
}
}
};
const renderIcon = (iconName: string) => {
const IconComponent = (Icons as any)[iconName];
if (!IconComponent) return <div className="w-5 h-5 bg-gray-200 rounded-full" />;
return <IconComponent className="w-5 h-5 text-accent" />;
};
return (
<>
<div className="flex justify-between items-center mb-10">
<h1 className="text-4xl font-black font-serif text-dark tracking-tight">Özellikler</h1>
<div className="flex items-center gap-3">
<form action={loadMockFeatures}>
<button type="submit" className="bg-white text-dark border border-gray-200 px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-gray-50 hover:shadow-lg transition-all text-sm flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
Örnek Verileri Yükle
</button>
</form>
<button
onClick={openAddModal}
className="bg-dark text-white px-6 py-2.5 rounded-xl font-bold tracking-wide hover:bg-dark/90 hover:shadow-lg transition-all text-sm flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Yeni Ekle
</button>
</div>
</div>
{features.length === 0 ? (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 p-12 text-center text-gray-500 font-medium">
Henüz özellik eklenmemiş. Sağ üstten yeni ekleyebilir veya örnek verileri yükleyebilirsiniz.
</div>
) : (
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden animate-fade-up">
<table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-50/50 border-b border-gray-100 text-gray-500 uppercase tracking-widest text-[10px] font-bold">
<tr>
<th className="px-8 py-5">İkon</th>
<th className="px-8 py-5">Ana Başlık</th>
<th className="px-8 py-5">Alt Başlık</th>
<th className="px-8 py-5">Dil</th>
<th className="px-8 py-5">Sıra</th>
<th className="px-8 py-5 text-right">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{features.map(feat => (
<tr key={feat.id} className="hover:bg-gray-50/80 transition-colors group">
<td className="px-8 py-5">
{renderIcon(feat.icon)}
</td>
<td className="px-8 py-5 font-bold text-dark">{feat.label}</td>
<td className="px-8 py-5">{feat.sub}</td>
<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">{feat.lang}</span>
</td>
<td className="px-8 py-5 font-medium">{feat.order}</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
onClick={() => openEditModal(feat)}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
title="Düzenle"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(feat.id)}
disabled={isPending}
className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500/20 transition-colors disabled:opacity-50"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<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="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">
{editingFeature ? "Özellik Düzenle" : "Yeni Özellik Ekle"}
</h2>
<button onClick={closeModal} className="p-2 text-gray-400 hover:text-dark hover:bg-gray-200 rounded-full transition-colors">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<form action={handleSubmit} className="space-y-6">
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Dil</label>
<select
name="lang"
defaultValue={editingFeature?.lang || "tr"}
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"
>
<option value="tr">Türkçe</option>
<option value="en">English</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Ana Başlık</label>
<input
type="text"
name="label"
defaultValue={editingFeature?.label || ""}
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: Kitesurf Okulu"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Alt Başlık</label>
<input
type="text"
name="sub"
defaultValue={editingFeature?.sub || ""}
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: IKO Sertifikalı"
/>
</div>
<div>
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">Sıra</label>
<input
type="number"
name="order"
defaultValue={editingFeature?.order || 0}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-xs font-bold text-gray-500 tracking-wider uppercase mb-2">İkon Adı (Lucide React)</label>
<input
type="text"
name="icon"
defaultValue={editingFeature?.icon || "Wind"}
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: Wind, Anchor, Utensils, BedDouble, Sun"
/>
<p className="text-xs text-gray-400 mt-2">
Geçerli ikon isimleri: Wind, Anchor, Utensils, BedDouble, Sun, vb.
</p>
</div>
</div>
<div className="flex justify-end pt-6 mt-6 border-t border-gray-100">
<button
type="submit"
className="flex items-center gap-2 bg-accent text-white font-bold tracking-widest uppercase py-4 px-8 rounded-xl hover:bg-accent/90 transition-all shadow-lg shadow-accent/20 text-sm"
>
<Save className="w-4 h-4" />
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
)}
</>
);
}
+17
View File
@@ -0,0 +1,17 @@
import prisma from "@/lib/prisma";
import FeaturesClient from "./FeaturesClient";
export default async function FeaturesPage() {
const features = await prisma.feature.findMany({
orderBy: [
{ lang: 'asc' },
{ order: 'asc' }
]
});
return (
<div className="max-w-5xl mx-auto w-full">
<FeaturesClient features={features} />
</div>
);
}
+169
View File
@@ -0,0 +1,169 @@
"use client";
import { useActionState, useState } from "react";
import { updateHero } from "@/app/actions/hero";
import { Save, CheckCircle2 } from "lucide-react";
import { HeroSection } from "@prisma/client";
export default function HeroForm({ trData, enData }: { trData: HeroSection | null, enData: HeroSection | null }) {
const [state, formAction, isPending] = useActionState(updateHero, null);
const [activeTab, setActiveTab] = useState<"tr" | "en">("tr");
const fallbacks = {
tr: {
lang: "tr",
location: "Akyaka, Gökova",
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",
exploreActivities: "Aktiviteleri Keşfet",
scroll: "Aşağı Kaydır"
},
en: {
lang: "en",
location: "Akyaka, Gokova",
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",
exploreActivities: "Explore Activities",
scroll: "Scroll Down"
}
};
const currentData = activeTab === "tr" ? (trData || fallbacks.tr) : (enData || fallbacks.en);
return (
<>
{state?.success && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center gap-3 text-green-800 animate-fade-in">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<p className="font-medium">{state.message}</p>
</div>
)}
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden relative">
{/* Tabs */}
<div className="flex border-b border-gray-200">
<button
type="button"
onClick={() => setActiveTab("tr")}
className={`flex-1 py-5 text-center font-bold tracking-wide uppercase text-sm transition-all ${activeTab === "tr" ? "text-dark border-b-2 border-dark bg-gray-50" : "text-gray-400 hover:text-gray-600 hover:bg-gray-50/50"}`}
>
🇹🇷 Türkçe
</button>
<button
type="button"
onClick={() => setActiveTab("en")}
className={`flex-1 py-5 text-center font-bold tracking-wide uppercase text-sm transition-all ${activeTab === "en" ? "text-dark border-b-2 border-dark bg-gray-50" : "text-gray-400 hover:text-gray-600 hover:bg-gray-50/50"}`}
>
🇬🇧 English
</button>
</div>
<form action={formAction} className="p-8 md:p-10">
<input type="hidden" name="lang" value={activeTab} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Karşılama / Konum (Location)</label>
<input
type="text"
name="location"
defaultValue={currentData.location}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Karşılama Metni (Welcome)</label>
<input
type="text"
name="welcome"
defaultValue={currentData.welcome}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Ana Başlık (Title)</label>
<input
type="text"
name="title"
defaultValue={currentData.title}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Alt Başlık (Subtitle)</label>
<textarea
name="subtitle"
defaultValue={currentData.subtitle}
required
rows={3}
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 resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Rezervasyon Butonu (Book Now)</label>
<input
type="text"
name="bookNow"
defaultValue={currentData.bookNow}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Aktiviteler Butonu (Explore)</label>
<input
type="text"
name="exploreActivities"
defaultValue={currentData.exploreActivities}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Aşağı Kaydır Yazısı (Scroll)</label>
<input
type="text"
name="scroll"
defaultValue={currentData.scroll}
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"
/>
</div>
</div>
<div className="flex justify-end border-t border-gray-100 pt-6">
<button
type="submit"
disabled={isPending}
className="flex items-center justify-center gap-3 bg-accent text-white font-bold tracking-widest uppercase py-4 px-10 rounded-xl hover:bg-accent/90 transition-all disabled:opacity-70 disabled:cursor-not-allowed shadow-lg shadow-accent/20"
>
{isPending ? (
<>Kaydediliyor...</>
) : (
<>
<Save className="w-5 h-5" />
{activeTab.toUpperCase()} Ayarlarını Kaydet
</>
)}
</button>
</div>
</form>
</div>
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import prisma from "@/lib/prisma";
import HeroForm from "./HeroForm";
export default async function HeroPage() {
const trData = await prisma.heroSection.findUnique({ where: { lang: "tr" } });
const enData = await prisma.heroSection.findUnique({ where: { lang: "en" } });
return (
<div className="max-w-4xl mx-auto w-full">
<h1 className="text-3xl font-bold font-serif mb-6 text-dark">Hero Bölümü</h1>
<HeroForm trData={trData} enData={enData} />
</div>
);
}
+76
View File
@@ -0,0 +1,76 @@
import { Inter } from "next/font/google";
import "@/app/globals.css";
import { cookies } from "next/headers";
import { logout } from "@/app/actions/auth";
import Link from "next/link";
import SidebarNav from "./components/SidebarNav";
import { User, LogOut } from "lucide-react";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Admin Panel - Kite Beach",
description: "Kite Beach Akyaka Admin Paneli",
};
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const isLoggedIn = cookieStore.get("admin_session")?.value === "true";
return (
<html lang="tr" suppressHydrationWarning>
<body className={`${inter.className} bg-[#f8f9fa] text-gray-900 min-h-screen flex flex-col`} suppressHydrationWarning>
{isLoggedIn ? (
<div className="flex flex-1 min-h-screen">
{/* Sidebar (Dark Glassmorphism) */}
<aside className="w-72 bg-dark/95 backdrop-blur-xl border-r border-white/10 text-white flex flex-col shadow-2xl z-20">
<div className="p-8 flex items-center justify-center border-b border-white/5">
<span className="font-serif font-black tracking-widest uppercase text-2xl text-transparent bg-clip-text bg-gradient-to-r from-white to-white/60">
Kite Beach
</span>
</div>
<SidebarNav />
<div className="p-6 border-t border-white/5 bg-white/5">
<form action={logout}>
<button type="submit" className="w-full flex items-center justify-center gap-3 bg-red-500/10 hover:bg-red-500/20 text-red-400 hover:text-red-300 px-4 py-3 rounded-xl transition-all duration-300 group font-medium text-sm tracking-wide">
<LogOut className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
Oturumu Kapat
</button>
</form>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col overflow-auto relative">
{/* Decorative Background Blob */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-primary/5 rounded-full blur-[120px] pointer-events-none" />
<header className="bg-white/70 backdrop-blur-lg p-5 flex justify-between items-center border-b border-gray-100 sticky top-0 z-10">
<div className="text-gray-500 text-sm font-medium">Hoş Geldiniz, Yönetici</div>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-accent/10 flex items-center justify-center text-accent">
<User className="w-5 h-5" />
</div>
</div>
</header>
<div className="p-8 relative z-0 animate-fade-up">
{children}
</div>
</main>
</div>
) : (
<main className="flex-grow flex flex-col">
{children}
</main>
)}
</body>
</html>
);
}
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { useActionState } from "react";
import { login } from "@/app/actions/auth";
import { Lock, ArrowRight } from "lucide-react";
import Image from "next/image";
export default function LoginPage() {
const [state, formAction, isPending] = useActionState(login, null);
return (
<div className="flex-grow flex items-center justify-center p-4 relative min-h-screen overflow-hidden bg-dark">
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1526367790999-0150786686a2?q=80&w=2940&auto=format&fit=crop"
alt="Kite Beach Akyaka"
fill
className="object-cover opacity-30 scale-105 animate-[pulse_10s_ease-in-out_infinite]"
/>
<div className="absolute inset-0 bg-gradient-to-b from-dark/90 via-dark/80 to-dark/95" />
</div>
<div className="relative z-10 w-full max-w-md animate-fade-up">
{/* Logo or Brand */}
<div className="text-center mb-10">
<h1 className="font-serif font-black tracking-[0.2em] uppercase text-4xl text-transparent bg-clip-text bg-gradient-to-r from-white to-white/60 mb-2">
KITE BEACH
</h1>
<p className="text-white/50 tracking-widest text-xs uppercase">Yönetim Paneli</p>
</div>
<div className="bg-white/10 backdrop-blur-xl p-8 sm:p-10 rounded-[2rem] shadow-2xl border border-white/20 relative overflow-hidden">
{/* Decorative Glow */}
<div className="absolute -top-20 -right-20 w-40 h-40 bg-accent/40 rounded-full blur-[60px] pointer-events-none" />
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-primary/40 rounded-full blur-[60px] pointer-events-none" />
<div className="flex justify-center mb-8 relative">
<div className="w-16 h-16 bg-white/10 rounded-2xl flex items-center justify-center backdrop-blur-md border border-white/20 shadow-inner">
<Lock className="w-8 h-8 text-white" />
</div>
</div>
<form action={formAction} className="space-y-6 relative z-10">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-white/70 tracking-wider uppercase ml-1">Kullanıcı Adı</label>
<input
type="text"
name="username"
required
className="w-full px-5 py-3.5 bg-white/5 border border-white/10 text-white placeholder-white/30 rounded-xl focus:ring-2 focus:ring-accent focus:border-transparent outline-none transition-all"
placeholder="Yönetici adınızı girin"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-white/70 tracking-wider uppercase ml-1">Şifre</label>
<input
type="password"
name="password"
required
className="w-full px-5 py-3.5 bg-white/5 border border-white/10 text-white placeholder-white/30 rounded-xl focus:ring-2 focus:ring-accent focus:border-transparent outline-none transition-all"
placeholder="••••••••"
/>
</div>
{state?.error && (
<div className="text-red-300 text-sm text-center bg-red-500/20 border border-red-500/30 p-3 rounded-xl animate-fade-up">
{state.error}
</div>
)}
<button
type="submit"
disabled={isPending}
className="w-full group relative flex items-center justify-center gap-3 bg-accent text-white font-bold tracking-widest uppercase py-4 rounded-xl mt-6 hover:bg-accent/90 transition-all disabled:opacity-70 disabled:cursor-not-allowed overflow-hidden shadow-lg shadow-accent/20"
>
<div className="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-out" />
<span className="relative z-10 text-xs">{isPending ? "Giriş yapılıyor..." : "Giriş Yap"}</span>
{!isPending && <ArrowRight className="w-4 h-4 relative z-10 group-hover:translate-x-1 transition-transform" />}
</button>
</form>
</div>
</div>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
export default function AdminDashboard() {
return (
<div className="max-w-5xl mx-auto w-full">
<h1 className="text-3xl font-bold font-serif mb-6 text-dark">Hoş Geldiniz</h1>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
<h2 className="text-xl font-semibold mb-4 text-primary">Sistem Durumu</h2>
<p className="text-gray-600 mb-4">
Burası Kite Beach Akyaka yönetim panelidir.
</p>
<p className="text-gray-600">
İlerleyen aşamalarda içerik yönetimi, rezervasyon takibi veya etkinlik ekleme gibi modüller buraya eklenecektir. Şu an için altyapı ve güvenli giriş sistemi başarıyla kurulmuştur.
</p>
</div>
</div>
);
}
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { useActionState } from "react";
import { updateSettings } from "@/app/actions/settings";
import { Save, CheckCircle2 } from "lucide-react";
import { SiteSettings } from "@prisma/client";
export default function SettingsForm({ defaultValues }: { defaultValues: SiteSettings | null }) {
const [state, formAction, isPending] = useActionState(updateSettings, null);
// Fallback data if db is empty
const fallback = {
title: "Kite Beach Akyaka",
email: "info@kitebeach.com",
phone: "+90 555 123 4567",
address: "Akyaka, Muğla, Türkiye",
instagram: "https://instagram.com/kitebeachakyaka"
};
const values = defaultValues || fallback;
return (
<>
{state?.success && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center gap-3 text-green-800 animate-fade-in">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<p className="font-medium">{state.message}</p>
</div>
)}
<div className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden relative">
<form action={formAction} className="p-8 md:p-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Site Başlığı</label>
<input
type="text"
name="title"
defaultValue={values.title}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">İletişim E-posta</label>
<input
type="email"
name="email"
defaultValue={values.email}
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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Telefon Numarası</label>
<input
type="tel"
name="phone"
defaultValue={values.phone}
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"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Açık Adres</label>
<textarea
name="address"
defaultValue={values.address}
required
rows={3}
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 resize-none"
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Instagram Bağlantısı</label>
<input
type="url"
name="instagram"
defaultValue={values.instagram || ""}
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"
/>
</div>
</div>
<div className="flex justify-end border-t border-gray-100 pt-6">
<button
type="submit"
disabled={isPending}
className="flex items-center justify-center gap-3 bg-accent text-white font-bold tracking-widest uppercase py-4 px-10 rounded-xl hover:bg-accent/90 transition-all disabled:opacity-70 disabled:cursor-not-allowed shadow-lg shadow-accent/20"
>
{isPending ? (
<>Kaydediliyor...</>
) : (
<>
<Save className="w-5 h-5" />
Ayarları Kaydet
</>
)}
</button>
</div>
</form>
</div>
</>
);
}
+15
View File
@@ -0,0 +1,15 @@
import prisma from "@/lib/prisma";
import SettingsForm from "./SettingsForm";
export default async function SettingsPage() {
const settings = await prisma.siteSettings.findUnique({
where: { id: 1 }
});
return (
<div className="max-w-4xl mx-auto w-full">
<h1 className="text-3xl font-bold font-serif mb-6 text-dark">Site Ayarları</h1>
<SettingsForm defaultValues={settings} />
</div>
);
}
-170
View File
@@ -1,170 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowRight, Clock, Users } from "lucide-react";
import ScrollReveal from "@/components/ScrollReveal";
const activities = [
{
img: "https://images.unsplash.com/photo-1533000756353-909d73d2a3c7?q=80&w=2940&auto=format&fit=crop",
tag: "IKO Sertifikalı",
tagColor: "bg-accent text-white",
name: "Kitesurf Okulu",
desc: "Akyaka'nın meşhur termal rüzgarında kitesurf yapmayı öğrenin. IKO sertifikalı eğitmenlerimizle başlangıç, orta ve ileri seviye ders seçenekleri sunuyoruz.",
duration: "24 Saat",
capacity: "13 Kişi",
price: "₺2.000",
unit: "Saat",
levels: ["Başlangıç", "Orta", "İleri"],
},
{
img: "https://images.unsplash.com/photo-1599901860904-17e08627c271?q=80&w=2940&auto=format&fit=crop",
tag: "Açık Hava",
tagColor: "bg-primary text-white",
name: "Yoga Seansları",
desc: "Nehir kıyısındaki özel ahşap platformumuzda güne yoga ile başlayın. Hafıza seansları, sunset yoga ve özel birebir dersler mevcuttur.",
duration: "6090 Dak",
capacity: "Grup / Özel",
price: "₺400",
unit: "Kişi",
levels: ["Tüm Seviyeler"],
},
{
img: "https://images.unsplash.com/photo-1544606771-46abcf9dbb83?q=80&w=2924&auto=format&fit=crop",
tag: "Ekipman Dahil",
tagColor: "bg-dark text-white",
name: "Stand-Up Paddle (SUP)",
desc: "Azmak nehrinin berrak ve sakin sularında kürek çekerek doğanın gizli güzelliklerini keşfedin. Ekipman kiralama ve rehberli tur seçenekleri mevcuttur.",
duration: "13 Saat",
capacity: "Bireysel",
price: "₺300",
unit: "Saat",
levels: ["Kolay"],
},
{
img: "https://images.unsplash.com/photo-1511994298241-608e28f14fde?q=80&w=2940&auto=format&fit=crop",
tag: "Günlük Tur",
tagColor: "bg-primary text-white",
name: "Bisiklet Kiralama",
desc: "Akyaka'nın çam ormanları arasındaki yollarını ve mimarisiyle ünlü sokaklarını bisikletle gezin. Günlük ve saatlik kiralama seçenekleri ile rotaları keşfedin.",
duration: "Tam Gün",
capacity: "Bireysel / Grup",
price: "₺250",
unit: "Gün",
levels: ["Kolay"],
},
];
export default function AktivitelerPage() {
return (
<div className="min-h-screen bg-background">
{/* 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-1533000756353-909d73d2a3c7?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
</span>
<h1 className="font-serif text-5xl md:text-7xl text-white font-bold animate-fade-up delay-100">
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>
</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">
{activities.map((act, i) => (
<ScrollReveal key={i} delay={i * 80}>
<div className="card-lift group bg-white rounded-3xl overflow-hidden border border-sand flex flex-col h-full">
<div className="relative h-64 overflow-hidden">
<Image
src={act.img}
alt={act.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-widest uppercase ${act.tagColor}`}>
{act.tag}
</span>
</div>
<div className="p-8 flex-grow flex flex-col">
<h2 className="font-serif text-2xl font-bold text-dark mb-3">{act.name}</h2>
<p className="text-gray-500 text-[14px] leading-relaxed mb-6 flex-grow">{act.desc}</p>
{/* Meta */}
<div className="flex gap-6 mb-6">
<div className="flex items-center gap-2 text-[12px] text-muted">
<Clock size={13} className="text-primary" />
{act.duration}
</div>
<div className="flex items-center gap-2 text-[12px] text-muted">
<Users size={13} className="text-primary" />
{act.capacity}
</div>
</div>
{/* Levels */}
<div className="flex flex-wrap gap-2 mb-7">
{act.levels.map((level) => (
<span
key={level}
className="text-[10px] font-semibold bg-background text-primary px-3 py-1 rounded-full border border-primary/20 tracking-wide"
>
{level}
</span>
))}
</div>
<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>
<span className="text-muted text-xs ml-1">/ {act.unit}</span>
</div>
<Link
href="/iletisim"
className="inline-flex items-center gap-2 text-accent font-bold text-[12px] tracking-wider uppercase hover:text-primary transition-colors group/link"
>
Bilgi Al
<ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</div>
</ScrollReveal>
))}
</div>
</section>
{/* CTA */}
<section className="py-20 bg-primary">
<div className="max-w-3xl mx-auto px-4 text-center">
<h3 className="font-serif text-4xl text-white font-bold mb-4">
Özel Paket Teklifi Al
</h3>
<p className="text-white/60 mb-8">
Kitesurf + konaklama veya yoga + SUP gibi özel aktivite paketleri için bizimle iletişime geçin.
</p>
<Link
href="/iletisim"
className="btn-shimmer inline-block bg-accent text-white px-10 py-4 rounded-full text-xs font-bold tracking-[0.15em] uppercase"
>
İletişime Geç
</Link>
</div>
</section>
</div>
);
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+16 -10
View File
@@ -1,4 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { MapPin, Phone, Mail } from "lucide-react"; import { MapPin, Phone, Mail } from "lucide-react";
const quickLinks = [ const quickLinks = [
@@ -50,12 +51,13 @@ export default function Footer() {
{/* Brand */} {/* Brand */}
<div className="md:col-span-4"> <div className="md:col-span-4">
<Link href="/" className="inline-block mb-5"> <Link href="/" className="inline-block mb-5">
<div> <Image
<span className="font-serif text-2xl font-bold tracking-[0.08em] text-white block"> src="/logo.png"
KITE BEACH alt="Kite Beach Logo"
</span> width={160}
<span className="text-[9px] tracking-[0.3em] text-white/30 uppercase">Akyaka</span> height={55}
</div> className="object-contain brightness-0 invert opacity-90 hover:opacity-100 transition-opacity"
/>
</Link> </Link>
<p className="text-white/40 text-[13px] leading-relaxed mb-7 max-w-xs"> <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ı. Nehrin huzuru, denizin enerjisi tek bir noktada. Akyaka'nın kalbinde butik bir kaçış noktası.
@@ -145,13 +147,17 @@ export default function Footer() {
© 2026 Kite Beach Akyaka. Tüm hakları saklıdır. © 2026 Kite Beach Akyaka. Tüm hakları saklıdır.
</p> </p>
<div className="flex gap-6"> <div className="flex gap-6">
{["KVKK", "Gizlilik Politikası", "Çerez Politikası"].map((item) => ( {[
{ name: "KVKK", href: "/kvkk" },
{ name: "Gizlilik Politikası", href: "/gizlilik" },
{ name: "Çerez Politikası", href: "/cerez" }
].map((item) => (
<Link <Link
key={item} key={item.name}
href="#" href={item.href}
className="text-white/25 hover:text-white/60 transition-colors text-xs" className="text-white/25 hover:text-white/60 transition-colors text-xs"
> >
{item} {item.name}
</Link> </Link>
))} ))}
</div> </div>
+10 -15
View File
@@ -2,6 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react"; import { Menu, X } from "lucide-react";
@@ -43,21 +44,15 @@ export default function Navbar() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{/* Logo */} {/* Logo */}
<Link href="/" className="flex-shrink-0 flex flex-col leading-none group"> <Link href="/" className="flex-shrink-0 flex items-center group">
<span <Image
className={`font-serif text-xl font-bold tracking-[0.08em] transition-colors ${ src="/logo.png"
scrolled ? "text-primary" : isHome ? "text-white" : "text-primary" alt="Kite Beach Logo"
}`} width={140}
> height={48}
KITE BEACH className={`object-contain transition-all duration-300 ${!scrolled && isHome ? 'brightness-0 invert' : ''}`}
</span> priority
<span />
className={`text-[9px] tracking-[0.3em] uppercase font-medium transition-colors ${
scrolled ? "text-muted" : isHome ? "text-white/50" : "text-muted"
}`}
>
Akyaka
</span>
</Link> </Link>
{/* Desktop links */} {/* Desktop links */}
+11
View File
@@ -0,0 +1,11 @@
{
"home": {
"location": "Akyaka · Mugla · Turkey",
"welcome": "WELCOME TO",
"title": "KITE BEACH",
"subtitle": "\"The tranquility of the river, the energy of the sea — in one place.\"",
"bookNow": "Book Now",
"exploreActivities": "Explore Activities",
"scroll": "Discover"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"home": {
"location": "Akyaka · Muğla · Türkiye",
"welcome": "WELCOME TO",
"title": "KITE BEACH",
"subtitle": "\"Nehrin huzuru, denizin enerjisi — tek bir noktada.\"",
"bookNow": "Rezervasyon Yap",
"exploreActivities": "Aktiviteleri Keşfet",
"scroll": "Keşfet"
}
}
+18
View File
@@ -0,0 +1,18 @@
import { PrismaClient } from "@prisma/client";
import { Pool } from "pg";
import { PrismaPg } from "@prisma/adapter-pg";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
let prisma: PrismaClient;
if (globalForPrisma.prisma) {
prisma = globalForPrisma.prisma;
} else {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
prisma = new PrismaClient({ adapter });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
}
export default prisma;
+1
View File
@@ -1,6 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone",
images: { images: {
remotePatterns: [ remotePatterns: [
{ {
+1264 -15
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -9,19 +9,28 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@formatjs/intl-localematcher": "^0.8.10",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"framer-motion": "^12.40.0", "framer-motion": "^12.40.0",
"lucide-react": "^1.17.0", "lucide-react": "^1.17.0",
"negotiator": "^1.0.0",
"next": "16.2.7", "next": "16.2.7",
"pg": "^8.21.0",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/negotiator": "^0.6.4",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^17.4.2",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.7", "eslint-config-next": "16.2.7",
"prisma": "^7.8.0",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
} }
+14
View File
@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});
@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "SiteSettings" (
"id" INTEGER NOT NULL DEFAULT 1,
"title" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"address" TEXT NOT NULL,
"instagram" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "HeroSection" (
"lang" TEXT NOT NULL,
"location" TEXT NOT NULL,
"welcome" TEXT NOT NULL,
"title" TEXT NOT NULL,
"subtitle" TEXT NOT NULL,
"bookNow" TEXT NOT NULL,
"exploreActivities" TEXT NOT NULL,
"scroll" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HeroSection_pkey" PRIMARY KEY ("lang")
);
@@ -0,0 +1,32 @@
-- CreateTable
CREATE TABLE "Activity" (
"id" SERIAL NOT NULL,
"lang" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"price" TEXT NOT NULL,
"image" TEXT NOT NULL,
"badge" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Activity_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Event" (
"id" SERIAL NOT NULL,
"lang" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"date" TEXT NOT NULL,
"time" TEXT NOT NULL,
"location" TEXT NOT NULL,
"image" TEXT NOT NULL,
"badge" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "Feature" (
"id" SERIAL NOT NULL,
"lang" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"label" TEXT NOT NULL,
"sub" TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Feature_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "AboutSection" (
"lang" TEXT NOT NULL,
"badge" TEXT NOT NULL,
"titleLine1" TEXT NOT NULL,
"titleLine2" TEXT NOT NULL,
"titleHighlight" TEXT NOT NULL,
"description1" TEXT NOT NULL,
"description2" TEXT NOT NULL,
"image" TEXT NOT NULL,
"statsGuestCount" TEXT NOT NULL,
"statsGuestLabel" TEXT NOT NULL,
"statsYearCount" TEXT NOT NULL,
"statsYearLabel" TEXT NOT NULL,
"badgeIkoTitle" TEXT NOT NULL,
"badgeIkoLabel" TEXT NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AboutSection_pkey" PRIMARY KEY ("lang")
);
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+85
View File
@@ -0,0 +1,85 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
model SiteSettings {
id Int @id @default(1)
title String
email String
phone String
address String
instagram String?
updatedAt DateTime @updatedAt
}
model HeroSection {
lang String @id // "tr" or "en"
location String
welcome String
title String
subtitle String
bookNow String
exploreActivities String
scroll String
updatedAt DateTime @updatedAt
}
model Activity {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
title String
description String
price String
image String
badge String? // e.g. "IKO Sertifikalı"
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Event {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
title String
description String
date String
time String
location String
image String
badge String? // e.g. "Yaklaşan Etkinlik"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Feature {
id Int @id @default(autoincrement())
lang String // "tr" or "en"
icon String
label String
sub String
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AboutSection {
lang String @id // "tr" or "en"
badge String
titleLine1 String
titleLine2 String
titleHighlight String
description1 String
description2 String
image String
statsGuestCount String
statsGuestLabel String
statsYearCount String
statsYearLabel String
badgeIkoTitle String
badgeIkoLabel String
updatedAt DateTime @updatedAt
}
+75
View File
@@ -0,0 +1,75 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
let locales = ["tr", "en"];
let defaultLocale = "tr";
// Get the preferred locale
function getLocale(request: NextRequest): string {
const headers = new Headers(request.headers);
const acceptLanguage = headers.get("accept-language");
if (!acceptLanguage) return defaultLocale;
const negotiatorHeaders: Record<string, string> = {};
headers.forEach((value, key) => (negotiatorHeaders[key] = value));
try {
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
return match(languages, locales, defaultLocale);
} catch (e) {
return defaultLocale;
}
}
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip public files and internal next paths
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.')
) {
return;
}
// Handle Admin Panel Authentication & Routing Bypass
if (pathname.startsWith('/admin')) {
// Exclude /admin/login from the check
if (pathname === '/admin/login') {
return;
}
// Check for admin session
const session = request.cookies.get('admin_session');
if (!session || session.value !== 'true') {
request.nextUrl.pathname = '/admin/login';
return NextResponse.redirect(request.nextUrl);
}
// Allow authenticated admin traffic
return;
}
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// Redirect if there is no locale
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: [
// Skip all internal paths (_next, static files, api)
'/((?!_next|api|favicon.ico|.*\\..*).*)',
],
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB