initial commit: project completion with proper gitignore
This commit is contained in:
32
app/admin/(dashboard)/projects/[id]/page.tsx
Normal file
32
app/admin/(dashboard)/projects/[id]/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getProjectByIdAdmin, getServicesAdmin, getPartnersAdmin } from '../../../actions';
|
||||
import ProjectForm from '../new/ProjectForm';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default async function EditProjectPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const project = await getProjectByIdAdmin(Number(id));
|
||||
const services = await getServicesAdmin();
|
||||
const partners = await getPartnersAdmin();
|
||||
|
||||
if (!project) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-5xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/projects" className="p-2 bg-white/5 hover:bg-white/10 rounded-xl transition-colors group">
|
||||
<ArrowLeft className="w-5 h-5 text-white/40 group-hover:text-white" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Projeyi Düzenle</h1>
|
||||
<p className="text-white/40">{project.title} projesini güncelliyorsunuz.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectForm initialData={project} services={services} partners={partners} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
207
app/admin/(dashboard)/projects/new/ProjectForm.tsx
Normal file
207
app/admin/(dashboard)/projects/new/ProjectForm.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createProjectAdmin, updateProjectAdmin } from '../../../actions';
|
||||
import { Save, CheckCircle2, AlertCircle, X } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const slugify = (text: string) => {
|
||||
return text
|
||||
.toString()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w-]+/g, '')
|
||||
.replace(/--+/g, '-');
|
||||
};
|
||||
|
||||
export default function ProjectForm({
|
||||
initialData,
|
||||
services = [],
|
||||
partners = []
|
||||
}: {
|
||||
initialData?: any,
|
||||
services?: any[],
|
||||
partners?: any[]
|
||||
}) {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [title, setTitle] = useState(initialData?.title || '');
|
||||
const [slug, setSlug] = useState(initialData?.slug || '');
|
||||
const router = useRouter();
|
||||
|
||||
// Auto-slug generation when title changes (only if it's a new project or manually changed title)
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
if (!initialData) {
|
||||
setSlug(slugify(newTitle));
|
||||
}
|
||||
};
|
||||
|
||||
const parseSafe = (data: any) => {
|
||||
let current = data;
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (typeof current === 'string' && (current.trim().startsWith('[') || current.trim().startsWith('"'))) {
|
||||
current = JSON.parse(current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
} catch (e) {
|
||||
return Array.isArray(current) ? current : (typeof current === 'string' && current ? [current] : []);
|
||||
}
|
||||
};
|
||||
|
||||
const initialTechStack = parseSafe(initialData?.tech_stack);
|
||||
const initialGallery = parseSafe(initialData?.gallery);
|
||||
const initialCategories = parseSafe(initialData?.category);
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
setStatus('loading');
|
||||
let res;
|
||||
if (initialData?.id) {
|
||||
res = await updateProjectAdmin(initialData.id, formData);
|
||||
} else {
|
||||
res = await createProjectAdmin(formData);
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
setErrorMsg(res.error);
|
||||
setStatus('error');
|
||||
} else {
|
||||
setStatus('success');
|
||||
setTimeout(() => {
|
||||
router.push('/admin/projects');
|
||||
router.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit} className="bg-zinc-950 border border-white/10 rounded-3xl p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Proje Adı (Partner Seçin)</label>
|
||||
<select
|
||||
name="title"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => handleTitleChange(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none"
|
||||
>
|
||||
<option value="" className="bg-zinc-900">Seçiniz...</option>
|
||||
{partners.map(p => (
|
||||
<option key={p.id} value={p.name} className="bg-zinc-900">{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kısa Başlık (URL Slug)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value)}
|
||||
required
|
||||
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none font-mono text-xs"
|
||||
placeholder="örn: marka-ismi"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 lg:col-span-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Kategoriler (Hizmetler - Çoklu Seçim)</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4 bg-white/5 border border-white/10 rounded-xl">
|
||||
{services.map(s => {
|
||||
const isChecked = initialCategories.includes(s.title);
|
||||
return (
|
||||
<label key={s.id} className="flex items-center gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="category"
|
||||
value={s.title}
|
||||
defaultChecked={isChecked}
|
||||
className="w-4 h-4 rounded border-white/10 bg-white/5 accent-[#1e9a83]"
|
||||
/>
|
||||
<span className="text-[11px] font-bold uppercase text-white/60 group-hover:text-white transition-colors">
|
||||
{s.title}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Yıl</label>
|
||||
<input type="text" name="year" defaultValue={initialData?.year || new Date().getFullYear()} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: 2024" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Müşteri (Görünen İsim)</label>
|
||||
<input type="text" name="client" defaultValue={initialData?.client || title} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: ABC A.Ş." />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Lokasyon</label>
|
||||
<input type="text" name="location" defaultValue={initialData?.location || 'Muğla, TR'} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Örn: Muğla, TR" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Alt Başlık (Kısa Açıklama)</label>
|
||||
<input type="text" name="subtitle" defaultValue={initialData?.subtitle} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Projenin kısa özeti..." />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Ana Görsel (URL veya Yol)</label>
|
||||
<input type="text" name="hero_image" defaultValue={initialData?.hero_image} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="/images/projects/1.jpg" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-white/60 mt-8 mb-4 border-b border-white/10 pb-2">Hikaye & Detay</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Hikaye Başlığı</label>
|
||||
<input type="text" name="narrative_title" defaultValue={initialData?.narrative_title} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" placeholder="Geleceği Yeniden Şekillendirmek" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Hikaye İçeriği (Paragraf)</label>
|
||||
<textarea name="narrative_desc" defaultValue={initialData?.narrative_desc} rows={4} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="Projenin detaylı açıklaması..."></textarea>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Teknolojiler (Her satıra bir tane)</label>
|
||||
<textarea name="tech_stack" defaultValue={initialTechStack.join('\n')} rows={5} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="React Next.js TailwindCSS"></textarea>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Galeri & Instagram Linkleri (Her satıra bir tane)</label>
|
||||
<textarea name="gallery" defaultValue={initialGallery.join('\n')} rows={5} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none resize-none" placeholder="https://instagram.com/p/... /img1.jpg /img2.jpg"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<input type="checkbox" name="is_featured" id="is_featured" defaultChecked={initialData?.is_featured} className="w-5 h-5 accent-[#1e9a83] rounded" />
|
||||
<label htmlFor="is_featured" className="text-sm font-bold text-white/80 cursor-pointer">Bu projeyi anasayfada (Öne Çıkanlar) göster</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<button type="submit" disabled={status === 'loading'} className="bg-[#1e9a83] hover:bg-[#157a67] text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 disabled:opacity-50">
|
||||
<Save className="w-5 h-5" />
|
||||
{status === 'loading' ? 'Kaydediliyor...' : initialData ? 'Projeyi Güncelle' : 'Projeyi Kaydet'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{status === 'success' && (
|
||||
<div className="flex items-center gap-2 text-green-500 text-sm font-bold bg-green-500/10 p-4 rounded-xl mt-4">
|
||||
<CheckCircle2 className="w-5 h-5" /> Proje başarıyla {initialData ? 'güncellendi' : 'eklendi'}! Yönlendiriliyorsunuz...
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div className="flex items-center gap-2 text-red-500 text-sm font-bold bg-red-500/10 p-4 rounded-xl mt-4">
|
||||
<AlertCircle className="w-5 h-5" /> {errorMsg}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
25
app/admin/(dashboard)/projects/new/page.tsx
Normal file
25
app/admin/(dashboard)/projects/new/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import ProjectForm from './ProjectForm';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { getServicesAdmin, getPartnersAdmin } from '../../../actions';
|
||||
|
||||
export default async function NewProjectPage() {
|
||||
const services = await getServicesAdmin();
|
||||
const partners = await getPartnersAdmin();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/admin/projects" className="p-2 bg-white/5 hover:bg-white/10 rounded-xl transition-colors">
|
||||
<ArrowLeft className="w-5 h-5 text-white/60" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Yeni Proje Ekle</h1>
|
||||
<p className="text-white/40">Sisteme yeni bir proje dahil edin.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectForm services={services} partners={partners} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
app/admin/(dashboard)/projects/page.tsx
Normal file
90
app/admin/(dashboard)/projects/page.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getProjectsAdmin, deleteProject } from '../../actions';
|
||||
import { Trash2, Edit } from 'lucide-react';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import Image from 'next/image';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
const projects = await getProjectsAdmin();
|
||||
|
||||
async function handleDelete(formData: FormData) {
|
||||
'use server';
|
||||
const id = Number(formData.get('id'));
|
||||
await deleteProject(id);
|
||||
revalidatePath('/admin/projects');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Projeler</h1>
|
||||
<p className="text-white/40">Sistemdeki tüm projelerin listesi.</p>
|
||||
</div>
|
||||
<Link href="/admin/projects/new" className="bg-[#1e9a83] text-white px-6 py-2 rounded-xl font-bold hover:bg-[#157a67] transition-colors">
|
||||
Yeni Proje Ekle
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-white/5 border-b border-white/10">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Görsel</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Proje Adı</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Kategori</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Öne Çıkan</th>
|
||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/10">
|
||||
{projects.map((project: any) => (
|
||||
<tr key={project.id} className="hover:bg-white/[0.02] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="w-16 h-12 relative rounded-lg overflow-hidden bg-white/10">
|
||||
{project.hero_image && (
|
||||
<Image src={project.hero_image} alt={project.title} fill className="object-cover" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-bold text-white">{project.title}</div>
|
||||
<div className="text-xs text-white/40">{project.slug}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="bg-white/5 px-2 py-1 rounded-md text-xs text-white/60">{project.category}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{project.is_featured ? (
|
||||
<span className="text-green-500 text-xs font-bold uppercase tracking-widest">Evet</span>
|
||||
) : (
|
||||
<span className="text-white/20 text-xs font-bold uppercase tracking-widest">Hayır</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right flex justify-end gap-2">
|
||||
<Link href={`/admin/projects/${project.id}`} className="p-2 bg-blue-500/10 text-blue-500 rounded-lg hover:bg-blue-500 hover:text-white transition-colors">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
<form action={handleDelete}>
|
||||
<input type="hidden" name="id" value={project.id} />
|
||||
<button type="submit" className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500 hover:text-white transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!projects || projects.length === 0) && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-white/40">
|
||||
Henüz proje eklenmemiş.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user