initial commit: project completion with proper gitignore

This commit is contained in:
AyrisAI
2026-05-16 00:43:22 +03:00
commit e708ba2156
84 changed files with 11035 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import Link from 'next/link';
import { LayoutDashboard, Users, Briefcase, Settings, FileText, LogOut, Handshake } from 'lucide-react';
import { logout } from '../actions';
export default function AdminLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-black text-white flex">
{/* Sidebar */}
<aside className="w-64 border-r border-white/10 flex flex-col bg-zinc-950 p-4">
<div className="mb-8 px-4">
<h1 className="text-xl font-black uppercase tracking-widest text-[#1e9a83]">Muğla Dijital</h1>
<p className="text-xs text-white/40 uppercase tracking-widest">Admin Panel</p>
</div>
<nav className="flex-1 space-y-2">
<Link href="/admin" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<LayoutDashboard className="w-5 h-5 text-white/60" />
Dashboard
</Link>
<Link href="/admin/leads" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<Users className="w-5 h-5 text-white/60" />
Mesajlar & Başvurular
</Link>
<Link href="/admin/projects" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<Briefcase className="w-5 h-5 text-white/60" />
Projeler
</Link>
<Link href="/admin/services" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<FileText className="w-5 h-5 text-white/60" />
Hizmetler
</Link>
<Link href="/admin/settings" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<Settings className="w-5 h-5 text-white/60" />
Site Ayarları
</Link>
<Link href="/admin/partners" className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-sm font-medium">
<Handshake className="w-5 h-5 text-white/60" />
Partnerler
</Link>
</nav>
<div className="mt-auto border-t border-white/10 pt-4">
<form action={logout}>
<button type="submit" className="flex w-full items-center gap-3 px-4 py-3 rounded-xl hover:bg-red-500/10 hover:text-red-500 transition-colors text-sm font-medium text-white/60">
<LogOut className="w-5 h-5" />
Çıkış Yap
</button>
</form>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<div className="max-w-6xl mx-auto p-8">
{children}
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,71 @@
import { getLeadsAdmin, deleteLead } from '../../actions';
import { Trash2, Mail, User, Briefcase, Calendar } from 'lucide-react';
import { revalidatePath } from 'next/cache';
export default async function LeadsPage() {
const leads = await getLeadsAdmin();
async function handleDelete(formData: FormData) {
'use server';
const id = Number(formData.get('id'));
await deleteLead(id);
revalidatePath('/admin/leads');
}
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Mesajlar & Başvurular</h1>
<p className="text-white/40">İletişim formundan gelen müşteri talepleri.</p>
</div>
<div className="space-y-4">
{leads.map((lead: any) => (
<div key={lead.id} className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex flex-col md:flex-row gap-6 justify-between md:items-center group hover:border-[#1e9a83]/50 transition-colors">
<div className="space-y-3 flex-1">
<div className="flex items-center gap-4">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<User className="w-4 h-4 text-[#1e9a83]" />
{lead.full_name}
</h3>
<span className="bg-white/5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest text-white/60 flex items-center gap-2">
<Briefcase className="w-3 h-3" />
{lead.service_type}
</span>
</div>
<div className="flex flex-col md:flex-row gap-4 text-sm text-white/60">
<a href={`mailto:${lead.email}`} className="flex items-center gap-2 hover:text-white transition-colors">
<Mail className="w-4 h-4" /> {lead.email}
</a>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{new Date(lead.created_at).toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<div className="bg-white/5 p-4 rounded-xl mt-4">
<p className="text-white/80 text-sm leading-relaxed">{lead.message}</p>
</div>
</div>
<div>
<form action={handleDelete}>
<input type="hidden" name="id" value={lead.id} />
<button type="submit" className="p-3 bg-red-500/10 text-red-500 rounded-xl hover:bg-red-500 hover:text-white transition-colors">
<Trash2 className="w-5 h-5" />
</button>
</form>
</div>
</div>
))}
{(!leads || leads.length === 0) && (
<div className="text-center py-12 bg-white/5 rounded-2xl border border-white/10 border-dashed">
<p className="text-white/40">Henüz bir mesaj bulunmuyor.</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { getDashboardStats } from '../actions';
import { Users, Briefcase, FileText } from 'lucide-react';
export default async function DashboardPage() {
const stats = await getDashboardStats();
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Dashboard</h1>
<p className="text-white/40">Sistem istatistikleri ve özet görünüm.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
<div className="w-14 h-14 bg-blue-500/10 text-blue-500 rounded-2xl flex items-center justify-center">
<Users className="w-6 h-6" />
</div>
<div>
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Mesajlar</p>
<p className="text-3xl font-black">{stats.leads}</p>
</div>
</div>
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
<div className="w-14 h-14 bg-[#1e9a83]/10 text-[#1e9a83] rounded-2xl flex items-center justify-center">
<Briefcase className="w-6 h-6" />
</div>
<div>
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Projeler</p>
<p className="text-3xl font-black">{stats.projects}</p>
</div>
</div>
<div className="bg-zinc-950 border border-white/10 rounded-2xl p-6 flex items-center gap-6">
<div className="w-14 h-14 bg-purple-500/10 text-purple-500 rounded-2xl flex items-center justify-center">
<FileText className="w-6 h-6" />
</div>
<div>
<p className="text-sm text-white/40 font-bold uppercase tracking-widest">Hizmetler</p>
<p className="text-3xl font-black">{stats.services}</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
"use client";
import { useState } from "react";
import { Trash2, Save, Edit2, X } from "lucide-react";
import { updatePartner } from "../../actions";
import { useRouter } from "next/navigation";
import Image from "next/image";
export default function PartnerRow({ partner, onDelete }: { partner: any, onDelete: any }) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(partner.name);
const [displayOrder, setDisplayOrder] = useState(partner.display_order);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleSave = async () => {
setIsLoading(true);
const res = await updatePartner(partner.id, name, Number(displayOrder));
setIsLoading(false);
if (res.success) {
setIsEditing(false);
router.refresh();
} else {
alert("Güncellenirken bir hata oluştu");
}
};
return (
<tr className="hover:bg-white/[0.02] transition-colors border-b border-white/10 last:border-0">
<td className="px-6 py-4">
{isEditing ? (
<input
type="number"
value={displayOrder}
onChange={(e) => setDisplayOrder(e.target.value)}
className="w-16 bg-white/5 border border-white/10 rounded px-2 py-1 text-white focus:border-primary outline-none"
/>
) : (
<span className="font-bold text-white/40">{partner.display_order}</span>
)}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-4">
{/* Logo Preview */}
<div className="w-24 h-16 bg-white/5 border border-white/10 rounded-lg flex items-center justify-center overflow-hidden p-2 group relative">
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name}
className="w-full h-full object-contain opacity-90 group-hover:opacity-100 transition-opacity transform scale-[4]"
/>
) : (
<span className="text-[10px] text-white/20">Logo Yok</span>
)}
</div>
{/* Tiny Path Info */}
<div className="hidden xl:block">
<div className="text-[10px] text-white/20 font-mono max-w-[200px] truncate">
{partner.logo}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
{isEditing ? (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full max-w-md bg-white/5 border border-white/10 rounded px-3 py-1.5 text-white focus:border-primary outline-none"
/>
) : (
<div className="font-bold text-white tracking-wide">{partner.name}</div>
)}
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end items-center gap-2">
{isEditing ? (
<>
<button
onClick={handleSave}
disabled={isLoading}
className="p-2 bg-green-500/10 text-green-500 rounded-lg hover:bg-green-500 hover:text-white transition-all disabled:opacity-50"
title="Kaydet"
>
<Save className="w-4 h-4" />
</button>
<button
onClick={() => {
setIsEditing(false);
setName(partner.name);
setDisplayOrder(partner.display_order);
}}
className="p-2 bg-white/5 text-white/40 rounded-lg hover:bg-white/10 hover:text-white transition-all"
title="İptal"
>
<X className="w-4 h-4" />
</button>
</>
) : (
<>
<button
onClick={() => setIsEditing(true)}
className="p-2 bg-white/5 text-white/40 rounded-lg hover:bg-primary hover:text-white transition-all"
title="Düzenle"
>
<Edit2 className="w-4 h-4" />
</button>
<form action={onDelete} className="inline">
<input type="hidden" name="id" value={partner.id} />
<button
type="submit"
className="p-2 bg-red-500/10 text-red-500 rounded-lg hover:bg-red-500 hover:text-white transition-all"
title="Sil"
>
<Trash2 className="w-4 h-4" />
</button>
</form>
</>
)}
</div>
</td>
</tr>
);
}

View File

@@ -0,0 +1,55 @@
import { getPartnersAdmin, deletePartner } from '../../actions';
import { Plus } from 'lucide-react';
import { revalidatePath } from 'next/cache';
import PartnerRow from './PartnerRow';
export default async function PartnersAdminPage() {
const partners = await getPartnersAdmin();
async function handleDelete(formData: FormData) {
'use server';
const id = Number(formData.get('id'));
await deletePartner(id);
revalidatePath('/admin/partners');
}
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">Partnerler</h1>
<p className="text-white/40">Referans markaların listesi.</p>
</div>
<button className="bg-primary text-white px-6 py-2 rounded-xl font-bold hover:bg-primary/80 transition-colors flex items-center gap-2">
<Plus className="w-4 h-4" />
Yeni Partner Ekle
</button>
</div>
<div className="bg-zinc-950 border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
<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 w-24">Sıra</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Logo Önizleme</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Marka Adı</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40 text-right">İşlemler</th>
</tr>
</thead>
<tbody>
{partners.map((partner: any) => (
<PartnerRow key={partner.id} partner={partner} onDelete={handleDelete} />
))}
{(!partners || partners.length === 0) && (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-white/40">
Henüz partner eklenmemiş.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}

View 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>
);
}

View 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 ı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ıı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&#10;Next.js&#10;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/...&#10;/img1.jpg&#10;/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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,82 @@
import { getServicesAdmin, deleteService } from '../../actions';
import { Trash2, Edit } from 'lucide-react';
import { revalidatePath } from 'next/cache';
export default async function ServicesPage() {
const services = await getServicesAdmin();
async function handleDelete(formData: FormData) {
'use server';
const id = Number(formData.get('id'));
await deleteService(id);
revalidatePath('/admin/services');
}
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">Hizmetler</h1>
<p className="text-white/40">Sistemdeki tüm hizmetlerin listesi.</p>
</div>
<button className="bg-[#1e9a83] text-white px-6 py-2 rounded-xl font-bold hover:bg-[#157a67] transition-colors">
Yeni Hizmet Ekle
</button>
</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 w-16">Sıra</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">İkon Adı</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-widest text-white/40">Hizmet Adı</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">
{services.map((service: any) => (
<tr key={service.id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-4">
<span className="font-bold text-white/40">{service.display_order}</span>
</td>
<td className="px-6 py-4">
<span className="bg-white/5 px-2 py-1 rounded-md text-xs text-[#1e9a83] font-mono">{service.icon_name || '-'}</span>
</td>
<td className="px-6 py-4">
<div className="font-bold text-white">{service.title}</div>
</td>
<td className="px-6 py-4">
{service.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">
<button 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" />
</button>
<form action={handleDelete}>
<input type="hidden" name="id" value={service.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>
))}
{(!services || services.length === 0) && (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-white/40">
Henüz hizmet eklenmemiş.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
'use client';
import { useState } from 'react';
import { updateSettings } from '../../actions';
import { Save, CheckCircle2, AlertCircle } from 'lucide-react';
export default function SettingsForm({ initialData }: { initialData: any }) {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
async function handleSubmit(formData: FormData) {
setStatus('loading');
const res = await updateSettings(formData);
if (res.error) setStatus('error');
else setStatus('success');
setTimeout(() => setStatus('idle'), 3000);
}
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">Site Adı</label>
<input type="text" name="site_name" defaultValue={initialData?.site_name} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">İletişim E-posta</label>
<input type="email" name="contact_email" defaultValue={initialData?.contact_email} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">İletişim Telefon</label>
<input type="text" name="contact_phone" defaultValue={initialData?.contact_phone} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Rozet Metni</label>
<input type="text" name="status_badge_text" defaultValue={initialData?.status_badge_text} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Site ıklaması (SEO)</label>
<textarea name="site_description" rows={3} defaultValue={initialData?.site_description} 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" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Ofis Adresi</label>
<textarea name="office_address" rows={2} defaultValue={initialData?.office_address} 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" />
</div>
<h3 className="text-sm font-bold uppercase tracking-widest text-white/60 mt-8 mb-4 border-b border-white/10 pb-2">Sosyal Medya Linkleri</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Instagram URL</label>
<input type="url" name="instagram_url" defaultValue={initialData?.instagram_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Twitter URL</label>
<input type="url" name="twitter_url" defaultValue={initialData?.twitter_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">LinkedIn URL</label>
<input type="url" name="linkedin_url" defaultValue={initialData?.linkedin_url} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-[#1e9a83] outline-none" />
</div>
</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...' : 'Ayarları 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" /> Ayarlar başarıyla güncellendi!
</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" /> Güncelleme sırasında bir hata oluştu.
</div>
)}
</form>
);
}

View File

@@ -0,0 +1,17 @@
import { getSettings } from '@/app/actions';
import SettingsForm from './SettingsForm';
export default async function SettingsPage() {
const settings = await getSettings();
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-black uppercase tracking-widest text-white mb-2">Site Ayarları</h1>
<p className="text-white/40">Genel site bilgilerini ve iletişim adreslerini buradan güncelleyebilirsiniz.</p>
</div>
<SettingsForm initialData={settings} />
</div>
);
}

260
app/admin/actions.ts Normal file
View File

@@ -0,0 +1,260 @@
'use server';
import sql from '@/lib/db';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export async function login(prevState: any, formData: FormData) {
const password = formData.get('password') as string;
if (password === process.env.ADMIN_PASSWORD) {
const cookieStore = await cookies();
cookieStore.set('admin_session', 'authenticated', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/',
});
redirect('/admin');
}
return { error: 'Hatalı şifre' };
}
export async function logout() {
const cookieStore = await cookies();
cookieStore.delete('admin_session');
redirect('/admin/login');
}
export async function getDashboardStats() {
try {
const [leadsCount] = await sql`SELECT count(*) FROM leads`;
const [projectsCount] = await sql`SELECT count(*) FROM projects`;
const [servicesCount] = await sql`SELECT count(*) FROM services`;
return {
leads: Number(leadsCount.count),
projects: Number(projectsCount.count),
services: Number(servicesCount.count),
};
} catch (e) {
return { leads: 0, projects: 0, services: 0 };
}
}
export async function getLeadsAdmin() {
try {
return await sql`SELECT * FROM leads ORDER BY created_at DESC`;
} catch (e) {
return [];
}
}
export async function deleteLead(id: number) {
try {
await sql`DELETE FROM leads WHERE id = ${id}`;
return { success: true };
} catch (e) {
return { error: 'Silinemedi' };
}
}
export async function updateSettings(formData: FormData) {
try {
const data = {
site_name: formData.get('site_name') as string,
site_description: formData.get('site_description') as string,
office_address: formData.get('office_address') as string,
contact_email: formData.get('contact_email') as string,
contact_phone: formData.get('contact_phone') as string,
instagram_url: formData.get('instagram_url') as string,
twitter_url: formData.get('twitter_url') as string,
linkedin_url: formData.get('linkedin_url') as string,
status_badge_text: formData.get('status_badge_text') as string,
};
await sql`
UPDATE settings SET
site_name = ${data.site_name},
site_description = ${data.site_description},
office_address = ${data.office_address},
contact_email = ${data.contact_email},
contact_phone = ${data.contact_phone},
instagram_url = ${data.instagram_url},
twitter_url = ${data.twitter_url},
linkedin_url = ${data.linkedin_url},
status_badge_text = ${data.status_badge_text},
updated_at = CURRENT_TIMESTAMP
WHERE id = 1
`;
return { success: true };
} catch (e) {
return { error: 'Güncellenemedi' };
}
}
export async function getProjectsAdmin() {
try {
return await sql`SELECT * FROM projects ORDER BY created_at DESC`;
} catch (e) {
return [];
}
}
export async function getProjectByIdAdmin(id: number) {
try {
const result = await sql`SELECT * FROM projects WHERE id = ${id}`;
return result[0];
} catch (e) {
return null;
}
}
export async function deleteProject(id: number) {
try {
await sql`DELETE FROM projects WHERE id = ${id}`;
return { success: true };
} catch (e) {
return { error: 'Silinemedi' };
}
}
export async function createProjectAdmin(formData: FormData) {
try {
const slug = formData.get('slug') as string;
const title = formData.get('title') as string;
const subtitle = formData.get('subtitle') as string;
const year = formData.get('year') as string;
const hero_image = formData.get('hero_image') as string;
const client = formData.get('client') as string;
const role = formData.get('role') as string;
const location = formData.get('location') as string;
const narrative_title = formData.get('narrative_title') as string;
const narrative_desc = formData.get('narrative_desc') as string;
const is_featured = formData.get('is_featured') === 'on';
const categoriesRaw = formData.getAll('category') as string[];
const techStackRaw = formData.get('tech_stack') as string;
const galleryRaw = formData.get('gallery') as string;
const category = JSON.stringify(categoriesRaw.filter(Boolean));
const tech_stack = JSON.stringify(techStackRaw ? techStackRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
const gallery = JSON.stringify(galleryRaw ? galleryRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
await sql`
INSERT INTO projects (
slug, title, subtitle, category, year, hero_image, client, role, location,
narrative_title, narrative_desc, is_featured, tech_stack, gallery
) VALUES (
${slug}, ${title}, ${subtitle}, ${category}::jsonb, ${year}, ${hero_image}, ${client}, ${role}, ${location},
${narrative_title}, ${narrative_desc}, ${is_featured}, ${tech_stack}::jsonb, ${gallery}::jsonb
)
`;
return { success: true };
} catch (e: any) {
console.error('Error creating project:', e);
return { error: 'Ekleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
}
}
export async function updateProjectAdmin(id: number, formData: FormData) {
try {
const slug = formData.get('slug') as string;
const title = formData.get('title') as string;
const subtitle = formData.get('subtitle') as string;
const year = formData.get('year') as string;
const hero_image = formData.get('hero_image') as string;
const client = formData.get('client') as string;
const role = formData.get('role') as string;
const location = formData.get('location') as string;
const narrative_title = formData.get('narrative_title') as string;
const narrative_desc = formData.get('narrative_desc') as string;
const is_featured = formData.get('is_featured') === 'on';
const categoriesRaw = formData.getAll('category') as string[];
const techStackRaw = formData.get('tech_stack') as string;
const galleryRaw = formData.get('gallery') as string;
const category = JSON.stringify(categoriesRaw.filter(Boolean));
const tech_stack = JSON.stringify(techStackRaw ? techStackRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
const gallery = JSON.stringify(galleryRaw ? galleryRaw.split(/[\n,]/).map(s => s.trim()).filter(Boolean) : []);
await sql`
UPDATE projects SET
slug = ${slug}, title = ${title}, subtitle = ${subtitle}, category = ${category}::jsonb,
year = ${year}, hero_image = ${hero_image}, client = ${client}, role = ${role},
location = ${location}, narrative_title = ${narrative_title},
narrative_desc = ${narrative_desc}, is_featured = ${is_featured},
tech_stack = ${tech_stack}::jsonb, gallery = ${gallery}::jsonb
WHERE id = ${id}
`;
return { success: true };
} catch (e: any) {
console.error('Error updating project:', e);
return { error: 'Güncelleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
}
}
export async function getServicesAdmin() {
try {
return await sql`SELECT * FROM services ORDER BY display_order ASC`;
} catch (e) {
return [];
}
}
export async function deleteService(id: number) {
try {
await sql`DELETE FROM services WHERE id = ${id}`;
return { success: true };
} catch (e) {
return { error: 'Silinemedi' };
}
}
export async function getPartnersAdmin() {
try {
return await sql`SELECT * FROM partners ORDER BY display_order ASC`;
} catch (e) {
return [];
}
}
export async function deletePartner(id: number) {
try {
await sql`DELETE FROM partners WHERE id = ${id}`;
return { success: true };
} catch (e) {
return { error: 'Silinemedi' };
}
}
export async function createPartnerAdmin(formData: FormData) {
try {
const name = formData.get('name') as string;
const logo = formData.get('logo') as string;
const display_order = Number(formData.get('display_order')) || 0;
await sql`
INSERT INTO partners (name, logo, display_order)
VALUES (${name}, ${logo}, ${display_order})
`;
return { success: true };
} catch (e: any) {
console.error('Error creating partner:', e);
return { error: 'Ekleme başarısız: ' + (e.message || 'Bilinmeyen hata') };
}
}
export async function updatePartner(id: number, name: string, display_order: number) {
try {
await sql`
UPDATE partners
SET name = ${name}, display_order = ${display_order}
WHERE id = ${id}
`;
return { success: true };
} catch (e) {
return { error: 'Güncellenemedi' };
}
}

51
app/admin/login/page.tsx Normal file
View File

@@ -0,0 +1,51 @@
'use client';
import { useActionState } from 'react';
import { login } from '../actions';
import { Lock, ArrowRight } from 'lucide-react';
export default function LoginPage() {
const [state, formAction, pending] = useActionState(login, null);
return (
<div className="min-h-screen bg-black flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-zinc-950 border border-white/10 rounded-3xl p-8 space-y-8">
<div className="text-center space-y-2">
<div className="w-16 h-16 bg-[#1e9a83]/10 rounded-full flex items-center justify-center mx-auto mb-6">
<Lock className="w-8 h-8 text-[#1e9a83]" />
</div>
<h1 className="text-2xl font-black uppercase tracking-widest text-white">Yönetici Girişi</h1>
<p className="text-white/40 text-sm">Devam etmek için şifrenizi giriniz.</p>
</div>
<form action={formAction} className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-white/40 ml-1">Şifre</label>
<input
type="password"
name="password"
required
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder:text-white/20 focus:border-[#1e9a83] focus:ring-1 focus:ring-[#1e9a83] outline-none transition-all"
placeholder="••••••••"
/>
</div>
{state?.error && (
<p className="text-red-500 text-xs font-bold text-center bg-red-500/10 py-2 rounded-lg">{state.error}</p>
)}
<button
type="submit"
disabled={pending}
className="w-full bg-[#1e9a83] hover:bg-[#157a67] text-white font-bold py-3 px-4 rounded-xl transition-colors flex items-center justify-center gap-2 disabled:opacity-50"
>
{pending ? 'Giriş Yapılıyor...' : 'Giriş Yap'}
{!pending && <ArrowRight className="w-4 h-4" />}
</button>
</form>
</div>
</div>
</div>
);
}