From 2e001680bf577420ef61bf9684988b472ac2fa3a Mon Sep 17 00:00:00 2001 From: egecankomur Date: Tue, 2 Jun 2026 18:21:58 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=C3=87=C3=B6z=C3=BCmler=20b=C3=B6l?= =?UTF-8?q?=C3=BCm=C3=BC=20+=20mobil=20men=C3=BC;=20admin=20parser=20d?= =?UTF-8?q?=C3=BCzeltmeleri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Çözümler: solutions tablosu, /cozumler liste + detay sayfası, anasayfa bölümü, tam admin CRUD (/admin/cozumler), header & footer linkleri, projelerde solution_slug ilişkisi, services-grid genelleştirildi - Mobil menü (hamburger drawer) eklendi — header artık < lg'de gezilebilir - Site ayarları parser: textarea CRLF (\r\n) normalizasyonu — neden biz, süreç adımları, değerler ve SSS blokları artık doğru parçalanıyor - homepage_faq + garanti (title/description/items) saveSiteSettings'e bağlandı (daha önce hiç kaydedilmiyordu) --- app/(site)/cozumler/[slug]/page.tsx | 124 +++++++++++ app/(site)/cozumler/page.tsx | 35 +++ app/(site)/page.tsx | 44 +++- .../(protected)/cozumler/[id]/edit/page.tsx | 16 ++ app/admin/(protected)/cozumler/form.tsx | 148 +++++++++++++ app/admin/(protected)/cozumler/new/page.tsx | 5 + app/admin/(protected)/cozumler/page.tsx | 80 +++++++ app/admin/(protected)/projeler/form.tsx | 27 ++- app/admin/(protected)/site/page.tsx | 23 ++ components/admin/sidebar.tsx | 2 + components/footer.tsx | 5 + components/header.tsx | 16 +- components/mobile-menu.tsx | 168 ++++++++++++++ components/services-grid.tsx | 16 +- components/solution-hero.tsx | 209 ++++++++++++++++++ components/solution-sidebar.tsx | 145 ++++++++++++ lib/admin-actions.ts | 112 ++++++++-- lib/appwrite-rest.ts | 1 + lib/data.ts | 17 ++ lib/site-config.ts | 6 + lib/types.ts | 19 ++ 21 files changed, 1191 insertions(+), 27 deletions(-) create mode 100644 app/(site)/cozumler/[slug]/page.tsx create mode 100644 app/(site)/cozumler/page.tsx create mode 100644 app/admin/(protected)/cozumler/[id]/edit/page.tsx create mode 100644 app/admin/(protected)/cozumler/form.tsx create mode 100644 app/admin/(protected)/cozumler/new/page.tsx create mode 100644 app/admin/(protected)/cozumler/page.tsx create mode 100644 components/mobile-menu.tsx create mode 100644 components/solution-hero.tsx create mode 100644 components/solution-sidebar.tsx diff --git a/app/(site)/cozumler/[slug]/page.tsx b/app/(site)/cozumler/[slug]/page.tsx new file mode 100644 index 0000000..37faa19 --- /dev/null +++ b/app/(site)/cozumler/[slug]/page.tsx @@ -0,0 +1,124 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { CheckCircle2 } from "lucide-react"; +import { renderContent } from "@/lib/content-render"; +import { getSolutionBySlug, getSiteSettings, listProjects } from "@/lib/data"; +import { buildMetadata } from "@/lib/seo"; +import { ProjectsGrid } from "@/components/projects-grid"; +import { SectionTitle } from "@/components/section-title"; +import { FaqList } from "@/components/faq-list"; +import { SolutionHero } from "@/components/solution-hero"; +import { SolutionSidebar } from "@/components/solution-sidebar"; +import type { FaqItem } from "@/lib/types"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await params; + const solution = await getSolutionBySlug(slug); + if (!solution) return { title: "Çözüm bulunamadı" }; + return buildMetadata(`/cozumler/${slug}`, { + title: solution.title, + description: solution.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 SolutionDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + const solution = await getSolutionBySlug(slug); + if (!solution) notFound(); + + const [relatedProjects, settings] = await Promise.all([ + listProjects({ solutionSlug: slug, limit: 6 }), + getSiteSettings(), + ]); + + const faqItems = parseFaq(solution.faq); + const html = renderContent(solution.content); + + return ( + <> + + +
+
+ {solution.features && solution.features.length > 0 && ( +
+

+ Bu çözüm kapsamında +

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

+ Sıkça sorulan sorular +

+
+ +
+
+ )} +
+ + +
+ + {relatedProjects.length > 0 && ( +
+
+ +
+ +
+
+
+ )} + + ); +} diff --git a/app/(site)/cozumler/page.tsx b/app/(site)/cozumler/page.tsx new file mode 100644 index 0000000..95629a6 --- /dev/null +++ b/app/(site)/cozumler/page.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import { SectionTitle } from "@/components/section-title"; +import { ServicesGrid } from "@/components/services-grid"; +import { listSolutions } from "@/lib/data"; +import { siteConfig } from "@/lib/site-config"; +import { buildMetadata } from "@/lib/seo"; + +export async function generateMetadata(): Promise { + return buildMetadata("/cozumler", { + title: "Çözümler", + description: + "İşletmeniz için uçtan uca dijital çözümler: kurumsal dijitalleşme, online satış altyapısı, CRM ve büyüme paketleri.", + }); +} + +export default async function SolutionsPage() { + const solutions = await listSolutions(); + + return ( +
+ +
+ +
+
+ ); +} diff --git a/app/(site)/page.tsx b/app/(site)/page.tsx index 545c148..fa30d9f 100644 --- a/app/(site)/page.tsx +++ b/app/(site)/page.tsx @@ -18,6 +18,7 @@ import { getSiteSettings, listProjects, listServices, + listSolutions, listTestimonials, } from "@/lib/data"; import { buildMetadata } from "@/lib/seo"; @@ -28,12 +29,14 @@ export async function generateMetadata(): Promise { } export default async function Home() { - const [services, projects, testimonials, settings] = await Promise.all([ - listServices({ featured: true }), - listProjects({ featured: true, limit: 6 }), - listTestimonials({ featured: true }), - getSiteSettings(), - ]); + const [services, solutions, projects, testimonials, settings] = + await Promise.all([ + listServices({ featured: true }), + listSolutions({ featured: true }), + listProjects({ featured: true, limit: 6 }), + listTestimonials({ featured: true }), + getSiteSettings(), + ]); const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw; const phone = settings?.contact_phone ?? siteConfig.contact.phone; @@ -118,6 +121,35 @@ export default async function Home() { +
+
+ +
+ +
+
+ + Tüm çözümleri gör + + +
+
+
+ diff --git a/app/admin/(protected)/cozumler/[id]/edit/page.tsx b/app/admin/(protected)/cozumler/[id]/edit/page.tsx new file mode 100644 index 0000000..8c16b57 --- /dev/null +++ b/app/admin/(protected)/cozumler/[id]/edit/page.tsx @@ -0,0 +1,16 @@ +import { notFound } from "next/navigation"; +import { getRow } from "@/lib/data"; +import { TABLES } from "@/lib/appwrite-rest"; +import type { SolutionRow } from "@/lib/types"; +import { SolutionForm } from "../../form"; + +export default async function EditSolutionPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const solution = await getRow(TABLES.solutions, id); + if (!solution) notFound(); + return ; +} diff --git a/app/admin/(protected)/cozumler/form.tsx b/app/admin/(protected)/cozumler/form.tsx new file mode 100644 index 0000000..ea30fc1 --- /dev/null +++ b/app/admin/(protected)/cozumler/form.tsx @@ -0,0 +1,148 @@ +import { Save } from "lucide-react"; +import { + Checkbox, + Field, + FormActions, + FormShell, + GhostLink, + PageHeader, + PrimaryButton, + Textarea, +} from "@/components/admin/form"; +import { MediaPicker } from "@/components/admin/media-picker"; +import { RichEditor } from "@/components/admin/rich-editor"; +import { saveSolution } from "@/lib/admin-actions"; +import type { FaqItem, SolutionRow } from "@/lib/types"; + +const ICON_OPTIONS = [ + "Globe", + "ShoppingCart", + "Smartphone", + "Code2", + "Users", + "TrendingUp", + "Share2", + "Megaphone", + "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 SolutionForm({ solution }: { solution?: SolutionRow }) { + return ( +
+ +
+ {solution && } + +
+ + + + + +
+ +
+