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:
+65
-10
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user