108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
// Helpers for Persian (Shamsi) dates and relative time labels used across the site.
|
||
|
||
const PERSIAN_DIGITS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
|
||
|
||
/** Convert any latin digits in a string (or a number) to Persian digits. */
|
||
export const toPersianDigits = (value: string | number): string =>
|
||
String(value).replace(/\d/g, (digit) => PERSIAN_DIGITS[Number(digit)] ?? digit);
|
||
|
||
type DatedPost = { publishedAt?: string; slug: string; time: string };
|
||
|
||
/**
|
||
* Resolve the publish timestamp of a post.
|
||
*
|
||
* Posts created by the TASS agent embed `Date.now()` at the end of their slug,
|
||
* so we can recover a real timestamp even for older posts that only stored the
|
||
* placeholder string "هماکنون".
|
||
*/
|
||
export const getPublishedAt = (post: DatedPost): Date | null => {
|
||
if (post.publishedAt) {
|
||
const date = new Date(post.publishedAt);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
|
||
const match = post.slug.match(/(\d{13})$/);
|
||
if (match) {
|
||
const date = new Date(Number(match[1]));
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
/** Derive an ISO timestamp for a post, or null when none can be inferred. */
|
||
export const derivePublishedAt = (post: DatedPost): string | undefined => {
|
||
const date = getPublishedAt(post);
|
||
return date ? date.toISOString() : undefined;
|
||
};
|
||
|
||
/** Exact Shamsi date + time, e.g. "۱۴ تیر ۱۴۰۵، ۱۴:۳۰". */
|
||
export const formatShamsiDate = (date: Date): string => {
|
||
const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
|
||
return toPersianDigits(formatter.format(date));
|
||
};
|
||
|
||
/** Short Shamsi date without time, e.g. "۱۴ تیر ۱۴۰۵". */
|
||
export const formatShamsiDay = (date: Date): string => {
|
||
const formatter = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
});
|
||
|
||
return toPersianDigits(formatter.format(date));
|
||
};
|
||
|
||
/**
|
||
* Relative Persian time label: "هماکنون", "۲ دقیقه پیش", "۱۰ دقیقه پیش",
|
||
* "۳ ساعت پیش", "۵ روز پیش" — and falls back to an exact Shamsi date for
|
||
* anything older than a month.
|
||
*/
|
||
export const formatRelativeTime = (date: Date): string => {
|
||
const now = Date.now();
|
||
const diffSeconds = Math.max(0, Math.round((now - date.getTime()) / 1000));
|
||
|
||
if (diffSeconds < 60) {
|
||
return 'هماکنون';
|
||
}
|
||
|
||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||
|
||
if (diffMinutes < 60) {
|
||
return `${toPersianDigits(diffMinutes)} دقیقه پیش`;
|
||
}
|
||
|
||
const diffHours = Math.floor(diffMinutes / 60);
|
||
|
||
if (diffHours < 24) {
|
||
return `${toPersianDigits(diffHours)} ساعت پیش`;
|
||
}
|
||
|
||
const diffDays = Math.floor(diffHours / 24);
|
||
|
||
if (diffDays < 30) {
|
||
return `${toPersianDigits(diffDays)} روز پیش`;
|
||
}
|
||
|
||
return formatShamsiDay(date);
|
||
};
|
||
|
||
/** Relative label for a post, falling back to its stored `time` string. */
|
||
export const formatPostRelativeTime = (post: DatedPost): string => {
|
||
const date = getPublishedAt(post);
|
||
return date ? formatRelativeTime(date) : post.time;
|
||
};
|
||
|
||
/** Exact Shamsi label for a post, falling back to its stored `time` string. */
|
||
export const formatPostShamsiDate = (post: DatedPost): string => {
|
||
const date = getPublishedAt(post);
|
||
return date ? formatShamsiDate(date) : post.time;
|
||
};
|