فرانتاند: ویرایشگر WYSIWYG وبلاگ (TipTap) با آپلود تصویر در متن
- ArticleEditor: ویرایشگر TipTap v3 راستبهچسب با نوار ابزار فارسی — بولد/ایتالیک/زیرخط/خطخورده، تیتر H2/H3، لیست نقطهای/شمارهای، نقلقول، لینک (مسدودسازی javascript: با isAllowedUri)، واگرد/ازنو - درج تصویر داخل متن: دکمه آپلود، چسباندن یا کشیدن فایل — ذخیره در MinIO از طریق /api/admin/upload و درج URL نسبی /media/... - ذخیره خودکار: debounce ۸۰۰ms + flush هنگام بستن کشو - کشوی مقاله عریضتر (max-w-3xl)؛ فیلد body مقالات بهجای لیست متنی، ویرایشگر WYSIWYG رندر میکند - رندر صفحه مقاله: تبدیل سند به HTML با @tiptap/static-renderer (فقط تگهای شناختهشده — بدون ریسک XSS) + استایلهای .article-body (تیتر با خط مسی، لیست، نقلقول، تصویر) + پشتیبانی از ردیفهای قدیمی - وابستگیهای جدید: @tiptap/react، @tiptap/pm، @tiptap/starter-kit، @tiptap/extension-image، @tiptap/extension-link، @tiptap/static-renderer
This commit is contained in:
@@ -1,11 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { ArrowLeftIcon, ChevronLeftIcon, ClockIcon } from 'lucide-react';
|
||||
import { renderToHTMLString } from '@tiptap/static-renderer';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
import { ButtonLink } from '../components/Button';
|
||||
import { ArticleCard } from '../components/ArticleCard';
|
||||
import { Newsletter } from '../components/home/Newsletter';
|
||||
import { useSite } from '../api';
|
||||
import { ARTICLE_EXTENSIONS } from '../components/admin/ArticleEditor';
|
||||
import type { Article } from '../data/site';
|
||||
|
||||
/** بدنه مقاله را به HTML تبدیل میکند — سند TipTap جدید یا پاراگرافهای قدیمی */
|
||||
function renderBody(article: Article): { html: string | null; legacy: string[] } {
|
||||
const body = article.body as unknown;
|
||||
if (Array.isArray(body)) return { html: null, legacy: body };
|
||||
if (body && typeof body === 'object' && (body as { type?: string }).type === 'doc') {
|
||||
try {
|
||||
return {
|
||||
html: renderToHTMLString({ extensions: ARTICLE_EXTENSIONS, content: body as never }),
|
||||
legacy: [],
|
||||
};
|
||||
} catch {
|
||||
return { html: null, legacy: [] };
|
||||
}
|
||||
}
|
||||
return { html: null, legacy: [] };
|
||||
}
|
||||
|
||||
export function ArticleDetail() {
|
||||
const { slug } = useParams<{slug: string;}>();
|
||||
@@ -25,6 +45,7 @@ export function ArticleDetail() {
|
||||
|
||||
}
|
||||
|
||||
const { html, legacy } = renderBody(article);
|
||||
const related = articles.filter((a) => a.slug !== article.slug).slice(0, 3);
|
||||
|
||||
return (
|
||||
@@ -36,7 +57,7 @@ export function ArticleDetail() {
|
||||
className="absolute inset-0"
|
||||
style={{ background: 'linear-gradient(to top, #27221c 6%, rgba(39,34,28,0.15) 70%)' }}
|
||||
aria-hidden="true" />
|
||||
|
||||
|
||||
<div className="relative mx-auto w-full max-w-site px-6 pb-16 lg:px-10">
|
||||
<nav aria-label="مسیر صفحه" className="flex items-center gap-2 text-xs font-light text-ivory/50">
|
||||
<Link to="/" className="transition-colors hover:text-copper">
|
||||
@@ -67,10 +88,18 @@ export function ArticleDetail() {
|
||||
{article.excerpt}
|
||||
</p>
|
||||
</Reveal>
|
||||
{article.body.map((paragraph, i) =>
|
||||
<Reveal key={paragraph} delay={0.03 * i}>
|
||||
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||
{html ? (
|
||||
<Reveal delay={0.05}>
|
||||
<div
|
||||
className="article-body mt-4 text-base font-light text-ivory/70"
|
||||
dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Reveal>
|
||||
) : (
|
||||
legacy.map((paragraph, i) =>
|
||||
<Reveal key={i} delay={0.03 * i}>
|
||||
<p className="mt-8 text-base font-light leading-10 text-ivory/70">{paragraph}</p>
|
||||
</Reveal>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
@@ -92,4 +121,4 @@ export function ArticleDetail() {
|
||||
<Newsletter />
|
||||
</>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user