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
+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>
);
}