From c0da5ae8d3c3a44b60929aa4b34d57355dabfa99 Mon Sep 17 00:00:00 2001 From: Ege Can Komur Date: Wed, 20 May 2026 02:46:11 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20hizmet=20ve=20proje=20detay=20sayfalar?= =?UTF-8?q?=C4=B1=20+=20galeri=20sistemi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/(site)/hizmetler/[slug]/page.tsx | 197 +++++++++++++++++ app/(site)/projeler/[slug]/page.tsx | 209 ++++++++++++++++++ app/admin/(protected)/hizmetler/form.tsx | 64 +++++- .../(protected)/projeler/[id]/edit/page.tsx | 2 +- app/admin/(protected)/projeler/form.tsx | 65 +++++- app/admin/(protected)/projeler/new/page.tsx | 4 +- components/faq-list.tsx | 40 ++++ components/gallery.tsx | 114 ++++++++++ components/projects-grid.tsx | 61 ++--- components/services-grid.tsx | 15 +- lib/admin-actions.ts | 30 +++ lib/data.ts | 23 +- lib/types.ts | 15 ++ 13 files changed, 792 insertions(+), 47 deletions(-) create mode 100644 app/(site)/hizmetler/[slug]/page.tsx create mode 100644 app/(site)/projeler/[slug]/page.tsx create mode 100644 components/faq-list.tsx create mode 100644 components/gallery.tsx 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ı. - +
-
+ +