Files
kovakyazilim/app/admin/(protected)/site/page.tsx
T
Ege Can Komur aa2b7280b6 feat: TR reklam trafiği için anasayfa CRO optimizasyonu
Yeni bölümler ve component'ler:
- WhatsAppFloat: sağ altta her sayfada görünen 'pulse' animasyonlu WhatsApp butonu
- MobileCtaBar: mobilde alt sabit bar — Ara / WhatsApp / Teklif Al üç buton
- TrustBand: hero altı 4 trust kartı (Google ★, proje sayısı, dönüş süresi, garanti)
  + Google rating + yorum sayısı satırı
- LogoCloud: müşteri logoları grayscale strip
- QuickLeadForm: ad + telefon iki alanlı inline mini form (anasayfada)
  - app/actions submitContact 'source' alanını destekliyor (quick lead → message zorunlu değil)
- WhyUs: 4 USP kartı (Hızlı teslim, Yerel destek, Modern tech, Satış sonrası)
- ProcessSteps: 4 adımlı 'nasıl çalışıyoruz' süreç akışı (numaralı timeline)

Schema (JSON-LD):
- OrganizationLd: LocalBusiness + Address + AggregateRating (Google review puanı/sayısı)
- ServiceLd: hizmet detay sayfaları için
- FaqLd: hizmet FAQ'leri için
- BreadcrumbLd, ArticleLd: hazır
Anasayfaya OrganizationLd ekli — Google Ads quality score + organic rich results.

Performans:
- REST GET çağrıları cache:'no-store' yerine next.revalidate=60 (ISR)
- Public sayfalar artık static rendering — LCP düşer
- Mutations ve session GET'ler hâlâ no-store

site_settings yeni alanları (panelden yönetilebilir):
- whatsapp_message (default WhatsApp opener)
- client_logos[] (logo URL listesi)
- trust_items[] (JSON: icon|value|label)
- why_us[] (JSON: icon, title, description)
- process_steps[] (JSON: title, description)
- lead_form_title, lead_form_description
- google_rating, google_review_count, google_review_url

Admin /admin/site formuna yeni 'Conversion / reklam optimizasyonu',
'Neden Biz?' ve 'Süreç adımları' bölümleri eklendi.

Mevcut anasayfa yapısı (üstten alta):
1. Hero
2. TrustBand (mini güven sinyalleri)
3. LogoCloud (varsa müşteri logoları)
4. Hızlı iletişim + QuickLeadForm (2 sütun: tel/WA CTA + mini form)
5. Hizmetler
6. WhyUs (Neden Biz?)
7. ProcessSteps (Nasıl çalışıyoruz?)
8. Projeler
9. Testimonials
10. CTA (Final + WhatsApp)
2026-05-20 03:08:05 +03:00

472 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from "next";
import { Save } from "lucide-react";
import {
Field,
FormActions,
FormShell,
PageHeader,
PrimaryButton,
Textarea,
} from "@/components/admin/form";
import { getSiteSettings } from "@/lib/data";
import { saveSiteSettings } from "@/lib/admin-actions";
import type {
ProcessStep,
StatItem,
TrustItem,
WhyUsItem,
} from "@/lib/types";
export const metadata: Metadata = { title: "Site Ayarları" };
function statsToText(items?: string[] | null): string {
if (!items) return "";
const parsed: StatItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<StatItem>;
if (obj.value && obj.label) parsed.push({ value: obj.value, label: obj.label });
} catch {
/* ignore */
}
}
return parsed.map((s) => `${s.value} | ${s.label}`).join("\n");
}
function trustToText(items?: string[] | null): string {
if (!items) return "";
const parsed: TrustItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<TrustItem>;
if (obj.value && obj.label)
parsed.push({
icon: obj.icon ?? "Sparkles",
value: obj.value,
label: obj.label,
});
} catch {
/* ignore */
}
}
return parsed.map((t) => `${t.icon} | ${t.value} | ${t.label}`).join("\n");
}
function whyUsToText(items?: string[] | null): string {
if (!items) return "";
const parsed: WhyUsItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<WhyUsItem>;
if (obj.title && obj.description)
parsed.push({
icon: obj.icon ?? "Sparkles",
title: obj.title,
description: obj.description,
});
} catch {
/* ignore */
}
}
return parsed
.map((w) => `${w.icon} | ${w.title}\n${w.description}`)
.join("\n---\n");
}
function processToText(items?: string[] | null): string {
if (!items) return "";
const parsed: ProcessStep[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<ProcessStep>;
if (obj.title && obj.description)
parsed.push({ title: obj.title, description: obj.description });
} catch {
/* ignore */
}
}
return parsed.map((p) => `${p.title}\n${p.description}`).join("\n---\n");
}
function Section({
title,
description,
children,
}: {
title: string;
description?: string;
children: React.ReactNode;
}) {
return (
<section className="border-t border-[var(--border)] pt-8 first:border-0 first:pt-0">
<h2 className="text-base font-semibold text-[var(--navy)]">{title}</h2>
{description && (
<p className="mt-1 text-xs text-[var(--muted)]">{description}</p>
)}
<div className="mt-5 space-y-5">{children}</div>
</section>
);
}
export default async function SiteSettingsPage() {
const s = await getSiteSettings();
return (
<div>
<PageHeader
title="Site Ayarları"
description="Anasayfa içeriği, iletişim bilgileri ve sosyal medya — tek yerden yönet."
/>
<form action={saveSiteSettings}>
<FormShell>
<div className="space-y-10">
<Section
title="Hero (anasayfa üst alan)"
description="İlk açılışta görünen ana başlık ve buton metinleri."
>
<Field
label="Rozet metni"
name="hero_badge"
defaultValue={s?.hero_badge}
placeholder="Kocaeli'nin teknoloji ajansı"
/>
<Field
label="Ana başlık"
name="hero_title"
defaultValue={s?.hero_title}
/>
<Textarea
label="Alt metin"
name="hero_subtitle"
rows={2}
defaultValue={s?.hero_subtitle}
/>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Birincil buton metni"
name="hero_cta_primary_label"
defaultValue={s?.hero_cta_primary_label}
/>
<Field
label="Birincil buton URL"
name="hero_cta_primary_href"
defaultValue={s?.hero_cta_primary_href}
placeholder="/iletisim"
/>
<Field
label="İkincil buton metni"
name="hero_cta_secondary_label"
defaultValue={s?.hero_cta_secondary_label}
/>
<Field
label="İkincil buton URL"
name="hero_cta_secondary_href"
defaultValue={s?.hero_cta_secondary_href}
placeholder="/hizmetler"
/>
</div>
<Textarea
label="İstatistikler"
name="hero_stats"
rows={3}
defaultValue={statsToText(s?.hero_stats)}
placeholder={"50+ | Tamamlanan proje\n10+ | Yıllık deneyim\n24/7 | Teknik destek"}
help='Her satıra "değer | etiket" formatında.'
/>
</Section>
<Section
title="Hizmetler bölümü başlığı"
description="Anasayfadaki hizmet kartlarının üstündeki yazı."
>
<div className="grid gap-5 md:grid-cols-3">
<Field
label="Eyebrow"
name="services_eyebrow"
defaultValue={s?.services_eyebrow}
/>
<Field
label="Başlık"
name="services_title"
defaultValue={s?.services_title}
/>
<Field
label="Açıklama"
name="services_description"
defaultValue={s?.services_description}
/>
</div>
</Section>
<Section
title="Projeler bölümü başlığı"
description="Anasayfadaki proje kartlarının üstündeki yazı."
>
<div className="grid gap-5 md:grid-cols-3">
<Field
label="Eyebrow"
name="projects_eyebrow"
defaultValue={s?.projects_eyebrow}
/>
<Field
label="Başlık"
name="projects_title"
defaultValue={s?.projects_title}
/>
<Field
label="Açıklama"
name="projects_description"
defaultValue={s?.projects_description}
/>
</div>
</Section>
<Section
title="Referanslar bölümü başlığı"
description="Anasayfadaki yorumların üstündeki yazı."
>
<div className="grid gap-5 md:grid-cols-3">
<Field
label="Eyebrow"
name="testimonials_eyebrow"
defaultValue={s?.testimonials_eyebrow}
/>
<Field
label="Başlık"
name="testimonials_title"
defaultValue={s?.testimonials_title}
/>
<Field
label="Açıklama"
name="testimonials_description"
defaultValue={s?.testimonials_description}
/>
</div>
</Section>
<Section
title="Alt CTA bölümü"
description="Anasayfanın altındaki büyük çağrı bölümü."
>
<Field
label="Başlık"
name="cta_title"
defaultValue={s?.cta_title}
/>
<Textarea
label="Açıklama"
name="cta_description"
rows={2}
defaultValue={s?.cta_description}
/>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Buton metni"
name="cta_button_label"
defaultValue={s?.cta_button_label}
/>
<Field
label="Buton URL"
name="cta_button_href"
defaultValue={s?.cta_button_href}
/>
</div>
</Section>
<Section
title="İletişim bilgileri"
description="Footer ve iletişim sayfasında gösterilir."
>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Telefon (görünen)"
name="contact_phone"
defaultValue={s?.contact_phone}
placeholder="+90 551 590 29 35"
/>
<Field
label="Telefon (ham — tel: linki için)"
name="contact_phone_raw"
defaultValue={s?.contact_phone_raw}
placeholder="+905515902935"
/>
<Field
label="E-posta"
name="contact_email"
type="email"
defaultValue={s?.contact_email}
/>
<Field
label="Çalışma saatleri (hafta içi)"
name="contact_hours_weekday"
defaultValue={s?.contact_hours_weekday}
/>
<Field
label="Çalışma saatleri (hafta sonu)"
name="contact_hours_weekend"
defaultValue={s?.contact_hours_weekend}
/>
</div>
<Textarea
label="Adres"
name="contact_address"
rows={2}
defaultValue={s?.contact_address}
/>
</Section>
<Section title="Sosyal medya">
<div className="grid gap-5 md:grid-cols-2">
<Field
label="LinkedIn URL"
name="social_linkedin"
type="url"
defaultValue={s?.social_linkedin}
/>
<Field
label="Instagram URL"
name="social_instagram"
type="url"
defaultValue={s?.social_instagram}
/>
<Field
label="Twitter / X URL"
name="social_twitter"
type="url"
defaultValue={s?.social_twitter}
/>
<Field
label="Facebook URL"
name="social_facebook"
type="url"
defaultValue={s?.social_facebook}
/>
</div>
</Section>
<Section title="Footer">
<Textarea
label="Footer tagline"
name="footer_tagline"
rows={2}
defaultValue={s?.footer_tagline}
help="Logo altındaki kısa açıklama metni."
/>
</Section>
<Section
title="Conversion / reklam optimizasyonu"
description="Trust bandı, mini lead form ve WhatsApp metni."
>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Lead form başlığı"
name="lead_form_title"
defaultValue={s?.lead_form_title}
placeholder="Ücretsiz teklif alın"
/>
<Field
label="Lead form açıklaması"
name="lead_form_description"
defaultValue={s?.lead_form_description}
/>
</div>
<Textarea
label="WhatsApp varsayılan mesajı"
name="whatsapp_message"
rows={2}
defaultValue={s?.whatsapp_message}
placeholder="Merhaba, web siteniz üzerinden ulaşıyorum…"
help="Kullanıcı WhatsApp butonuna tıkladığında otomatik açılan mesaj."
/>
<Textarea
label="Trust bandı (hero altı 4 kart)"
name="trust_items"
rows={4}
defaultValue={trustToText(s?.trust_items)}
placeholder={
"Star | 4.9 | Google yıldızı\nBriefcase | 50+ | Tamamlanan proje\nClock | 24 saat | İçinde dönüş\nShield | 100% | Memnuniyet garantisi"
}
help='Her satır: "İkonAdı | Değer | Etiket" (örn. Star | 4.9 | Google yıldızı)'
/>
<Textarea
label="Müşteri logoları"
name="client_logos"
rows={4}
defaultValue={s?.client_logos?.join("\n")}
placeholder={"https://example.com/logo1.png\nhttps://example.com/logo2.png"}
help="Her satıra bir URL. Logoların grayscale + opaque versiyonu gösterilir."
/>
<div className="grid gap-5 md:grid-cols-3">
<Field
label="Google Rating (0-5)"
name="google_rating"
type="number"
defaultValue={s?.google_rating ?? ""}
placeholder="4.9"
/>
<Field
label="Yorum sayısı"
name="google_review_count"
type="number"
defaultValue={s?.google_review_count ?? ""}
placeholder="47"
/>
<Field
label="Google review URL"
name="google_review_url"
type="url"
defaultValue={s?.google_review_url}
placeholder="https://g.page/r/..."
/>
</div>
</Section>
<Section
title="Neden Biz? kartları"
description="Anasayfada görünen 4 USP kartı."
>
<Textarea
label="Neden Biz?"
name="why_us"
rows={12}
defaultValue={whyUsToText(s?.why_us)}
placeholder={
"Zap | Hızlı teslim\n2-3 hafta içinde…\n---\nAward | Yerel destek\nKocaeli ofisimizde…"
}
help='Her blok "---" ile ayrılır. İlk satır: "İkon | Başlık". Sonraki satırlar: açıklama.'
/>
</Section>
<Section
title="Nasıl çalışıyoruz? adımları"
description="4 adımlı süreç akışı."
>
<Textarea
label="Süreç adımları"
name="process_steps"
rows={10}
defaultValue={processToText(s?.process_steps)}
placeholder={
"Ücretsiz keşif görüşmesi\n30 dakika dinleme.\n---\nTeklif ve plan\nYazılı teklif sunuyoruz."
}
help='Her blok "---" ile ayrılır. İlk satır başlık, sonrası açıklama.'
/>
</Section>
</div>
<FormActions>
<PrimaryButton>
<Save className="size-4" /> Tüm değişiklikleri kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}