Files
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

203 lines
4.9 KiB
TypeScript

import Link from "next/link";
import { ArrowLeft } from "lucide-react";
export function PageHeader({
title,
description,
backHref,
action,
}: {
title: string;
description?: string;
backHref?: string;
action?: React.ReactNode;
}) {
return (
<div className="flex items-start justify-between gap-4">
<div>
{backHref && (
<Link
href={backHref}
className="mb-2 inline-flex items-center gap-1 text-xs text-[var(--muted)] hover:text-[var(--navy)]"
>
<ArrowLeft className="size-3" />
Geri
</Link>
)}
<h1 className="text-2xl font-bold text-[var(--navy)]">{title}</h1>
{description && (
<p className="mt-1 text-sm text-[var(--muted)]">{description}</p>
)}
</div>
{action}
</div>
);
}
export function Field({
label,
name,
type = "text",
defaultValue,
placeholder,
required,
help,
}: {
label: string;
name: string;
type?: string;
defaultValue?: string | number | null;
placeholder?: string;
required?: boolean;
help?: string;
}) {
return (
<label className="block">
<span className="text-sm font-medium text-[var(--navy)]">
{label}
{required && <span className="text-red-500"> *</span>}
</span>
<input
name={name}
type={type}
required={required}
defaultValue={defaultValue ?? undefined}
placeholder={placeholder}
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none transition focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
/>
{help && <span className="mt-1 block text-xs text-[var(--muted)]">{help}</span>}
</label>
);
}
export function Textarea({
label,
name,
defaultValue,
rows = 4,
placeholder,
required,
help,
}: {
label: string;
name: string;
defaultValue?: string | null;
rows?: number;
placeholder?: string;
required?: boolean;
help?: string;
}) {
return (
<label className="block">
<span className="text-sm font-medium text-[var(--navy)]">
{label}
{required && <span className="text-red-500"> *</span>}
</span>
<textarea
name={name}
required={required}
rows={rows}
defaultValue={defaultValue ?? undefined}
placeholder={placeholder}
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none transition focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
/>
{help && <span className="mt-1 block text-xs text-[var(--muted)]">{help}</span>}
</label>
);
}
export function Select({
label,
name,
options,
defaultValue,
}: {
label: string;
name: string;
options: { value: string; label: string }[];
defaultValue?: string | null;
}) {
return (
<label className="block">
<span className="text-sm font-medium text-[var(--navy)]">{label}</span>
<select
name={name}
defaultValue={defaultValue ?? undefined}
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none transition focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
>
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
);
}
export function Checkbox({
label,
name,
defaultChecked,
}: {
label: string;
name: string;
defaultChecked?: boolean;
}) {
return (
<label className="inline-flex items-center gap-2">
<input
type="checkbox"
name={name}
defaultChecked={defaultChecked}
className="size-4 rounded border-[var(--border)] text-[var(--navy)] focus:ring-[var(--sky)]"
/>
<span className="text-sm text-[var(--foreground)]">{label}</span>
</label>
);
}
export function FormShell({ children }: { children: React.ReactNode }) {
return (
<div className="mt-6 rounded-2xl border border-[var(--border)] bg-white p-6 sm:p-8">
{children}
</div>
);
}
export function FormActions({ children }: { children: React.ReactNode }) {
return (
<div className="mt-6 flex flex-wrap items-center justify-end gap-3 border-t border-[var(--border)] pt-6">
{children}
</div>
);
}
export function PrimaryButton({ children }: { children: React.ReactNode }) {
return (
<button
type="submit"
className="inline-flex items-center gap-2 rounded-full bg-[var(--navy)] px-5 py-2.5 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
>
{children}
</button>
);
}
export function GhostLink({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) {
return (
<Link
href={href}
className="text-sm font-medium text-[var(--muted)] hover:text-[var(--navy)]"
>
{children}
</Link>
);
}