Files
Ege Can Komur deff889f0c feat: WordPress tarzı rich editor (TipTap + slash menu + MediaPicker)
WordPress Gutenberg + Notion karışımı blok editor. 4 admin formunda
markdown textarea yerine gerçek WYSIWYG editor.

RichEditor component (components/admin/rich-editor.tsx):
- TipTap v3 (@tiptap/react + starter-kit + link + image + placeholder + underline)
- Üst toolbar (her zaman görünür):
  - B / I / U (bold, italic, underline)
  - H1 / H2 / H3
  - Bullet list / Ordered list / Quote / Code block
  - Link (URL prompt)
  - Görsel ekle (MediaPicker modal)
  - Undo / Redo
- Slash menu: '/' yazınca blok seçim menüsü açılır
  - Notion tarzı keyboard navigation (↓↑ Enter Esc)
  - 8 blok tipi: H1/H2/H3/ul/ol/quote/code/hr
- Image picker modal (toolbar görsel butonundan)
  - Mevcut MediaPicker'ı kullanır
  - 'Yeni görsel yükle' (progress bar ile) + 'Kütüphaneden seç' grid
- HTML çıktı (hidden input ile form'a)
- Mevcut content alanlarıyla backward compat

Formlarda değişiklik (4 dosya):
- app/admin/(protected)/blog/form.tsx → content
- app/admin/(protected)/hizmetler/form.tsx → content
- app/admin/(protected)/projeler/form.tsx → content
- app/admin/(protected)/sektorler/form.tsx → content

Public render (lib/content-render.ts):
- renderContent() yardımcısı:
  - İçerik '<' ile başlıyorsa → HTML (direkt döner)
  - Aksi halde → markdown (marked.parse)
- 4 detay sayfası bu helper'ı kullanıyor (blog/[slug], projeler/[slug],
  hizmetler/[slug], sektor/[slug])
- Eski markdown içerikler hala çalışıyor, yeni içerikler HTML olarak gelir

37 route, build temiz.
2026-05-20 18:34:44 +03:00

148 lines
4.5 KiB
TypeScript
Raw Permalink 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 { Save } from "lucide-react";
import {
Checkbox,
Field,
FormActions,
FormShell,
GhostLink,
PageHeader,
PrimaryButton,
Textarea,
} from "@/components/admin/form";
import { MediaPicker } from "@/components/admin/media-picker";
import { RichEditor } from "@/components/admin/rich-editor";
import { saveIndustry } from "@/lib/admin-actions";
import type { FaqItem, IndustryRow } from "@/lib/types";
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");
}
export function IndustryForm({ row }: { row?: IndustryRow }) {
return (
<div>
<PageHeader
title={row ? "Sektörü düzenle" : "Yeni sektör"}
backHref="/admin/sektorler"
description="Örn: 'Avukat web tasarımı', 'Doktor web tasarımı', 'İnşaat firması web tasarımı'"
/>
<form action={saveIndustry}>
{row && <input type="hidden" name="id" value={row.$id} />}
<FormShell>
<div className="grid gap-5 md:grid-cols-2">
<Field
label="Başlık"
name="title"
required
defaultValue={row?.title}
placeholder="Avukat Web Tasarımı"
/>
<Field
label="Slug"
name="slug"
defaultValue={row?.slug}
placeholder="avukat-web-tasarimi"
/>
<MediaPicker
label="Hero görsel"
name="hero_image"
defaultValue={row?.hero_image}
help="Sektör sayfasının üst kısmında gösterilir."
/>
<Field
label="Sıra"
name="order"
type="number"
defaultValue={row?.order ?? 0}
/>
</div>
<div className="mt-5 space-y-5">
<Textarea
label="Alt başlık / kısa açıklama"
name="subtitle"
rows={2}
defaultValue={row?.subtitle}
placeholder="Avukatlar için KVKK uyumlu, randevu sistemli, SEO odaklı modern web siteleri."
/>
<div>
<span className="text-sm font-medium text-[var(--navy)]">
İçerik
</span>
<div className="mt-1.5">
<RichEditor
name="content"
defaultValue={row?.content}
placeholder="Sektörünüz için içerik yazın…"
/>
</div>
</div>
<Textarea
label="Özellikler"
name="features"
rows={3}
defaultValue={row?.features?.join(", ")}
placeholder="KVKK uyumlu form, Randevu sistemi, Blog modülü, Çoklu dil"
help="Virgülle ayırın."
/>
<Textarea
label="SSS"
name="faq"
rows={8}
defaultValue={faqToText(row?.faq)}
placeholder={"Avukatlar için web sitesi neden önemli?\nKVKK uyumu için..."}
help='Her blok "---" ile ayrılır. İlk satır soru, kalanı cevap.'
/>
</div>
<h3 className="mt-8 text-sm font-semibold uppercase tracking-wider text-[var(--muted)]">
SEO
</h3>
<div className="mt-3 space-y-5">
<Field
label="SEO başlığı"
name="seo_title"
defaultValue={row?.seo_title}
placeholder="Avukat Web Tasarımı | Kocaeli — KVKK Uyumlu Modern Site"
/>
<Textarea
label="SEO açıklaması"
name="seo_description"
rows={2}
defaultValue={row?.seo_description}
/>
</div>
<div className="mt-5">
<Checkbox
label="Öne çıkar"
name="featured"
defaultChecked={row?.featured ?? false}
/>
</div>
<FormActions>
<GhostLink href="/admin/sektorler">İptal</GhostLink>
<PrimaryButton>
<Save className="size-4" /> Kaydet
</PrimaryButton>
</FormActions>
</FormShell>
</form>
</div>
);
}