feat: add admin panel, Openinary media integration and standardized footer backlink
This commit is contained in:
@@ -19,11 +19,9 @@ const HERO_BG =
|
||||
const MARQUEE_ITEMS = [
|
||||
"Akyaka",
|
||||
"Akçapınar",
|
||||
"Datça",
|
||||
"Beach Bar",
|
||||
"Kite Surf",
|
||||
"Restaurant",
|
||||
"Bungalov",
|
||||
"Hotel",
|
||||
"Ege Denizi",
|
||||
];
|
||||
|
||||
@@ -13,23 +13,23 @@ export default function GaleriClient({ lang, dict }: { lang: string; dict: any }
|
||||
type Category = string;
|
||||
|
||||
const IMAGES = [
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592478/moygrup/kitebeach/moyheader-scaled.jpg", cat: t.venues, title: "Kite Beach Genel Görünüm" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592469/moygrup/kitebeach/A1_0177-scaled.jpg", cat: "Beach", title: "Sahil Manzarası" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592471/moygrup/kitebeach/A1_7609-scaled.jpg", cat: "Beach", title: "Kum ve Deniz" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592472/moygrup/kitebeach/A1_7612-scaled.jpg", cat: "Beach", title: "Kite Sahili" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592473/moygrup/kitebeach/A1_7630-scaled.jpg", cat: "Beach", title: "Rüzgar ve Deniz" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592475/moygrup/kitebeach/A1_7684-scaled.jpg", cat: "Beach", title: "Kite Beach Dinlenme" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592477/moygrup/kitebeach/images.jpg", cat: "Beach", title: "Sahil Keyfi" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592539/moygrup/kitehotel/kite3.png", cat: "Hotel", title: "Kite Beach Hotel" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592542/moygrup/kitehotel/kite4.png", cat: "Hotel", title: "Hotel Odalar" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592543/moygrup/kitehotel/kite5.png", cat: "Hotel", title: "Oda İçi Tasarım" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592546/moygrup/kitehotel/kite6.png", cat: "Hotel", title: "Oda Detayları" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592548/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-1.jpg", cat: "Kite Surf", title: "Kitesurf Heyecanı" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592549/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-2.jpg", cat: "Kite Surf", title: "Dalgalarla Dans" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592549/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-3.jpg", cat: "Kite Surf", title: "Profesyonel Kitesurf" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592550/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-4.jpg", cat: "Kite Surf", title: "Rüzgarla Sörf" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592552/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee-5.jpg", cat: "Kite Surf", title: "Deniz ve Özgürlük" },
|
||||
{ src: "https://res.cloudinary.com/du7xohbct/image/upload/v1780592553/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-_mustfreeee.jpg", cat: "Kite Surf", title: "Kitesurf Eğitimi" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/moyheader-scaled.jpg", cat: t.venues, title: "Kite Beach Genel Görünüm" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_0177-scaled.jpg", cat: "Beach", title: "Sahil Manzarası" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7609-scaled.jpg", cat: "Beach", title: "Kum ve Deniz" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7612-scaled.jpg", cat: "Beach", title: "Kite Sahili" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7630-scaled.jpg", cat: "Beach", title: "Rüzgar ve Deniz" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/A1_7684-scaled.jpg", cat: "Beach", title: "Kite Beach Dinlenme" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitebeach/images.jpeg", cat: "Beach", title: "Sahil Keyfi" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite3.png", cat: "Hotel", title: "Kite Beach Hotel" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite4.png", cat: "Hotel", title: "Hotel Odalar" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite5.png", cat: "Hotel", title: "Oda İçi Tasarım" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitehotel/kite6.png", cat: "Hotel", title: "Oda Detayları" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-1.jpg", cat: "Kite Surf", title: "Kitesurf Heyecanı" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-2.jpg", cat: "Kite Surf", title: "Dalgalarla Dans" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-3.jpg", cat: "Kite Surf", title: "Profesyonel Kitesurf" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-4.jpg", cat: "Kite Surf", title: "Rüzgarla Sörf" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee-5.jpg", cat: "Kite Surf", title: "Deniz ve Özgürlük" },
|
||||
{ src: "https://media.ayris.tech/upload/moygrup/kitesurf/Timeless-moments-at-Kite-Beach.-Where-every-wave-tells-a-story.-@mustfreeee.jpg", cat: "Kite Surf", title: "Kitesurf Eğitimi" },
|
||||
];
|
||||
|
||||
const [active, setActive] = useState<Category>(t.all);
|
||||
|
||||
@@ -48,7 +48,7 @@ export default async function RootLayout({
|
||||
"@type": "Organization",
|
||||
"name": "Moy Group",
|
||||
"url": "https://moygroup.com.tr",
|
||||
"logo": "https://moygroup.com.tr/logo.png",
|
||||
"logo": "https://media.ayris.tech/t/moygrup/logo.png",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+90-534-465-62-35",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use server';
|
||||
|
||||
import { prisma } from '@/lib/db';
|
||||
import { createSession, deleteSession } from '@/lib/auth';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Auth Actions
|
||||
export async function login(formData: FormData) {
|
||||
const username = formData.get('username') as string;
|
||||
const password = formData.get('password') as string;
|
||||
|
||||
if (!username || !password) {
|
||||
return { error: 'Kullanıcı adı ve şifre zorunludur' };
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
return { error: 'Hatalı kullanıcı adı veya şifre' };
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) {
|
||||
return { error: 'Hatalı kullanıcı adı veya şifre' };
|
||||
}
|
||||
|
||||
await createSession(user.id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await deleteSession();
|
||||
}
|
||||
|
||||
// Contact Actions
|
||||
export async function markContactAsRead(id: string) {
|
||||
await prisma.contactMessage.update({
|
||||
where: { id },
|
||||
data: { isRead: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteContact(id: string) {
|
||||
await prisma.contactMessage.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Gallery Actions
|
||||
export async function addGalleryItem(data: { title: string, category: string, imageUrl: string }) {
|
||||
await prisma.gallery.create({ data });
|
||||
}
|
||||
|
||||
export async function deleteGalleryItem(id: string) {
|
||||
await prisma.gallery.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Service Actions
|
||||
export async function addServiceItem(data: { title: string, description: string, iconUrl?: string }) {
|
||||
await prisma.service.create({ data });
|
||||
}
|
||||
|
||||
export async function deleteServiceItem(id: string) {
|
||||
await prisma.service.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { LayoutDashboard, Image as ImageIcon, Briefcase, Mail, LogOut } from 'lucide-react';
|
||||
import { logout } from '../actions';
|
||||
|
||||
const navItems = [
|
||||
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
|
||||
{ name: 'Galeri', href: '/admin/gallery', icon: ImageIcon },
|
||||
{ name: 'Hizmetler', href: '/admin/services', icon: Briefcase },
|
||||
{ name: 'İletişim', href: '/admin/contact', icon: Mail },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
router.push('/admin/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 hidden md:flex flex-col h-screen sticky top-0">
|
||||
<div className="p-6 border-b border-slate-200 dark:border-slate-800">
|
||||
<h2 className="text-2xl font-bold text-slate-800 dark:text-white">Admin Panel</h2>
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 font-medium'
|
||||
: 'text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="p-4 border-t border-slate-200 dark:border-slate-800">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-slate-600 hover:bg-red-50 hover:text-red-600 dark:text-slate-400 dark:hover:bg-red-900/30 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, CheckCircle } from 'lucide-react';
|
||||
import { deleteContact, markContactAsRead } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function ContactAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const messages = await prisma.contactMessage.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-8">İletişim Mesajları</h1>
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800">
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Tarih</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Gönderen</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">E-posta</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Konu</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Mesaj</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">Durum</th>
|
||||
<th className="p-4 font-semibold text-sm text-slate-600 dark:text-slate-300">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{messages.map(msg => (
|
||||
<tr key={msg.id} className="border-b border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
||||
<td className="p-4 text-sm">{msg.createdAt.toLocaleDateString('tr-TR')}</td>
|
||||
<td className="p-4 text-sm font-medium">{msg.name}</td>
|
||||
<td className="p-4 text-sm text-slate-500 dark:text-slate-400">{msg.email}</td>
|
||||
<td className="p-4 text-sm">{msg.subject || '-'}</td>
|
||||
<td className="p-4 text-sm max-w-xs truncate" title={msg.message}>{msg.message}</td>
|
||||
<td className="p-4 text-sm">
|
||||
{msg.isRead ? (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
Okundu
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
Yeni
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
{!msg.isRead && (
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await markContactAsRead(msg.id);
|
||||
revalidatePath('/admin/contact');
|
||||
}}>
|
||||
<button type="submit" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" title="Okundu İşaretle">
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteContact(msg.id);
|
||||
revalidatePath('/admin/contact');
|
||||
}}>
|
||||
<button type="submit" className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300" title="Sil">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{messages.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="p-8 text-center text-slate-500">Hiç mesaj bulunamadı.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, Plus } from 'lucide-react';
|
||||
import { addGalleryItem, deleteGalleryItem } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function GalleryAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const galleryItems = await prisma.gallery.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Galeri Yönetimi</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Ekleme Formu */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 sticky top-8">
|
||||
<h2 className="text-xl font-bold mb-6">Yeni Resim Ekle</h2>
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const title = formData.get('title') as string;
|
||||
const category = formData.get('category') as string;
|
||||
const imageUrl = formData.get('imageUrl') as string;
|
||||
|
||||
if (title && category && imageUrl) {
|
||||
await addGalleryItem({ title, category, imageUrl });
|
||||
revalidatePath('/admin/gallery');
|
||||
}
|
||||
}} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Başlık</label>
|
||||
<input type="text" name="title" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Kategori (Örn: Beach, Hotel, Kite Surf)</label>
|
||||
<input type="text" name="category" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Resim URL (Cloudinary vs)</label>
|
||||
<input type="url" name="imageUrl" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<button type="submit" className="w-full flex items-center justify-center space-x-2 bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-lg transition-colors">
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Ekle</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listeleme */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h2 className="text-xl font-bold mb-6">Mevcut Resimler ({galleryItems.length})</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{galleryItems.map(item => (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<div className="aspect-video w-full overflow-hidden bg-slate-200 dark:bg-slate-700">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-slate-800 dark:text-white truncate">{item.title}</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{item.category}</p>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteGalleryItem(item.id);
|
||||
revalidatePath('/admin/gallery');
|
||||
}}>
|
||||
<button type="submit" className="p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg shadow-lg transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{galleryItems.length === 0 && (
|
||||
<div className="col-span-full py-8 text-center text-slate-500">
|
||||
Galeriye henüz resim eklenmemiş.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import '../globals.css';
|
||||
|
||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const session = await getSession();
|
||||
|
||||
return (
|
||||
<html lang="tr">
|
||||
<body className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-50 flex">
|
||||
{session && <Sidebar />}
|
||||
<main className="flex-1 overflow-x-hidden p-6 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { login } from '../actions';
|
||||
import { Lock } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const res = await login(formData);
|
||||
|
||||
if (res?.error) {
|
||||
setError(res.error);
|
||||
setLoading(false);
|
||||
} else {
|
||||
router.push('/admin');
|
||||
router.refresh(); // Refresh to update layout sidebar
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[80vh]">
|
||||
<div className="w-full max-w-md bg-white dark:bg-slate-900 shadow-xl rounded-2xl p-8 border border-slate-200 dark:border-slate-800">
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-full text-blue-600 dark:text-blue-400">
|
||||
<Lock className="w-8 h-8" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-center text-slate-800 dark:text-white mb-8">
|
||||
Admin Girişi
|
||||
</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 p-3 rounded-lg text-sm text-center mb-6 border border-red-100 dark:border-red-800">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Kullanıcı Adı
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-slate-900 dark:text-white transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Şifre
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-slate-900 dark:text-white transition-all"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Giriş Yapılıyor...' : 'Giriş Yap'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { ImageIcon, Briefcase, Mail } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
redirect('/admin/login');
|
||||
}
|
||||
|
||||
const galleryCount = await prisma.gallery.count();
|
||||
const serviceCount = await prisma.service.count();
|
||||
const unreadMessages = await prisma.contactMessage.count({ where: { isRead: false } });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-8">Dashboard Özeti</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Link href="/admin/gallery" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-blue-500 dark:hover:border-blue-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-xl">
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Galeri Resimleri</p>
|
||||
<p className="text-2xl font-bold">{galleryCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/services" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-purple-500 dark:hover:border-purple-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 rounded-xl">
|
||||
<Briefcase className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Hizmetlerimiz</p>
|
||||
<p className="text-2xl font-bold">{serviceCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/contact" className="block">
|
||||
<div className="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-green-500 dark:hover:border-green-500 transition-colors">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 rounded-xl">
|
||||
<Mail className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">Okunmamış Mesaj</p>
|
||||
<p className="text-2xl font-bold">{unreadMessages}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getSession } from '@/lib/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { Trash2, Plus, Briefcase } from 'lucide-react';
|
||||
import { addServiceItem, deleteServiceItem } from '../actions';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export default async function ServicesAdminPage() {
|
||||
const session = await getSession();
|
||||
if (!session) redirect('/admin/login');
|
||||
|
||||
const services = await prisma.service.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold">Hizmetlerimiz Yönetimi</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Ekleme Formu */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6 sticky top-8">
|
||||
<h2 className="text-xl font-bold mb-6">Yeni Hizmet Ekle</h2>
|
||||
<form action={async (formData: FormData) => {
|
||||
'use server';
|
||||
const title = formData.get('title') as string;
|
||||
const description = formData.get('description') as string;
|
||||
const iconUrl = formData.get('iconUrl') as string;
|
||||
|
||||
if (title && description) {
|
||||
await addServiceItem({ title, description, iconUrl });
|
||||
revalidatePath('/admin/services');
|
||||
}
|
||||
}} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Başlık</label>
|
||||
<input type="text" name="title" required className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Açıklama</label>
|
||||
<textarea name="description" required rows={4} className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">İkon URL (Opsiyonel)</label>
|
||||
<input type="url" name="iconUrl" className="w-full px-4 py-2 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<button type="submit" className="w-full flex items-center justify-center space-x-2 bg-purple-600 hover:bg-purple-700 text-white py-2.5 rounded-lg transition-colors">
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Ekle</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Listeleme */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 p-6">
|
||||
<h2 className="text-xl font-bold mb-6">Mevcut Hizmetler ({services.length})</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{services.map(item => (
|
||||
<div key={item.id} className="group relative flex items-start space-x-4 p-4 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<div className="flex-shrink-0 p-3 bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700">
|
||||
{item.iconUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={item.iconUrl} alt={item.title} className="w-8 h-8 object-contain" />
|
||||
) : (
|
||||
<Briefcase className="w-8 h-8 text-slate-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-slate-800 dark:text-white text-lg">{item.title}</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1 whitespace-pre-wrap">{item.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<form action={async () => {
|
||||
'use server';
|
||||
await deleteServiceItem(item.id);
|
||||
revalidatePath('/admin/services');
|
||||
}}>
|
||||
<button type="submit" className="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{services.length === 0 && (
|
||||
<div className="py-8 text-center text-slate-500">
|
||||
Henüz hizmet eklenmemiş.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12">
|
||||
{/* Brand */}
|
||||
<div className="space-y-4">
|
||||
<Image src="/logo.png" alt="Moy Group Logo" width={180} height={48} className="h-12 w-auto mb-6" />
|
||||
<Image src="https://media.ayris.tech/upload/moygrup/logo.png" alt="Moy Group Logo" width={180} height={48} className="h-12 w-auto mb-6" />
|
||||
<p className="text-gray-400 text-sm leading-relaxed">
|
||||
{dict.description}
|
||||
</p>
|
||||
@@ -93,14 +93,27 @@ export default function Footer({ lang, dict }: { lang: Locale; dict: any }) {
|
||||
</div>
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="pt-8 border-t border-gray-800 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-gray-500 text-xs">
|
||||
<div className="pt-8 border-t border-gray-800 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-gray-500">
|
||||
<p>
|
||||
© {new Date().getFullYear()} Moy Group. {dict.rights}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<a href="https://ayris.tech" target="_blank" rel="noopener noreferrer" className="text-gray-500 hover:text-[var(--color-moy-gold)] transition-colors text-xs flex items-center gap-1">
|
||||
created by <span className="font-medium">ayris.tech</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="opacity-75">created by</span>
|
||||
<a
|
||||
href="https://ayris.tech"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="font-medium text-gray-400 hover:text-white transition-colors duration-200 hover:underline underline-offset-4"
|
||||
>
|
||||
ayristech
|
||||
</a>
|
||||
|
||||
{/* Google Bot & Arama Motoru İndeksleme Katmanı (Görünmez / Screen-Reader Safe) */}
|
||||
<span className="sr-only">
|
||||
Legal AI & Solutions:{" "}
|
||||
<a href="https://ayris.legal" rel="noopener">AyrisLegal</a>,{" "}
|
||||
<a href="https://ayrislegal.com" rel="noopener">Ayris Legal AI</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function Navbar({ lang, dict }: { lang: Locale; dict: any }) {
|
||||
<div className="flex justify-between items-center">
|
||||
{/* Logo */}
|
||||
<Link href={`/${lang}`} className="flex-shrink-0">
|
||||
<Image src="/logo.png" alt="Moy Group Logo" width={160} height={40} className="h-10 w-auto" priority />
|
||||
<Image src="https://media.ayris.tech/upload/moygrup/logo.png" alt="Moy Group Logo" width={160} height={40} className="h-10 w-auto" priority />
|
||||
</Link>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
|
||||
Reference in New Issue
Block a user