Files
kovakyazilim/app/admin/(protected)/page.tsx
T
Ege Can Komur f833d429fc 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.
2026-05-20 02:13:09 +03:00

125 lines
4.6 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.
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>
);
}