feat: Çözümler bölümü + mobil menü; admin parser düzeltmeleri

- Çö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)
This commit is contained in:
egecankomur
2026-06-02 18:21:58 +03:00
parent f49df9cbeb
commit 2e001680bf
21 changed files with 1191 additions and 27 deletions
+98 -14
View File
@@ -45,6 +45,12 @@ function strArr(v: FormDataEntryValue | null) {
.map((x) => x.trim())
.filter(Boolean);
}
// Çok satırlı (textarea) alanlar için ham metin. Tarayıcılar textarea
// içeriğini CRLF (\r\n) ile gönderir; satır-tabanlı parser'lar \n beklediği
// için (özellikle "\n---\n" blok ayracı) okurken normalize ediyoruz.
function raw(v: FormDataEntryValue | null) {
return String(v ?? "").replace(/\r\n?/g, "\n");
}
// ─── Media ───────────────────────────────────────────────────────
@@ -130,7 +136,7 @@ export async function saveService(formData: FormData) {
const slug = str(formData.get("slug")) || slugify(title);
// FAQ as JSON-encoded array. Each item: {"q":"...","a":"..."}
const faqRaw = String(formData.get("faq") ?? "");
const faqRaw = raw(formData.get("faq"));
const faq = faqRaw
.split("\n---\n")
.map((block) => {
@@ -178,6 +184,60 @@ export async function deleteService(formData: FormData) {
revalidatePath("/hizmetler");
}
// ─── Solutions ───────────────────────────────────────────────────
export async function saveSolution(formData: FormData) {
const secret = await requireSessionSecret();
const id = str(formData.get("id"));
const title = str(formData.get("title"));
if (!title) throw new Error("Başlık zorunlu");
const description = str(formData.get("description"));
if (!description) throw new Error("Açıklama zorunlu");
const slug = str(formData.get("slug")) || slugify(title);
// FAQ as JSON-encoded array. Each item: {"q":"...","a":"..."}
const faqRaw = raw(formData.get("faq"));
const faq = faqRaw
.split("\n---\n")
.map((block) => {
const lines = block.trim().split("\n");
const q = lines[0]?.trim();
const a = lines.slice(1).join("\n").trim();
if (!q || !a) return null;
return JSON.stringify({ q, a });
})
.filter((x): x is string => x !== null);
const data = {
slug,
title,
description,
icon: str(formData.get("icon")),
order: num(formData.get("order")) ?? 0,
featured: bool(formData.get("featured")),
content: str(formData.get("content")),
features: strArr(formData.get("features"))?.filter(Boolean) ?? null,
faq: faq.length > 0 ? faq : null,
hero_image: str(formData.get("hero_image")),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.solutions, id, data, secret);
} else {
await tablesDB.createRow(DATABASE_ID, TABLES.solutions, slug, data, secret);
}
revalidatePath("/admin/cozumler");
revalidatePath("/cozumler");
revalidatePath("/");
}
export async function deleteSolution(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.solutions, id, secret);
revalidatePath("/admin/cozumler");
revalidatePath("/cozumler");
}
// ─── Projects ────────────────────────────────────────────────────
function parseMetricsInput(raw: string): string[] {
@@ -201,7 +261,7 @@ export async function saveProject(formData: FormData) {
if (!description) throw new Error("Açıklama zorunlu");
// Gallery: one URL per line
const galleryRaw = String(formData.get("gallery") ?? "");
const galleryRaw = raw(formData.get("gallery"));
const gallery = galleryRaw
.split("\n")
.map((s) => s.trim())
@@ -223,8 +283,9 @@ export async function saveProject(formData: FormData) {
industry: str(formData.get("industry")),
duration: str(formData.get("duration")),
service_slug: str(formData.get("service_slug")),
solution_slug: str(formData.get("solution_slug")),
metrics: (() => {
const m = parseMetricsInput(String(formData.get("metrics") ?? ""));
const m = parseMetricsInput(raw(formData.get("metrics")));
return m.length > 0 ? m : null;
})(),
};
@@ -302,7 +363,7 @@ export async function saveSiteSettings(formData: FormData) {
const secret = await requireSessionSecret();
// Hero stats: 3 satır halinde "value|label" formatında — JSON array'e çevir
const statsRaw = String(formData.get("hero_stats") ?? "");
const statsRaw = raw(formData.get("hero_stats"));
const stats = statsRaw
.split("\n")
.map((line) => {
@@ -313,7 +374,7 @@ export async function saveSiteSettings(formData: FormData) {
.filter((x): x is string => x !== null);
// Trust items: "icon|value|label" satırlar
const trustRaw = String(formData.get("trust_items") ?? "");
const trustRaw = raw(formData.get("trust_items"));
const trust = trustRaw
.split("\n")
.map((line) => {
@@ -350,21 +411,18 @@ export async function saveSiteSettings(formData: FormData) {
.filter((x): x is string => x !== null);
}
const whyUs = parseBlocks(String(formData.get("why_us") ?? ""), true);
const processSteps = parseBlocks(
String(formData.get("process_steps") ?? ""),
false,
);
const whyUs = parseBlocks(raw(formData.get("why_us")), true);
const processSteps = parseBlocks(raw(formData.get("process_steps")), false);
// Client logos: her satıra bir URL
const logosRaw = String(formData.get("client_logos") ?? "");
const logosRaw = raw(formData.get("client_logos"));
const logos = logosRaw
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
// Hakkımızda values: blok '---' ile, ilk satır title, kalanı description
const aboutValuesRaw = String(formData.get("about_values") ?? "");
const aboutValuesRaw = raw(formData.get("about_values"));
const aboutValues = aboutValuesRaw
.split("\n---\n")
.map((block) => {
@@ -377,7 +435,7 @@ export async function saveSiteSettings(formData: FormData) {
.filter((x): x is string => x !== null);
// Hakkımızda stats: 'value | label' satırlar
const aboutStatsRaw = String(formData.get("about_stats") ?? "");
const aboutStatsRaw = raw(formData.get("about_stats"));
const aboutStats = aboutStatsRaw
.split("\n")
.map((line) => {
@@ -387,6 +445,24 @@ export async function saveSiteSettings(formData: FormData) {
})
.filter((x): x is string => x !== null);
// Garanti maddeleri: her satır bir madde
const guaranteeItems = raw(formData.get("guarantee_items"))
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
// Anasayfa SSS: blok '---' ile ayrılır, ilk satır soru, kalanı cevap
const homepageFaq = raw(formData.get("homepage_faq"))
.split("\n---\n")
.map((block) => {
const lines = block.trim().split("\n");
const q = lines[0]?.trim();
const a = lines.slice(1).join("\n").trim();
if (!q || !a) return null;
return JSON.stringify({ q, a });
})
.filter((x): x is string => x !== null);
const data = {
hero_badge: str(formData.get("hero_badge")),
hero_title: str(formData.get("hero_title")),
@@ -401,6 +477,10 @@ export async function saveSiteSettings(formData: FormData) {
services_title: str(formData.get("services_title")),
services_description: str(formData.get("services_description")),
solutions_eyebrow: str(formData.get("solutions_eyebrow")),
solutions_title: str(formData.get("solutions_title")),
solutions_description: str(formData.get("solutions_description")),
projects_eyebrow: str(formData.get("projects_eyebrow")),
projects_title: str(formData.get("projects_title")),
projects_description: str(formData.get("projects_description")),
@@ -433,6 +513,10 @@ export async function saveSiteSettings(formData: FormData) {
trust_items: trust.length > 0 ? trust : null,
why_us: whyUs.length > 0 ? whyUs : null,
process_steps: processSteps.length > 0 ? processSteps : null,
homepage_faq: homepageFaq.length > 0 ? homepageFaq : null,
guarantee_title: str(formData.get("guarantee_title")),
guarantee_description: str(formData.get("guarantee_description")),
guarantee_items: guaranteeItems.length > 0 ? guaranteeItems : null,
lead_form_title: str(formData.get("lead_form_title")),
lead_form_description: str(formData.get("lead_form_description")),
google_review_url: str(formData.get("google_review_url")),
@@ -596,7 +680,7 @@ export async function saveIndustry(formData: FormData) {
if (!title) throw new Error("Başlık zorunlu");
const slug = str(formData.get("slug")) || slugify(title);
const faqRaw = String(formData.get("faq") ?? "");
const faqRaw = raw(formData.get("faq"));
const faq = faqRaw
.split("\n---\n")
.map((block) => {