b
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user