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,
|
||||
listProjects,
|
||||
listServices,
|
||||
listSolutions,
|
||||
listTestimonials,
|
||||
} from "@/lib/data";
|
||||
import { buildMetadata } from "@/lib/seo";
|
||||
@@ -28,8 +29,10 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
}
|
||||
|
||||
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 }),
|
||||
listSolutions({ featured: true }),
|
||||
listProjects({ featured: true, limit: 6 }),
|
||||
listTestimonials({ featured: true }),
|
||||
getSiteSettings(),
|
||||
@@ -118,6 +121,35 @@ export default async function Home() {
|
||||
</div>
|
||||
</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} />
|
||||
|
||||
<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 { RichEditor } from "@/components/admin/rich-editor";
|
||||
import { saveProject } from "@/lib/admin-actions";
|
||||
import { listServices } from "@/lib/data";
|
||||
import { listServices, listSolutions } from "@/lib/data";
|
||||
import type { ProjectRow } from "@/lib/types";
|
||||
|
||||
export async function ProjectForm({ project }: { project?: ProjectRow }) {
|
||||
const services = await listServices();
|
||||
const [services, solutions] = await Promise.all([
|
||||
listServices(),
|
||||
listSolutions(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<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.
|
||||
</span>
|
||||
</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="Sektör" name="industry" defaultValue={project?.industry} />
|
||||
<Field
|
||||
|
||||
@@ -231,6 +231,29 @@ export default async function SiteSettingsPage() {
|
||||
</div>
|
||||
</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
|
||||
title="Projeler bölümü başlığı"
|
||||
description="Anasayfadaki proje kartlarının üstündeki yazı."
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Settings,
|
||||
Newspaper,
|
||||
Layers,
|
||||
Boxes,
|
||||
Briefcase,
|
||||
MessageSquareQuote,
|
||||
Search,
|
||||
@@ -25,6 +26,7 @@ const items: Item[] = [
|
||||
{ href: "/admin/site", label: "Site Ayarları", icon: Settings },
|
||||
{ href: "/admin/blog", label: "Blog", icon: Newspaper },
|
||||
{ href: "/admin/hizmetler", label: "Hizmetler", icon: Layers },
|
||||
{ href: "/admin/cozumler", label: "Çözümler", icon: Boxes },
|
||||
{ href: "/admin/projeler", label: "Projeler", icon: Briefcase },
|
||||
{ href: "/admin/sektorler", label: "Sektörler", icon: Building2 },
|
||||
{ href: "/admin/ekip", label: "Ekip", icon: UsersIcon },
|
||||
|
||||
@@ -57,6 +57,11 @@ export async function Footer() {
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li className="pt-1">
|
||||
<Link href="/cozumler" className="font-medium text-white/90 hover:text-white">
|
||||
Çözümler →
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
+15
-1
@@ -4,6 +4,7 @@ import { ChevronDown, Phone } from "lucide-react";
|
||||
import { getSiteSettings, listServices } from "@/lib/data";
|
||||
import { siteConfig } from "@/lib/site-config";
|
||||
import { HeaderScrollEffect } from "@/components/header-scroll";
|
||||
import { MobileMenu } from "@/components/mobile-menu";
|
||||
|
||||
export async function Header() {
|
||||
const [settings, services] = await Promise.all([
|
||||
@@ -111,6 +112,12 @@ export async function Header() {
|
||||
</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
|
||||
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"
|
||||
@@ -162,10 +169,17 @@ export async function Header() {
|
||||
</a>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
{/* Mobil menü (hamburger) — sadece < lg */}
|
||||
<MobileMenu
|
||||
services={services.map((s) => ({ slug: s.slug, title: s.title }))}
|
||||
phone={phone}
|
||||
phoneRaw={phoneRaw}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
</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 { Icon } from "@/components/icon";
|
||||
import { siteConfig } from "@/lib/site-config";
|
||||
import type { ServiceRow } from "@/lib/types";
|
||||
|
||||
type ServiceLike = {
|
||||
slug: string;
|
||||
@@ -11,18 +10,27 @@ type ServiceLike = {
|
||||
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[] =
|
||||
services.length > 0
|
||||
? services
|
||||
: (siteConfig.fallbackServices as readonly ServiceLike[]).slice();
|
||||
: ((fallback ?? siteConfig.fallbackServices) as readonly ServiceLike[]).slice();
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((s) => (
|
||||
<Link
|
||||
key={s.slug}
|
||||
href={`/hizmetler/${s.slug}`}
|
||||
href={`${basePath}/${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"
|
||||
>
|
||||
|
||||
@@ -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())
|
||||
.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