4096b3d87b
Sorun:
- node-appwrite paketi 'node-fetch-native-with-agent' polyfill'i kullanıyor
- Node.js 26'nın undici implementation'ı ile uyumsuz
- 'fetch failed / InvalidArgumentError: invalid onError method' hatası
- Login dahil tüm Appwrite çağrıları başarısız
Çözüm:
- Tüm node-appwrite kullanımını browser SDK 'appwrite'a geçir
- Browser SDK native fetch kullanıyor, Node 26 uyumlu
- API key tabanlı admin client yerine session cookie tabanlı user client
- Public reads (read('any')): publicDB (auth'suz client)
- Admin CRUD: userDB(sessionSecret) (cookie'deki session)
- Storage upload doğrudan File objesi alıyor (InputFile.fromBuffer gerekmez)
Etkilenen dosyalar:
- lib/appwrite-server.ts: publicClient + sessionClient
- lib/auth.ts: requireSessionSecret eklendi
- lib/admin-actions.ts: tüm action'lar sessionClient kullanıyor
- app/actions.ts: publicDB
- lib/data.ts: publicDB
- app/admin/login/actions.ts: appwrite SDK
- app/admin/(protected)/page.tsx, medya/page.tsx: userDB/userStorage
End-to-end test edildi:
✓ Login (401 doğru hata)
✓ Public read (services)
✓ Anonim create (contact form)
✓ npm run build 23 route
127 lines
4.7 KiB
TypeScript
127 lines
4.7 KiB
TypeScript
import Link from "next/link";
|
||
import { ArrowRight } from "lucide-react";
|
||
import { Query } from "appwrite";
|
||
import { DATABASE_ID, TABLES, userDB } from "@/lib/appwrite-server";
|
||
import { requireSessionSecret } from "@/lib/auth";
|
||
|
||
async function safeCount(tableId: string, queries: string[] = []) {
|
||
try {
|
||
const secret = await requireSessionSecret();
|
||
const res = await userDB(secret).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>
|
||
);
|
||
}
|