feat: hizmet ve proje detay sayfaları + galeri sistemi

Yeni Appwrite kolonları:
- services: content (markdown), features[], faq[] (JSON-encoded), hero_image
- projects: gallery[], content (markdown), client_name, industry, duration, service_slug

Public sayfalar:
- /hizmetler/[slug]: hero + features checklist + markdown content + FAQ accordion
  + ilgili projeler (service_slug eşleşmesi)
- /projeler/[slug]: hero + meta tablosu (müşteri/sektör/süre/yıl) + kapak görseli
  + markdown vaka çalışması + lightbox galeri + diğer projeler

Yeni componentler:
- components/gallery.tsx: lightbox galeri (keyboard nav, prev/next, ESC kapat)
- components/faq-list.tsx: accordion FAQ (tek seferde tek açık)

Admin formları:
- Hizmet formu: hero_image, content (markdown), features (virgülle), FAQ
  (her blok '---' ile ayrılır, ilk satır soru, kalanı cevap)
- Proje formu: gallery (her satıra bir URL), content (markdown), client_name,
  industry, duration, service_slug (dropdown — hizmetlerden seçim)

Linkler:
- ServicesGrid kartları → /hizmetler/[slug]
- ProjectsGrid kartları → /projeler/[slug] (live_url butonu ayrı, target=_blank)

29 route üretiliyor.
This commit is contained in:
Ege Can Komur
2026-05-20 02:46:11 +03:00
parent edd0af76dc
commit c0da5ae8d3
13 changed files with 792 additions and 47 deletions
+57 -7
View File
@@ -10,7 +10,7 @@ import {
Textarea,
} from "@/components/admin/form";
import { saveService } from "@/lib/admin-actions";
import type { ServiceRow } from "@/lib/types";
import type { FaqItem, ServiceRow } from "@/lib/types";
const ICON_OPTIONS = [
"Globe",
@@ -24,6 +24,20 @@ const ICON_OPTIONS = [
"Layers",
];
function faqToText(items?: string[] | null): string {
if (!items) return "";
const parsed: FaqItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<FaqItem>;
if (obj.q && obj.a) parsed.push({ q: obj.q, a: obj.a });
} catch {
/* ignore */
}
}
return parsed.map((it) => `${it.q}\n${it.a}`).join("\n---\n");
}
export function ServiceForm({ service }: { service?: ServiceRow }) {
return (
<div>
@@ -56,20 +70,56 @@ export function ServiceForm({ service }: { service?: ServiceRow }) {
</option>
))}
</select>
<span className="mt-1 block text-xs text-[var(--muted)]">
Lucide icon adı.
</span>
</label>
<Field
label="Hero görsel URL"
name="hero_image"
type="url"
defaultValue={service?.hero_image}
help="Detay sayfası başında gösterilir (opsiyonel)."
/>
</div>
<div className="mt-5">
<div className="mt-5 space-y-5">
<Textarea
label="Açıklama"
label="Kısa açıklama (kart için)"
name="description"
required
defaultValue={service?.description}
rows={4}
rows={3}
help="Listede ve anasayfa kartında gösterilir."
/>
<Textarea
label="Detay içerik (Markdown)"
name="content"
defaultValue={service?.content}
rows={10}
placeholder={"## Yaklaşım\n\nMarkdown desteklenir…"}
help="Hizmet detay sayfasında ana içerik olarak gösterilir."
/>
<Textarea
label="Özellikler"
name="features"
defaultValue={service?.features?.join(", ")}
rows={3}
placeholder="SEO uyumlu kod, Mobil responsive, Hızlı yüklenme, …"
help="Virgülle ayırın. Detay sayfasında checklist olarak gösterilir."
/>
<Textarea
label="SSS"
name="faq"
defaultValue={faqToText(service?.faq)}
rows={8}
placeholder={
"Soru 1?\nCevap 1 burada.\n---\nSoru 2?\nCevap 2 burada."
}
help="Her soru/cevap blokunu '---' ile ayırın. İlk satır soru, kalanı cevap."
/>
</div>
<div className="mt-5">
<Checkbox
label="Öne çıkar (Anasayfada göster)"