Files
kovakyazilim/app/admin/(protected)/site/page.tsx
T
Ege Can Komur dbc55e7527 feat: MediaPicker — sürükle-bırak + progress bar + kütüphane modal
Mevcut sorun:
- Her görsel için medya sayfasına git, yükle, URL kopyala, forma yapıştır → 4 adım
- Sürükle-bırak yok, progress yok, hangi dosyanın yüklendiği belirsiz

Çözüm: MediaPicker component (tek/çoklu mode)

API route'ları:
- POST /api/admin/media/upload — session auth + Appwrite Storage upload
- GET /api/admin/media/list — kütüphane modal için dosya listesi

Component özellikleri:
- Sürükle-bırak drop zone (hover state ile)
- Multiple file upload (çoklu mode)
- XHR ile gerçek progress bar (%) — Server Action ile alınamazdı
- Görsel preview (single: aspect-video, multiple: aspect-square grid)
- Hover'da × ile kaldırma
- Multiple mode'da sırasını değiştirme
- 'Kütüphaneden seç' modal — daha önce yüklenmiş görselleri grid'de göster, tıklayınca seç
- Error handling (dosya boyutu, ağ hatası vb.)
- Başarılı yüklemeyi 2 saniye gösterip kaybetme

Form alanları → MediaPicker (URL field'ları kaldırıldı):
- Blog: cover_image, seo_image
- Hizmet: hero_image
- Proje: image_url (kapak), gallery (çoklu)
- Referans: image_url
- Sektör: hero_image
- Ekip: photo_url
- SEO sayfa: og_image
- SEO global: default_og_image
- Site Settings: client_logos (çoklu)

Backward compat: form data formatı aynı kalıyor — hidden input ile URL satır satır.
admin-actions değişmedi. URL elle yapıştırmak hala mümkün (kütüphaneden URL kopyala).
2026-05-20 04:11:41 +03:00

530 lines
18 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 { MediaPicker } from "@/components/admin/media-picker";
import type {
FaqItem,
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 faqToText(items?: string[] | null): string {
if (!items) return "";
const parsed: FaqItem[] = [];
for (const raw of items) {
try {
const obj = JSON.parse(raw) as Partial<FaqItem>;
if (obj.q && obj.a) parsed.push({ q: obj.q, a: obj.a });
} catch {
/* ignore */
}
}
return parsed.map((it) => `${it.q}\n${it.a}`).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ı)'
/>
<MediaPicker
label="Müşteri logoları"
name="client_logos"
multiple
defaultValue={s?.client_logos ?? []}
maxSizeMB={3}
help="Anasayfada grayscale logo şeridi olarak gösterilir. Tercihen şeffaf PNG."
/>
<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>
<Section
title="Risk reversal / Garanti bölümü"
description="Anasayfada güven yaratan büyük garanti satırı."
>
<Field
label="Garanti başlığı"
name="guarantee_title"
defaultValue={s?.guarantee_title}
placeholder="İlk taslak ücretsiz, memnun değilseniz devam etmiyoruz"
/>
<Textarea
label="Garanti açıklaması"
name="guarantee_description"
rows={3}
defaultValue={s?.guarantee_description}
/>
<Textarea
label="Garanti maddeleri"
name="guarantee_items"
rows={5}
defaultValue={s?.guarantee_items?.join("\n")}
placeholder={"İlk tasarım taslağı ücretsiz\n1 yıl ücretsiz teknik destek\nKaynak kodlar size aittir"}
help="Her satır bir madde. Checklist olarak gösterilir."
/>
</Section>
<Section
title="Anasayfa SSS"
description="Reklam trafiği için kritik — fiyat, süre, ödeme gibi en sık soruları yanıtlar."
>
<Textarea
label="SSS"
name="homepage_faq"
rows={20}
defaultValue={faqToText(s?.homepage_faq)}
placeholder={
"Bir web sitesi ne kadar sürer?\n2-3 hafta…\n---\nFiyatlar ne kadar?\n15.000₺'den başlar…"
}
help='Her blok "---" ile ayrılır. İlk satır soru, kalanı cevap.'
/>
</Section>
</div>
<FormActions>
<PrimaryButton>
<Save className="size-4" /> Tüm değişiklikleri kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}