Files
kovakyazilim/components/testimonials.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

51 lines
1.8 KiB
TypeScript

import { Quote, Star } from "lucide-react";
import Image from "next/image";
import type { TestimonialRow } from "@/lib/types";
export function TestimonialsCarousel({ items }: { items: TestimonialRow[] }) {
return (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{items.map((t) => (
<figure
key={t.$id}
className="relative rounded-2xl border border-[var(--border)] bg-white p-6"
>
<Quote
className="absolute right-5 top-5 size-8 text-[var(--sky-50)]"
aria-hidden
/>
<div className="flex items-center gap-0.5 text-amber-500">
{Array.from({ length: t.rating ?? 5 }).map((_, i) => (
<Star key={i} className="size-3.5 fill-current" />
))}
</div>
<blockquote className="mt-4 text-sm leading-relaxed text-[var(--foreground)]">
{t.message}
</blockquote>
<figcaption className="mt-5 flex items-center gap-3 border-t border-[var(--border)] pt-4">
{t.image_url ? (
<Image
src={t.image_url}
alt={t.name}
width={40}
height={40}
className="rounded-full object-cover"
/>
) : (
<div className="flex size-10 items-center justify-center rounded-full bg-[var(--navy-50)] text-sm font-semibold text-[var(--navy)]">
{t.name.charAt(0)}
</div>
)}
<div>
<p className="text-sm font-semibold text-[var(--navy)]">{t.name}</p>
<p className="text-xs text-[var(--muted)]">
{[t.role, t.company].filter(Boolean).join(" — ")}
</p>
</div>
</figcaption>
</figure>
))}
</div>
);
}