Port modular price, product, and PWA fixes into boursecu

Sync live quote handling, SSE product images, store hours, file URL normalization, PWA icon cache, TV monitor, about software, SuperAdmin menu gates, price alerts, and related UX from modular while preserving boursecu branding and config constraints.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 08:26:52 -04:00
co-authored by Cursor
parent e9012e26fb
commit 9356a2ab5e
112 changed files with 6044 additions and 583 deletions
+65 -10
View File
@@ -8,28 +8,83 @@ export interface ProductFile {
full_src?: string
}
function projectBase(): string {
return resolveProjectBase().replace(/\/$/, '')
}
function storageRelativePath(value: string): string | null {
const match = value.match(/(?:^|\/)(storage\/.+)$/i)
return match ? match[1].replace(/^\/+/, '') : null
}
function joinProjectUrl(relative: string): string {
const path = relative.replace(/^\//, '')
let base = projectBase()
if (!base && typeof window !== 'undefined') {
base = `${window.location.origin}/server`
}
if (!base) {
return `/server/${path}`
}
base = base.replace(/\/$/, '')
if (path.startsWith('storage/') && !/\/(server|api)(\/|$)/i.test(base)) {
base = `${base}/server`
}
let url = `${base}/${path}`
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
url = url.replace(/^http:\/\//i, 'https://')
}
return url
}
/**
* URL فایل روی API — مسیرهای `storage/` و آدرس‌های localhost/http
* را به `node_server` می‌چسباند تا روی اندروید (PWA/TWA) لود شوند.
*/
export function normalizePublicFileUrl(raw?: string | null): string | null {
if (raw == null) return null
const trimmed = String(raw).trim()
if (!trimmed || trimmed.toLowerCase() === 'null' || trimmed.toLowerCase() === 'undefined') {
return null
}
if (/^(data:|blob:)/i.test(trimmed)) return trimmed
const storagePath = storageRelativePath(trimmed)
if (storagePath) {
return joinProjectUrl(storagePath)
}
if (/^https?:\/\//i.test(trimmed)) {
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
return trimmed.replace(/^http:\/\//i, 'https://')
}
return trimmed
}
return joinProjectUrl(trimmed)
}
export function getFileUrl(file?: ProductFile | null): string | null {
if (!file) return null
for (const value of [file.full_src, file.url, file.src]) {
if (!value) continue
if (/^https?:\/\//i.test(value)) return value
const normalized = normalizePublicFileUrl(value)
if (normalized) return normalized
}
const relative = file.src || file.full_src
if (!relative) return null
const base = resolveProjectBase().replace(/\/$/, '')
if (!base) return relative.startsWith('/') ? relative : `/${relative}`
const path = String(relative).replace(/^\//, '')
return `${base}/${path}`
return null
}
/** Resolve profile avatar URL from GET /customer/me payload */
export function getUserAvatarUrl(user?: User | null): string | null {
if (!user) return null
if (typeof user.avatar_url === 'string' && user.avatar_url) return user.avatar_url
if (typeof user.full_src === 'string' && user.full_src) return user.full_src
if (typeof user.avatar_url === 'string' && user.avatar_url) {
return normalizePublicFileUrl(user.avatar_url)
}
if (typeof user.full_src === 'string' && user.full_src) {
return normalizePublicFileUrl(user.full_src)
}
const nested = user.avatar || user.file
return getFileUrl(nested as ProductFile | undefined)
}