feat: Hakkımızda sayfası yönetilebilir (site_settings + /admin/site)
Önce hard-coded olan tüm metinler artık /admin/site > 'Hakkımızda sayfası'
bölümünden düzenlenebilir.
site_settings'e 9 yeni alan eklendi:
- about_eyebrow, about_title, about_description (üst hero)
- about_values (string array JSON {title, description}) — 4 değer kartı
- about_hero_image (opsiyonel, boşsa logo gösterilir)
- about_team_eyebrow, about_team_title, about_team_description
- about_stats (string array JSON {value, label}) — alt navy bant
Mevcut WP değerleri default olarak seed edildi.
Hakkımızda sayfası (app/(site)/hakkimizda/page.tsx) artık:
- Tüm metinler settings'ten okunuyor (fallback default'lar var)
- Hero image varsa logo yerine onu gösteriyor
- Stats sıfırdan farklı sayıda olabilir (3 yerine 2/4)
Admin form (/admin/site):
- Yeni 'Hakkımızda sayfası' section
- 4 alt-bölüm: Üst hero / Değerler / Ekip / Stats
- MediaPicker ile hero image
- Markdown benzeri textarea'lar (--- ayırıcı, | seperator)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { SectionTitle } from "@/components/section-title";
|
||||
import { CheckCircle2 } from "lucide-react";
|
||||
import { SectionTitle } from "@/components/section-title";
|
||||
import { TeamGrid } from "@/components/team-grid";
|
||||
import { listTeamMembers } from "@/lib/data";
|
||||
import { getSiteSettings, listTeamMembers } from "@/lib/data";
|
||||
import { buildMetadata } from "@/lib/seo";
|
||||
import type { AboutValue, StatItem } from "@/lib/types";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return buildMetadata("/hakkimizda", {
|
||||
@@ -14,31 +15,68 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
});
|
||||
}
|
||||
|
||||
const values = [
|
||||
{
|
||||
title: "Uçtan uca üretim",
|
||||
description:
|
||||
"Fikir aşamasından lansmana, lansman sonrası bakıma kadar tek bir ekip.",
|
||||
},
|
||||
{
|
||||
title: "Ölçülebilir sonuç",
|
||||
description:
|
||||
"Her projeyi performans, dönüşüm ve kullanıcı deneyimi metrikleriyle değerlendiriyoruz.",
|
||||
},
|
||||
{
|
||||
title: "Şeffaf süreç",
|
||||
description:
|
||||
"Her sprint demo ile başlar, her engel açıkça konuşulur. Sürprize yer yok.",
|
||||
},
|
||||
{
|
||||
title: "Uzun vadeli ortaklık",
|
||||
description:
|
||||
"Proje biter, iş büyür. Bakım ve geliştirme süreçlerinde yanınızdayız.",
|
||||
},
|
||||
const DEFAULT_VALUES: AboutValue[] = [
|
||||
{ title: "Uçtan uca üretim", description: "Fikir aşamasından lansmana, lansman sonrası bakıma kadar tek bir ekip." },
|
||||
{ title: "Ölçülebilir sonuç", description: "Her projeyi performans, dönüşüm ve kullanıcı deneyimi metrikleriyle değerlendiriyoruz." },
|
||||
{ title: "Şeffaf süreç", description: "Her sprint demo ile başlar, her engel açıkça konuşulur. Sürprize yer yok." },
|
||||
{ title: "Uzun vadeli ortaklık", description: "Proje biter, iş büyür. Bakım ve geliştirme süreçlerinde yanınızdayız." },
|
||||
];
|
||||
|
||||
const DEFAULT_STATS: StatItem[] = [
|
||||
{ value: "50+", label: "Tamamlanan proje" },
|
||||
{ value: "30+", label: "Mutlu müşteri" },
|
||||
{ value: "10+", label: "Yıllık deneyim" },
|
||||
];
|
||||
|
||||
function parseValues(items?: string[] | null): AboutValue[] {
|
||||
if (!items || items.length === 0) return DEFAULT_VALUES;
|
||||
const out: AboutValue[] = [];
|
||||
for (const raw of items) {
|
||||
try {
|
||||
const obj = JSON.parse(raw) as Partial<AboutValue>;
|
||||
if (obj.title && obj.description) out.push({ title: obj.title, description: obj.description });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return out.length > 0 ? out : DEFAULT_VALUES;
|
||||
}
|
||||
|
||||
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 default async function AboutPage() {
|
||||
const team = await listTeamMembers();
|
||||
const [team, settings] = await Promise.all([
|
||||
listTeamMembers(),
|
||||
getSiteSettings(),
|
||||
]);
|
||||
|
||||
const eyebrow = settings?.about_eyebrow ?? "Hakkımızda";
|
||||
const title = settings?.about_title ?? "Kocaeli'den dünyaya dijital ürünler";
|
||||
const description =
|
||||
settings?.about_description ??
|
||||
"Kovak Yazılım, kurumsal markalardan girişimlere kadar geniş bir yelpazedeki müşterileri için web, mobil ve CRM çözümleri üretir. Hızlı, ölçeklenebilir ve estetik.";
|
||||
const values = parseValues(settings?.about_values);
|
||||
const heroImage = settings?.about_hero_image ?? null;
|
||||
|
||||
const teamEyebrow = settings?.about_team_eyebrow ?? "Ekibimiz";
|
||||
const teamTitle = settings?.about_team_title ?? "Projenizde Kimlerle Çalışırsınız?";
|
||||
const teamDescription =
|
||||
settings?.about_team_description ??
|
||||
"Sizin projenizde birebir çalışacak kurucular — teknik altyapı ve ürün geliştirmenin arkasındaki isimler.";
|
||||
|
||||
const stats = parseStats(settings?.about_stats);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,9 +85,9 @@ export default async function AboutPage() {
|
||||
<div>
|
||||
<SectionTitle
|
||||
align="left"
|
||||
eyebrow="Hakkımızda"
|
||||
title="Kocaeli'den dünyaya dijital ürünler"
|
||||
description="Kovak Yazılım, kurumsal markalardan girişimlere kadar geniş bir yelpazedeki müşterileri için web, mobil ve CRM çözümleri üretir. Hızlı, ölçeklenebilir ve estetik."
|
||||
eyebrow={eyebrow}
|
||||
title={title}
|
||||
description={description}
|
||||
/>
|
||||
|
||||
<ul className="mt-10 space-y-4">
|
||||
@@ -67,14 +105,25 @@ export default async function AboutPage() {
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 -z-10 rounded-3xl bg-gradient-to-br from-[var(--sky-50)] to-[var(--navy-50)]" />
|
||||
<div className="flex aspect-square items-center justify-center p-12">
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Kovak Yazılım"
|
||||
width={400}
|
||||
height={400}
|
||||
className="size-full object-contain drop-shadow-xl"
|
||||
/>
|
||||
<div className="relative flex aspect-square items-center justify-center overflow-hidden rounded-3xl p-12">
|
||||
{heroImage ? (
|
||||
<Image
|
||||
src={heroImage}
|
||||
alt={title}
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Kovak Yazılım"
|
||||
width={400}
|
||||
height={400}
|
||||
className="size-full object-contain drop-shadow-xl"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,9 +133,9 @@ export default async function AboutPage() {
|
||||
<section className="border-y border-[var(--border)] bg-gray-50 py-20">
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<SectionTitle
|
||||
eyebrow="Ekibimiz"
|
||||
title="Projenizde Kimlerle Çalışırsınız?"
|
||||
description="Sizin projenizde birebir çalışacak kurucular — teknik altyapı ve ürün geliştirmenin arkasındaki isimler."
|
||||
eyebrow={teamEyebrow}
|
||||
title={teamTitle}
|
||||
description={teamDescription}
|
||||
/>
|
||||
<div className="mt-14">
|
||||
<TeamGrid members={team} />
|
||||
@@ -95,20 +144,18 @@ export default async function AboutPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="bg-[var(--navy)] py-20 text-white">
|
||||
<div className="mx-auto grid max-w-7xl gap-12 px-6 md:grid-cols-3">
|
||||
{[
|
||||
{ value: "50+", label: "Tamamlanan proje" },
|
||||
{ value: "30+", label: "Mutlu müşteri" },
|
||||
{ value: "10+", label: "Yıllık deneyim" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="text-center">
|
||||
<p className="text-5xl font-bold">{s.value}</p>
|
||||
<p className="mt-2 text-sm text-white/70">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{stats.length > 0 && (
|
||||
<section className="bg-[var(--navy)] py-20 text-white">
|
||||
<div className="mx-auto grid max-w-7xl gap-12 px-6 md:grid-cols-3">
|
||||
{stats.map((s) => (
|
||||
<div key={s.label} className="text-center">
|
||||
<p className="text-5xl font-bold">{s.value}</p>
|
||||
<p className="mt-2 text-sm text-white/70">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getSiteSettings } from "@/lib/data";
|
||||
import { saveSiteSettings } from "@/lib/admin-actions";
|
||||
import { MediaPicker } from "@/components/admin/media-picker";
|
||||
import type {
|
||||
AboutValue,
|
||||
FaqItem,
|
||||
ProcessStep,
|
||||
StatItem,
|
||||
@@ -104,6 +105,21 @@ function faqToText(items?: string[] | null): string {
|
||||
return parsed.map((it) => `${it.q}\n${it.a}`).join("\n---\n");
|
||||
}
|
||||
|
||||
function aboutValuesToText(items?: string[] | null): string {
|
||||
if (!items) return "";
|
||||
const parsed: AboutValue[] = [];
|
||||
for (const raw of items) {
|
||||
try {
|
||||
const obj = JSON.parse(raw) as Partial<AboutValue>;
|
||||
if (obj.title && obj.description)
|
||||
parsed.push({ title: obj.title, description: obj.description });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return parsed.map((v) => `${v.title}\n${v.description}`).join("\n---\n");
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
description,
|
||||
@@ -371,6 +387,96 @@ export default async function SiteSettingsPage() {
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Hakkımızda sayfası"
|
||||
description="/hakkimizda sayfasındaki metinler ve görsel."
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<Field
|
||||
label="Eyebrow"
|
||||
name="about_eyebrow"
|
||||
defaultValue={s?.about_eyebrow}
|
||||
placeholder="Hakkımızda"
|
||||
/>
|
||||
<Field
|
||||
label="Başlık"
|
||||
name="about_title"
|
||||
defaultValue={s?.about_title}
|
||||
placeholder="Kocaeli'den dünyaya dijital ürünler"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
label="Açıklama paragrafı"
|
||||
name="about_description"
|
||||
rows={3}
|
||||
defaultValue={s?.about_description}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Değerler (4 madde önerilir)"
|
||||
name="about_values"
|
||||
rows={10}
|
||||
defaultValue={aboutValuesToText(s?.about_values)}
|
||||
placeholder={
|
||||
"Uçtan uca üretim\nFikir aşamasından lansmana, tek bir ekip.\n---\nÖlçülebilir sonuç\nHer projeyi metriklerle değerlendiriyoruz."
|
||||
}
|
||||
help='Her blok "---" ile ayrılır. İlk satır başlık, sonrası açıklama.'
|
||||
/>
|
||||
|
||||
<MediaPicker
|
||||
label="Hero görsel (opsiyonel)"
|
||||
name="about_hero_image"
|
||||
defaultValue={s?.about_hero_image}
|
||||
help="Boşsa logo gösterilir. Görsel eklersen logo yerine geçer."
|
||||
/>
|
||||
|
||||
<div className="border-t border-[var(--border)] pt-5">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
||||
Ekip bölümü
|
||||
</p>
|
||||
<div className="mt-3 grid gap-5 md:grid-cols-3">
|
||||
<Field
|
||||
label="Ekip eyebrow"
|
||||
name="about_team_eyebrow"
|
||||
defaultValue={s?.about_team_eyebrow}
|
||||
placeholder="Ekibimiz"
|
||||
/>
|
||||
<Field
|
||||
label="Ekip başlığı"
|
||||
name="about_team_title"
|
||||
defaultValue={s?.about_team_title}
|
||||
placeholder="Projenizde Kimlerle Çalışırsınız?"
|
||||
/>
|
||||
<Field
|
||||
label="Ekip açıklaması"
|
||||
name="about_team_description"
|
||||
defaultValue={s?.about_team_description}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[var(--muted)]">
|
||||
Ekip üyeleri /admin/ekip üzerinden yönetilir.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--border)] pt-5">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
||||
İstatistikler (en altta navy bant)
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<Textarea
|
||||
label="Stats"
|
||||
name="about_stats"
|
||||
rows={4}
|
||||
defaultValue={statsToText(s?.about_stats)}
|
||||
placeholder={
|
||||
"50+ | Tamamlanan proje\n30+ | Mutlu müşteri\n10+ | Yıllık deneyim"
|
||||
}
|
||||
help='Her satır "değer | etiket" formatında.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Conversion / reklam optimizasyonu"
|
||||
description="Trust bandı, mini lead form ve WhatsApp metni."
|
||||
|
||||
Reference in New Issue
Block a user