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:
@@ -0,0 +1,202 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user