fix(blog): تاریخ/زمان مطالعه کارتها در /en انگلیسی شد؛ کرسر فیلدهای تصویر دیگر به آخر متن نمیپرد (مقدار محلی با همگامسازی فوکوسآگاه)
This commit is contained in:
@@ -2,12 +2,12 @@ import React from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
|
import { ArrowLeftIcon, ClockIcon } from 'lucide-react'
|
||||||
import type { Article } from '../data/site'
|
import type { Article } from '../data/site'
|
||||||
import { formatJalali } from '../utils/date'
|
import { formatJalali, formatEnDate } from '../utils/date'
|
||||||
import { useLang } from '../i18n/lang'
|
import { useLang } from '../i18n/lang'
|
||||||
import { toPersianDigits } from '../utils/digits'
|
import { toPersianDigits } from '../utils/digits'
|
||||||
|
|
||||||
export function ArticleCard({ article }: { article: Article }) {
|
export function ArticleCard({ article }: { article: Article }) {
|
||||||
const { t, href } = useLang()
|
const { t, href, lang } = useLang()
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={href(`/blog/${article.slug}`)}
|
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">
|
<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 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">
|
<span className="flex items-center gap-2">
|
||||||
<ClockIcon className="h-3.5 w-3.5 text-copper/80" />
|
<ClockIcon className="h-3.5 w-3.5 text-copper/80" />
|
||||||
{toPersianDigits(article.read)}
|
{lang === 'en' ? article.read : toPersianDigits(article.read)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CalendarIcon, ClockIcon } from 'lucide-react'
|
import { CalendarIcon, ClockIcon } from 'lucide-react'
|
||||||
import type { Article } from '../data/site'
|
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 { toPersianDigits } from '../utils/digits'
|
||||||
import { useLang } from '../i18n/lang'
|
import { useLang } from '../i18n/lang'
|
||||||
|
|
||||||
@@ -20,9 +20,8 @@ export function ArticleMeta({ article, className = '' }: ArticleMetaProps) {
|
|||||||
const initial = author.charAt(0)
|
const initial = author.charAt(0)
|
||||||
|
|
||||||
// فارسی: تاریخ شمسی با ارقام فارسی؛ انگلیسی: تاریخ میلادی لاتین
|
// فارسی: تاریخ شمسی با ارقام فارسی؛ انگلیسی: تاریخ میلادی لاتین
|
||||||
const enDateLabel = rawDate
|
const enDateLabel = formatEnDate(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())
|
||||||
: 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 todayLabel = `${formatJalali(new Date())} — ساعت ${JALALI_TIME_FORMATTER.format(new Date())}`
|
||||||
const dateLabel = lang === 'en' ? enDateLabel : formatJalali(rawDate, todayLabel)
|
const dateLabel = lang === 'en' ? enDateLabel : formatJalali(rawDate, todayLabel)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,35 @@ const DELETE_BUTTON_CLASS =
|
|||||||
const INLINE_FIELD_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'
|
'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 =
|
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'
|
'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 [captionOpen, setCaptionOpen] = useState(true)
|
||||||
const [rehosting, setRehosting] = useState(false)
|
const [rehosting, setRehosting] = useState(false)
|
||||||
const captionInputRef = useRef<HTMLInputElement>(null)
|
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 focusOnOpen = useRef(false)
|
||||||
const showCaption = captionOpen || hasCaption
|
const showCaption = captionOpen || hasCaption
|
||||||
const external = isExternalImage(src)
|
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">
|
<div contentEditable={false} className="mt-1.5 grid gap-1.5 sm:grid-cols-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={alt ?? ''}
|
{...altField}
|
||||||
onChange={(event) => updateAttributes({ alt: event.target.value })}
|
|
||||||
placeholder="نام تصویر / متن جایگزین (Alt)…"
|
placeholder="نام تصویر / متن جایگزین (Alt)…"
|
||||||
aria-label="متن جایگزین تصویر"
|
aria-label="متن جایگزین تصویر"
|
||||||
className={INLINE_FIELD_CLASS}
|
className={INLINE_FIELD_CLASS}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={title ?? ''}
|
{...titleField}
|
||||||
onChange={(event) => updateAttributes({ title: event.target.value })}
|
|
||||||
placeholder="عنوان تصویر (Title)…"
|
placeholder="عنوان تصویر (Title)…"
|
||||||
aria-label="عنوان تصویر"
|
aria-label="عنوان تصویر"
|
||||||
className={INLINE_FIELD_CLASS}
|
className={INLINE_FIELD_CLASS}
|
||||||
@@ -143,18 +174,15 @@ export function ImageNodeView({ node, updateAttributes, deleteNode, selected }:
|
|||||||
{showCaption ? (
|
{showCaption ? (
|
||||||
<figcaption className="article-figure__caption" contentEditable={false}>
|
<figcaption className="article-figure__caption" contentEditable={false}>
|
||||||
<input
|
<input
|
||||||
ref={captionInputRef}
|
|
||||||
type="text"
|
type="text"
|
||||||
value={caption ?? ''}
|
{...captionField}
|
||||||
onChange={(event) => updateAttributes({ caption: event.target.value })}
|
|
||||||
placeholder="توضیح تصویر (کپشن)…"
|
placeholder="توضیح تصویر (کپشن)…"
|
||||||
aria-label="کپشن تصویر"
|
aria-label="کپشن تصویر"
|
||||||
className={INLINE_FIELD_CLASS}
|
className={INLINE_FIELD_CLASS}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={captionHref ?? ''}
|
{...captionHrefField}
|
||||||
onChange={(event) => updateAttributes({ caption_href: event.target.value })}
|
|
||||||
placeholder="لینک کپشن (اختیاری) — مثل منبع عکس…"
|
placeholder="لینک کپشن (اختیاری) — مثل منبع عکس…"
|
||||||
aria-label="لینک کپشن تصویر"
|
aria-label="لینک کپشن تصویر"
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useSite } from '../api'
|
|||||||
import { useLang } from '../i18n/lang'
|
import { useLang } from '../i18n/lang'
|
||||||
import { EditableItem } from '../admin/Editable'
|
import { EditableItem } from '../admin/Editable'
|
||||||
import { useAdmin } from '../admin/AdminContext'
|
import { useAdmin } from '../admin/AdminContext'
|
||||||
import { formatJalali } from '../utils/date'
|
import { formatJalali, formatEnDate } from '../utils/date'
|
||||||
import { toPersianDigits } from '../utils/digits'
|
import { toPersianDigits } from '../utils/digits'
|
||||||
import { PlusIcon } from 'lucide-react'
|
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>
|
<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">
|
<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 className="flex items-center gap-4">
|
||||||
<span>{lang === 'en'
|
<span>{lang === 'en' ? formatEnDate(featured.date) : formatJalali(featured.date)}</span>
|
||||||
? new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(featured.date))
|
|
||||||
: formatJalali(featured.date)}</span>
|
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<ClockIcon className="h-3.5 w-3.5" />
|
<ClockIcon className="h-3.5 w-3.5" />
|
||||||
{lang === 'en' ? featured.read : toPersianDigits(featured.read)}
|
{lang === 'en' ? featured.read : toPersianDigits(featured.read)}
|
||||||
|
|||||||
@@ -97,3 +97,11 @@ export function toPublishedISO(value?: string | null): string | null {
|
|||||||
})
|
})
|
||||||
return jalali.isValid ? toISODate(jalali) : 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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user