299 lines
15 KiB
TypeScript
299 lines
15 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import Image from 'next/image';
|
||
import { createProject, deleteProject, updateProject } from './actions';
|
||
|
||
interface Project {
|
||
id: number;
|
||
title: string;
|
||
year: string;
|
||
location: string;
|
||
image: string;
|
||
gallery: string[];
|
||
category: string | null;
|
||
description: string | null;
|
||
slug: string;
|
||
}
|
||
|
||
export default function ProjectsClient({ initialProjects }: { initialProjects: Project[] }) {
|
||
const [projects, setProjects] = useState<Project[]>(initialProjects);
|
||
const [isAdding, setIsAdding] = useState(false);
|
||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
||
const [currentCover, setCurrentCover] = useState<string | null>(null);
|
||
const [currentGallery, setCurrentGallery] = useState<string[]>([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
|
||
const filteredProjects = projects.filter(p =>
|
||
p.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
p.location.toLowerCase().includes(searchQuery.toLowerCase())
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (editingProject) {
|
||
setCurrentCover(editingProject.image || null);
|
||
setCurrentGallery(editingProject.gallery || []);
|
||
} else {
|
||
setCurrentCover(null);
|
||
setCurrentGallery([]);
|
||
}
|
||
}, [editingProject]);
|
||
|
||
const handleDelete = async (id: number) => {
|
||
if (confirm('Emin misiniz? Bu işlem projeyi kalıcı olarak silecektir.')) {
|
||
const prevProjects = [...projects];
|
||
setProjects(projects.filter(p => p.id !== id));
|
||
try {
|
||
await deleteProject(id);
|
||
} catch (err) {
|
||
setProjects(prevProjects);
|
||
alert('Proje silinemedi');
|
||
}
|
||
}
|
||
};
|
||
|
||
const removeGalleryImage = (url: string) => {
|
||
setCurrentGallery(prev => prev.filter(img => img !== url));
|
||
};
|
||
|
||
const removeCoverImage = () => {
|
||
setCurrentCover(null);
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
setIsLoading(true);
|
||
const formData = new FormData(e.currentTarget);
|
||
formData.append('existingCover', currentCover || '');
|
||
formData.append('keepGallery', JSON.stringify(currentGallery));
|
||
|
||
try {
|
||
if (editingProject) {
|
||
await updateProject(editingProject.id, formData);
|
||
} else {
|
||
await createProject(formData);
|
||
}
|
||
setIsAdding(false);
|
||
setEditingProject(null);
|
||
window.location.reload();
|
||
} catch (err) {
|
||
alert('Kaydedilemedi. Bağlantınızı veya dosya boyutlarını kontrol edin.');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const openEdit = (project: Project) => {
|
||
setEditingProject(project);
|
||
setIsAdding(true);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<header className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-12">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-white mb-1">Proje Yönetimi</h1>
|
||
<p className="text-gray-500 text-sm font-mono uppercase tracking-widest">Veritabanı Bağlı // {projects.length} Toplam Kayıt</p>
|
||
</div>
|
||
|
||
<div className="flex gap-4 w-full md:w-auto">
|
||
<input
|
||
type="text"
|
||
placeholder="Proje ara..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="flex-1 md:w-64 bg-white/5 border border-white/10 rounded-2xl px-6 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500/50 transition-all backdrop-blur-md"
|
||
/>
|
||
<button
|
||
onClick={() => { setEditingProject(null); setIsAdding(true); }}
|
||
className="bg-cyan-500 hover:bg-cyan-400 text-black font-bold py-3 px-8 rounded-2xl transition-all shadow-lg shadow-cyan-500/20 active:scale-95 whitespace-nowrap"
|
||
>
|
||
Yeni Proje Ekle
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||
<AnimatePresence mode='popLayout'>
|
||
{filteredProjects.map((project) => (
|
||
<motion.div
|
||
layout
|
||
key={project.id}
|
||
initial={{ opacity: 0, scale: 0.9 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.8 }}
|
||
className="bg-white/5 border border-white/10 rounded-[32px] overflow-hidden group hover:border-cyan-500/30 transition-all flex flex-col h-full"
|
||
>
|
||
<div className="aspect-[16/10] relative overflow-hidden bg-black">
|
||
{project.image ? (
|
||
<Image
|
||
src={project.image}
|
||
alt={project.title}
|
||
fill
|
||
sizes="(max-width: 768px) 100vw, 25vw"
|
||
className="object-cover transition-transform duration-700 group-hover:scale-110 grayscale group-hover:grayscale-0"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center text-gray-700 font-mono text-[10px] tracking-widest opacity-20">GÖRSEL_YOK</div>
|
||
)}
|
||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60" />
|
||
<div className="absolute top-4 right-4 bg-black/60 backdrop-blur-md px-3 py-1 rounded-full text-[10px] font-bold text-cyan-400 border border-white/10">
|
||
{project.category}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-6 flex-1 flex flex-col">
|
||
<div className="flex justify-between items-start mb-2">
|
||
<h3 className="font-bold text-lg leading-tight group-hover:text-cyan-400 transition-colors uppercase tracking-tight">{project.title}</h3>
|
||
<span className="text-[10px] font-mono text-gray-500 mt-1">{project.year}</span>
|
||
</div>
|
||
<p className="text-[10px] text-gray-500 mb-6 flex-1 italic font-mono uppercase tracking-widest">{project.location}</p>
|
||
|
||
<div className="grid grid-cols-2 gap-3 mt-auto">
|
||
<button
|
||
onClick={() => openEdit(project)}
|
||
className="py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-xs font-bold transition-colors"
|
||
>
|
||
Düzenle
|
||
</button>
|
||
<button
|
||
onClick={() => handleDelete(project.id)}
|
||
className="py-2.5 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-bold transition-colors"
|
||
>
|
||
Sil
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
))}
|
||
</AnimatePresence>
|
||
</div>
|
||
|
||
<AnimatePresence>
|
||
{isAdding && (
|
||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6">
|
||
<motion.div
|
||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||
onClick={() => !isLoading && (setIsAdding(false), setEditingProject(null))}
|
||
className="absolute inset-0 bg-black/90 backdrop-blur-sm"
|
||
/>
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||
className="bg-[#0a0a0a] border border-white/10 rounded-[40px] p-10 w-full max-w-3xl relative z-10 shadow-2xl max-h-[95vh] overflow-y-auto no-scrollbar"
|
||
>
|
||
<h2 className="text-2xl font-bold mb-8 flex items-center gap-3">
|
||
<span className="w-1.5 h-6 bg-cyan-500 rounded-full"></span>
|
||
{editingProject ? 'Projeyi Düzenle' : 'Yeni Proje Ekle'}
|
||
</h2>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500 ml-1">Proje Adı</label>
|
||
<input name="title" required defaultValue={editingProject?.title} type="text" className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500 ml-1">Yapım Yılı</label>
|
||
<input name="year" required defaultValue={editingProject?.year} type="text" className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500 ml-1">Konum</label>
|
||
<input name="location" required defaultValue={editingProject?.location} type="text" className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500 ml-1">Kategori</label>
|
||
<select name="category" required defaultValue={editingProject?.category || 'Konut'} className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all appearance-none">
|
||
<option value="Konut" className="bg-black">Konut</option>
|
||
<option value="Ticari" className="bg-black">Ticari</option>
|
||
<option value="Kültürel" className="bg-black">Kültürel</option>
|
||
<option value="Restorasyon" className="bg-black">Restorasyon</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500 ml-1">Proje Açıklaması</label>
|
||
<textarea
|
||
name="description"
|
||
defaultValue={editingProject?.description || ''}
|
||
rows={4}
|
||
className="w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all resize-none"
|
||
placeholder="Proje detaylarını buraya yazın..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-6 bg-white/3 rounded-[32px] border border-white/5 space-y-6">
|
||
<h3 className="text-xs font-bold text-gray-400 uppercase tracking-widest">Medya Dosyaları</h3>
|
||
|
||
{currentCover && (
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between items-center pr-2">
|
||
<label className="text-[9px] font-bold uppercase text-gray-600">Mevcut Kapak</label>
|
||
<button type="button" onClick={removeCoverImage} className="text-[9px] font-bold text-rose-500 hover:text-rose-400">KAPAĞI KALDIR</button>
|
||
</div>
|
||
<div className="relative w-40 aspect-video rounded-xl overflow-hidden border border-white/10 group/cover">
|
||
<Image src={currentCover} alt="Preview" fill sizes="160px" className="object-cover" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||
{currentCover ? 'Kapağı Değiştir' : 'Kapak Resmi Yükle'}
|
||
</label>
|
||
<input name="image" required={!currentCover} type="file" accept="image/*" className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-cyan-500 transition-all" />
|
||
</div>
|
||
|
||
{currentGallery.length > 0 && (
|
||
<div className="space-y-3">
|
||
<label className="text-[9px] font-bold uppercase text-gray-600">Mevcut Galeri ({currentGallery.length})</label>
|
||
<div className="grid grid-cols-4 md:grid-cols-6 gap-3">
|
||
{currentGallery.map((url, i) => (
|
||
<div key={i} className="relative aspect-square rounded-lg overflow-hidden border border-white/10 group/img">
|
||
<Image src={url} alt="Gallery item" fill sizes="80px" className="object-cover transition-transform group-hover/img:scale-110" />
|
||
<button
|
||
type="button"
|
||
onClick={() => removeGalleryImage(url)}
|
||
className="absolute inset-0 bg-rose-500/60 opacity-0 group-hover/img:opacity-100 flex items-center justify-center transition-opacity"
|
||
>
|
||
<span className="text-[10px] font-bold text-white">SİL</span>
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
|
||
Yeni Galeri Resimleri Ekle
|
||
</label>
|
||
<input name="gallery" type="file" accept="image/*" multiple className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-cyan-500 transition-all" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-6 flex gap-4">
|
||
<button type="button" disabled={isLoading} onClick={() => { setIsAdding(false); setEditingProject(null); }} className="flex-1 py-4 rounded-3xl bg-white/5 hover:bg-white/10 font-bold transition-all disabled:opacity-50">İptal</button>
|
||
<button type="submit" disabled={isLoading} className="flex-2 py-4 rounded-3xl bg-gradient-to-r from-cyan-500 to-blue-600 text-black font-extrabold tracking-widest uppercase text-sm transition-all shadow-xl shadow-cyan-500/20 active:scale-95 disabled:opacity-50 flex items-center justify-center gap-2">
|
||
{isLoading ? (
|
||
<>
|
||
<div className="w-4 h-4 border-2 border-black/30 border-t-black rounded-full animate-spin" />
|
||
Aktarılıyor...
|
||
</>
|
||
) : (editingProject ? 'Değişiklikleri Uygula' : 'Yayına Al')}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
</>
|
||
);
|
||
}
|