Files
kovakyazilim/app/(site)/projeler/[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

210 lines
7.8 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, Building2, Calendar, Clock, ExternalLink, Tag } from "lucide-react";
import { marked } from "marked";
import { getProjectBySlug, listProjects } from "@/lib/data";
import { buildMetadata } from "@/lib/seo";
import { Gallery } from "@/components/gallery";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) return { title: "Proje bulunamadı" };
return buildMetadata(`/projeler/${slug}`, {
title: project.title,
description: project.description.slice(0, 160),
openGraph: {
title: project.title,
description: project.description.slice(0, 160),
images: project.image_url ? [{ url: project.image_url }] : undefined,
type: "article",
},
});
}
export default async function ProjectDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const project = await getProjectBySlug(slug);
if (!project) notFound();
const html = project.content
? (marked.parse(project.content, { async: false }) as string)
: "";
const meta: { icon: React.ReactNode; label: string; value: string }[] = [];
if (project.client_name)
meta.push({ icon: <Building2 className="size-4" />, label: "Müşteri", value: project.client_name });
if (project.industry)
meta.push({ icon: <Tag className="size-4" />, label: "Sektör", value: project.industry });
if (project.duration)
meta.push({ icon: <Clock className="size-4" />, label: "Süre", value: project.duration });
if (project.year)
meta.push({ icon: <Calendar className="size-4" />, label: "Yıl", value: String(project.year) });
const relatedProjects = (
await listProjects({ limit: 4 })
).filter((p) => p.$id !== project.$id).slice(0, 3);
return (
<>
<section className="border-b border-[var(--border)]">
<div className="mx-auto max-w-7xl px-6 py-12">
<Link
href="/projeler"
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
>
<ArrowLeft className="size-3.5" /> Tüm projeler
</Link>
<div className="mt-6 grid items-start gap-10 lg:grid-cols-[1.4fr_1fr]">
<div>
{project.category && (
<span className="inline-flex rounded-full bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
{project.category}
</span>
)}
<h1 className="mt-3 text-4xl font-bold tracking-tight text-[var(--navy)] sm:text-5xl">
{project.title}
</h1>
<p className="mt-4 text-lg leading-relaxed text-[var(--muted)]">
{project.description}
</p>
{project.live_url && (
<a
href={project.live_url}
target="_blank"
rel="noopener noreferrer"
className="mt-6 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)]"
>
Projeyi canlı görüntüle
<ExternalLink className="size-4" />
</a>
)}
{project.technologies && project.technologies.length > 0 && (
<div className="mt-8">
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
Teknolojiler
</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{project.technologies.map((t) => (
<span
key={t}
className="rounded-md bg-[var(--navy-50)] px-2.5 py-1 text-xs text-[var(--navy-700)]"
>
{t}
</span>
))}
</div>
</div>
)}
</div>
{meta.length > 0 && (
<dl className="grid grid-cols-2 gap-4 rounded-2xl border border-[var(--border)] bg-white p-6">
{meta.map((m) => (
<div key={m.label}>
<dt className="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wider text-[var(--muted)]">
{m.icon}
{m.label}
</dt>
<dd className="mt-1 text-sm font-semibold text-[var(--navy)]">
{m.value}
</dd>
</div>
))}
</dl>
)}
</div>
{project.image_url && (
<div className="relative mt-10 aspect-video overflow-hidden rounded-2xl">
<Image
src={project.image_url}
alt={project.title}
fill
sizes="(min-width: 1024px) 1024px, 100vw"
className="object-cover"
priority
/>
</div>
)}
</div>
</section>
<div className="mx-auto max-w-5xl px-6 py-16">
{html && (
<article
className="prose prose-lg max-w-none text-[var(--foreground)]"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
{project.gallery && project.gallery.length > 0 && (
<section className={html ? "mt-16" : ""}>
<h2 className="text-2xl font-bold text-[var(--navy)]">Galeri</h2>
<p className="mt-1 text-sm text-[var(--muted)]">
Görsellerin üzerine tıklayarak büyütebilirsiniz.
</p>
<div className="mt-6">
<Gallery images={project.gallery} alt={project.title} />
</div>
</section>
)}
</div>
{relatedProjects.length > 0 && (
<section className="border-t border-[var(--border)] bg-[var(--navy-50)]/40 py-16">
<div className="mx-auto max-w-7xl px-6">
<h2 className="text-2xl font-bold text-[var(--navy)]">
Diğer projeler
</h2>
<div className="mt-8 grid gap-6 md:grid-cols-3">
{relatedProjects.map((p) => (
<Link
key={p.$id}
href={`/projeler/${p.slug}`}
className="group overflow-hidden rounded-2xl border border-[var(--border)] bg-white transition hover:shadow-lg"
>
<div className="relative aspect-video overflow-hidden bg-[var(--navy-50)]">
{p.image_url ? (
<Image
src={p.image_url}
alt={p.title}
fill
sizes="(min-width: 1024px) 33vw, 100vw"
className="object-cover transition group-hover:scale-105"
/>
) : (
<div className="flex h-full items-center justify-center text-3xl font-bold text-[var(--navy)]/30">
{p.title.charAt(0)}
</div>
)}
</div>
<div className="p-5">
<h3 className="font-semibold text-[var(--navy)] group-hover:text-[var(--sky-600)]">
{p.title}
</h3>
<p className="mt-1 text-xs text-[var(--muted)]">{p.category}</p>
</div>
</Link>
))}
</div>
</div>
</section>
)}
</>
);
}