Files
aycanurmimarl-k/app/admin/projects/ProjectsClient.tsx
2026-04-17 11:16:00 +03:00

299 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 ı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>
</>
);
}