fix(blog): تاریخ/زمان مطالعه کارت‌ها در /en انگلیسی شد؛ کرسر فیلدهای تصویر دیگر به آخر متن نمی‌پرد (مقدار محلی با همگام‌سازی فوکوس‌آگاه)

This commit is contained in:
aramesh-site deploy
2026-10-07 12:30:28 +00:00
parent bef132f82f
commit 9dab4f929c
5 changed files with 54 additions and 21 deletions
+4 -4
View File
@@ -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 (
<Link
to={href(`/blog/${article.slug}`)}
@@ -39,10 +39,10 @@ export function ArticleCard({ article }: { article: Article }) {
<div className="mt-auto flex items-center justify-between border-t border-white/10 pt-6 md:pt-8 text-xs font-light text-ivory/50">
<span className="flex items-center gap-4">
<span>{formatJalali(article.date)}</span>
<span>{lang === 'en' ? formatEnDate(article.date) : formatJalali(article.date)}</span>
<span className="flex items-center gap-2">
<ClockIcon className="h-3.5 w-3.5 text-copper/80" />
{toPersianDigits(article.read)}
{lang === 'en' ? article.read : toPersianDigits(article.read)}
</span>
</span>
+3 -4
View File
@@ -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)
@@ -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<HTMLInputElement | null>,
) {
const ownRef = useRef<HTMLInputElement | null>(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<HTMLInputElement>) => {
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<HTMLInputElement>(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 }:
<div contentEditable={false} className="mt-1.5 grid gap-1.5 sm:grid-cols-2">
<input
type="text"
value={alt ?? ''}
onChange={(event) => updateAttributes({ alt: event.target.value })}
{...altField}
placeholder="نام تصویر / متن جایگزین (Alt)…"
aria-label="متن جایگزین تصویر"
className={INLINE_FIELD_CLASS}
/>
<input
type="text"
value={title ?? ''}
onChange={(event) => 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 ? (
<figcaption className="article-figure__caption" contentEditable={false}>
<input
ref={captionInputRef}
type="text"
value={caption ?? ''}
onChange={(event) => updateAttributes({ caption: event.target.value })}
{...captionField}
placeholder="توضیح تصویر (کپشن)…"
aria-label="کپشن تصویر"
className={INLINE_FIELD_CLASS}
/>
<input
type="url"
value={captionHref ?? ''}
onChange={(event) => updateAttributes({ caption_href: event.target.value })}
{...captionHrefField}
placeholder="لینک کپشن (اختیاری) — مثل منبع عکس…"
aria-label="لینک کپشن تصویر"
dir="ltr"
+2 -4
View File
@@ -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() {
<p className="mt-6 text-sm font-light leading-10 text-ivory/65">{featured.excerpt}</p>
<div className="mt-10 flex items-center justify-between border-t border-ivory/10 pt-8 text-xs font-light text-ivory/50">
<span className="flex items-center gap-4">
<span>{lang === 'en'
? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(featured.date))
: formatJalali(featured.date)}</span>
<span>{lang === 'en' ? formatEnDate(featured.date) : formatJalali(featured.date)}</span>
<span className="flex items-center gap-1.5">
<ClockIcon className="h-3.5 w-3.5" />
{lang === 'en' ? featured.read : toPersianDigits(featured.read)}
+8
View File
@@ -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)
}