Files
kovakyazilim/app/admin/(protected)/blog/form.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

136 lines
4.2 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 {
Field,
FormActions,
FormShell,
GhostLink,
PageHeader,
PrimaryButton,
Select,
Textarea,
} from "@/components/admin/form";
import { MediaPicker } from "@/components/admin/media-picker";
import { saveBlogPost } from "@/lib/admin-actions";
import type { BlogPostRow } from "@/lib/types";
import { Save } from "lucide-react";
export function BlogForm({ post }: { post?: BlogPostRow }) {
return (
<div>
<PageHeader
title={post ? "Yazıyı düzenle" : "Yeni yazı"}
backHref="/admin/blog"
description="Markdown formatında içerik yazabilirsiniz."
/>
<form action={saveBlogPost}>
{post && <input type="hidden" name="id" value={post.$id} />}
<FormShell>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Başlık"
name="title"
required
defaultValue={post?.title}
placeholder="Yazı başlığı"
/>
<Field
label="Slug"
name="slug"
defaultValue={post?.slug}
placeholder="otomatik-uretilir"
help="Boş bırakırsanız başlıktan üretilir."
/>
<Field label="Yazar" name="author" defaultValue={post?.author} />
<Select
label="Durum"
name="status"
defaultValue={post?.status ?? "draft"}
options={[
{ value: "draft", label: "Taslak" },
{ value: "published", label: "Yayında" },
]}
/>
<Field
label="Yayın tarihi (ISO)"
name="published_at"
type="datetime-local"
defaultValue={post?.published_at?.slice(0, 16)}
help="Boş bırakırsanız yayına alındığı an kullanılır."
/>
<Field
label="Etiketler"
name="tags"
defaultValue={post?.tags?.join(", ")}
placeholder="seo, web tasarım, kocaeli"
help="Virgülle ayırın."
/>
</div>
<div className="mt-5 space-y-5">
<Textarea
label="Özet"
name="excerpt"
defaultValue={post?.excerpt}
rows={3}
placeholder="Liste/kart görünümünde gösterilecek kısa özet"
/>
<Textarea
label="İçerik (Markdown)"
name="content"
defaultValue={post?.content}
rows={14}
placeholder={"# Başlık\n\nMarkdown desteklenir…"}
/>
<MediaPicker
label="Kapak görseli"
name="cover_image"
defaultValue={post?.cover_image}
help="Yazı listesinde ve detay sayfasının üst kısmında gösterilir."
/>
<input
type="hidden"
name="cover_file_id"
defaultValue={post?.cover_file_id ?? ""}
/>
</div>
<h3 className="mt-8 text-sm font-semibold uppercase tracking-wider text-[var(--muted)]">
SEO
</h3>
<div className="mt-3 grid gap-5 md:grid-cols-2">
<Field
label="SEO başlığı"
name="seo_title"
defaultValue={post?.seo_title}
help="Boş bırakırsanız yazı başlığı kullanılır."
/>
<MediaPicker
label="SEO OG görseli"
name="seo_image"
defaultValue={post?.seo_image}
help="Sosyal medyada paylaşılınca görünecek görsel (1200×630 ideal)."
/>
</div>
<div className="mt-5">
<Textarea
label="SEO açıklaması"
name="seo_description"
defaultValue={post?.seo_description}
rows={2}
help="150160 karakter ideal."
/>
</div>
<FormActions>
<GhostLink href="/admin/blog">İptal</GhostLink>
<PrimaryButton>
<Save className="size-4" /> Kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}