feat: anasayfa içeriği, iletişim ve sosyal medya yönetilebilir

Yeni site_settings tablosu (singleton, rowId='homepage'):
- Hero: badge, title, subtitle, 2 CTA (label+href), stats (JSON array)
- Section başlıkları: services/projects/testimonials eyebrow + title + description
- Alt CTA: title, description, button label+href
- Contact: phone (görünen + tel: ham), email, address, hafta içi/sonu saatleri
- Social: linkedin/instagram/twitter/facebook URL'leri
- Footer tagline

Mevcut hardcoded değerler seed edildi.

Admin:
- /admin/site sayfası eklendi (sidebar'a 'Site Ayarları' linki)
- Bölümlü tek form: Hero / Hizmetler / Projeler / Referanslar / Alt CTA / İletişim / Sosyal / Footer
- Stats için 'değer | etiket' satır formatı

Public bağlantılar:
- Hero component artık settings prop alıyor (fallback değerlerle)
- Anasayfa: tüm section başlıkları ve alt CTA settings'ten geliyor
- Header: telefon settings'ten
- Footer: tagline, adres, telefon, email, sosyal linkler settings'ten
  (sosyal link sadece dolu olanlar gösteriliyor)
- Footer'da hizmetler artık /hizmetler/[slug] detay sayfalarına bağlı
- İletişim sayfası: adres, telefon, email, saatler settings'ten

30 route üretiliyor.
This commit is contained in:
Ege Can Komur
2026-05-20 02:56:45 +03:00
parent c0da5ae8d3
commit 1444aa3995
11 changed files with 621 additions and 91 deletions
+17 -8
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Mail, MapPin, Phone, Clock } from "lucide-react";
import { SectionTitle } from "@/components/section-title";
import { ContactForm } from "@/components/contact-form";
import { getSiteSettings } from "@/lib/data";
import { siteConfig } from "@/lib/site-config";
import { buildMetadata } from "@/lib/seo";
@@ -13,7 +14,15 @@ export async function generateMetadata(): Promise<Metadata> {
});
}
export default function ContactPage() {
export default async function ContactPage() {
const s = await getSiteSettings();
const address = s?.contact_address ?? siteConfig.contact.address;
const phone = s?.contact_phone ?? siteConfig.contact.phone;
const phoneRaw = s?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
const email = s?.contact_email ?? siteConfig.contact.email;
const weekday = s?.contact_hours_weekday ?? "Hafta içi 09:00 — 18:00";
const weekend = s?.contact_hours_weekend ?? "Cumartesi 10:00 — 14:00";
return (
<div className="mx-auto max-w-7xl px-6 py-20">
<SectionTitle
@@ -31,17 +40,17 @@ export default function ContactPage() {
<InfoCard
icon={<MapPin className="size-5" />}
title="Adres"
content={siteConfig.contact.address}
content={address}
/>
<InfoCard
icon={<Phone className="size-5" />}
title="Telefon"
content={
<a
href={`tel:${siteConfig.contact.phoneRaw}`}
href={`tel:${phoneRaw}`}
className="hover:text-[var(--navy)]"
>
{siteConfig.contact.phone}
{phone}
</a>
}
/>
@@ -50,10 +59,10 @@ export default function ContactPage() {
title="E-posta"
content={
<a
href={`mailto:${siteConfig.contact.email}`}
href={`mailto:${email}`}
className="hover:text-[var(--navy)]"
>
{siteConfig.contact.email}
{email}
</a>
}
/>
@@ -62,9 +71,9 @@ export default function ContactPage() {
title="Çalışma Saatleri"
content={
<>
Hafta içi 09:00 18:00
{weekday}
<br />
Cumartesi 10:00 14:00
{weekend}
</>
}
/>
+34 -17
View File
@@ -6,7 +6,12 @@ import { SectionTitle } from "@/components/section-title";
import { ServicesGrid } from "@/components/services-grid";
import { ProjectsGrid } from "@/components/projects-grid";
import { TestimonialsCarousel } from "@/components/testimonials";
import { listProjects, listServices, listTestimonials } from "@/lib/data";
import {
getSiteSettings,
listProjects,
listServices,
listTestimonials,
} from "@/lib/data";
import { buildMetadata } from "@/lib/seo";
export async function generateMetadata(): Promise<Metadata> {
@@ -14,22 +19,26 @@ export async function generateMetadata(): Promise<Metadata> {
}
export default async function Home() {
const [services, projects, testimonials] = await Promise.all([
const [services, projects, testimonials, settings] = await Promise.all([
listServices({ featured: true }),
listProjects({ featured: true, limit: 6 }),
listTestimonials({ featured: true }),
getSiteSettings(),
]);
return (
<>
<Hero />
<Hero settings={settings} />
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
<div className="mx-auto max-w-7xl px-6">
<SectionTitle
eyebrow="Ne yapıyoruz?"
title="Uçtan uca dijital çözümler"
description="Strateji, tasarım, geliştirme ve büyüme — tek bir ekip, tek bir vizyon."
eyebrow={settings?.services_eyebrow ?? "Ne yapıyoruz?"}
title={settings?.services_title ?? "Uçtan uca dijital çözümler"}
description={
settings?.services_description ??
"Strateji, tasarım, geliştirme ve büyüme — tek bir ekip, tek bir vizyon."
}
/>
<div className="mt-12">
<ServicesGrid services={services} />
@@ -42,9 +51,12 @@ export default async function Home() {
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-end">
<SectionTitle
align="left"
eyebrow="Çalışmalarımız"
title="Öne çıkan projeler"
description="Müşterilerimiz için tasarladığımız ve geliştirdiğimiz seçili işler."
eyebrow={settings?.projects_eyebrow ?? "Çalışmalarımız"}
title={settings?.projects_title ?? "Öne çıkan projeler"}
description={
settings?.projects_description ??
"Müşterilerimiz için tasarladığımız ve geliştirdiğimiz seçili işler."
}
/>
<Link
href="/projeler"
@@ -63,9 +75,14 @@ export default async function Home() {
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
<div className="mx-auto max-w-7xl px-6">
<SectionTitle
eyebrow="Referanslar"
title="Müşterilerimiz ne diyor?"
description="Birlikte çalıştığımız markalardan geri bildirimler."
eyebrow={settings?.testimonials_eyebrow ?? "Referanslar"}
title={
settings?.testimonials_title ?? "Müşterilerimiz ne diyor?"
}
description={
settings?.testimonials_description ??
"Birlikte çalıştığımız markalardan geri bildirimler."
}
/>
<div className="mt-12">
<TestimonialsCarousel items={testimonials} />
@@ -78,17 +95,17 @@ export default async function Home() {
<div className="absolute -left-20 top-0 size-96 rounded-full bg-[var(--sky)]/20 blur-3xl" aria-hidden />
<div className="relative mx-auto max-w-4xl px-6 text-center">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
Projenizi konuşalım
{settings?.cta_title ?? "Projenizi konuşalım"}
</h2>
<p className="mx-auto mt-4 max-w-xl text-white/70">
İhtiyacınızı dinleyip size en uygun çözümü öneren bir ekip arıyorsanız,
ilk görüşme bizden.
{settings?.cta_description ??
"İhtiyacınızı dinleyip size en uygun çözümü öneren bir ekip arıyorsanız, ilk görüşme bizden."}
</p>
<Link
href="/iletisim"
href={settings?.cta_button_href ?? "/iletisim"}
className="mt-8 inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 text-sm font-medium text-[var(--navy)] transition hover:bg-[var(--sky-50)]"
>
Ücretsiz keşif görüşmesi
{settings?.cta_button_label ?? "Ücretsiz keşif görüşmesi"}
<ArrowRight className="size-4" />
</Link>
</div>
+308
View File
@@ -0,0 +1,308 @@
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 { StatItem } 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 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>
</div>
<FormActions>
<PrimaryButton>
<Save className="size-4" /> Tüm değişiklikleri kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}