feat: Çözümler bölümü + mobil menü; admin parser düzeltmeleri

- Çözümler: solutions tablosu, /cozumler liste + detay sayfası, anasayfa
  bölümü, tam admin CRUD (/admin/cozumler), header & footer linkleri,
  projelerde solution_slug ilişkisi, services-grid genelleştirildi
- Mobil menü (hamburger drawer) eklendi — header artık < lg'de gezilebilir
- Site ayarları parser: textarea CRLF (\r\n) normalizasyonu — neden biz,
  süreç adımları, değerler ve SSS blokları artık doğru parçalanıyor
- homepage_faq + garanti (title/description/items) saveSiteSettings'e
  bağlandı (daha önce hiç kaydedilmiyordu)
This commit is contained in:
egecankomur
2026-06-02 18:21:58 +03:00
parent f49df9cbeb
commit 2e001680bf
21 changed files with 1191 additions and 27 deletions
+2
View File
@@ -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 },
+5
View File
@@ -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
View File
@@ -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>
+168
View File
@@ -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>
);
}
+12 -4
View File
@@ -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"
>
+209
View File
@@ -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>
);
}
+145
View File
@@ -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>
);
}