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:
Ege Can Komur
2026-05-20 20:50:30 +03:00
parent f3604d96b8
commit f49df9cbeb
4 changed files with 255 additions and 52 deletions
+83 -36
View File
@@ -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,7 +105,17 @@ 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">
<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"
@@ -75,6 +123,7 @@ export default async function AboutPage() {
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,13 +144,10 @@ export default async function AboutPage() {
</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">
{[
{ value: "50+", label: "Tamamlanan proje" },
{ value: "30+", label: "Mutlu müşteri" },
{ value: "10+", label: "Yıllık deneyim" },
].map((s) => (
{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>
@@ -109,6 +155,7 @@ export default async function AboutPage() {
))}
</div>
</section>
)}
</>
);
}
+106
View File
@@ -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."
+34
View File
@@ -363,6 +363,30 @@ export async function saveSiteSettings(formData: FormData) {
.map((s) => s.trim())
.filter(Boolean);
// Hakkımızda values: blok '---' ile, ilk satır title, kalanı description
const aboutValuesRaw = String(formData.get("about_values") ?? "");
const aboutValues = aboutValuesRaw
.split("\n---\n")
.map((block) => {
const lines = block.trim().split("\n");
const title = lines[0]?.trim();
const description = lines.slice(1).join("\n").trim();
if (!title || !description) return null;
return JSON.stringify({ title, description });
})
.filter((x): x is string => x !== null);
// Hakkımızda stats: 'value | label' satırlar
const aboutStatsRaw = String(formData.get("about_stats") ?? "");
const aboutStats = aboutStatsRaw
.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")),
@@ -414,6 +438,16 @@ export async function saveSiteSettings(formData: FormData) {
google_review_url: str(formData.get("google_review_url")),
google_rating: num(formData.get("google_rating")),
google_review_count: num(formData.get("google_review_count")),
about_eyebrow: str(formData.get("about_eyebrow")),
about_title: str(formData.get("about_title")),
about_description: str(formData.get("about_description")),
about_values: aboutValues.length > 0 ? aboutValues : null,
about_hero_image: str(formData.get("about_hero_image")),
about_team_eyebrow: str(formData.get("about_team_eyebrow")),
about_team_title: str(formData.get("about_team_title")),
about_team_description: str(formData.get("about_team_description")),
about_stats: aboutStats.length > 0 ? aboutStats : null,
};
try {
+16
View File
@@ -148,6 +148,22 @@ export interface SiteSettingsRow extends AwRow {
guarantee_title?: string | null;
guarantee_description?: string | null;
guarantee_items?: string[] | null;
// Hakkımızda sayfası
about_eyebrow?: string | null;
about_title?: string | null;
about_description?: string | null;
about_values?: string[] | null; // JSON {"title","description"}
about_hero_image?: string | null;
about_team_eyebrow?: string | null;
about_team_title?: string | null;
about_team_description?: string | null;
about_stats?: string[] | null; // JSON {"value","label"}
}
export interface AboutValue {
title: string;
description: string;
}
export interface TeamMemberRow extends AwRow {