Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m24s
Docker images / frontend-image (push) Successful in 2m6s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 7s
Replaced direct fetch calls with requestJson and requestBlob utility functions across multiple components for improved consistency and error handling. This change enhances the maintainability of the codebase by centralizing API request logic and ensuring uniform handling of authentication and response parsing.
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import { createContext, useContext, useEffect, useState, useCallback } from "react"
|
|
import {
|
|
configuredBackendUrl,
|
|
defaultDataSourceMode,
|
|
isBackendUrlLocked,
|
|
isMockDataSourceAvailable,
|
|
LOCAL_DEFAULT_BACKEND_URL,
|
|
resolveStoredBackendUrl,
|
|
} from "@/lib/backend-url"
|
|
import { resolveApiUrl } from "@/shared/api/http-client"
|
|
|
|
// ── types ─────────────────────────────────────────────────────────────────────
|
|
|
|
export type DataSourceMode = "mock" | "live"
|
|
|
|
/** Режим «Живые» в настройках: моковые данные не подставлять. */
|
|
export function isLiveDataSourceMode(mode: DataSourceMode): boolean {
|
|
return mode === "live"
|
|
}
|
|
|
|
interface DataSourceContextValue {
|
|
mode: DataSourceMode
|
|
setMode: (m: DataSourceMode) => void
|
|
backendUrl: string
|
|
setBackendUrl: (url: string) => void
|
|
backendUrlLocked: boolean
|
|
mockModeAvailable: boolean
|
|
/** localStorage для mode/backendUrl применён на клиенте (после первого paint). */
|
|
prefsHydrated: boolean
|
|
/** undefined = не проверялось, true = OK, false = недоступен */
|
|
backendStatus: boolean | undefined
|
|
checkBackend: () => Promise<void>
|
|
}
|
|
|
|
// ── context ───────────────────────────────────────────────────────────────────
|
|
|
|
const DataSourceContext = createContext<DataSourceContextValue | null>(null)
|
|
|
|
const LS_MODE = "routerlists:data-source"
|
|
const LS_BACKEND = "routerlists:backend-url"
|
|
|
|
function readStoredMode(): DataSourceMode {
|
|
if (!isMockDataSourceAvailable()) return "live"
|
|
if (typeof window === "undefined") return defaultDataSourceMode()
|
|
const stored = localStorage.getItem(LS_MODE) as DataSourceMode | null
|
|
if (stored === "live") return "live"
|
|
if (stored === "mock") return "mock"
|
|
return defaultDataSourceMode()
|
|
}
|
|
|
|
function initialBackendUrl(): string {
|
|
if (typeof window === "undefined") {
|
|
const cfg = configuredBackendUrl()
|
|
if (cfg.kind === "same-origin") return ""
|
|
if (cfg.kind === "fixed") return cfg.url
|
|
return LOCAL_DEFAULT_BACKEND_URL
|
|
}
|
|
return resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
|
|
}
|
|
|
|
function normalizeBackendUrl(url: string): string {
|
|
return url.trim().replace(/\/$/, "")
|
|
}
|
|
|
|
export function DataSourceProvider({ children }: { children: React.ReactNode }) {
|
|
const [mode, setModeState] = useState<DataSourceMode>(defaultDataSourceMode)
|
|
const [backendUrl, setBackendUrlState] = useState(initialBackendUrl)
|
|
const [prefsHydrated, setPrefsHydrated] = useState(false)
|
|
const [backendStatus, setBackendStatus] = useState<boolean | undefined>(undefined)
|
|
const backendUrlLocked = isBackendUrlLocked()
|
|
const mockModeAvailable = isMockDataSourceAvailable()
|
|
|
|
useEffect(() => {
|
|
const storedMode = readStoredMode()
|
|
const url = resolveStoredBackendUrl(localStorage.getItem(LS_BACKEND))
|
|
setModeState(storedMode)
|
|
setBackendUrlState(url)
|
|
setPrefsHydrated(true)
|
|
}, [])
|
|
|
|
const setMode = useCallback((m: DataSourceMode) => {
|
|
if (!mockModeAvailable && m === "mock") return
|
|
setModeState(m)
|
|
localStorage.setItem(LS_MODE, m)
|
|
}, [mockModeAvailable])
|
|
|
|
const setBackendUrl = useCallback((url: string) => {
|
|
if (backendUrlLocked) return
|
|
const normalized = normalizeBackendUrl(url)
|
|
setBackendUrlState(normalized)
|
|
localStorage.setItem(LS_BACKEND, normalized)
|
|
}, [backendUrlLocked])
|
|
|
|
const checkBackend = useCallback(async () => {
|
|
const healthUrl = resolveApiUrl(backendUrl, "/health")
|
|
try {
|
|
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(3000) })
|
|
setBackendStatus(res.ok)
|
|
} catch {
|
|
setBackendStatus(false)
|
|
}
|
|
}, [backendUrl])
|
|
|
|
useEffect(() => {
|
|
if (!prefsHydrated || mode !== "live") return
|
|
queueMicrotask(() => { void checkBackend() })
|
|
}, [prefsHydrated, mode, checkBackend])
|
|
|
|
return (
|
|
<DataSourceContext.Provider
|
|
value={{ mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, prefsHydrated, backendStatus, checkBackend }}
|
|
>
|
|
{children}
|
|
</DataSourceContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useDataSource(): DataSourceContextValue {
|
|
const ctx = useContext(DataSourceContext)
|
|
if (!ctx) throw new Error("useDataSource must be used inside <DataSourceProvider>")
|
|
return ctx
|
|
}
|