d146e45e2f
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files. The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
204 lines
5.6 KiB
TypeScript
Vendored
204 lines
5.6 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { useEffect, useCallback } from 'react'
|
|
import {
|
|
useSystemConfigStore,
|
|
type CurrencyConfig,
|
|
type CurrencyDisplayType,
|
|
type SystemConfig,
|
|
DEFAULT_CURRENCY_CONFIG,
|
|
} from '@/stores/system-config-store'
|
|
import { DEFAULT_SYSTEM_NAME, DEFAULT_LOGO } from '@/lib/constants'
|
|
import { applyFaviconToDom } from '@/lib/dom-utils'
|
|
|
|
interface UseSystemConfigOptions {
|
|
/** Automatically fetch config from backend (use only in root component) */
|
|
autoLoad?: boolean
|
|
}
|
|
|
|
interface StatusApiResponse {
|
|
success: boolean
|
|
data: {
|
|
system_name?: string
|
|
logo?: string
|
|
footer_html?: string
|
|
demo_site_enabled?: boolean
|
|
display_token_stat_enabled?: boolean
|
|
display_in_currency?: boolean
|
|
quota_display_type?: CurrencyDisplayType
|
|
quota_per_unit?: number
|
|
usd_exchange_rate?: number
|
|
custom_currency_symbol?: string
|
|
custom_currency_exchange_rate?: number
|
|
}
|
|
}
|
|
|
|
function toNumber(value: unknown, fallback: number): number {
|
|
if (typeof value === 'number' && !Number.isNaN(value)) return value
|
|
if (typeof value === 'string') {
|
|
const parsed = Number(value)
|
|
if (!Number.isNaN(parsed)) return parsed
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
/**
|
|
* Map `/api/status` response data to our persisted system config structure
|
|
*/
|
|
export function mapStatusDataToConfig(
|
|
data: StatusApiResponse['data'] | undefined
|
|
): Partial<SystemConfig> {
|
|
if (!data) return {}
|
|
|
|
const quotaDisplayType =
|
|
(data.quota_display_type as CurrencyDisplayType | undefined) ??
|
|
DEFAULT_CURRENCY_CONFIG.quotaDisplayType
|
|
|
|
const currency: CurrencyConfig = {
|
|
displayInCurrency:
|
|
data.display_in_currency ?? DEFAULT_CURRENCY_CONFIG.displayInCurrency,
|
|
quotaDisplayType,
|
|
quotaPerUnit: toNumber(
|
|
data.quota_per_unit,
|
|
DEFAULT_CURRENCY_CONFIG.quotaPerUnit
|
|
),
|
|
usdExchangeRate: toNumber(
|
|
data.usd_exchange_rate,
|
|
DEFAULT_CURRENCY_CONFIG.usdExchangeRate
|
|
),
|
|
customCurrencySymbol:
|
|
data.custom_currency_symbol?.trim() ||
|
|
DEFAULT_CURRENCY_CONFIG.customCurrencySymbol,
|
|
customCurrencyExchangeRate: toNumber(
|
|
data.custom_currency_exchange_rate,
|
|
DEFAULT_CURRENCY_CONFIG.customCurrencyExchangeRate
|
|
),
|
|
}
|
|
|
|
return {
|
|
systemName: data.system_name || DEFAULT_SYSTEM_NAME,
|
|
logo: data.logo || DEFAULT_LOGO,
|
|
footerHtml: data.footer_html,
|
|
demoSiteEnabled: data.demo_site_enabled,
|
|
displayTokenStatEnabled: data.display_token_stat_enabled,
|
|
currency,
|
|
}
|
|
}
|
|
|
|
// Fetch system config from API
|
|
async function fetchSystemConfig(): Promise<Partial<SystemConfig>> {
|
|
const response = await fetch('/api/status')
|
|
if (!response.ok) throw new Error('Failed to fetch status')
|
|
|
|
const data: StatusApiResponse = await response.json()
|
|
if (!data.success) throw new Error('API returned error')
|
|
|
|
return mapStatusDataToConfig(data.data)
|
|
}
|
|
|
|
// Preload image and return cleanup function
|
|
function preloadImage(
|
|
src: string,
|
|
onLoad: () => void,
|
|
onError: () => void
|
|
): () => void {
|
|
const img = new Image()
|
|
img.onload = onLoad
|
|
img.onerror = onError
|
|
img.src = src
|
|
|
|
return () => {
|
|
img.onload = null
|
|
img.onerror = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* System configuration hook with auto-loading and logo preloading
|
|
*
|
|
* @example
|
|
* // Root component - auto-load from backend
|
|
* useSystemConfig({ autoLoad: true })
|
|
*
|
|
* @example
|
|
* // Other components - use cached config
|
|
* const { systemName, logo, loading } = useSystemConfig()
|
|
*/
|
|
export function useSystemConfig(options: UseSystemConfigOptions = {}) {
|
|
const { autoLoad = false } = options
|
|
const {
|
|
config,
|
|
loading,
|
|
loadedLogoUrl,
|
|
setConfig,
|
|
setLoadedLogoUrl,
|
|
setLoading,
|
|
} = useSystemConfigStore()
|
|
|
|
// Load config from backend
|
|
const loadConfig = useCallback(async () => {
|
|
try {
|
|
setLoading(true)
|
|
const newConfig = await fetchSystemConfig()
|
|
setConfig(newConfig)
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Failed to load system config:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [setConfig, setLoading])
|
|
|
|
useEffect(() => {
|
|
if (autoLoad) loadConfig()
|
|
}, [autoLoad, loadConfig])
|
|
|
|
// Preload logo image when URL changes
|
|
useEffect(() => {
|
|
const { logo } = config
|
|
|
|
// Skip if logo is already loaded
|
|
if (!logo || logo === loadedLogoUrl) return
|
|
|
|
// Preload new logo
|
|
return preloadImage(
|
|
logo,
|
|
() => {
|
|
setLoadedLogoUrl(logo)
|
|
applyFaviconToDom(logo)
|
|
},
|
|
() => {
|
|
if (logo !== DEFAULT_LOGO) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('Failed to load logo:', logo)
|
|
}
|
|
// Mark as loaded even on error to prevent infinite retry
|
|
setLoadedLogoUrl(logo)
|
|
}
|
|
)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [config.logo, loadedLogoUrl, setLoadedLogoUrl])
|
|
|
|
return {
|
|
...config,
|
|
loading,
|
|
logoLoaded: config.logo === loadedLogoUrl && !!loadedLogoUrl,
|
|
}
|
|
}
|