diff --git a/frontend/src/components/ArticleCard.tsx b/frontend/src/components/ArticleCard.tsx index c651017..f8dc1e1 100644 --- a/frontend/src/components/ArticleCard.tsx +++ b/frontend/src/components/ArticleCard.tsx @@ -2,12 +2,12 @@ import React from 'react' import { Link } from 'react-router-dom' import { ArrowLeftIcon, ClockIcon } from 'lucide-react' import type { Article } from '../data/site' -import { formatJalali } from '../utils/date' +import { formatJalali, formatEnDate } from '../utils/date' import { useLang } from '../i18n/lang' import { toPersianDigits } from '../utils/digits' export function ArticleCard({ article }: { article: Article }) { - const { t, href } = useLang() + const { t, href, lang } = useLang() return ( - {formatJalali(article.date)} + {lang === 'en' ? formatEnDate(article.date) : formatJalali(article.date)} - {toPersianDigits(article.read)} + {lang === 'en' ? article.read : toPersianDigits(article.read)} diff --git a/frontend/src/components/ArticleMeta.tsx b/frontend/src/components/ArticleMeta.tsx index bf5a078..12490ed 100644 --- a/frontend/src/components/ArticleMeta.tsx +++ b/frontend/src/components/ArticleMeta.tsx @@ -1,6 +1,6 @@ import { CalendarIcon, ClockIcon } from 'lucide-react' import type { Article } from '../data/site' -import { JALALI_TIME_FORMATTER, formatJalali } from '../utils/date' +import { JALALI_TIME_FORMATTER, formatJalali, formatEnDate } from '../utils/date' import { toPersianDigits } from '../utils/digits' import { useLang } from '../i18n/lang' @@ -20,9 +20,8 @@ export function ArticleMeta({ article, className = '' }: ArticleMetaProps) { const initial = author.charAt(0) // فارسی: تاریخ شمسی با ارقام فارسی؛ انگلیسی: تاریخ میلادی لاتین - const enDateLabel = rawDate - ? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(rawDate)) - : new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date()) + const enDateLabel = formatEnDate(rawDate) + || new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date()) const todayLabel = `${formatJalali(new Date())} — ساعت ${JALALI_TIME_FORMATTER.format(new Date())}` const dateLabel = lang === 'en' ? enDateLabel : formatJalali(rawDate, todayLabel) diff --git a/frontend/src/components/admin/ImageNodeView.tsx b/frontend/src/components/admin/ImageNodeView.tsx index d2792a9..52a6dd5 100644 --- a/frontend/src/components/admin/ImageNodeView.tsx +++ b/frontend/src/components/admin/ImageNodeView.tsx @@ -19,6 +19,35 @@ const DELETE_BUTTON_CLASS = const INLINE_FIELD_CLASS = 'w-full border border-ivory/15 bg-ink px-3 py-1.5 text-xs font-light text-ivory placeholder:text-ivory/30 focus:border-copper focus:outline-none' +/** + * فیلد متصل به attrs گره — چون مقدار جدید از پروسس‌میرور با یک رندر تأخیر + * می‌رسد، ورودی کنترل‌شدهٔ مستقیم کرسر را به آخر متن می‌اندازد. اینجا مقدار + * محلی نگه داشته می‌شود و فقط وقتی ورودی فوکوس ندارد از بیرون همگام می‌شود. + */ +function useAttrField( + attrValue: string, + commit: (value: string) => void, + externalRef?: React.MutableRefObject, +) { + const ownRef = useRef(null) + const ref = externalRef ?? ownRef + const [local, setLocal] = useState(attrValue) + + useEffect(() => { + if (document.activeElement !== ref.current) setLocal(attrValue) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [attrValue]) + + return { + ref, + value: local, + onChange: (event: React.ChangeEvent) => { + setLocal(event.target.value) + commit(event.target.value) + }, + } +} + const ADD_CAPTION_CLASS = 'mt-1.5 inline-flex items-center gap-1.5 border border-dashed border-ivory/25 px-3 py-1 text-[11px] font-light text-ivory/50 transition-colors duration-150 hover:border-copper hover:text-copper' @@ -52,6 +81,10 @@ export function ImageNodeView({ node, updateAttributes, deleteNode, selected }: const [captionOpen, setCaptionOpen] = useState(true) const [rehosting, setRehosting] = useState(false) const captionInputRef = useRef(null) + const altField = useAttrField(alt ?? '', (v) => updateAttributes({ alt: v })) + const titleField = useAttrField(title ?? '', (v) => updateAttributes({ title: v })) + const captionField = useAttrField(caption ?? '', (v) => updateAttributes({ caption: v }), captionInputRef) + const captionHrefField = useAttrField(captionHref ?? '', (v) => updateAttributes({ caption_href: v })) const focusOnOpen = useRef(false) const showCaption = captionOpen || hasCaption const external = isExternalImage(src) @@ -124,16 +157,14 @@ export function ImageNodeView({ node, updateAttributes, deleteNode, selected }:
updateAttributes({ alt: event.target.value })} + {...altField} placeholder="نام تصویر / متن جایگزین (Alt)…" aria-label="متن جایگزین تصویر" className={INLINE_FIELD_CLASS} /> updateAttributes({ title: event.target.value })} + {...titleField} placeholder="عنوان تصویر (Title)…" aria-label="عنوان تصویر" className={INLINE_FIELD_CLASS} @@ -143,18 +174,15 @@ export function ImageNodeView({ node, updateAttributes, deleteNode, selected }: {showCaption ? (
updateAttributes({ caption: event.target.value })} + {...captionField} placeholder="توضیح تصویر (کپشن)…" aria-label="کپشن تصویر" className={INLINE_FIELD_CLASS} /> updateAttributes({ caption_href: event.target.value })} + {...captionHrefField} placeholder="لینک کپشن (اختیاری) — مثل منبع عکس…" aria-label="لینک کپشن تصویر" dir="ltr" diff --git a/frontend/src/pages/Blog.tsx b/frontend/src/pages/Blog.tsx index 623fdb0..fd27eea 100644 --- a/frontend/src/pages/Blog.tsx +++ b/frontend/src/pages/Blog.tsx @@ -9,7 +9,7 @@ import { useSite } from '../api' import { useLang } from '../i18n/lang' import { EditableItem } from '../admin/Editable' import { useAdmin } from '../admin/AdminContext' -import { formatJalali } from '../utils/date' +import { formatJalali, formatEnDate } from '../utils/date' import { toPersianDigits } from '../utils/digits' import { PlusIcon } from 'lucide-react' @@ -96,9 +96,7 @@ export function Blog() {

{featured.excerpt}

- {lang === 'en' - ? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(featured.date)) - : formatJalali(featured.date)} + {lang === 'en' ? formatEnDate(featured.date) : formatJalali(featured.date)} {lang === 'en' ? featured.read : toPersianDigits(featured.read)} diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index 94ca961..114d7c5 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -97,3 +97,11 @@ export function toPublishedISO(value?: string | null): string | null { }) return jalali.isValid ? toISODate(jalali) : null } + +/** تاریخ میلادی انگلیسی برای سایت /en — با محافظ مقادیر نامعتبر/خالی */ +export function formatEnDate(value?: string | null): string { + if (!value) return '' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '' + return new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(date) +}