Files
kovakyazilim/app/(site)/projeler/[slug]/page.tsx
T
Ege Can Komur f88b76546c fix: blog empty state + proje detay yarı boş meta tablosu
Claude vision ile localhost'ta yapılan tarama sonrası 2 sorun:

1) Blog sayfası — henüz yazı yoksa empty state küçük bir kart olup
   altında ~1000px beyaz alan kalıyordu, footer çok aşağıdaydı.

   Çözüm: Empty state için zengin bir layout:
   - Gradient hero ('Blog yazılarımızı hazırlıyoruz' + 3 CTA:
     keşif görüşmesi / WhatsApp / Telefon)
   - 'Bu arada hizmetlerimize göz atın' başlıkla 6 hizmet grid
   - 'Site analiz raporu' lead magnet kartı
   Yazı geldiğinde otomatik normal grid'e döner.

2) Proje detay sayfası — sağ üstte 2 sütunlu meta card sadece
   'Yıl: 2025' gösteriyordu (client_name/industry/duration boş),
   yarı boş görünüyor + sağ tarafta büyük boşluk.

   Çözüm:
   - meta.length >= 2 → eski 2x2 grid card
   - meta.length === 1 → inline pill strip altta
   - meta yok → grid tek sütuna döner (lg:grid-cols-[1.4fr_1fr]
     conditional)

Claude vision ile 10 sayfa screenshot alındı (/tmp/kovak-screenshots).
Diğer sayfalar (anasayfa, hizmet detay, sektör, iletişim, vs)
tasarım açısından temiz görünüyor.
2026-05-20 19:07:26 +03:00

272 lines
10 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 { renderContent } from "@/lib/content-render";
import { getProjectBySlug, listProjects } from "@/lib/data";
import { buildMetadata } from "@/lib/seo";
import { Gallery } from "@/components/gallery";
import { TrendingUp } from "lucide-react";
import type { ProjectMetric } from "@/lib/types";
function parseMetrics(items?: string[] | null): ProjectMetric[] {
if (!items) return [];
const out: ProjectMetric[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<ProjectMetric>;
if (obj.value && obj.label) out.push({ value: obj.value, label: obj.label });
} catch {
const [value, label] = raw.split("|").map((s) => s.trim());
if (value && label) out.push({ value, label });
}
}
return out;
}
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 = renderContent(project.content);
const metrics = parseMetrics(project.metrics);
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 ${
meta.length >= 2 ? "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 tablo sadece 2+ alan dolu ise gösterilir — yarı boş card görünmesin */}
{meta.length >= 2 && (
<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>
{/* Tek-iki meta varsa daha kompakt inline strip olarak gösterilir */}
{meta.length > 0 && meta.length < 2 && (
<div className="mt-6 flex flex-wrap gap-3">
{meta.map((m) => (
<span
key={m.label}
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-3 py-1.5 text-xs text-[var(--muted)]"
>
{m.icon}
<span className="font-medium text-[var(--navy)]">{m.label}:</span>{" "}
{m.value}
</span>
))}
</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>
)}
{metrics.length > 0 && (
<div className="mt-10">
<p className="flex items-center gap-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-[var(--sky-600)]">
<TrendingUp className="size-3.5" />
Sonuçlar
</p>
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{metrics.map((m, i) => (
<div
key={i}
className="rounded-2xl border border-[var(--border)] bg-gradient-to-br from-white to-[var(--sky-50)]/40 p-6"
>
<p className="text-3xl font-bold text-[var(--navy)] md:text-4xl">
{m.value}
</p>
<p className="mt-1 text-xs text-[var(--muted)]">
{m.label}
</p>
</div>
))}
</div>
</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>
)}
</>
);
}