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:
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Mail, MapPin, Phone, Clock } from "lucide-react";
|
import { Mail, MapPin, Phone, Clock } from "lucide-react";
|
||||||
import { SectionTitle } from "@/components/section-title";
|
import { SectionTitle } from "@/components/section-title";
|
||||||
import { ContactForm } from "@/components/contact-form";
|
import { ContactForm } from "@/components/contact-form";
|
||||||
|
import { getSiteSettings } from "@/lib/data";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
import { siteConfig } from "@/lib/site-config";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
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 (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-20">
|
<div className="mx-auto max-w-7xl px-6 py-20">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
@@ -31,17 +40,17 @@ export default function ContactPage() {
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<MapPin className="size-5" />}
|
icon={<MapPin className="size-5" />}
|
||||||
title="Adres"
|
title="Adres"
|
||||||
content={siteConfig.contact.address}
|
content={address}
|
||||||
/>
|
/>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
icon={<Phone className="size-5" />}
|
icon={<Phone className="size-5" />}
|
||||||
title="Telefon"
|
title="Telefon"
|
||||||
content={
|
content={
|
||||||
<a
|
<a
|
||||||
href={`tel:${siteConfig.contact.phoneRaw}`}
|
href={`tel:${phoneRaw}`}
|
||||||
className="hover:text-[var(--navy)]"
|
className="hover:text-[var(--navy)]"
|
||||||
>
|
>
|
||||||
{siteConfig.contact.phone}
|
{phone}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -50,10 +59,10 @@ export default function ContactPage() {
|
|||||||
title="E-posta"
|
title="E-posta"
|
||||||
content={
|
content={
|
||||||
<a
|
<a
|
||||||
href={`mailto:${siteConfig.contact.email}`}
|
href={`mailto:${email}`}
|
||||||
className="hover:text-[var(--navy)]"
|
className="hover:text-[var(--navy)]"
|
||||||
>
|
>
|
||||||
{siteConfig.contact.email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -62,9 +71,9 @@ export default function ContactPage() {
|
|||||||
title="Çalışma Saatleri"
|
title="Çalışma Saatleri"
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
Hafta içi 09:00 — 18:00
|
{weekday}
|
||||||
<br />
|
<br />
|
||||||
Cumartesi 10:00 — 14:00
|
{weekend}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+34
-17
@@ -6,7 +6,12 @@ import { SectionTitle } from "@/components/section-title";
|
|||||||
import { ServicesGrid } from "@/components/services-grid";
|
import { ServicesGrid } from "@/components/services-grid";
|
||||||
import { ProjectsGrid } from "@/components/projects-grid";
|
import { ProjectsGrid } from "@/components/projects-grid";
|
||||||
import { TestimonialsCarousel } from "@/components/testimonials";
|
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";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
@@ -14,22 +19,26 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const [services, projects, testimonials] = await Promise.all([
|
const [services, projects, testimonials, settings] = await Promise.all([
|
||||||
listServices({ featured: true }),
|
listServices({ featured: true }),
|
||||||
listProjects({ featured: true, limit: 6 }),
|
listProjects({ featured: true, limit: 6 }),
|
||||||
listTestimonials({ featured: true }),
|
listTestimonials({ featured: true }),
|
||||||
|
getSiteSettings(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero />
|
<Hero settings={settings} />
|
||||||
|
|
||||||
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
eyebrow="Ne yapıyoruz?"
|
eyebrow={settings?.services_eyebrow ?? "Ne yapıyoruz?"}
|
||||||
title="Uçtan uca dijital çözümler"
|
title={settings?.services_title ?? "Uçtan uca dijital çözümler"}
|
||||||
description="Strateji, tasarım, geliştirme ve büyüme — tek bir ekip, tek bir vizyon."
|
description={
|
||||||
|
settings?.services_description ??
|
||||||
|
"Strateji, tasarım, geliştirme ve büyüme — tek bir ekip, tek bir vizyon."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<ServicesGrid services={services} />
|
<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">
|
<div className="flex flex-col items-start justify-between gap-4 md:flex-row md:items-end">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
align="left"
|
align="left"
|
||||||
eyebrow="Çalışmalarımız"
|
eyebrow={settings?.projects_eyebrow ?? "Çalışmalarımız"}
|
||||||
title="Öne çıkan projeler"
|
title={settings?.projects_title ?? "Öne çıkan projeler"}
|
||||||
description="Müşterilerimiz için tasarladığımız ve geliştirdiğimiz seçili işler."
|
description={
|
||||||
|
settings?.projects_description ??
|
||||||
|
"Müşterilerimiz için tasarladığımız ve geliştirdiğimiz seçili işler."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
href="/projeler"
|
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">
|
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
eyebrow="Referanslar"
|
eyebrow={settings?.testimonials_eyebrow ?? "Referanslar"}
|
||||||
title="Müşterilerimiz ne diyor?"
|
title={
|
||||||
description="Birlikte çalıştığımız markalardan geri bildirimler."
|
settings?.testimonials_title ?? "Müşterilerimiz ne diyor?"
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
settings?.testimonials_description ??
|
||||||
|
"Birlikte çalıştığımız markalardan geri bildirimler."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<TestimonialsCarousel items={testimonials} />
|
<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="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">
|
<div className="relative mx-auto max-w-4xl px-6 text-center">
|
||||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||||
Projenizi konuşalım
|
{settings?.cta_title ?? "Projenizi konuşalım"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mx-auto mt-4 max-w-xl text-white/70">
|
<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,
|
{settings?.cta_description ??
|
||||||
ilk görüşme bizden.
|
"İhtiyacınızı dinleyip size en uygun çözümü öneren bir ekip arıyorsanız, ilk görüşme bizden."}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<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)]"
|
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" />
|
<ArrowRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import Link from "next/link";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
|
Settings,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
Layers,
|
Layers,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
@@ -19,6 +20,7 @@ type Item = { href: string; label: string; icon: LucideIcon };
|
|||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{ href: "/admin", label: "Pano", icon: LayoutDashboard },
|
{ href: "/admin", label: "Pano", icon: LayoutDashboard },
|
||||||
|
{ href: "/admin/site", label: "Site Ayarları", icon: Settings },
|
||||||
{ href: "/admin/blog", label: "Blog", icon: Newspaper },
|
{ href: "/admin/blog", label: "Blog", icon: Newspaper },
|
||||||
{ href: "/admin/hizmetler", label: "Hizmetler", icon: Layers },
|
{ href: "/admin/hizmetler", label: "Hizmetler", icon: Layers },
|
||||||
{ href: "/admin/projeler", label: "Projeler", icon: Briefcase },
|
{ href: "/admin/projeler", label: "Projeler", icon: Briefcase },
|
||||||
|
|||||||
+46
-30
@@ -1,10 +1,33 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
|
||||||
import { Mail, MapPin, Phone } from "lucide-react";
|
import { Mail, MapPin, Phone } from "lucide-react";
|
||||||
import { LinkedinIcon, InstagramIcon, TwitterIcon } from "@/components/social-icons";
|
import { LinkedinIcon, InstagramIcon, TwitterIcon } from "@/components/social-icons";
|
||||||
|
import { getSiteSettings, listServices } from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
|
||||||
|
export async function Footer() {
|
||||||
|
const [settings, services] = await Promise.all([
|
||||||
|
getSiteSettings(),
|
||||||
|
listServices(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tagline = settings?.footer_tagline ?? siteConfig.tagline;
|
||||||
|
const address = settings?.contact_address ?? siteConfig.contact.address;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const email = settings?.contact_email ?? siteConfig.contact.email;
|
||||||
|
|
||||||
|
const socials = [
|
||||||
|
{ url: settings?.social_linkedin, label: "LinkedIn", Icon: LinkedinIcon },
|
||||||
|
{ url: settings?.social_instagram, label: "Instagram", Icon: InstagramIcon },
|
||||||
|
{ url: settings?.social_twitter, label: "Twitter / X", Icon: TwitterIcon },
|
||||||
|
].filter((s) => s.url);
|
||||||
|
|
||||||
|
const footerServices =
|
||||||
|
services.length > 0
|
||||||
|
? services.slice(0, 5)
|
||||||
|
: siteConfig.fallbackServices.slice(0, 5);
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
return (
|
||||||
<footer className="mt-24 border-t border-[var(--border)] bg-[var(--navy)] text-white">
|
<footer className="mt-24 border-t border-[var(--border)] bg-[var(--navy)] text-white">
|
||||||
<div className="mx-auto grid max-w-7xl gap-10 px-6 py-14 md:grid-cols-4">
|
<div className="mx-auto grid max-w-7xl gap-10 px-6 py-14 md:grid-cols-4">
|
||||||
@@ -19,9 +42,7 @@ export function Footer() {
|
|||||||
/>
|
/>
|
||||||
<span className="text-lg font-semibold">{siteConfig.name}</span>
|
<span className="text-lg font-semibold">{siteConfig.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-white/70">
|
<p className="mt-4 text-sm leading-relaxed text-white/70">{tagline}</p>
|
||||||
{siteConfig.tagline}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -29,9 +50,9 @@ export function Footer() {
|
|||||||
Hizmetler
|
Hizmetler
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="mt-4 space-y-2 text-sm text-white/70">
|
<ul className="mt-4 space-y-2 text-sm text-white/70">
|
||||||
{siteConfig.fallbackServices.slice(0, 5).map((s) => (
|
{footerServices.map((s) => (
|
||||||
<li key={s.slug}>
|
<li key={s.slug}>
|
||||||
<Link href={`/hizmetler#${s.slug}`} className="hover:text-white">
|
<Link href={`/hizmetler/${s.slug}`} className="hover:text-white">
|
||||||
{s.title}
|
{s.title}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -46,56 +67,51 @@ export function Footer() {
|
|||||||
<ul className="mt-4 space-y-3 text-sm text-white/70">
|
<ul className="mt-4 space-y-3 text-sm text-white/70">
|
||||||
<li className="flex items-start gap-2">
|
<li className="flex items-start gap-2">
|
||||||
<MapPin className="mt-0.5 size-4 shrink-0" />
|
<MapPin className="mt-0.5 size-4 shrink-0" />
|
||||||
<span>{siteConfig.contact.address}</span>
|
<span>{address}</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<Phone className="size-4 shrink-0" />
|
<Phone className="size-4 shrink-0" />
|
||||||
<a href={`tel:${siteConfig.contact.phoneRaw}`} className="hover:text-white">
|
<a href={`tel:${phoneRaw}`} className="hover:text-white">
|
||||||
{siteConfig.contact.phone}
|
{phone}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-2">
|
<li className="flex items-center gap-2">
|
||||||
<Mail className="size-4 shrink-0" />
|
<Mail className="size-4 shrink-0" />
|
||||||
<a href={`mailto:${siteConfig.contact.email}`} className="hover:text-white">
|
<a href={`mailto:${email}`} className="hover:text-white">
|
||||||
{siteConfig.contact.email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{socials.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-white/80">
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-white/80">
|
||||||
Sosyal Medya
|
Sosyal Medya
|
||||||
</h3>
|
</h3>
|
||||||
<div className="mt-4 flex gap-3">
|
<div className="mt-4 flex gap-3">
|
||||||
|
{socials.map(({ url, label, Icon }) => (
|
||||||
<a
|
<a
|
||||||
href={siteConfig.social.linkedin}
|
key={label}
|
||||||
aria-label="LinkedIn"
|
href={url as string}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label={label}
|
||||||
className="flex size-9 items-center justify-center rounded-full bg-white/10 transition hover:bg-white/20"
|
className="flex size-9 items-center justify-center rounded-full bg-white/10 transition hover:bg-white/20"
|
||||||
>
|
>
|
||||||
<LinkedinIcon className="size-4" />
|
<Icon className="size-4" />
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={siteConfig.social.instagram}
|
|
||||||
aria-label="Instagram"
|
|
||||||
className="flex size-9 items-center justify-center rounded-full bg-white/10 transition hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<InstagramIcon className="size-4" />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={siteConfig.social.twitter}
|
|
||||||
aria-label="Twitter"
|
|
||||||
className="flex size-9 items-center justify-center rounded-full bg-white/10 transition hover:bg-white/20"
|
|
||||||
>
|
|
||||||
<TwitterIcon className="size-4" />
|
|
||||||
</a>
|
</a>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-white/10">
|
<div className="border-t border-white/10">
|
||||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-2 px-6 py-5 text-xs text-white/60 md:flex-row">
|
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-2 px-6 py-5 text-xs text-white/60 md:flex-row">
|
||||||
<p>© {new Date().getFullYear()} {siteConfig.name}. Tüm hakları saklıdır.</p>
|
<p>
|
||||||
|
© {new Date().getFullYear()} {siteConfig.name}. Tüm hakları saklıdır.
|
||||||
|
</p>
|
||||||
<p>Kocaeli, Türkiye</p>
|
<p>Kocaeli, Türkiye</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
|
||||||
import { Phone } from "lucide-react";
|
import { Phone } from "lucide-react";
|
||||||
|
import { getSiteSettings } from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ href: "/", label: "Anasayfa" },
|
{ href: "/", label: "Anasayfa" },
|
||||||
@@ -12,7 +13,11 @@ const nav = [
|
|||||||
{ href: "/iletisim", label: "İletişim" },
|
{ href: "/iletisim", label: "İletişim" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Header() {
|
export async function Header() {
|
||||||
|
const settings = await getSiteSettings();
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-[var(--border)] bg-white/90 backdrop-blur">
|
<header className="sticky top-0 z-40 border-b border-[var(--border)] bg-white/90 backdrop-blur">
|
||||||
<div className="mx-auto flex max-w-7xl items-center justify-between gap-6 px-6 py-3">
|
<div className="mx-auto flex max-w-7xl items-center justify-between gap-6 px-6 py-3">
|
||||||
@@ -36,11 +41,11 @@ export function Header() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`tel:${siteConfig.contact.phoneRaw}`}
|
href={`tel:${phoneRaw}`}
|
||||||
className="hidden items-center gap-2 rounded-full bg-[var(--navy)] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[var(--navy-700)] sm:inline-flex"
|
className="hidden items-center gap-2 rounded-full bg-[var(--navy)] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[var(--navy-700)] sm:inline-flex"
|
||||||
>
|
>
|
||||||
<Phone className="size-4" />
|
<Phone className="size-4" />
|
||||||
{siteConfig.contact.phone}
|
{phone}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+49
-16
@@ -1,8 +1,43 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, Sparkles } from "lucide-react";
|
import { ArrowRight, Sparkles } from "lucide-react";
|
||||||
|
import type { SiteSettingsRow, StatItem } from "@/lib/types";
|
||||||
|
|
||||||
|
const DEFAULT_STATS: StatItem[] = [
|
||||||
|
{ value: "50+", label: "Tamamlanan proje" },
|
||||||
|
{ value: "10+", label: "Yıllık deneyim" },
|
||||||
|
{ value: "24/7", label: "Teknik destek" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseStats(items?: string[] | null): StatItem[] {
|
||||||
|
if (!items || items.length === 0) return DEFAULT_STATS;
|
||||||
|
const out: StatItem[] = [];
|
||||||
|
for (const raw of items) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(raw) as Partial<StatItem>;
|
||||||
|
if (obj.value && obj.label) out.push({ value: obj.value, label: obj.label });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.length > 0 ? out : DEFAULT_STATS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hero({ settings }: { settings?: SiteSettingsRow | null }) {
|
||||||
|
const badge = settings?.hero_badge ?? "Kocaeli'nin teknoloji ajansı";
|
||||||
|
const title =
|
||||||
|
settings?.hero_title ?? "Fikirden ürüne tek bir partner ile yola çıkın";
|
||||||
|
const subtitle =
|
||||||
|
settings?.hero_subtitle ??
|
||||||
|
"Web, mobil ve CRM çözümlerinde uçtan uca geliştirme. Markanıza özel tasarım, ölçeklenebilir altyapı ve uzun vadeli destek.";
|
||||||
|
const primaryLabel =
|
||||||
|
settings?.hero_cta_primary_label ?? "Proje görüşmesi başlat";
|
||||||
|
const primaryHref = settings?.hero_cta_primary_href ?? "/iletisim";
|
||||||
|
const secondaryLabel =
|
||||||
|
settings?.hero_cta_secondary_label ?? "Hizmetlerimizi inceleyin";
|
||||||
|
const secondaryHref = settings?.hero_cta_secondary_href ?? "/hizmetler";
|
||||||
|
const stats = parseStats(settings?.hero_stats);
|
||||||
|
|
||||||
export function Hero() {
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden">
|
<section className="relative overflow-hidden">
|
||||||
<div className="absolute inset-0 hero-grid opacity-60" aria-hidden />
|
<div className="absolute inset-0 hero-grid opacity-60" aria-hidden />
|
||||||
@@ -12,47 +47,45 @@ export function Hero() {
|
|||||||
<div>
|
<div>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--sky)]/30 bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--sky)]/30 bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
||||||
<Sparkles className="size-3.5" />
|
<Sparkles className="size-3.5" />
|
||||||
Kocaeli'nin teknoloji ajansı
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h1 className="mt-6 text-4xl font-bold leading-tight tracking-tight text-[var(--navy)] sm:text-5xl md:text-6xl">
|
<h1 className="mt-6 text-4xl font-bold leading-tight tracking-tight text-[var(--navy)] sm:text-5xl md:text-6xl">
|
||||||
Fikirden ürüne{" "}
|
{title}
|
||||||
<span className="gradient-text">tek bir partner</span> ile yola çıkın
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-6 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
<p className="mt-6 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
||||||
Web, mobil ve CRM çözümlerinde uçtan uca geliştirme. Markanıza
|
{subtitle}
|
||||||
özel tasarım, ölçeklenebilir altyapı ve uzun vadeli destek.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
<Link
|
<Link
|
||||||
href="/iletisim"
|
href={primaryHref}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-[var(--navy)] px-6 py-3 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
|
className="inline-flex items-center justify-center gap-2 rounded-full bg-[var(--navy)] px-6 py-3 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
|
||||||
>
|
>
|
||||||
Proje görüşmesi başlat
|
{primaryLabel}
|
||||||
<ArrowRight className="size-4" />
|
<ArrowRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/hizmetler"
|
href={secondaryHref}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-full border border-[var(--border)] bg-white px-6 py-3 text-sm font-medium text-[var(--navy)] transition hover:border-[var(--navy)]"
|
className="inline-flex items-center justify-center gap-2 rounded-full border border-[var(--border)] bg-white px-6 py-3 text-sm font-medium text-[var(--navy)] transition hover:border-[var(--navy)]"
|
||||||
>
|
>
|
||||||
Hizmetlerimizi inceleyin
|
{secondaryLabel}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{stats.length > 0 && (
|
||||||
<dl className="mt-12 grid max-w-md grid-cols-3 gap-6">
|
<dl className="mt-12 grid max-w-md grid-cols-3 gap-6">
|
||||||
{[
|
{stats.map((stat) => (
|
||||||
{ value: "50+", label: "Tamamlanan proje" },
|
|
||||||
{ value: "10+", label: "Yıllık deneyim" },
|
|
||||||
{ value: "24/7", label: "Teknik destek" },
|
|
||||||
].map((stat) => (
|
|
||||||
<div key={stat.label}>
|
<div key={stat.label}>
|
||||||
<dt className="text-2xl font-bold text-[var(--navy)]">{stat.value}</dt>
|
<dt className="text-2xl font-bold text-[var(--navy)]">
|
||||||
|
{stat.value}
|
||||||
|
</dt>
|
||||||
<dd className="mt-1 text-xs text-[var(--muted)]">{stat.label}</dd>
|
<dd className="mt-1 text-xs text-[var(--muted)]">{stat.label}</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
|
|||||||
@@ -281,6 +281,85 @@ export async function deleteTestimonial(formData: FormData) {
|
|||||||
revalidatePath("/admin/referanslar");
|
revalidatePath("/admin/referanslar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Site Settings (homepage content) ────────────────────────────
|
||||||
|
|
||||||
|
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 stats = statsRaw
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
const [value, label] = line.split("|").map((s) => s.trim());
|
||||||
|
if (!value || !label) return null;
|
||||||
|
return JSON.stringify({ value, label });
|
||||||
|
})
|
||||||
|
.filter((x): x is string => x !== null);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
hero_badge: str(formData.get("hero_badge")),
|
||||||
|
hero_title: str(formData.get("hero_title")),
|
||||||
|
hero_subtitle: str(formData.get("hero_subtitle")),
|
||||||
|
hero_cta_primary_label: str(formData.get("hero_cta_primary_label")),
|
||||||
|
hero_cta_primary_href: str(formData.get("hero_cta_primary_href")),
|
||||||
|
hero_cta_secondary_label: str(formData.get("hero_cta_secondary_label")),
|
||||||
|
hero_cta_secondary_href: str(formData.get("hero_cta_secondary_href")),
|
||||||
|
hero_stats: stats.length > 0 ? stats : null,
|
||||||
|
|
||||||
|
services_eyebrow: str(formData.get("services_eyebrow")),
|
||||||
|
services_title: str(formData.get("services_title")),
|
||||||
|
services_description: str(formData.get("services_description")),
|
||||||
|
|
||||||
|
projects_eyebrow: str(formData.get("projects_eyebrow")),
|
||||||
|
projects_title: str(formData.get("projects_title")),
|
||||||
|
projects_description: str(formData.get("projects_description")),
|
||||||
|
|
||||||
|
testimonials_eyebrow: str(formData.get("testimonials_eyebrow")),
|
||||||
|
testimonials_title: str(formData.get("testimonials_title")),
|
||||||
|
testimonials_description: str(formData.get("testimonials_description")),
|
||||||
|
|
||||||
|
cta_title: str(formData.get("cta_title")),
|
||||||
|
cta_description: str(formData.get("cta_description")),
|
||||||
|
cta_button_label: str(formData.get("cta_button_label")),
|
||||||
|
cta_button_href: str(formData.get("cta_button_href")),
|
||||||
|
|
||||||
|
contact_phone: str(formData.get("contact_phone")),
|
||||||
|
contact_phone_raw: str(formData.get("contact_phone_raw")),
|
||||||
|
contact_email: str(formData.get("contact_email")),
|
||||||
|
contact_address: str(formData.get("contact_address")),
|
||||||
|
contact_hours_weekday: str(formData.get("contact_hours_weekday")),
|
||||||
|
contact_hours_weekend: str(formData.get("contact_hours_weekend")),
|
||||||
|
|
||||||
|
social_linkedin: str(formData.get("social_linkedin")),
|
||||||
|
social_instagram: str(formData.get("social_instagram")),
|
||||||
|
social_twitter: str(formData.get("social_twitter")),
|
||||||
|
social_facebook: str(formData.get("social_facebook")),
|
||||||
|
|
||||||
|
footer_tagline: str(formData.get("footer_tagline")),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tablesDB.updateRow(
|
||||||
|
DATABASE_ID,
|
||||||
|
TABLES.siteSettings,
|
||||||
|
"homepage",
|
||||||
|
data,
|
||||||
|
secret,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
await tablesDB.createRow(
|
||||||
|
DATABASE_ID,
|
||||||
|
TABLES.siteSettings,
|
||||||
|
"homepage",
|
||||||
|
data,
|
||||||
|
secret,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
revalidatePath("/", "layout");
|
||||||
|
revalidatePath("/admin/site");
|
||||||
|
}
|
||||||
|
|
||||||
// ─── SEO Settings ────────────────────────────────────────────────
|
// ─── SEO Settings ────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function saveSeoSettings(formData: FormData) {
|
export async function saveSeoSettings(formData: FormData) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const TABLES = {
|
|||||||
testimonials: "testimonials",
|
testimonials: "testimonials",
|
||||||
seoPages: "seo_pages",
|
seoPages: "seo_pages",
|
||||||
seoSettings: "seo_settings",
|
seoSettings: "seo_settings",
|
||||||
|
siteSettings: "site_settings",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class AppwriteError extends Error {
|
export class AppwriteError extends Error {
|
||||||
|
|||||||
+13
@@ -8,6 +8,7 @@ import type {
|
|||||||
ServiceRow,
|
ServiceRow,
|
||||||
SeoPageRow,
|
SeoPageRow,
|
||||||
SeoSettingsRow,
|
SeoSettingsRow,
|
||||||
|
SiteSettingsRow,
|
||||||
TestimonialRow,
|
TestimonialRow,
|
||||||
} from "@/lib/types";
|
} from "@/lib/types";
|
||||||
|
|
||||||
@@ -118,6 +119,18 @@ export async function listSeoPages() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSiteSettings(): Promise<SiteSettingsRow | null> {
|
||||||
|
try {
|
||||||
|
return await tablesDB.getRow<SiteSettingsRow>(
|
||||||
|
DATABASE_ID,
|
||||||
|
TABLES.siteSettings,
|
||||||
|
"homepage",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSeoSettings(): Promise<SeoSettingsRow | null> {
|
export async function getSeoSettings(): Promise<SeoSettingsRow | null> {
|
||||||
try {
|
try {
|
||||||
return await tablesDB.getRow<SeoSettingsRow>(
|
return await tablesDB.getRow<SeoSettingsRow>(
|
||||||
|
|||||||
@@ -86,6 +86,53 @@ export interface SeoSettingsRow extends AwRow {
|
|||||||
gtm_id?: string | null;
|
gtm_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StatItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteSettingsRow extends AwRow {
|
||||||
|
hero_badge?: string | null;
|
||||||
|
hero_title?: string | null;
|
||||||
|
hero_subtitle?: string | null;
|
||||||
|
hero_cta_primary_label?: string | null;
|
||||||
|
hero_cta_primary_href?: string | null;
|
||||||
|
hero_cta_secondary_label?: string | null;
|
||||||
|
hero_cta_secondary_href?: string | null;
|
||||||
|
hero_stats?: string[] | null; // each item JSON: {"value":"...","label":"..."}
|
||||||
|
|
||||||
|
services_eyebrow?: string | null;
|
||||||
|
services_title?: string | null;
|
||||||
|
services_description?: string | null;
|
||||||
|
|
||||||
|
projects_eyebrow?: string | null;
|
||||||
|
projects_title?: string | null;
|
||||||
|
projects_description?: string | null;
|
||||||
|
|
||||||
|
testimonials_eyebrow?: string | null;
|
||||||
|
testimonials_title?: string | null;
|
||||||
|
testimonials_description?: string | null;
|
||||||
|
|
||||||
|
cta_title?: string | null;
|
||||||
|
cta_description?: string | null;
|
||||||
|
cta_button_label?: string | null;
|
||||||
|
cta_button_href?: string | null;
|
||||||
|
|
||||||
|
contact_phone?: string | null;
|
||||||
|
contact_phone_raw?: string | null;
|
||||||
|
contact_email?: string | null;
|
||||||
|
contact_address?: string | null;
|
||||||
|
contact_hours_weekday?: string | null;
|
||||||
|
contact_hours_weekend?: string | null;
|
||||||
|
|
||||||
|
social_linkedin?: string | null;
|
||||||
|
social_instagram?: string | null;
|
||||||
|
social_twitter?: string | null;
|
||||||
|
social_facebook?: string | null;
|
||||||
|
|
||||||
|
footer_tagline?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContactMessageRow extends AwRow {
|
export interface ContactMessageRow extends AwRow {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user