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:
@@ -0,0 +1,124 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
import { renderContent } from "@/lib/content-render";
|
||||||
|
import { getSolutionBySlug, getSiteSettings, listProjects } from "@/lib/data";
|
||||||
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
import { ProjectsGrid } from "@/components/projects-grid";
|
||||||
|
import { SectionTitle } from "@/components/section-title";
|
||||||
|
import { FaqList } from "@/components/faq-list";
|
||||||
|
import { SolutionHero } from "@/components/solution-hero";
|
||||||
|
import { SolutionSidebar } from "@/components/solution-sidebar";
|
||||||
|
import type { FaqItem } from "@/lib/types";
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const solution = await getSolutionBySlug(slug);
|
||||||
|
if (!solution) return { title: "Çözüm bulunamadı" };
|
||||||
|
return buildMetadata(`/cozumler/${slug}`, {
|
||||||
|
title: solution.title,
|
||||||
|
description: solution.description.slice(0, 160),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFaq(items?: string[] | null): FaqItem[] {
|
||||||
|
if (!items) return [];
|
||||||
|
const out: FaqItem[] = [];
|
||||||
|
for (const raw of items) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw) as Partial<FaqItem>;
|
||||||
|
if (obj.q && obj.a) out.push({ q: obj.q, a: obj.a });
|
||||||
|
} catch {
|
||||||
|
const [q, a] = raw.split("|||").map((s) => s.trim());
|
||||||
|
if (q && a) out.push({ q, a });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SolutionDetailPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
}) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const solution = await getSolutionBySlug(slug);
|
||||||
|
if (!solution) notFound();
|
||||||
|
|
||||||
|
const [relatedProjects, settings] = await Promise.all([
|
||||||
|
listProjects({ solutionSlug: slug, limit: 6 }),
|
||||||
|
getSiteSettings(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const faqItems = parseFaq(solution.faq);
|
||||||
|
const html = renderContent(solution.content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SolutionHero solution={solution} settings={settings} />
|
||||||
|
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-12 px-6 py-16 lg:grid-cols-[1.5fr_1fr]">
|
||||||
|
<div>
|
||||||
|
{solution.features && solution.features.length > 0 && (
|
||||||
|
<section className="mb-12">
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--navy)]">
|
||||||
|
Bu çözüm kapsamında
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
|
{solution.features.map((f) => (
|
||||||
|
<li
|
||||||
|
key={f}
|
||||||
|
className="flex items-start gap-2 rounded-xl border border-[var(--border)] bg-white p-4"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="mt-0.5 size-5 shrink-0 text-[var(--sky-600)]" />
|
||||||
|
<span className="text-sm text-[var(--foreground)]">{f}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{html && (
|
||||||
|
<article
|
||||||
|
className="prose prose-lg max-w-none text-[var(--foreground)]"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{faqItems.length > 0 && (
|
||||||
|
<section className="mt-12">
|
||||||
|
<h2 className="text-2xl font-bold text-[var(--navy)]">
|
||||||
|
Sıkça sorulan sorular
|
||||||
|
</h2>
|
||||||
|
<div className="mt-6">
|
||||||
|
<FaqList items={faqItems} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SolutionSidebar currentSlug={slug} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{relatedProjects.length > 0 && (
|
||||||
|
<section className="border-t border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
|
<SectionTitle
|
||||||
|
align="left"
|
||||||
|
eyebrow="Referanslar"
|
||||||
|
title={`${solution.title} alanındaki projelerimiz`}
|
||||||
|
description="Bu çözümde tamamladığımız işlerden seçkiler."
|
||||||
|
/>
|
||||||
|
<div className="mt-10">
|
||||||
|
<ProjectsGrid projects={relatedProjects} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { SectionTitle } from "@/components/section-title";
|
||||||
|
import { ServicesGrid } from "@/components/services-grid";
|
||||||
|
import { listSolutions } from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
return buildMetadata("/cozumler", {
|
||||||
|
title: "Çözümler",
|
||||||
|
description:
|
||||||
|
"İşletmeniz için uçtan uca dijital çözümler: kurumsal dijitalleşme, online satış altyapısı, CRM ve büyüme paketleri.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SolutionsPage() {
|
||||||
|
const solutions = await listSolutions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-7xl px-6 py-20">
|
||||||
|
<SectionTitle
|
||||||
|
eyebrow="Çözümlerimiz"
|
||||||
|
title="İşletmenize özel dijital çözümler"
|
||||||
|
description="Tek tek hizmetleri değil, işinizi büyüten bütün paketleri tek elden kuruyoruz."
|
||||||
|
/>
|
||||||
|
<div className="mt-14">
|
||||||
|
<ServicesGrid
|
||||||
|
services={solutions}
|
||||||
|
basePath="/cozumler"
|
||||||
|
fallback={siteConfig.fallbackSolutions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+33
-1
@@ -18,6 +18,7 @@ import {
|
|||||||
getSiteSettings,
|
getSiteSettings,
|
||||||
listProjects,
|
listProjects,
|
||||||
listServices,
|
listServices,
|
||||||
|
listSolutions,
|
||||||
listTestimonials,
|
listTestimonials,
|
||||||
} from "@/lib/data";
|
} from "@/lib/data";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
@@ -28,8 +29,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const [services, projects, testimonials, settings] = await Promise.all([
|
const [services, solutions, projects, testimonials, settings] =
|
||||||
|
await Promise.all([
|
||||||
listServices({ featured: true }),
|
listServices({ featured: true }),
|
||||||
|
listSolutions({ featured: true }),
|
||||||
listProjects({ featured: true, limit: 6 }),
|
listProjects({ featured: true, limit: 6 }),
|
||||||
listTestimonials({ featured: true }),
|
listTestimonials({ featured: true }),
|
||||||
getSiteSettings(),
|
getSiteSettings(),
|
||||||
@@ -118,6 +121,35 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="border-b border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
||||||
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
|
<SectionTitle
|
||||||
|
eyebrow={settings?.solutions_eyebrow ?? "İşletmeler için"}
|
||||||
|
title={settings?.solutions_title ?? "Hazır dijital çözüm paketleri"}
|
||||||
|
description={
|
||||||
|
settings?.solutions_description ??
|
||||||
|
"Tek tek hizmetleri değil; işinizi büyüten bütün paketleri tek elden kuruyoruz."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-12">
|
||||||
|
<ServicesGrid
|
||||||
|
services={solutions}
|
||||||
|
basePath="/cozumler"
|
||||||
|
fallback={siteConfig.fallbackSolutions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 text-center">
|
||||||
|
<Link
|
||||||
|
href="/cozumler"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-white px-5 py-2.5 text-sm font-semibold text-[var(--navy)] transition hover:border-[var(--sky)] hover:text-[var(--sky-600)]"
|
||||||
|
>
|
||||||
|
Tüm çözümleri gör
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<WhyUs settings={settings} />
|
<WhyUs settings={settings} />
|
||||||
|
|
||||||
<Guarantee settings={settings} />
|
<Guarantee settings={settings} />
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getRow } from "@/lib/data";
|
||||||
|
import { TABLES } from "@/lib/appwrite-rest";
|
||||||
|
import type { SolutionRow } from "@/lib/types";
|
||||||
|
import { SolutionForm } from "../../form";
|
||||||
|
|
||||||
|
export default async function EditSolutionPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
const solution = await getRow<SolutionRow>(TABLES.solutions, id);
|
||||||
|
if (!solution) notFound();
|
||||||
|
return <SolutionForm solution={solution} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Save } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Field,
|
||||||
|
FormActions,
|
||||||
|
FormShell,
|
||||||
|
GhostLink,
|
||||||
|
PageHeader,
|
||||||
|
PrimaryButton,
|
||||||
|
Textarea,
|
||||||
|
} from "@/components/admin/form";
|
||||||
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
|
import { saveSolution } from "@/lib/admin-actions";
|
||||||
|
import type { FaqItem, SolutionRow } from "@/lib/types";
|
||||||
|
|
||||||
|
const ICON_OPTIONS = [
|
||||||
|
"Globe",
|
||||||
|
"ShoppingCart",
|
||||||
|
"Smartphone",
|
||||||
|
"Code2",
|
||||||
|
"Users",
|
||||||
|
"TrendingUp",
|
||||||
|
"Share2",
|
||||||
|
"Megaphone",
|
||||||
|
"Layers",
|
||||||
|
];
|
||||||
|
|
||||||
|
function faqToText(items?: string[] | null): string {
|
||||||
|
if (!items) return "";
|
||||||
|
const parsed: FaqItem[] = [];
|
||||||
|
for (const raw of items) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw) as Partial<FaqItem>;
|
||||||
|
if (obj.q && obj.a) parsed.push({ q: obj.q, a: obj.a });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed.map((it) => `${it.q}\n${it.a}`).join("\n---\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SolutionForm({ solution }: { solution?: SolutionRow }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title={solution ? "Çözümü düzenle" : "Yeni çözüm"}
|
||||||
|
backHref="/admin/cozumler"
|
||||||
|
/>
|
||||||
|
<form action={saveSolution}>
|
||||||
|
{solution && <input type="hidden" name="id" value={solution.$id} />}
|
||||||
|
<FormShell>
|
||||||
|
<div className="grid gap-5 md:grid-cols-2">
|
||||||
|
<Field label="Başlık" name="title" required defaultValue={solution?.title} />
|
||||||
|
<Field label="Slug" name="slug" defaultValue={solution?.slug} />
|
||||||
|
<Field
|
||||||
|
label="Sıra"
|
||||||
|
name="order"
|
||||||
|
type="number"
|
||||||
|
defaultValue={solution?.order ?? 0}
|
||||||
|
/>
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-sm font-medium text-[var(--navy)]">İkon</span>
|
||||||
|
<select
|
||||||
|
name="icon"
|
||||||
|
defaultValue={solution?.icon ?? "Layers"}
|
||||||
|
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
|
||||||
|
>
|
||||||
|
{ICON_OPTIONS.map((i) => (
|
||||||
|
<option key={i} value={i}>
|
||||||
|
{i}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<MediaPicker
|
||||||
|
label="Hero görsel"
|
||||||
|
name="hero_image"
|
||||||
|
defaultValue={solution?.hero_image}
|
||||||
|
help="Detay sayfasının üst kısmında gösterilir (opsiyonel)."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 space-y-5">
|
||||||
|
<Textarea
|
||||||
|
label="Kısa açıklama (kart için)"
|
||||||
|
name="description"
|
||||||
|
required
|
||||||
|
defaultValue={solution?.description}
|
||||||
|
rows={3}
|
||||||
|
help="Listede ve anasayfa kartında gösterilir."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
Detay içerik
|
||||||
|
</span>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<RichEditor
|
||||||
|
name="content"
|
||||||
|
defaultValue={solution?.content}
|
||||||
|
placeholder="Çözümün detaylarını anlatın… `/` ile blok ekleyin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||||
|
Çözüm detay sayfasında ana içerik olarak gösterilir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Özellikler"
|
||||||
|
name="features"
|
||||||
|
defaultValue={solution?.features?.join(", ")}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Uçtan uca kurulum, Eğitim ve devir, 1 yıl destek, …"
|
||||||
|
help="Virgülle ayırın. Detay sayfasında checklist olarak gösterilir."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="SSS"
|
||||||
|
name="faq"
|
||||||
|
defaultValue={faqToText(solution?.faq)}
|
||||||
|
rows={8}
|
||||||
|
placeholder={
|
||||||
|
"Soru 1?\nCevap 1 burada.\n---\nSoru 2?\nCevap 2 burada."
|
||||||
|
}
|
||||||
|
help="Her soru/cevap blokunu '---' ile ayırın. İlk satır soru, kalanı cevap."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<Checkbox
|
||||||
|
label="Öne çıkar (Anasayfada göster)"
|
||||||
|
name="featured"
|
||||||
|
defaultChecked={solution?.featured ?? false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormActions>
|
||||||
|
<GhostLink href="/admin/cozumler">İptal</GhostLink>
|
||||||
|
<PrimaryButton>
|
||||||
|
<Save className="size-4" /> Kaydet
|
||||||
|
</PrimaryButton>
|
||||||
|
</FormActions>
|
||||||
|
</FormShell>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { SolutionForm } from "../form";
|
||||||
|
|
||||||
|
export default function NewSolutionPage() {
|
||||||
|
return <SolutionForm />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Plus, Edit } from "lucide-react";
|
||||||
|
import { PageHeader } from "@/components/admin/form";
|
||||||
|
import { DeleteButton } from "@/components/admin/delete-button";
|
||||||
|
import { listSolutions } from "@/lib/data";
|
||||||
|
import { deleteSolution } from "@/lib/admin-actions";
|
||||||
|
|
||||||
|
export default async function SolutionsAdminPage() {
|
||||||
|
const solutions = await listSolutions();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Çözümler"
|
||||||
|
description="Anasayfa ve /cozumler sayfasında gösterilen çözüm kartları."
|
||||||
|
action={
|
||||||
|
<Link
|
||||||
|
href="/admin/cozumler/new"
|
||||||
|
className="inline-flex items-center gap-2 rounded-full bg-[var(--navy)] px-4 py-2 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
<Plus className="size-4" /> Yeni çözüm
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="mt-6 overflow-hidden rounded-2xl border border-[var(--border)] bg-white">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-[var(--navy-50)] text-xs uppercase tracking-wider text-[var(--muted)]">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left">Sıra</th>
|
||||||
|
<th className="px-4 py-3 text-left">Başlık</th>
|
||||||
|
<th className="px-4 py-3 text-left">Slug</th>
|
||||||
|
<th className="px-4 py-3 text-left">İkon</th>
|
||||||
|
<th className="px-4 py-3 text-left">Öne çıkan</th>
|
||||||
|
<th className="px-4 py-3 text-right">İşlem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{solutions.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-4 py-12 text-center text-[var(--muted)]">
|
||||||
|
Çözüm eklenmemiş.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{solutions.map((s) => (
|
||||||
|
<tr key={s.$id} className="border-t border-[var(--border)]">
|
||||||
|
<td className="px-4 py-3 text-[var(--muted)]">{s.order ?? 0}</td>
|
||||||
|
<td className="px-4 py-3 font-medium text-[var(--navy)]">{s.title}</td>
|
||||||
|
<td className="px-4 py-3 text-[var(--muted)]">{s.slug}</td>
|
||||||
|
<td className="px-4 py-3 text-[var(--muted)]">{s.icon ?? "—"}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{s.featured ? (
|
||||||
|
<span className="rounded-full bg-[var(--sky-50)] px-2 py-0.5 text-xs text-[var(--sky-600)]">
|
||||||
|
Öne çıkan
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-[var(--muted)]">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Link
|
||||||
|
href={`/admin/cozumler/${s.$id}/edit`}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md border border-[var(--border)] bg-white px-2.5 py-1.5 text-xs font-medium text-[var(--navy)] hover:bg-[var(--navy-50)]"
|
||||||
|
>
|
||||||
|
<Edit className="size-3.5" /> Düzenle
|
||||||
|
</Link>
|
||||||
|
<form action={deleteSolution}>
|
||||||
|
<input type="hidden" name="id" value={s.$id} />
|
||||||
|
<DeleteButton />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,11 +12,14 @@ import {
|
|||||||
import { MediaPicker } from "@/components/admin/media-picker";
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
import { RichEditor } from "@/components/admin/rich-editor";
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
import { saveProject } from "@/lib/admin-actions";
|
import { saveProject } from "@/lib/admin-actions";
|
||||||
import { listServices } from "@/lib/data";
|
import { listServices, listSolutions } from "@/lib/data";
|
||||||
import type { ProjectRow } from "@/lib/types";
|
import type { ProjectRow } from "@/lib/types";
|
||||||
|
|
||||||
export async function ProjectForm({ project }: { project?: ProjectRow }) {
|
export async function ProjectForm({ project }: { project?: ProjectRow }) {
|
||||||
const services = await listServices();
|
const [services, solutions] = await Promise.all([
|
||||||
|
listServices(),
|
||||||
|
listSolutions(),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -51,6 +54,26 @@ export async function ProjectForm({ project }: { project?: ProjectRow }) {
|
|||||||
Bu projenin ait olduğu hizmet — detay sayfasında "ilgili projeler" olarak görünür.
|
Bu projenin ait olduğu hizmet — detay sayfasında "ilgili projeler" olarak görünür.
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
İlgili çözüm
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
name="solution_slug"
|
||||||
|
defaultValue={project?.solution_slug ?? ""}
|
||||||
|
className="mt-1.5 w-full rounded-xl border border-[var(--border)] bg-white px-4 py-2.5 text-sm outline-none focus:border-[var(--sky)] focus:ring-2 focus:ring-[var(--sky)]/20"
|
||||||
|
>
|
||||||
|
<option value="">— Yok —</option>
|
||||||
|
{solutions.map((s) => (
|
||||||
|
<option key={s.slug} value={s.slug}>
|
||||||
|
{s.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<span className="mt-1 block text-xs text-[var(--muted)]">
|
||||||
|
Bu projenin ait olduğu çözüm — çözüm detay sayfasında "ilgili projeler" olarak görünür.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
<Field label="Müşteri" name="client_name" defaultValue={project?.client_name} />
|
<Field label="Müşteri" name="client_name" defaultValue={project?.client_name} />
|
||||||
<Field label="Sektör" name="industry" defaultValue={project?.industry} />
|
<Field label="Sektör" name="industry" defaultValue={project?.industry} />
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
@@ -231,6 +231,29 @@ export default async function SiteSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
title="Çözümler bölümü başlığı"
|
||||||
|
description="Anasayfadaki çözüm kartlarının üstündeki yazı."
|
||||||
|
>
|
||||||
|
<div className="grid gap-5 md:grid-cols-3">
|
||||||
|
<Field
|
||||||
|
label="Eyebrow"
|
||||||
|
name="solutions_eyebrow"
|
||||||
|
defaultValue={s?.solutions_eyebrow}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Başlık"
|
||||||
|
name="solutions_title"
|
||||||
|
defaultValue={s?.solutions_title}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Açıklama"
|
||||||
|
name="solutions_description"
|
||||||
|
defaultValue={s?.solutions_description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
title="Projeler bölümü başlığı"
|
title="Projeler bölümü başlığı"
|
||||||
description="Anasayfadaki proje kartlarının üstündeki yazı."
|
description="Anasayfadaki proje kartlarının üstündeki yazı."
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
Layers,
|
Layers,
|
||||||
|
Boxes,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
MessageSquareQuote,
|
MessageSquareQuote,
|
||||||
Search,
|
Search,
|
||||||
@@ -25,6 +26,7 @@ const items: Item[] = [
|
|||||||
{ href: "/admin/site", label: "Site Ayarları", icon: Settings },
|
{ href: "/admin/site", label: "Site Ayarları", icon: Settings },
|
||||||
{ href: "/admin/blog", label: "Blog", icon: Newspaper },
|
{ href: "/admin/blog", label: "Blog", icon: Newspaper },
|
||||||
{ href: "/admin/hizmetler", label: "Hizmetler", icon: Layers },
|
{ href: "/admin/hizmetler", label: "Hizmetler", icon: Layers },
|
||||||
|
{ href: "/admin/cozumler", label: "Çözümler", icon: Boxes },
|
||||||
{ href: "/admin/projeler", label: "Projeler", icon: Briefcase },
|
{ href: "/admin/projeler", label: "Projeler", icon: Briefcase },
|
||||||
{ href: "/admin/sektorler", label: "Sektörler", icon: Building2 },
|
{ href: "/admin/sektorler", label: "Sektörler", icon: Building2 },
|
||||||
{ href: "/admin/ekip", label: "Ekip", icon: UsersIcon },
|
{ href: "/admin/ekip", label: "Ekip", icon: UsersIcon },
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export async function Footer() {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<li className="pt-1">
|
||||||
|
<Link href="/cozumler" className="font-medium text-white/90 hover:text-white">
|
||||||
|
Çözümler →
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -4,6 +4,7 @@ import { ChevronDown, Phone } from "lucide-react";
|
|||||||
import { getSiteSettings, listServices } from "@/lib/data";
|
import { getSiteSettings, listServices } from "@/lib/data";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
import { siteConfig } from "@/lib/site-config";
|
||||||
import { HeaderScrollEffect } from "@/components/header-scroll";
|
import { HeaderScrollEffect } from "@/components/header-scroll";
|
||||||
|
import { MobileMenu } from "@/components/mobile-menu";
|
||||||
|
|
||||||
export async function Header() {
|
export async function Header() {
|
||||||
const [settings, services] = await Promise.all([
|
const [settings, services] = await Promise.all([
|
||||||
@@ -111,6 +112,12 @@ export async function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/cozumler"
|
||||||
|
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
Çözümler
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/projeler"
|
href="/projeler"
|
||||||
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
|
||||||
@@ -162,10 +169,17 @@ export async function Header() {
|
|||||||
</a>
|
</a>
|
||||||
<Link
|
<Link
|
||||||
href="/iletisim"
|
href="/iletisim"
|
||||||
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg bg-[var(--navy)] px-4 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[var(--navy-700)]"
|
className="hidden h-9 items-center justify-center whitespace-nowrap rounded-lg bg-[var(--navy)] px-4 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[var(--navy-700)] sm:inline-flex"
|
||||||
>
|
>
|
||||||
Ücretsiz Teklif
|
Ücretsiz Teklif
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{/* Mobil menü (hamburger) — sadece < lg */}
|
||||||
|
<MobileMenu
|
||||||
|
services={services.map((s) => ({ slug: s.slug, title: s.title }))}
|
||||||
|
phone={phone}
|
||||||
|
phoneRaw={phoneRaw}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { Menu, X, ChevronDown, Phone, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
type NavService = { slug: string; title: string };
|
||||||
|
|
||||||
|
const LINKS = [
|
||||||
|
{ href: "/", label: "Anasayfa" },
|
||||||
|
{ href: "/cozumler", label: "Çözümler" },
|
||||||
|
{ href: "/projeler", label: "Projeler" },
|
||||||
|
{ href: "/blog", label: "Blog" },
|
||||||
|
{ href: "/hakkimizda", label: "Hakkımızda" },
|
||||||
|
{ href: "/iletisim", label: "İletişim" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function MobileMenu({
|
||||||
|
services,
|
||||||
|
phone,
|
||||||
|
phoneRaw,
|
||||||
|
}: {
|
||||||
|
services: NavService[];
|
||||||
|
phone: string;
|
||||||
|
phoneRaw: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [servicesOpen, setServicesOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Rota değişince menüyü kapat
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
// Açıkken arka plan kaydırmasını kilitle
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = open ? "hidden" : "";
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label="Menüyü aç"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="inline-flex size-9 items-center justify-center rounded-lg text-gray-700 transition-colors hover:bg-gray-100 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<Menu className="size-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Overlay + drawer */}
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-[60] lg:hidden ${
|
||||||
|
open ? "" : "pointer-events-none"
|
||||||
|
}`}
|
||||||
|
aria-hidden={!open}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className={`absolute inset-0 bg-black/40 backdrop-blur-sm transition-opacity duration-300 ${
|
||||||
|
open ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Panel */}
|
||||||
|
<div
|
||||||
|
className={`absolute right-0 top-0 flex h-full w-[86%] max-w-sm flex-col bg-white shadow-2xl transition-transform duration-300 ease-out ${
|
||||||
|
open ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Üst bar */}
|
||||||
|
<div className="flex h-14 items-center justify-between border-b border-gray-100 px-5">
|
||||||
|
<span className="text-sm font-semibold tracking-tight text-[var(--navy)]">
|
||||||
|
Menü
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-label="Menüyü kapat"
|
||||||
|
className="inline-flex size-9 items-center justify-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Linkler */}
|
||||||
|
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="block rounded-xl px-4 py-3 text-base font-medium text-gray-800 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Anasayfa
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Hizmetler — açılır */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setServicesOpen((v) => !v)}
|
||||||
|
aria-expanded={servicesOpen}
|
||||||
|
className="flex w-full items-center justify-between rounded-xl px-4 py-3 text-base font-medium text-gray-800 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Hizmetler
|
||||||
|
<ChevronDown
|
||||||
|
className={`size-4 transition-transform duration-200 ${
|
||||||
|
servicesOpen ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{servicesOpen && (
|
||||||
|
<div className="mb-1 ml-3 border-l border-gray-100 pl-3">
|
||||||
|
{services.map((s) => (
|
||||||
|
<Link
|
||||||
|
key={s.slug}
|
||||||
|
href={`/hizmetler/${s.slug}`}
|
||||||
|
className="block rounded-lg px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<Link
|
||||||
|
href="/hizmetler"
|
||||||
|
className="block rounded-lg px-3 py-2 text-sm font-semibold text-[var(--sky-600)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Tüm hizmetleri gör →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{LINKS.filter((l) => l.href !== "/").map((l) => (
|
||||||
|
<Link
|
||||||
|
key={l.href}
|
||||||
|
href={l.href}
|
||||||
|
className="block rounded-xl px-4 py-3 text-base font-medium text-gray-800 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Alt CTA */}
|
||||||
|
<div className="space-y-2 border-t border-gray-100 p-4">
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl border border-gray-200 px-4 py-3 text-sm font-semibold text-[var(--navy)] transition-colors hover:border-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<Phone className="size-4" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl bg-[var(--navy)] px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Ücretsiz Teklif
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import Link from "next/link";
|
|||||||
import { ArrowUpRight } from "lucide-react";
|
import { ArrowUpRight } from "lucide-react";
|
||||||
import { Icon } from "@/components/icon";
|
import { Icon } from "@/components/icon";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
import { siteConfig } from "@/lib/site-config";
|
||||||
import type { ServiceRow } from "@/lib/types";
|
|
||||||
|
|
||||||
type ServiceLike = {
|
type ServiceLike = {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -11,18 +10,27 @@ type ServiceLike = {
|
|||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ServicesGrid({ services }: { services: ServiceRow[] }) {
|
// Hem Hizmetler hem Çözümler için kullanılır — sadece basePath ve fallback değişir.
|
||||||
|
export function ServicesGrid({
|
||||||
|
services,
|
||||||
|
basePath = "/hizmetler",
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
services: ServiceLike[];
|
||||||
|
basePath?: string;
|
||||||
|
fallback?: readonly ServiceLike[];
|
||||||
|
}) {
|
||||||
const items: ServiceLike[] =
|
const items: ServiceLike[] =
|
||||||
services.length > 0
|
services.length > 0
|
||||||
? services
|
? services
|
||||||
: (siteConfig.fallbackServices as readonly ServiceLike[]).slice();
|
: ((fallback ?? siteConfig.fallbackServices) as readonly ServiceLike[]).slice();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{items.map((s) => (
|
{items.map((s) => (
|
||||||
<Link
|
<Link
|
||||||
key={s.slug}
|
key={s.slug}
|
||||||
href={`/hizmetler/${s.slug}`}
|
href={`${basePath}/${s.slug}`}
|
||||||
id={s.slug}
|
id={s.slug}
|
||||||
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white p-8 transition-all duration-300 hover:-translate-y-2 hover:border-[var(--sky)]/40 hover:shadow-2xl hover:shadow-[var(--navy)]/10"
|
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white p-8 transition-all duration-300 hover:-translate-y-2 hover:border-[var(--sky)]/40 hover:shadow-2xl hover:shadow-[var(--navy)]/10"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, ArrowRight, MessageCircle, Phone, Sparkles, CheckCircle2 } from "lucide-react";
|
||||||
|
import { Icon } from "@/components/icon";
|
||||||
|
import type { SolutionRow, SiteSettingsRow } from "@/lib/types";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
|
||||||
|
const QUICK_TRUST = [
|
||||||
|
"İşletmenize özel kurgu",
|
||||||
|
"Tek elden uçtan uca",
|
||||||
|
"Ücretsiz keşif görüşmesi",
|
||||||
|
"Yerel ekip — Kocaeli",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SolutionHero({
|
||||||
|
solution,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
solution: SolutionRow;
|
||||||
|
settings?: SiteSettingsRow | null;
|
||||||
|
}) {
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const wa = phoneRaw.replace(/[^\d]/g, "");
|
||||||
|
const waMessage = settings?.whatsapp_message ?? `Merhaba, ${solution.title} çözümü hakkında bilgi almak istiyorum.`;
|
||||||
|
const waHref = `https://wa.me/${wa}?text=${encodeURIComponent(waMessage)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden border-b border-[var(--border)] bg-gradient-to-br from-[var(--navy-50)]/60 via-white to-[var(--sky-50)]/40">
|
||||||
|
{/* Subtle grid + glow */}
|
||||||
|
<div className="absolute inset-0 hero-grid opacity-50" aria-hidden />
|
||||||
|
<div
|
||||||
|
className="absolute -right-32 top-1/2 size-[520px] -translate-y-1/2 rounded-full bg-gradient-to-br from-[var(--sky)]/15 to-transparent blur-3xl"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-16 lg:py-20">
|
||||||
|
<Link
|
||||||
|
href="/cozumler"
|
||||||
|
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-3.5" /> Tüm çözümler
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="mt-8 grid items-start gap-12 lg:grid-cols-[1.3fr_1fr]">
|
||||||
|
{/* Left — content */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 -z-10 rounded-2xl bg-gradient-to-br from-[var(--sky)] to-purple-500 blur-md opacity-50"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="flex size-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--sky)] to-purple-500 text-white shadow-lg">
|
||||||
|
<Icon name={solution.icon} className="size-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--sky)]/30 bg-white px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
||||||
|
<Sparkles className="size-3.5" />
|
||||||
|
İşletmenize özel çözüm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="mt-6 text-4xl font-extrabold leading-[1.1] tracking-tight text-[var(--navy)] sm:text-5xl lg:text-6xl">
|
||||||
|
<span className="gradient-text">{solution.title}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
||||||
|
{solution.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Quick trust strip */}
|
||||||
|
<ul className="mt-8 grid max-w-xl grid-cols-2 gap-2">
|
||||||
|
{QUICK_TRUST.map((it) => (
|
||||||
|
<li
|
||||||
|
key={it}
|
||||||
|
className="flex items-center gap-2 text-sm text-[var(--foreground)]"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="size-4 shrink-0 text-[var(--sky-600)]" />
|
||||||
|
{it}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[var(--navy)] px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[var(--navy)]/20 transition hover:-translate-y-0.5 hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Ücretsiz teklif al
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href={waHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[#25d366] px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[#25d366]/20 transition hover:-translate-y-0.5 hover:bg-[#1ebe5d]"
|
||||||
|
>
|
||||||
|
<MessageCircle className="size-4" />
|
||||||
|
WhatsApp'tan yaz
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl border border-[var(--border)] bg-white px-6 py-3.5 text-sm font-semibold text-[var(--navy)] transition hover:border-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<Phone className="size-4" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right — hero card */}
|
||||||
|
<div className="relative">
|
||||||
|
{solution.hero_image ? (
|
||||||
|
<div className="relative aspect-[4/5] overflow-hidden rounded-3xl shadow-2xl shadow-[var(--navy)]/10">
|
||||||
|
<Image
|
||||||
|
src={solution.hero_image}
|
||||||
|
alt={solution.title}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 1024px) 480px, 100vw"
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
{/* Floating badge */}
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 rounded-xl bg-white/95 p-4 backdrop-blur shadow-lg">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--sky-600)]">
|
||||||
|
Şimdi başla
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-bold text-[var(--navy)]">
|
||||||
|
Ücretsiz keşif görüşmesi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DecorativeSolutionCard solution={solution} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DecorativeSolutionCard({ solution }: { solution: SolutionRow }) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* Outer gradient frame */}
|
||||||
|
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-[var(--navy)] via-[var(--sky-600)] to-[var(--sky)] p-px shadow-2xl shadow-[var(--navy)]/20">
|
||||||
|
<div className="relative rounded-3xl bg-[#0f172a] p-8">
|
||||||
|
{/* Animated dots */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-20"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
|
||||||
|
backgroundSize: "24px 24px",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Glow */}
|
||||||
|
<div className="absolute -right-20 -top-20 size-64 rounded-full bg-[var(--sky)]/30 blur-3xl" aria-hidden />
|
||||||
|
|
||||||
|
{/* Card content */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex size-20 items-center justify-center rounded-2xl bg-white/10 backdrop-blur ring-1 ring-white/20">
|
||||||
|
<Icon name={solution.icon} className="size-10 text-[var(--sky)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 space-y-2 text-white">
|
||||||
|
<p className="text-[11px] font-mono uppercase tracking-[0.2em] text-[var(--sky)]">
|
||||||
|
kovak.yazilim
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold leading-tight">
|
||||||
|
{solution.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed text-white/60">
|
||||||
|
İşletmenize özel, uçtan uca çözüm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom badges */}
|
||||||
|
<div className="mt-8 flex flex-wrap gap-2">
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
⚡ Hızlı
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
🛡️ Garantili
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
📞 7/24 Destek
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating accent */}
|
||||||
|
<div className="absolute -right-4 -top-4 rounded-2xl bg-white p-4 shadow-xl ring-1 ring-[var(--border)]">
|
||||||
|
<p className="text-xs font-medium text-[var(--muted)]">Memnuniyet</p>
|
||||||
|
<p className="text-2xl font-bold text-[var(--navy)]">100%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute -bottom-4 -left-4 rounded-2xl bg-white p-4 shadow-xl ring-1 ring-[var(--border)]">
|
||||||
|
<p className="text-xs font-medium text-[var(--muted)]">Proje</p>
|
||||||
|
<p className="text-2xl font-bold text-[var(--navy)]">150+</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowRight, MessageCircle, Phone, ShieldCheck } from "lucide-react";
|
||||||
|
import { Icon } from "@/components/icon";
|
||||||
|
import { getSiteSettings, listSolutions } from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { QuickLeadForm } from "@/components/quick-lead-form";
|
||||||
|
|
||||||
|
export async function SolutionSidebar({
|
||||||
|
currentSlug,
|
||||||
|
}: {
|
||||||
|
currentSlug: string;
|
||||||
|
}) {
|
||||||
|
const [settings, solutions] = await Promise.all([
|
||||||
|
getSiteSettings(),
|
||||||
|
listSolutions(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const otherSolutions = solutions
|
||||||
|
.filter((s) => s.slug !== currentSlug)
|
||||||
|
.slice(0, 6);
|
||||||
|
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const wa = phoneRaw.replace(/[^\d]/g, "");
|
||||||
|
const waMessage = settings?.whatsapp_message ?? "";
|
||||||
|
const waHref = `https://wa.me/${wa}${
|
||||||
|
waMessage ? `?text=${encodeURIComponent(waMessage)}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="space-y-5 lg:sticky lg:top-24 lg:self-start">
|
||||||
|
{/* Quick lead form */}
|
||||||
|
<QuickLeadForm
|
||||||
|
title="Bu çözüm için teklif"
|
||||||
|
description="Adınızı ve telefonunuzu bırakın, 24 saat içinde sizi arayalım."
|
||||||
|
buttonLabel="Beni arayın"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* CTA card */}
|
||||||
|
<div className="overflow-hidden rounded-2xl border border-[var(--border)] bg-gradient-to-br from-[var(--navy)] to-[var(--sky-600)] p-6 text-white">
|
||||||
|
<h3 className="text-base font-bold">Hızlı iletişim</h3>
|
||||||
|
<p className="mt-1 text-sm text-white/80">
|
||||||
|
Telefon veya WhatsApp ile dakikalar içinde konuşalım.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl bg-white px-4 py-2.5 text-sm font-semibold text-[var(--navy)] transition hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
<Phone className="size-3.5" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={waHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl bg-[#25d366] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-[#1ebe5d]"
|
||||||
|
>
|
||||||
|
<MessageCircle className="size-3.5" />
|
||||||
|
WhatsApp'tan yaz
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Guarantee mini */}
|
||||||
|
<div className="rounded-2xl border border-[var(--sky)]/30 bg-[var(--sky-50)]/50 p-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="size-5 text-[var(--sky-600)]" />
|
||||||
|
<h3 className="text-sm font-bold text-[var(--navy)]">
|
||||||
|
Risk almazsınız
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-3 space-y-1.5 text-xs text-[var(--foreground)]">
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
Ücretsiz keşif görüşmesi
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
1 yıl ücretsiz teknik destek
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
Kaynak kodlar size ait
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diğer çözümler — full list */}
|
||||||
|
{otherSolutions.length > 0 && (
|
||||||
|
<div className="rounded-2xl border border-[var(--border)] bg-white p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-bold uppercase tracking-wider text-[var(--navy)]">
|
||||||
|
Diğer çözümler
|
||||||
|
</h3>
|
||||||
|
<Link
|
||||||
|
href="/cozumler"
|
||||||
|
className="text-xs text-[var(--sky-600)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Tümü →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-4 space-y-1">
|
||||||
|
{otherSolutions.map((s) => (
|
||||||
|
<li key={s.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/cozumler/${s.slug}`}
|
||||||
|
className="group flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition hover:bg-[var(--navy-50)]"
|
||||||
|
>
|
||||||
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--navy-50)] text-[var(--navy)] transition group-hover:bg-gradient-to-br group-hover:from-[var(--sky)] group-hover:to-purple-500 group-hover:text-white">
|
||||||
|
<Icon name={s.icon} className="size-4" />
|
||||||
|
</div>
|
||||||
|
<span className="flex-1 font-medium text-[var(--foreground)] group-hover:text-[var(--navy)]">
|
||||||
|
{s.title}
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="size-3 text-[var(--muted)] opacity-0 transition group-hover:opacity-100" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Site analizi lead magnet */}
|
||||||
|
<div className="rounded-2xl border border-dashed border-[var(--border)] bg-white p-5">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--sky-600)]">
|
||||||
|
Ücretsiz fırsat
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-1 text-sm font-bold text-[var(--navy)]">
|
||||||
|
Site analizi raporu
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-xs leading-relaxed text-[var(--muted)]">
|
||||||
|
Mevcut sitenizin SEO, hız ve dönüşüm performansını ücretsiz değerlendirelim.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/site-analizi"
|
||||||
|
className="mt-3 inline-flex items-center gap-1 text-xs font-semibold text-[var(--sky-600)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Hemen başla
|
||||||
|
<ArrowRight className="size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
+98
-14
@@ -45,6 +45,12 @@ function strArr(v: FormDataEntryValue | null) {
|
|||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
.filter(Boolean);
|
.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 ───────────────────────────────────────────────────────
|
// ─── Media ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -130,7 +136,7 @@ export async function saveService(formData: FormData) {
|
|||||||
const slug = str(formData.get("slug")) || slugify(title);
|
const slug = str(formData.get("slug")) || slugify(title);
|
||||||
|
|
||||||
// FAQ as JSON-encoded array. Each item: {"q":"...","a":"..."}
|
// FAQ as JSON-encoded array. Each item: {"q":"...","a":"..."}
|
||||||
const faqRaw = String(formData.get("faq") ?? "");
|
const faqRaw = raw(formData.get("faq"));
|
||||||
const faq = faqRaw
|
const faq = faqRaw
|
||||||
.split("\n---\n")
|
.split("\n---\n")
|
||||||
.map((block) => {
|
.map((block) => {
|
||||||
@@ -178,6 +184,60 @@ export async function deleteService(formData: FormData) {
|
|||||||
revalidatePath("/hizmetler");
|
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 ────────────────────────────────────────────────────
|
// ─── Projects ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function parseMetricsInput(raw: string): string[] {
|
function parseMetricsInput(raw: string): string[] {
|
||||||
@@ -201,7 +261,7 @@ export async function saveProject(formData: FormData) {
|
|||||||
if (!description) throw new Error("Açıklama zorunlu");
|
if (!description) throw new Error("Açıklama zorunlu");
|
||||||
|
|
||||||
// Gallery: one URL per line
|
// Gallery: one URL per line
|
||||||
const galleryRaw = String(formData.get("gallery") ?? "");
|
const galleryRaw = raw(formData.get("gallery"));
|
||||||
const gallery = galleryRaw
|
const gallery = galleryRaw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@@ -223,8 +283,9 @@ export async function saveProject(formData: FormData) {
|
|||||||
industry: str(formData.get("industry")),
|
industry: str(formData.get("industry")),
|
||||||
duration: str(formData.get("duration")),
|
duration: str(formData.get("duration")),
|
||||||
service_slug: str(formData.get("service_slug")),
|
service_slug: str(formData.get("service_slug")),
|
||||||
|
solution_slug: str(formData.get("solution_slug")),
|
||||||
metrics: (() => {
|
metrics: (() => {
|
||||||
const m = parseMetricsInput(String(formData.get("metrics") ?? ""));
|
const m = parseMetricsInput(raw(formData.get("metrics")));
|
||||||
return m.length > 0 ? m : null;
|
return m.length > 0 ? m : null;
|
||||||
})(),
|
})(),
|
||||||
};
|
};
|
||||||
@@ -302,7 +363,7 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
const secret = await requireSessionSecret();
|
const secret = await requireSessionSecret();
|
||||||
|
|
||||||
// Hero stats: 3 satır halinde "value|label" formatında — JSON array'e çevir
|
// 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
|
const stats = statsRaw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
@@ -313,7 +374,7 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
.filter((x): x is string => x !== null);
|
.filter((x): x is string => x !== null);
|
||||||
|
|
||||||
// Trust items: "icon|value|label" satırlar
|
// Trust items: "icon|value|label" satırlar
|
||||||
const trustRaw = String(formData.get("trust_items") ?? "");
|
const trustRaw = raw(formData.get("trust_items"));
|
||||||
const trust = trustRaw
|
const trust = trustRaw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
@@ -350,21 +411,18 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
.filter((x): x is string => x !== null);
|
.filter((x): x is string => x !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whyUs = parseBlocks(String(formData.get("why_us") ?? ""), true);
|
const whyUs = parseBlocks(raw(formData.get("why_us")), true);
|
||||||
const processSteps = parseBlocks(
|
const processSteps = parseBlocks(raw(formData.get("process_steps")), false);
|
||||||
String(formData.get("process_steps") ?? ""),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Client logos: her satıra bir URL
|
// Client logos: her satıra bir URL
|
||||||
const logosRaw = String(formData.get("client_logos") ?? "");
|
const logosRaw = raw(formData.get("client_logos"));
|
||||||
const logos = logosRaw
|
const logos = logosRaw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Hakkımızda values: blok '---' ile, ilk satır title, kalanı description
|
// 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
|
const aboutValues = aboutValuesRaw
|
||||||
.split("\n---\n")
|
.split("\n---\n")
|
||||||
.map((block) => {
|
.map((block) => {
|
||||||
@@ -377,7 +435,7 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
.filter((x): x is string => x !== null);
|
.filter((x): x is string => x !== null);
|
||||||
|
|
||||||
// Hakkımızda stats: 'value | label' satırlar
|
// 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
|
const aboutStats = aboutStatsRaw
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
@@ -387,6 +445,24 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
})
|
})
|
||||||
.filter((x): x is string => x !== null);
|
.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 = {
|
const data = {
|
||||||
hero_badge: str(formData.get("hero_badge")),
|
hero_badge: str(formData.get("hero_badge")),
|
||||||
hero_title: str(formData.get("hero_title")),
|
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_title: str(formData.get("services_title")),
|
||||||
services_description: str(formData.get("services_description")),
|
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_eyebrow: str(formData.get("projects_eyebrow")),
|
||||||
projects_title: str(formData.get("projects_title")),
|
projects_title: str(formData.get("projects_title")),
|
||||||
projects_description: str(formData.get("projects_description")),
|
projects_description: str(formData.get("projects_description")),
|
||||||
@@ -433,6 +513,10 @@ export async function saveSiteSettings(formData: FormData) {
|
|||||||
trust_items: trust.length > 0 ? trust : null,
|
trust_items: trust.length > 0 ? trust : null,
|
||||||
why_us: whyUs.length > 0 ? whyUs : null,
|
why_us: whyUs.length > 0 ? whyUs : null,
|
||||||
process_steps: processSteps.length > 0 ? processSteps : 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_title: str(formData.get("lead_form_title")),
|
||||||
lead_form_description: str(formData.get("lead_form_description")),
|
lead_form_description: str(formData.get("lead_form_description")),
|
||||||
google_review_url: str(formData.get("google_review_url")),
|
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");
|
if (!title) throw new Error("Başlık zorunlu");
|
||||||
const slug = str(formData.get("slug")) || slugify(title);
|
const slug = str(formData.get("slug")) || slugify(title);
|
||||||
|
|
||||||
const faqRaw = String(formData.get("faq") ?? "");
|
const faqRaw = raw(formData.get("faq"));
|
||||||
const faq = faqRaw
|
const faq = faqRaw
|
||||||
.split("\n---\n")
|
.split("\n---\n")
|
||||||
.map((block) => {
|
.map((block) => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const MEDIA_BUCKET_ID =
|
|||||||
export const TABLES = {
|
export const TABLES = {
|
||||||
contactMessages: "contact_messages",
|
contactMessages: "contact_messages",
|
||||||
services: "services",
|
services: "services",
|
||||||
|
solutions: "solutions",
|
||||||
projects: "projects",
|
projects: "projects",
|
||||||
blogPosts: "blog_posts",
|
blogPosts: "blog_posts",
|
||||||
testimonials: "testimonials",
|
testimonials: "testimonials",
|
||||||
|
|||||||
+17
@@ -7,6 +7,7 @@ import type {
|
|||||||
IndustryRow,
|
IndustryRow,
|
||||||
ProjectRow,
|
ProjectRow,
|
||||||
ServiceRow,
|
ServiceRow,
|
||||||
|
SolutionRow,
|
||||||
SeoPageRow,
|
SeoPageRow,
|
||||||
SeoSettingsRow,
|
SeoSettingsRow,
|
||||||
SiteSettingsRow,
|
SiteSettingsRow,
|
||||||
@@ -44,14 +45,22 @@ export async function listServices(opts?: { featured?: boolean }) {
|
|||||||
return safeList<ServiceRow>(TABLES.services, q);
|
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?: {
|
export async function listProjects(opts?: {
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
serviceSlug?: string;
|
serviceSlug?: string;
|
||||||
|
solutionSlug?: string;
|
||||||
}) {
|
}) {
|
||||||
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
|
const q = [Q.orderDesc("year"), Q.limit(opts?.limit ?? 50)];
|
||||||
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
if (opts?.featured) q.unshift(Q.equal("featured", true));
|
||||||
if (opts?.serviceSlug) q.unshift(Q.equal("service_slug", opts.serviceSlug));
|
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);
|
return safeList<ProjectRow>(TABLES.projects, q);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +72,14 @@ export async function getServiceBySlug(slug: string): Promise<ServiceRow | null>
|
|||||||
return res[0] ?? 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> {
|
export async function getProjectBySlug(slug: string): Promise<ProjectRow | null> {
|
||||||
const res = await safeList<ProjectRow>(TABLES.projects, [
|
const res = await safeList<ProjectRow>(TABLES.projects, [
|
||||||
Q.equal("slug", slug),
|
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: "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." },
|
{ 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;
|
} as const;
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ export interface ServiceRow extends AwRow {
|
|||||||
hero_image?: string | null;
|
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 {
|
export interface FaqItem {
|
||||||
q: string;
|
q: string;
|
||||||
a: string;
|
a: string;
|
||||||
@@ -36,6 +50,7 @@ export interface ProjectRow extends AwRow {
|
|||||||
industry?: string | null;
|
industry?: string | null;
|
||||||
duration?: string | null;
|
duration?: string | null;
|
||||||
service_slug?: string | null;
|
service_slug?: string | null;
|
||||||
|
solution_slug?: string | null;
|
||||||
metrics?: string[] | null; // JSON {"value":"+150%","label":"Trafik artışı"}
|
metrics?: string[] | null; // JSON {"value":"+150%","label":"Trafik artışı"}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +121,10 @@ export interface SiteSettingsRow extends AwRow {
|
|||||||
services_title?: string | null;
|
services_title?: string | null;
|
||||||
services_description?: string | null;
|
services_description?: string | null;
|
||||||
|
|
||||||
|
solutions_eyebrow?: string | null;
|
||||||
|
solutions_title?: string | null;
|
||||||
|
solutions_description?: string | null;
|
||||||
|
|
||||||
projects_eyebrow?: string | null;
|
projects_eyebrow?: string | null;
|
||||||
projects_title?: string | null;
|
projects_title?: string | null;
|
||||||
projects_description?: string | null;
|
projects_description?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user