Files
kovakyazilim/app/(site)/hizmetler/[slug]/page.tsx
T
Ege Can Komur c0da5ae8d3 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.
2026-05-20 02:46:11 +03:00

198 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Image from "next/image";
import Link from "next/link";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
import { marked } from "marked";
import { getServiceBySlug, listProjects } from "@/lib/data";
import { buildMetadata } from "@/lib/seo";
import { Icon } from "@/components/icon";
import { ProjectsGrid } from "@/components/projects-grid";
import { SectionTitle } from "@/components/section-title";
import { FaqList } from "@/components/faq-list";
import type { FaqItem } from "@/lib/types";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const service = await getServiceBySlug(slug);
if (!service) return { title: "Hizmet bulunamadı" };
return buildMetadata(`/hizmetler/${slug}`, {
title: service.title,
description: service.description.slice(0, 160),
});
}
function parseFaq(items?: string[] | null): FaqItem[] {
if (!items) return [];
const out: FaqItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<FaqItem>;
if (obj.q && obj.a) out.push({ q: obj.q, a: obj.a });
} catch {
const [q, a] = raw.split("|||").map((s) => s.trim());
if (q && a) out.push({ q, a });
}
}
return out;
}
export default async function ServiceDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const service = await getServiceBySlug(slug);
if (!service) notFound();
const [relatedProjects] = await Promise.all([
listProjects({ serviceSlug: slug, limit: 6 }),
]);
const faqItems = parseFaq(service.faq);
const html = service.content
? (marked.parse(service.content, { async: false }) as string)
: "";
return (
<>
<section className="relative overflow-hidden border-b border-[var(--border)]">
<div className="absolute inset-0 hero-grid opacity-50" aria-hidden />
<div className="relative mx-auto max-w-7xl px-6 py-20">
<Link
href="/hizmetler"
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
>
<ArrowLeft className="size-3.5" /> Tüm hizmetler
</Link>
<div className="mt-6 grid items-start gap-10 md:grid-cols-[1fr_auto]">
<div>
<div className="flex size-14 items-center justify-center rounded-2xl bg-[var(--navy-50)] text-[var(--navy)]">
<Icon name={service.icon} className="size-7" />
</div>
<h1 className="mt-5 text-4xl font-bold tracking-tight text-[var(--navy)] sm:text-5xl">
{service.title}
</h1>
<p className="mt-4 max-w-2xl text-lg leading-relaxed text-[var(--muted)]">
{service.description}
</p>
<Link
href="/iletisim"
className="mt-8 inline-flex items-center gap-2 rounded-full bg-[var(--navy)] px-6 py-3 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
>
Bu hizmet için teklif al
<ArrowRight className="size-4" />
</Link>
</div>
{service.hero_image && (
<div className="relative hidden aspect-square w-64 overflow-hidden rounded-2xl md:block lg:w-80">
<Image
src={service.hero_image}
alt={service.title}
fill
sizes="320px"
className="object-cover"
priority
/>
</div>
)}
</div>
</div>
</section>
<div className="mx-auto grid max-w-7xl gap-12 px-6 py-16 lg:grid-cols-[2fr_1fr]">
<div>
{service.features && service.features.length > 0 && (
<section className="mb-12">
<h2 className="text-2xl font-bold text-[var(--navy)]">
Bu hizmet kapsamında
</h2>
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
{service.features.map((f) => (
<li
key={f}
className="flex items-start gap-2 rounded-xl border border-[var(--border)] bg-white p-4"
>
<CheckCircle2 className="mt-0.5 size-5 shrink-0 text-[var(--sky-600)]" />
<span className="text-sm text-[var(--foreground)]">{f}</span>
</li>
))}
</ul>
</section>
)}
{html && (
<article
className="prose prose-lg max-w-none text-[var(--foreground)]"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
{faqItems.length > 0 && (
<section className="mt-12">
<h2 className="text-2xl font-bold text-[var(--navy)]">
Sıkça sorulan sorular
</h2>
<div className="mt-6">
<FaqList items={faqItems} />
</div>
</section>
)}
</div>
<aside className="space-y-4 lg:sticky lg:top-24 lg:self-start">
<div className="rounded-2xl border border-[var(--border)] bg-[var(--navy)] p-6 text-white">
<h3 className="text-base font-semibold">Bir proje mi var?</h3>
<p className="mt-2 text-sm text-white/70">
İhtiyacınızı anlatın, size en uygun çözümü hep birlikte planlayalım.
</p>
<Link
href="/iletisim"
className="mt-4 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-medium text-[var(--navy)] transition hover:bg-[var(--sky-50)]"
>
Bize ulaşın
<ArrowRight className="size-3.5" />
</Link>
</div>
<div className="rounded-2xl border border-[var(--border)] bg-white p-6">
<h3 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted)]">
Diğer hizmetler
</h3>
<Link
href="/hizmetler"
className="mt-3 inline-flex items-center gap-1 text-sm text-[var(--sky-600)] hover:text-[var(--navy)]"
>
Tümünü gör
<ArrowRight className="size-3.5" />
</Link>
</div>
</aside>
</div>
{relatedProjects.length > 0 && (
<section className="border-t border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
<div className="mx-auto max-w-7xl px-6">
<SectionTitle
align="left"
eyebrow="Referanslar"
title={`${service.title} alanındaki projelerimiz`}
description="Bu hizmette tamamladığımız işlerden seçkiler."
/>
<div className="mt-10">
<ProjectsGrid projects={relatedProjects} />
</div>
</div>
</section>
)}
</>
);
}