f833d429fc
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.
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
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 aç</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>
|
||
);
|
||
}
|