feat: admin paneli + blog + testimonials + SEO yöneticisi

Backend altyapısı:
- 4 yeni Appwrite tablosu: blog_posts, testimonials, seo_pages, seo_settings
- Appwrite Storage bucket: kovak-yazilim-media (görsel yüklemeleri)
- Appwrite Auth ile session cookie tabanlı koruma

Admin paneli (/admin):
- Login akışı (email/password) + protected layout
- Dashboard: sayım kartları + hızlı aksiyonlar
- Blog CRUD: markdown content, kapak görseli, draft/published, SEO alanları
- Services CRUD: lucide ikon seçici
- Projects CRUD: teknoloji etiketleri, live URL
- Testimonials CRUD: puanlama
- SEO yöneticisi: global ayarlar + sayfa bazlı override
- Mesaj inbox: status filtreleme + güncelleme
- Medya kütüphanesi: Appwrite Storage upload/delete

Public:
- /blog ve /blog/[slug] sayfaları (markdown render)
- Anasayfaya Testimonials bölümü
- Tüm public sayfalarda generateMetadata + seo_pages override
- Header'a Blog linki

Route yapısı:
- app/(site)/ — public site, Header/Footer ortak
- app/admin/login — auth dışı
- app/admin/(protected)/ — requireUser() korumalı

23 route üretiliyor, public static, admin dynamic.
This commit is contained in:
Ege Can Komur
2026-05-20 02:13:09 +03:00
parent 0f20309e4d
commit f833d429fc
52 changed files with 2999 additions and 81 deletions
+124
View File
@@ -0,0 +1,124 @@
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { adminDB, DATABASE_ID, TABLES } from "@/lib/appwrite-server";
import { Query } from "node-appwrite";
async function safeCount(tableId: string, queries: string[] = []) {
try {
const res = await adminDB.listRows({
databaseId: DATABASE_ID,
tableId,
queries: [...queries, Query.limit(1)],
});
return res.total ?? 0;
} catch {
return 0;
}
}
export default async function AdminDashboard() {
const [posts, drafts, services, projects, testimonials, newMessages, totalMessages] =
await Promise.all([
safeCount(TABLES.blogPosts, [Query.equal("status", "published")]),
safeCount(TABLES.blogPosts, [Query.equal("status", "draft")]),
safeCount(TABLES.services),
safeCount(TABLES.projects),
safeCount(TABLES.testimonials),
safeCount(TABLES.contactMessages, [Query.equal("status", "new")]),
safeCount(TABLES.contactMessages),
]);
const cards = [
{ label: "Yayında blog", value: posts, href: "/admin/blog", accent: "navy" },
{ label: "Taslak blog", value: drafts, href: "/admin/blog", accent: "amber" },
{ label: "Hizmet", value: services, href: "/admin/hizmetler", accent: "navy" },
{ label: "Proje", value: projects, href: "/admin/projeler", accent: "navy" },
{ label: "Referans", value: testimonials, href: "/admin/referanslar", accent: "navy" },
{ label: "Yeni mesaj", value: newMessages, href: "/admin/iletisim?filter=new", accent: "red" },
{ label: "Toplam mesaj", value: totalMessages, href: "/admin/iletisim", accent: "navy" },
];
return (
<div>
<header>
<h1 className="text-2xl font-bold text-[var(--navy)]">Pano</h1>
<p className="mt-1 text-sm text-[var(--muted)]">
Site içeriklerini buradan yönetin.
</p>
</header>
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{cards.map((c) => (
<Link
key={c.label}
href={c.href}
className="group rounded-2xl border border-[var(--border)] bg-white p-5 transition hover:border-[var(--sky)]/40 hover:shadow-md"
>
<p className="text-xs font-medium uppercase tracking-wider text-[var(--muted)]">
{c.label}
</p>
<div className="mt-2 flex items-end justify-between">
<p
className={`text-3xl font-bold ${
c.accent === "red"
? "text-red-600"
: c.accent === "amber"
? "text-amber-600"
: "text-[var(--navy)]"
}`}
>
{c.value}
</p>
<ArrowRight className="size-4 -translate-x-2 text-[var(--muted)] opacity-0 transition group-hover:translate-x-0 group-hover:opacity-100" />
</div>
</Link>
))}
</div>
<section className="mt-10 grid gap-6 lg:grid-cols-2">
<QuickActions />
<RecentLinks />
</section>
</div>
);
}
function QuickActions() {
const actions = [
{ href: "/admin/blog/new", label: "Yeni blog yazısı" },
{ href: "/admin/projeler/new", label: "Yeni proje ekle" },
{ href: "/admin/referanslar/new", label: "Yeni referans" },
{ href: "/admin/seo", label: "SEO ayarları" },
];
return (
<div className="rounded-2xl border border-[var(--border)] bg-white p-6">
<h2 className="text-base font-semibold text-[var(--navy)]">Hızlı aksiyonlar</h2>
<ul className="mt-4 space-y-2">
{actions.map((a) => (
<li key={a.href}>
<Link
href={a.href}
className="flex items-center justify-between rounded-lg border border-[var(--border)] px-3 py-2 text-sm text-[var(--navy)] transition hover:border-[var(--sky)]"
>
{a.label}
<ArrowRight className="size-4 text-[var(--sky-600)]" />
</Link>
</li>
))}
</ul>
</div>
);
}
function RecentLinks() {
return (
<div className="rounded-2xl border border-[var(--border)] bg-white p-6">
<h2 className="text-base font-semibold text-[var(--navy)]">Kısayollar</h2>
<ul className="mt-4 space-y-1 text-sm text-[var(--muted)]">
<li> <Link href="/" target="_blank" className="hover:text-[var(--navy)]">Siteyi yeni sekmede </Link></li>
<li> <Link href="/admin/medya" className="hover:text-[var(--navy)]">Medya kütüphanesi</Link></li>
<li> <Link href="/admin/seo" className="hover:text-[var(--navy)]">SEO yönetimi</Link></li>
</ul>
</div>
);
}