Files
kovakyazilim/lib/admin-actions.ts
T
Ege Can Komur cf46e30a7e feat: TR rakip analizi → satış blokerleri düzeltildi
Rakip analizi (kocaelidijital.com, promedyanet.com, lf.com.tr) sonrası
satış blokerleri tespit edildi ve aşağıdaki bölümler eklendi:

1. ANASAYFADA SSS (8 hazır soru, admin'den düzenlenebilir)
   - Fiyat, süre, ödeme, garanti, hosting, SEO, mevcut site yenileme,
     sadece tasarım hizmeti gibi en sık sorulan sorular
   - HomepageFaq component (sticky sol + accordion sağ)
   - site_settings.homepage_faq[] (JSON {q,a})

2. RISK REVERSAL bölümü (Guarantee component)
   - 'İlk taslak ücretsiz, memnun değilseniz devam etmiyoruz'
   - 4 garanti maddesi checklist
   - site_settings.guarantee_title/description/items

3. PROJE METRİKLERİ (vaka çalışması güçlendirme)
   - projects.metrics[] (JSON {value,label})
   - Detay sayfada büyük metric kartları
   - Admin formda 'değer | etiket' satır formatı

4. HERO COPY GÜNCELLEMESİ (admin'den düzenlenebilir)
   - 'Kocaeli'de 2-3 hafta içinde yayında olan, satan kurumsal web siteleri'
   - 'İlk tasarım taslakı ücretsiz' vurgusu
   - Trust band: 30 dk yanıt + ücretsiz taslak + 4.9 memnuniyet

5. /SITE-ANALIZI LEAD MAGNET SAYFASI
   - URL + ad + email + telefon formu
   - 6 analiz başlığı (CWV, mobil, SEO, güvenlik, içerik, rakip)
   - contact_messages'a source=quick-site-audit ile yazılır
   - 'subject' alanı ile inbox'ta ayırt edilebilir

6. EKİP BÖLÜMÜ (Hakkımızda sayfasında)
   - Yeni team_members tablosu (name, role, bio, photo, linkedin)
   - /admin/ekip CRUD sayfası
   - TeamGrid component

7. SEKTÖR LANDING SAYFALARI (/sektor/[slug])
   - Yeni industries tablosu (slug, title, content, features, faq, SEO)
   - /admin/sektorler CRUD sayfası
   - SEO + ad-targeted landing template
   - Hero + trust + features + content + garanti + projeler + hizmetler + FAQ + JSON-LD

Admin /admin/site formuna yeni bölümler:
- 'Risk reversal / Garanti' (title + description + items)
- 'Anasayfa SSS' (---' bloklarla)

App sidebar'a 'Sektörler' ve 'Ekip' linkleri eklendi.
Footer'a 'Ücretsiz Site Analizi' linki eklendi.

36 route üretiliyor (önceki 31'den +5: /site-analizi, /sektor/[slug],
/admin/ekip + alt, /admin/sektorler + alt).
2026-05-20 04:03:21 +03:00

637 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use server";
import { revalidatePath } from "next/cache";
import {
DATABASE_ID,
ID,
MEDIA_BUCKET_ID,
storage,
TABLES,
tablesDB,
} from "@/lib/appwrite-rest";
import { requireSessionSecret } from "@/lib/auth";
function slugify(s: string) {
return s
.toLowerCase()
.replace(/[ğ]/g, "g")
.replace(/[ü]/g, "u")
.replace(/[ş]/g, "s")
.replace(/[ı]/g, "i")
.replace(/[ö]/g, "o")
.replace(/[ç]/g, "c")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80);
}
function num(v: FormDataEntryValue | null) {
if (v === null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
function bool(v: FormDataEntryValue | null) {
return v === "on" || v === "true" || v === "1";
}
function str(v: FormDataEntryValue | null) {
if (v === null) return null;
const s = String(v).trim();
return s === "" ? null : s;
}
function strArr(v: FormDataEntryValue | null) {
if (v === null) return null;
return String(v)
.split(",")
.map((x) => x.trim())
.filter(Boolean);
}
// ─── Media ───────────────────────────────────────────────────────
export async function uploadMedia(formData: FormData): Promise<void> {
const secret = await requireSessionSecret();
const file = formData.get("file");
if (!(file instanceof File) || file.size === 0) {
throw new Error("Dosya seçilmedi");
}
await storage.createFile(MEDIA_BUCKET_ID, ID.unique(), file, secret);
revalidatePath("/admin/medya");
}
export async function deleteMediaFile(fileId: string) {
const secret = await requireSessionSecret();
await storage.deleteFile(MEDIA_BUCKET_ID, fileId, secret);
revalidatePath("/admin/medya");
}
// ─── Blog ────────────────────────────────────────────────────────
export async function saveBlogPost(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 slug = str(formData.get("slug")) || slugify(title);
const status =
(str(formData.get("status")) ?? "draft") as "draft" | "published";
const data = {
slug,
title,
excerpt: str(formData.get("excerpt")),
content: str(formData.get("content")),
cover_image: str(formData.get("cover_image")),
cover_file_id: str(formData.get("cover_file_id")),
author: str(formData.get("author")),
status,
published_at:
status === "published"
? str(formData.get("published_at")) || new Date().toISOString()
: null,
tags: strArr(formData.get("tags")),
seo_title: str(formData.get("seo_title")),
seo_description: str(formData.get("seo_description")),
seo_image: str(formData.get("seo_image")),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.blogPosts, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.blogPosts,
ID.unique(),
data,
secret,
);
}
revalidatePath("/admin/blog");
revalidatePath("/blog");
if (slug) revalidatePath(`/blog/${slug}`);
}
export async function deleteBlogPost(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.blogPosts, id, secret);
revalidatePath("/admin/blog");
revalidatePath("/blog");
}
// ─── Services ────────────────────────────────────────────────────
export async function saveService(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 = String(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.services, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.services,
slug,
data,
secret,
);
}
revalidatePath("/admin/hizmetler");
revalidatePath("/hizmetler");
revalidatePath("/");
}
export async function deleteService(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.services, id, secret);
revalidatePath("/admin/hizmetler");
revalidatePath("/hizmetler");
}
// ─── Projects ────────────────────────────────────────────────────
function parseMetricsInput(raw: string): string[] {
return raw
.split("\n")
.map((line) => {
const [value, label] = line.split("|").map((s) => s.trim());
if (!value || !label) return null;
return JSON.stringify({ value, label });
})
.filter((x): x is string => x !== null);
}
export async function saveProject(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 slug = str(formData.get("slug")) || slugify(title);
const description = str(formData.get("description"));
if (!description) throw new Error("Açıklama zorunlu");
// Gallery: one URL per line
const galleryRaw = String(formData.get("gallery") ?? "");
const gallery = galleryRaw
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const data = {
slug,
title,
description,
image_url: str(formData.get("image_url")),
live_url: str(formData.get("live_url")),
category: str(formData.get("category")),
technologies: strArr(formData.get("technologies")),
year: num(formData.get("year")),
featured: bool(formData.get("featured")),
gallery: gallery.length > 0 ? gallery : null,
content: str(formData.get("content")),
client_name: str(formData.get("client_name")),
industry: str(formData.get("industry")),
duration: str(formData.get("duration")),
service_slug: str(formData.get("service_slug")),
metrics: (() => {
const m = parseMetricsInput(String(formData.get("metrics") ?? ""));
return m.length > 0 ? m : null;
})(),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.projects, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.projects,
ID.unique(),
data,
secret,
);
}
revalidatePath("/admin/projeler");
revalidatePath("/projeler");
revalidatePath("/");
}
export async function deleteProject(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.projects, id, secret);
revalidatePath("/admin/projeler");
revalidatePath("/projeler");
}
// ─── Testimonials ────────────────────────────────────────────────
export async function saveTestimonial(formData: FormData) {
const secret = await requireSessionSecret();
const id = str(formData.get("id"));
const name = str(formData.get("name"));
if (!name) throw new Error("Ad zorunlu");
const message = str(formData.get("message"));
if (!message) throw new Error("Mesaj zorunlu");
const data = {
name,
role: str(formData.get("role")),
company: str(formData.get("company")),
message,
rating: num(formData.get("rating")) ?? 5,
image_url: str(formData.get("image_url")),
order: num(formData.get("order")) ?? 0,
featured: bool(formData.get("featured")),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.testimonials, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.testimonials,
ID.unique(),
data,
secret,
);
}
revalidatePath("/admin/referanslar");
revalidatePath("/");
}
export async function deleteTestimonial(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.testimonials, id, secret);
revalidatePath("/admin/referanslar");
}
// ─── Site Settings (homepage content) ────────────────────────────
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 stats = statsRaw
.split("\n")
.map((line) => {
const [value, label] = line.split("|").map((s) => s.trim());
if (!value || !label) return null;
return JSON.stringify({ value, label });
})
.filter((x): x is string => x !== null);
// Trust items: "icon|value|label" satırlar
const trustRaw = String(formData.get("trust_items") ?? "");
const trust = trustRaw
.split("\n")
.map((line) => {
const [icon, value, label] = line.split("|").map((s) => s.trim());
if (!value || !label) return null;
return JSON.stringify({ icon: icon || "Sparkles", value, label });
})
.filter((x): x is string => x !== null);
// Why us / Process: blok '---' ile ayrılır; her blok için:
// why_us: ilk satır = icon|title, kalanı description
// process: ilk satır = title, kalanı description
function parseBlocks(raw: string, withIcon: boolean): string[] {
return raw
.split("\n---\n")
.map((block) => {
const lines = block.trim().split("\n");
if (lines.length < 2) return null;
if (withIcon) {
const [icon, title] = lines[0].split("|").map((s) => s.trim());
const description = lines.slice(1).join("\n").trim();
if (!title || !description) return null;
return JSON.stringify({
icon: icon || "Sparkles",
title,
description,
});
}
const title = lines[0].trim();
const description = lines.slice(1).join("\n").trim();
if (!title || !description) return null;
return JSON.stringify({ title, description });
})
.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,
);
// Client logos: her satıra bir URL
const logosRaw = String(formData.get("client_logos") ?? "");
const logos = logosRaw
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
const data = {
hero_badge: str(formData.get("hero_badge")),
hero_title: str(formData.get("hero_title")),
hero_subtitle: str(formData.get("hero_subtitle")),
hero_cta_primary_label: str(formData.get("hero_cta_primary_label")),
hero_cta_primary_href: str(formData.get("hero_cta_primary_href")),
hero_cta_secondary_label: str(formData.get("hero_cta_secondary_label")),
hero_cta_secondary_href: str(formData.get("hero_cta_secondary_href")),
hero_stats: stats.length > 0 ? stats : null,
services_eyebrow: str(formData.get("services_eyebrow")),
services_title: str(formData.get("services_title")),
services_description: str(formData.get("services_description")),
projects_eyebrow: str(formData.get("projects_eyebrow")),
projects_title: str(formData.get("projects_title")),
projects_description: str(formData.get("projects_description")),
testimonials_eyebrow: str(formData.get("testimonials_eyebrow")),
testimonials_title: str(formData.get("testimonials_title")),
testimonials_description: str(formData.get("testimonials_description")),
cta_title: str(formData.get("cta_title")),
cta_description: str(formData.get("cta_description")),
cta_button_label: str(formData.get("cta_button_label")),
cta_button_href: str(formData.get("cta_button_href")),
contact_phone: str(formData.get("contact_phone")),
contact_phone_raw: str(formData.get("contact_phone_raw")),
contact_email: str(formData.get("contact_email")),
contact_address: str(formData.get("contact_address")),
contact_hours_weekday: str(formData.get("contact_hours_weekday")),
contact_hours_weekend: str(formData.get("contact_hours_weekend")),
social_linkedin: str(formData.get("social_linkedin")),
social_instagram: str(formData.get("social_instagram")),
social_twitter: str(formData.get("social_twitter")),
social_facebook: str(formData.get("social_facebook")),
footer_tagline: str(formData.get("footer_tagline")),
whatsapp_message: str(formData.get("whatsapp_message")),
client_logos: logos.length > 0 ? logos : null,
trust_items: trust.length > 0 ? trust : null,
why_us: whyUs.length > 0 ? whyUs : null,
process_steps: processSteps.length > 0 ? processSteps : 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")),
google_rating: num(formData.get("google_rating")),
google_review_count: num(formData.get("google_review_count")),
};
try {
await tablesDB.updateRow(
DATABASE_ID,
TABLES.siteSettings,
"homepage",
data,
secret,
);
} catch {
await tablesDB.createRow(
DATABASE_ID,
TABLES.siteSettings,
"homepage",
data,
secret,
);
}
revalidatePath("/", "layout");
revalidatePath("/admin/site");
}
// ─── SEO Settings ────────────────────────────────────────────────
export async function saveSeoSettings(formData: FormData) {
const secret = await requireSessionSecret();
const data = {
site_name: str(formData.get("site_name")),
site_description: str(formData.get("site_description")),
default_og_image: str(formData.get("default_og_image")),
twitter_handle: str(formData.get("twitter_handle")),
facebook_url: str(formData.get("facebook_url")),
linkedin_url: str(formData.get("linkedin_url")),
instagram_url: str(formData.get("instagram_url")),
google_site_verification: str(formData.get("google_site_verification")),
gtm_id: str(formData.get("gtm_id")),
};
try {
await tablesDB.updateRow(
DATABASE_ID,
TABLES.seoSettings,
"global",
data,
secret,
);
} catch {
await tablesDB.createRow(
DATABASE_ID,
TABLES.seoSettings,
"global",
data,
secret,
);
}
revalidatePath("/", "layout");
revalidatePath("/admin/seo");
}
// ─── SEO Page ────────────────────────────────────────────────────
export async function saveSeoPage(formData: FormData) {
const secret = await requireSessionSecret();
const id = str(formData.get("id"));
const path = str(formData.get("path"));
if (!path) throw new Error("Path zorunlu");
const data = {
path,
title: str(formData.get("title")),
description: str(formData.get("description")),
og_image: str(formData.get("og_image")),
canonical: str(formData.get("canonical")),
noindex: bool(formData.get("noindex")),
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.seoPages, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.seoPages,
ID.unique(),
data,
secret,
);
}
revalidatePath(path);
revalidatePath("/admin/seo");
}
export async function deleteSeoPage(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.seoPages, id, secret);
revalidatePath("/admin/seo");
}
// ─── Team Members ────────────────────────────────────────────────
export async function saveTeamMember(formData: FormData) {
const secret = await requireSessionSecret();
const id = str(formData.get("id"));
const name = str(formData.get("name"));
if (!name) throw new Error("İsim zorunlu");
const data = {
name,
role: str(formData.get("role")),
bio: str(formData.get("bio")),
photo_url: str(formData.get("photo_url")),
linkedin_url: str(formData.get("linkedin_url")),
order: num(formData.get("order")) ?? 0,
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.teamMembers, id, data, secret);
} else {
await tablesDB.createRow(
DATABASE_ID,
TABLES.teamMembers,
ID.unique(),
data,
secret,
);
}
revalidatePath("/admin/ekip");
revalidatePath("/hakkimizda");
}
export async function deleteTeamMember(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.teamMembers, id, secret);
revalidatePath("/admin/ekip");
revalidatePath("/hakkimizda");
}
// ─── Industries ──────────────────────────────────────────────────
export async function saveIndustry(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 slug = str(formData.get("slug")) || slugify(title);
const faqRaw = String(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,
subtitle: str(formData.get("subtitle")),
content: str(formData.get("content")),
hero_image: str(formData.get("hero_image")),
features: strArr(formData.get("features")),
faq: faq.length > 0 ? faq : null,
seo_title: str(formData.get("seo_title")),
seo_description: str(formData.get("seo_description")),
featured: bool(formData.get("featured")),
order: num(formData.get("order")) ?? 0,
};
if (id) {
await tablesDB.updateRow(DATABASE_ID, TABLES.industries, id, data, secret);
} else {
await tablesDB.createRow(DATABASE_ID, TABLES.industries, slug, data, secret);
}
revalidatePath("/admin/sektorler");
revalidatePath(`/sektor/${slug}`);
}
export async function deleteIndustry(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(DATABASE_ID, TABLES.industries, id, secret);
revalidatePath("/admin/sektorler");
}
// ─── Contact ─────────────────────────────────────────────────────
export async function updateMessageStatus(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
const status = String(formData.get("status")) as
| "new"
| "read"
| "replied"
| "archived";
await tablesDB.updateRow(
DATABASE_ID,
TABLES.contactMessages,
id,
{ status },
secret,
);
revalidatePath("/admin/iletisim");
}
export async function deleteMessage(formData: FormData) {
const secret = await requireSessionSecret();
const id = String(formData.get("id"));
await tablesDB.deleteRow(
DATABASE_ID,
TABLES.contactMessages,
id,
secret,
);
revalidatePath("/admin/iletisim");
}