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:
+98
-14
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user