Stage 11: polish, cross-cutting concerns & hardening
- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline - 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone - 11.3 Responsive sidebar: auto-collapse <640px, media query listener - 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated - 11.5 prefers-reduced-motion: disable sidebar transition - 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries - 11.7 Health transition logging (fail2ban_came_online/went_offline) - 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502 - 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean - Timezone endpoint: setup_service.get_timezone, 5 new tests
This commit is contained in:
95
frontend/src/providers/TimezoneProvider.tsx
Normal file
95
frontend/src/providers/TimezoneProvider.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* TimezoneProvider.
|
||||
*
|
||||
* Fetches the IANA timezone configured during the BanGUI setup wizard and
|
||||
* makes it available throughout the component tree via React Context.
|
||||
*
|
||||
* The timezone is fetched once at mount. On error or before the initial
|
||||
* fetch resolves, the value defaults to ``"UTC"`` so date-formatting callers
|
||||
* always receive a safe fallback.
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { fetchTimezone } from "../api/setup";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context definition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface TimezoneContextValue {
|
||||
/** IANA timezone string, e.g. ``"Europe/Berlin"`` or ``"UTC"``. */
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
const TimezoneContext = createContext<TimezoneContextValue>({ timezone: "UTC" });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TimezoneProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the application (or authenticated shell) with this provider to make the
|
||||
* configured timezone available via {@link useTimezone}.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <TimezoneProvider>
|
||||
* <App />
|
||||
* </TimezoneProvider>
|
||||
* ```
|
||||
*/
|
||||
export function TimezoneProvider({
|
||||
children,
|
||||
}: TimezoneProviderProps): React.JSX.Element {
|
||||
const [timezone, setTimezone] = useState<string>("UTC");
|
||||
|
||||
const load = useCallback((): void => {
|
||||
fetchTimezone()
|
||||
.then((resp) => { setTimezone(resp.timezone); })
|
||||
.catch(() => {
|
||||
// Silently fall back to UTC; the backend may not be reachable yet.
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const value = useMemo<TimezoneContextValue>(() => ({ timezone }), [timezone]);
|
||||
|
||||
return (
|
||||
<TimezoneContext.Provider value={value}>{children}</TimezoneContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the IANA timezone string configured during setup.
|
||||
*
|
||||
* Must be used inside a {@link TimezoneProvider}.
|
||||
*
|
||||
* @returns The configured timezone, e.g. ``"Europe/Berlin"``.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { timezone } = useTimezone();
|
||||
* const label = formatDate(item.created_at, timezone);
|
||||
* ```
|
||||
*/
|
||||
export function useTimezone(): string {
|
||||
return useContext(TimezoneContext).timezone;
|
||||
}
|
||||
Reference in New Issue
Block a user