From deff889f0cea0cec2d72bd77cda59aaab317d889 Mon Sep 17 00:00:00 2001 From: Ege Can Komur Date: Wed, 20 May 2026 18:34:44 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20WordPress=20tarz=C4=B1=20rich=20editor?= =?UTF-8?q?=20(TipTap=20+=20slash=20menu=20+=20MediaPicker)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/(site)/blog/[slug]/page.tsx | 4 +- app/(site)/hizmetler/[slug]/page.tsx | 6 +- app/(site)/projeler/[slug]/page.tsx | 6 +- app/(site)/sektor/[slug]/page.tsx | 6 +- app/admin/(protected)/blog/form.tsx | 21 +- app/admin/(protected)/hizmetler/form.tsx | 24 +- app/admin/(protected)/projeler/form.tsx | 26 +- app/admin/(protected)/sektorler/form.tsx | 20 +- components/admin/rich-editor.tsx | 574 +++++++++++++++++++ lib/content-render.ts | 17 + package-lock.json | 682 ++++++++++++++++++++++- package.json | 7 + 12 files changed, 1344 insertions(+), 49 deletions(-) create mode 100644 components/admin/rich-editor.tsx create mode 100644 lib/content-render.ts diff --git a/app/(site)/blog/[slug]/page.tsx b/app/(site)/blog/[slug]/page.tsx index 62a3ffa..259e878 100644 --- a/app/(site)/blog/[slug]/page.tsx +++ b/app/(site)/blog/[slug]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { ArrowLeft, Calendar } from "lucide-react"; -import { marked } from "marked"; +import { renderContent } from "@/lib/content-render"; import { getPostBySlug } from "@/lib/data"; import { buildMetadata } from "@/lib/seo"; @@ -36,7 +36,7 @@ export default async function BlogPostPage({ const post = await getPostBySlug(slug); if (!post || post.status !== "published") notFound(); - const html = post.content ? marked.parse(post.content, { async: false }) as string : ""; + const html = renderContent(post.content); return (
diff --git a/app/(site)/hizmetler/[slug]/page.tsx b/app/(site)/hizmetler/[slug]/page.tsx index 4a39bd7..53f392c 100644 --- a/app/(site)/hizmetler/[slug]/page.tsx +++ b/app/(site)/hizmetler/[slug]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react"; -import { marked } from "marked"; +import { renderContent } from "@/lib/content-render"; import { getServiceBySlug, listProjects } from "@/lib/data"; import { buildMetadata } from "@/lib/seo"; import { Icon } from "@/components/icon"; @@ -55,9 +55,7 @@ export default async function ServiceDetailPage({ ]); const faqItems = parseFaq(service.faq); - const html = service.content - ? (marked.parse(service.content, { async: false }) as string) - : ""; + const html = renderContent(service.content); return ( <> diff --git a/app/(site)/projeler/[slug]/page.tsx b/app/(site)/projeler/[slug]/page.tsx index 9622ec2..2cde4bb 100644 --- a/app/(site)/projeler/[slug]/page.tsx +++ b/app/(site)/projeler/[slug]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { ArrowLeft, Building2, Calendar, Clock, ExternalLink, Tag } from "lucide-react"; -import { marked } from "marked"; +import { renderContent } from "@/lib/content-render"; import { getProjectBySlug, listProjects } from "@/lib/data"; import { buildMetadata } from "@/lib/seo"; import { Gallery } from "@/components/gallery"; @@ -54,9 +54,7 @@ export default async function ProjectDetailPage({ const project = await getProjectBySlug(slug); if (!project) notFound(); - const html = project.content - ? (marked.parse(project.content, { async: false }) as string) - : ""; + const html = renderContent(project.content); const metrics = parseMetrics(project.metrics); diff --git a/app/(site)/sektor/[slug]/page.tsx b/app/(site)/sektor/[slug]/page.tsx index e21028e..571ec8d 100644 --- a/app/(site)/sektor/[slug]/page.tsx +++ b/app/(site)/sektor/[slug]/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { ArrowRight, ArrowLeft, CheckCircle2 } from "lucide-react"; -import { marked } from "marked"; +import { renderContent } from "@/lib/content-render"; import { getIndustryBySlug, listProjects, @@ -65,9 +65,7 @@ export default async function IndustryPage({ ]); const faqItems = parseFaq(industry.faq); - const html = industry.content - ? (marked.parse(industry.content, { async: false }) as string) - : ""; + const html = renderContent(industry.content); return ( <> diff --git a/app/admin/(protected)/blog/form.tsx b/app/admin/(protected)/blog/form.tsx index 797ce75..d1b3683 100644 --- a/app/admin/(protected)/blog/form.tsx +++ b/app/admin/(protected)/blog/form.tsx @@ -9,6 +9,7 @@ import { Textarea, } from "@/components/admin/form"; import { MediaPicker } from "@/components/admin/media-picker"; +import { RichEditor } from "@/components/admin/rich-editor"; import { saveBlogPost } from "@/lib/admin-actions"; import type { BlogPostRow } from "@/lib/types"; import { Save } from "lucide-react"; @@ -75,13 +76,19 @@ export function BlogForm({ post }: { post?: BlogPostRow }) { rows={3} placeholder="Liste/kart görünümünde gösterilecek kısa özet" /> -