Update blog editor features

This commit is contained in:
fatemeh sane
2026-09-13 10:39:25 +03:30
parent 20c95e73b3
commit bb6c586d99
21 changed files with 1235 additions and 187 deletions
+69
View File
@@ -0,0 +1,69 @@
import DateObject from 'react-date-object'
import gregorian from 'react-date-object/calendars/gregorian'
import persian from 'react-date-object/calendars/persian'
import persian_fa from 'react-date-object/locales/persian_fa'
/** قالب رسمی نمایش تاریخ جلالی — تقویم شمسی و اعداد فارسی */
export const JALALI_DATE_FORMATTER = new Intl.DateTimeFormat('fa-IR', {
dateStyle: 'medium',
calendar: 'persian',
})
/** ساعت به وقت جلالی (فارسی) */
export const JALALI_TIME_FORMATTER = new Intl.DateTimeFormat('fa-IR', {
hour: '2-digit',
minute: '2-digit',
})
const ISO_DATE = /^\d{4}-\d{2}-\d{2}([T\s].*)?$/
/**
* نمایش هر مقدار تاریخ به شکل جلالی (مثل «۲۲ شهریور ۱۴۰۵»).
* رشته‌هایی که تاریخ نیستند (مثل تاریخ شمسی دست‌نویس) دست‌نخورده برمی‌گردند.
*/
export function formatJalali(value?: string | number | Date | null, fallback = ''): string {
if (value === null || value === undefined || value === '') return fallback
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? fallback : JALALI_DATE_FORMATTER.format(value)
}
if (typeof value === 'number') {
const fromNumber = new Date(value)
return Number.isNaN(fromNumber.getTime()) ? fallback : JALALI_DATE_FORMATTER.format(fromNumber)
}
const raw = String(value).trim()
if (!raw) return fallback
if (ISO_DATE.test(raw)) {
const parsed = new Date(raw)
if (!Number.isNaN(parsed.getTime())) return JALALI_DATE_FORMATTER.format(parsed)
}
return raw
}
/** رشته ISO میلادی ذخیره‌شده (YYYY-MM-DD) → DateObject شمسی برای انتخابگر تاریخ */
export function toJalaliDateObject(value?: string | null): DateObject | null {
const raw = (value ?? '').trim()
if (!raw) return null
if (ISO_DATE.test(raw)) {
const parsed = new DateObject({ date: raw })
if (parsed.isValid) return parsed.convert(persian, persian_fa)
}
// تاریخ‌های شمسی دست‌نویس مثل ۱۴۰۳/۰۵/۱۲
const manual = new DateObject({ date: raw, calendar: persian, locale: persian_fa })
return manual.isValid ? manual : null
}
/** انتخاب انتخابگر تاریخ → رشته ISO میلادی برای ذخیره در دیتابیس */
export function toISODate(value?: DateObject | Date | null): string {
if (!value) return ''
const asObject = value instanceof Date ? new DateObject({ date: value }) : value
const gregorianDate = asObject.convert(gregorian)
const pad = (n: number) => String(n).padStart(2, '0')
return `${gregorianDate.year}-${pad(gregorianDate.month.number)}-${pad(gregorianDate.day)}`
}
+37
View File
@@ -0,0 +1,37 @@
import slugify from 'slugify'
/**
* تبدیل متن (فارسی یا انگلیسی) به شناسه/لنگر امن.
* slugify حروف فارسی و عربی را به معادل لاتین ترانویسی می‌کند تا لنگرهای
* فهرست مطالب در همه مرورگرها ASCII، پایدار و قابل اشتراک‌گذاری بمانند.
*/
export function slugifyFa(input: string): string {
const normalized = (input ?? '')
.toString()
.normalize('NFC')
// نیم‌فاصله، اتصال‌دهنده و نویسه‌های نامرئی جهت → فاصله
.replace(/[\u200b-\u200f\u202a-\u202e]/g, ' ')
// یکسان‌سازی حروف عربی با معادل فارسی
.replace(/[يى]/g, 'ی')
.replace(/ك/g, 'ک')
.replace(/[ۀة]/g, 'ه')
.trim()
const slug = slugify(normalized, {
lower: true,
trim: true,
replacement: '-',
// فقط حروف/اعداد/فاصله/خط تیره بماند — بقیه نشانه‌ها حذف شوند
remove: /[^\w\s-]/g,
})
return slug.replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '')
}
/** ساخت شناسه یکتا — تیترهای تکراری پسوند عددی می‌گیرند (مثل about، about-2) */
export function uniqueSlug(base: string, used: Map<string, number>): string {
const key = base || 'section'
const count = used.get(key) ?? 0
used.set(key, count + 1)
return count === 0 ? key : `${key}-${count + 1}`
}