new-api/web/default/src/main.tsx
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
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.
2026-05-09 11:35:07 +08:00

170 lines
5.1 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 { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { AxiosError } from 'axios'
import {
QueryCache,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import i18next from 'i18next'
import { toast } from 'sonner'
import { useAuthStore } from '@/stores/auth-store'
import { getStatus } from '@/lib/api'
import '@/lib/dayjs'
import { applyFaviconToDom } from '@/lib/dom-utils'
import { handleServerError } from '@/lib/handle-server-error'
import { DirectionProvider } from './context/direction-provider'
import { FontProvider } from './context/font-provider'
import { ThemeProvider } from './context/theme-provider'
import './i18n/config'
// Generated Routes
import { routeTree } from './routeTree.gen'
// Styles
import './styles/index.css'
// Ensure VChart theme is initialized before any chart mounts (prevents white default theme flash)
// VChart theme is driven by our ThemeProvider (html.light/html.dark) via per-chart `theme` prop.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// eslint-disable-next-line no-console
if (import.meta.env.DEV) console.log({ failureCount, error })
if (failureCount >= 0 && import.meta.env.DEV) return false
if (failureCount > 3 && import.meta.env.PROD) return false
return !(
error instanceof AxiosError &&
[401, 403].includes(error.response?.status ?? 0)
)
},
refetchOnWindowFocus: import.meta.env.PROD,
staleTime: 10 * 1000, // 10s
},
mutations: {
onError: (error) => {
handleServerError(error)
if (error instanceof AxiosError) {
if (error.response?.status === 304) {
toast.error(i18next.t('Content not modified!'))
}
}
},
},
},
queryCache: new QueryCache({
onError: (error) => {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
toast.error(i18next.t('Session expired!'))
useAuthStore.getState().auth.reset()
const redirect = `${router.history.location.href}`
router.navigate({ to: '/sign-in', search: { redirect } })
}
if (error.response?.status === 500) {
toast.error(i18next.t('Internal Server Error!'))
router.navigate({ to: '/500' })
}
}
},
}),
})
// Create a new router instance
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
// Render the app
const rootElement = document.getElementById('root')!
// Set document.title and favicon from cached status, then refresh from network
;(function initSystemBranding() {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return
const apply = (name: string) => {
document.title = name
const metaTitle = document.querySelector(
'meta[name="title"]'
) as HTMLMetaElement | null
if (metaTitle) metaTitle.setAttribute('content', name)
}
// Cache-first
try {
const saved = localStorage.getItem('status')
if (saved) {
const s = JSON.parse(saved)
if (s?.system_name) apply(s.system_name)
if (s?.logo) applyFaviconToDom(s.logo)
}
} catch {
/* empty */
}
// Background refresh
getStatus()
.then((s) => {
if (s?.system_name) {
apply(s.system_name as string)
try {
localStorage.setItem('status', JSON.stringify(s))
} catch {
/* empty */
}
}
if (s?.logo) applyFaviconToDom(s.logo as string)
})
.catch(() => {
/* empty */
})
} catch {
/* empty */
}
})()
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<FontProvider>
<DirectionProvider>
<RouterProvider router={router} />
</DirectionProvider>
</FontProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>
)
}