Files
kovakyazilim/app/(site)/projeler/[slug]/page.tsx
T
Ege Can Komur cf46e30a7e feat: TR rakip analizi → satış blokerleri düzeltildi
Rakip analizi (kocaelidijital.com, promedyanet.com, lf.com.tr) sonrası
satış blokerleri tespit edildi ve aşağıdaki bölümler eklendi:

1. ANASAYFADA SSS (8 hazır soru, admin'den düzenlenebilir)
   - Fiyat, süre, ödeme, garanti, hosting, SEO, mevcut site yenileme,
     sadece tasarım hizmeti gibi en sık sorulan sorular
   - HomepageFaq component (sticky sol + accordion sağ)
   - site_settings.homepage_faq[] (JSON {q,a})

2. RISK REVERSAL bölümü (Guarantee component)
   - 'İlk taslak ücretsiz, memnun değilseniz devam etmiyoruz'
   - 4 garanti maddesi checklist
   - site_settings.guarantee_title/description/items

3. PROJE METRİKLERİ (vaka çalışması güçlendirme)
   - projects.metrics[] (JSON {value,label})
   - Detay sayfada büyük metric kartları
   - Admin formda 'değer | etiket' satır formatı

4. HERO COPY GÜNCELLEMESİ (admin'den düzenlenebilir)
   - 'Kocaeli'de 2-3 hafta içinde yayında olan, satan kurumsal web siteleri'
   - 'İlk tasarım taslakı ücretsiz' vurgusu
   - Trust band: 30 dk yanıt + ücretsiz taslak + 4.9 memnuniyet

5. /SITE-ANALIZI LEAD MAGNET SAYFASI
   - URL + ad + email + telefon formu
   - 6 analiz başlığı (CWV, mobil, SEO, güvenlik, içerik, rakip)
   - contact_messages'a source=quick-site-audit ile yazılır
   - 'subject' alanı ile inbox'ta ayırt edilebilir

6. EKİP BÖLÜMÜ (Hakkımızda sayfasında)
   - Yeni team_members tablosu (name, role, bio, photo, linkedin)
   - /admin/ekip CRUD sayfası
   - TeamGrid component

7. SEKTÖR LANDING SAYFALARI (/sektor/[slug])
   - Yeni industries tablosu (slug, title, content, features, faq, SEO)
   - /admin/sektorler CRUD sayfası
   - SEO + ad-targeted landing template
   - Hero + trust + features + content + garanti + projeler + hizmetler + FAQ + JSON-LD

Admin /admin/site formuna yeni bölümler:
- 'Risk reversal / Garanti' (title + description + items)
- 'Anasayfa SSS' (---' bloklarla)

App sidebar'a 'Sektörler' ve 'Ekip' linkleri eklendi.
Footer'a 'Ücretsiz Site Analizi' linki eklendi.

36 route üretiliyor (önceki 31'den +5: /site-analizi, /sektor/[slug],
/admin/ekip + alt, /admin/sektorler + alt).
2026-05-20 04:03:21 +03:00

253 lines
9.3 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";
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 = project.content
? (marked.parse(project.content, { async: false }) as string)
: "";
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 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>
)}
{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>
)}
</>
);
}