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) => {
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user