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) => {
+1
View File
@@ -16,6 +16,7 @@ export const MEDIA_BUCKET_ID =
export const TABLES = {
contactMessages: "contact_messages",
services: "services",
solutions: "solutions",
projects: "projects",
blogPosts: "blog_posts",
testimonials: "testimonials",
+17
View File
@@ -7,6 +7,7 @@ import type {
IndustryRow,
ProjectRow,
ServiceRow,
SolutionRow,
SeoPageRow,
SeoSettingsRow,
SiteSettingsRow,
@@ -44,14 +45,22 @@ export async function listServices(opts?: { featured?: boolean }) {
return safeList<ServiceRow>(TABLES.services, q);
}
export async function listSolutions(opts?: { featured?: boolean }) {
const q = [Q.orderAsc("order"), Q.limit(50)];
if (opts?.featured) q.unshift(Q.equal("featured", true));
return safeList<SolutionRow>(TABLES.solutions, q);
}
export async function listProjects(opts?: {
featured?: boolean;
limit?: number;
serviceSlug?: string;
solutionSlug?: string;
}) {
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
if (opts?.featured) q.unshift(Q.equal("featured", true));
if (opts?.serviceSlug) q.unshift(Q.equal("service_slug", opts.serviceSlug));
if (opts?.solutionSlug) q.unshift(Q.equal("solution_slug", opts.solutionSlug));
return safeList<ProjectRow>(TABLES.projects, q);
}
@@ -63,6 +72,14 @@ export async function getServiceBySlug(slug: string): Promise<ServiceRow | null>
return res[0] ?? null;
}
export async function getSolutionBySlug(slug: string): Promise<SolutionRow | null> {
const res = await safeList<SolutionRow>(TABLES.solutions, [
Q.equal("slug", slug),
Q.limit(1),
]);
return res[0] ?? null;
}
export async function getProjectBySlug(slug: string): Promise<ProjectRow | null> {
const res = await safeList<ProjectRow>(TABLES.projects, [
Q.equal("slug", slug),
+6
View File
@@ -25,4 +25,10 @@ export const siteConfig = {
{ slug: "sosyal-medya-yonetimi", title: "Sosyal Medya Yönetimi", icon: "Share2", description: "Marka diliyle uyumlu içerik üretimi ve topluluk yönetimi." },
{ slug: "dijital-reklam", title: "Dijital Reklam", icon: "Megaphone", description: "Google Ads ve Meta Ads kampanyalarıyla hedefli erişim ve ölçülebilir sonuçlar." },
],
fallbackSolutions: [
{ slug: "kurumsal-dijitallesme", title: "Kurumsal Dijitalleşme", icon: "Layers", description: "Web, mobil ve iç sistemleri tek çatı altında toplayan uçtan uca dijitalleşme paketi." },
{ slug: "online-satis-altyapisi", title: "Online Satış Altyapısı", icon: "ShoppingCart", description: "E-ticaret, ödeme ve stok entegrasyonlarıyla satışa hazır komple altyapı." },
{ slug: "musteri-yonetimi-crm", title: "Müşteri Yönetimi (CRM)", icon: "Users", description: "Satış, destek ve operasyon süreçlerini tek panelde toplayan CRM çözümü." },
{ slug: "buyume-pazarlama", title: "Büyüme & Pazarlama", icon: "TrendingUp", description: "SEO, reklam ve içerikle ölçülebilir müşteri kazanımı sağlayan büyüme paketi." },
],
} as const;
+19
View File
@@ -15,6 +15,20 @@ export interface ServiceRow extends AwRow {
hero_image?: string | null;
}
// İşletmelere sunulan çözümler — Hizmetler ile birebir aynı yapı, ayrı tablo.
export interface SolutionRow extends AwRow {
slug: string;
title: string;
description: string;
icon?: string | null;
order?: number | null;
featured?: boolean | null;
content?: string | null;
features?: string[] | null;
faq?: string[] | null; // each item is JSON: {"q":"...","a":"..."}
hero_image?: string | null;
}
export interface FaqItem {
q: string;
a: string;
@@ -36,6 +50,7 @@ export interface ProjectRow extends AwRow {
industry?: string | null;
duration?: string | null;
service_slug?: string | null;
solution_slug?: string | null;
metrics?: string[] | null; // JSON {"value":"+150%","label":"Trafik artışı"}
}
@@ -106,6 +121,10 @@ export interface SiteSettingsRow extends AwRow {
services_title?: string | null;
services_description?: string | null;
solutions_eyebrow?: string | null;
solutions_title?: string | null;
solutions_description?: string | null;
projects_eyebrow?: string | null;
projects_title?: string | null;
projects_description?: string | null;