You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

97 lines
3.9 KiB

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
const AR_FA_TO_EN_DIGITS: Record<string, string> = {
'۰': '0', '۱': '1', '۲': '2', '۳': '3', '۴': '4',
'۵': '5', '۶': '6', '۷': '7', '۸': '8', '۹': '9',
'٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4',
'٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9',
}
/**
* تبدیل تمامی ارقام فارسی و عربی به ارقام استاندارد انگلیسی
*/
export const toEn = (n: number | string | null | undefined): string => {
if (n == null) return ''
return String(n).replace(/[۰-۹٠-٩]/g, (d) => AR_FA_TO_EN_DIGITS[d] ?? d)
}
/**
* طبق استاندارد پنل عقیله، کلیه اعداد در سراسر پنل به صورت انگلیسی نمایش داده می‌شوند.
* این تابع به عنوان آداپتور به toEn متصل است تا هیچ کدی نشکند و ارقام انگلیسی تولید شوند.
*/
export const toFa = toEn
/**
* استخراج URL تصویر از thumbnailِ خامِ بک‌اند.
* ورودی می‌تواند رشته باشد یا آبجکتی مثل { sm, md, lg } یا { thumb, medium, large }.
*/
export function thumbnailUrl(thumbnail: unknown): string | null {
if (!thumbnail) return null
if (typeof thumbnail === 'string') return thumbnail.trim() || null
if (typeof thumbnail === 'object') {
const o = thumbnail as Record<string, unknown>
const v = o.md ?? o.medium ?? o.lg ?? o.large ?? o.sm ?? o.thumb ?? Object.values(o)[0]
return v != null ? String(v).trim() || null : null
}
return null
}
export const REMOTE_MEDIA_ORIGIN = 'https://aqila.newhorizonco.uk'
/**
* دریافت آدرس جایگزین از سرور اصلی ریموت در صورتی که تصویر در لوکال هاست موجود نباشد
*/
export function getMediaFallbackUrl(url: string | null | undefined): string | null {
if (!url) return null
const trimmed = String(url).trim()
if (!trimmed || trimmed.startsWith('data:') || trimmed.startsWith('blob:')) return null
if (trimmed.startsWith(REMOTE_MEDIA_ORIGIN)) return trimmed
const cleanPath = trimmed
.replace(/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\//i, '')
.replace(/^\/?(media\/)?/, '')
return `${REMOTE_MEDIA_ORIGIN}/media/${cleanPath}`
}
/**
* استانداردسازی آدرس تصاویر و فایل‌های چندرسانه‌ای
* آدرس‌های نسبی، فاقد پروتکل یا لوکال‌هاست را به آدرس معتبر و قابل رندر تبدیل می‌کند.
*/
export function resolveMediaUrl(url: string | null | undefined): string {
if (!url) return ''
const trimmed = String(url).trim()
if (!trimmed) return ''
if (trimmed.startsWith('data:') || trimmed.startsWith('blob:')) {
return trimmed
}
// تبدیل URL کامل لوکال‌هاست به مسیر نسبی تا از طریق پراکسیِ Vite بدون خطای CORS بارگذاری شود
const localMatch = trimmed.match(/^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?\/(media|static)\/(.*)$/i)
if (localMatch && localMatch[1] && localMatch[2]) {
return `/${localMatch[1]}/${localMatch[2]}`
}
if (/^https?:\/\//i.test(trimmed)) {
return trimmed
}
if (trimmed.startsWith('media/') || trimmed.startsWith('static/')) {
return `/${trimmed}`
}
if (trimmed.startsWith('factors/')) {
return `/media/${trimmed}`
}
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`
}
/** ثانیه → «م:ثث» با ارقام فارسی (مثلاً ۳:۰۵). مقادیر نامعتبر → ۰:۰۰ */
export function formatTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0) seconds = 0
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return toFa(`${m}:${String(s).padStart(2, '0')}`)
}