/*
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 .
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 {
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> {
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,
}
}