diff --git a/app/(site)/hizmetler/[slug]/page.tsx b/app/(site)/hizmetler/[slug]/page.tsx new file mode 100644 index 0000000..4a39bd7 --- /dev/null +++ b/app/(site)/hizmetler/[slug]/page.tsx @@ -0,0 +1,197 @@ +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 { + 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; + 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 ( + <> +
+
+
+ + Tüm hizmetler + + +
+
+
+ +
+

+ {service.title} +

+

+ {service.description} +

+ + Bu hizmet için teklif al + + +
+ + {service.hero_image && ( +
+ {service.title} +
+ )} +
+
+
+ +
+
+ {service.features && service.features.length > 0 && ( +
+

+ Bu hizmet kapsamında +

+
    + {service.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ )} + + {html && ( +
+ )} + + {faqItems.length > 0 && ( +
+

+ Sıkça sorulan sorular +

+
+ +
+
+ )} +
+ + +
+ + {relatedProjects.length > 0 && ( +
+
+ +
+ +
+
+
+ )} + + ); +} diff --git a/app/(site)/projeler/[slug]/page.tsx b/app/(site)/projeler/[slug]/page.tsx new file mode 100644 index 0000000..7a7a951 --- /dev/null +++ b/app/(site)/projeler/[slug]/page.tsx @@ -0,0 +1,209 @@ +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 { + 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: , label: "Müşteri", value: project.client_name }); + if (project.industry) + meta.push({ icon: , label: "Sektör", value: project.industry }); + if (project.duration) + meta.push({ icon: , label: "Süre", value: project.duration }); + if (project.year) + meta.push({ icon: , label: "Yıl", value: String(project.year) }); + + const relatedProjects = ( + await listProjects({ limit: 4 }) + ).filter((p) => p.$id !== project.$id).slice(0, 3); + + return ( + <> +
+
+ + Tüm projeler + + +
+
+ {project.category && ( + + {project.category} + + )} +

+ {project.title} +

+

+ {project.description} +

+ + {project.live_url && ( + + Projeyi canlı görüntüle + + + )} + + {project.technologies && project.technologies.length > 0 && ( +
+

+ Teknolojiler +

+
+ {project.technologies.map((t) => ( + + {t} + + ))} +
+
+ )} +
+ + {meta.length > 0 && ( +
+ {meta.map((m) => ( +
+
+ {m.icon} + {m.label} +
+
+ {m.value} +
+
+ ))} +
+ )} +
+ + {project.image_url && ( +
+ {project.title} +
+ )} +
+
+ +
+ {html && ( +
+ )} + + {project.gallery && project.gallery.length > 0 && ( +
+

Galeri

+

+ Görsellerin üzerine tıklayarak büyütebilirsiniz. +

+
+ +
+
+ )} +
+ + {relatedProjects.length > 0 && ( +
+
+

+ Diğer projeler +

+
+ {relatedProjects.map((p) => ( + +
+ {p.image_url ? ( + {p.title} + ) : ( +
+ {p.title.charAt(0)} +
+ )} +
+
+

+ {p.title} +

+

{p.category}

+
+ + ))} +
+
+
+ )} + + ); +} diff --git a/app/admin/(protected)/hizmetler/form.tsx b/app/admin/(protected)/hizmetler/form.tsx index e347e59..26bc65b 100644 --- a/app/admin/(protected)/hizmetler/form.tsx +++ b/app/admin/(protected)/hizmetler/form.tsx @@ -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; + 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 (
@@ -56,20 +70,56 @@ export function ServiceForm({ service }: { service?: ServiceRow }) { ))} - - Lucide icon adı. - +
-
+ +