mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 14:31:22 -05:00
feat: Implement automatic API connectivity banner
This commit is contained in:
87
src/contexts/APIConnectivityContext.tsx
Normal file
87
src/contexts/APIConnectivityContext.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createContext, useContext, ReactNode, useState, useEffect } from 'react';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
interface APIConnectivityContextType {
|
||||
isAPIReachable: boolean;
|
||||
isBannerDismissed: boolean;
|
||||
dismissBanner: () => void;
|
||||
}
|
||||
|
||||
const APIConnectivityContext = createContext<APIConnectivityContextType | undefined>(undefined);
|
||||
|
||||
const DISMISSAL_DURATION = 15 * 60 * 1000; // 15 minutes
|
||||
const DISMISSAL_KEY = 'api-connectivity-dismissed-until';
|
||||
const REACHABILITY_KEY = 'api-reachable';
|
||||
|
||||
export function APIConnectivityProvider({ children }: { children: ReactNode }) {
|
||||
const [isAPIReachable, setIsAPIReachable] = useState<boolean>(() => {
|
||||
const stored = sessionStorage.getItem(REACHABILITY_KEY);
|
||||
return stored !== 'false'; // Default to true, only false if explicitly set
|
||||
});
|
||||
|
||||
const [dismissedUntil, setDismissedUntil] = useState<number | null>(() => {
|
||||
const stored = localStorage.getItem(DISMISSAL_KEY);
|
||||
return stored ? parseInt(stored) : null;
|
||||
});
|
||||
|
||||
const dismissBanner = () => {
|
||||
const until = Date.now() + DISMISSAL_DURATION;
|
||||
localStorage.setItem(DISMISSAL_KEY, until.toString());
|
||||
setDismissedUntil(until);
|
||||
logger.info('API status banner dismissed', { until: new Date(until).toISOString() });
|
||||
};
|
||||
|
||||
const isBannerDismissed = dismissedUntil ? Date.now() < dismissedUntil : false;
|
||||
|
||||
// Auto-clear dismissal when API is healthy again
|
||||
useEffect(() => {
|
||||
if (isAPIReachable && dismissedUntil) {
|
||||
localStorage.removeItem(DISMISSAL_KEY);
|
||||
setDismissedUntil(null);
|
||||
logger.info('API status banner dismissal cleared (API recovered)');
|
||||
}
|
||||
}, [isAPIReachable, dismissedUntil]);
|
||||
|
||||
// Listen for custom events from error handler
|
||||
useEffect(() => {
|
||||
const handleAPIDown = () => {
|
||||
logger.warn('API connectivity lost');
|
||||
setIsAPIReachable(false);
|
||||
sessionStorage.setItem(REACHABILITY_KEY, 'false');
|
||||
};
|
||||
|
||||
const handleAPIUp = () => {
|
||||
logger.info('API connectivity restored');
|
||||
setIsAPIReachable(true);
|
||||
sessionStorage.setItem(REACHABILITY_KEY, 'true');
|
||||
};
|
||||
|
||||
window.addEventListener('api-connectivity-down', handleAPIDown);
|
||||
window.addEventListener('api-connectivity-up', handleAPIUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('api-connectivity-down', handleAPIDown);
|
||||
window.removeEventListener('api-connectivity-up', handleAPIUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<APIConnectivityContext.Provider
|
||||
value={{
|
||||
isAPIReachable,
|
||||
isBannerDismissed,
|
||||
dismissBanner,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</APIConnectivityContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAPIConnectivity() {
|
||||
const context = useContext(APIConnectivityContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAPIConnectivity must be used within APIConnectivityProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { createContext, useContext, ReactNode, useState, useEffect } from 'react';
|
||||
import { useCronitorHealth as useCronitorHealthQuery } from '@/hooks/useCronitorHealth';
|
||||
|
||||
interface CronitorHealthContextType {
|
||||
passing: boolean | null; // null = loading/unknown
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
lastChecked: Date | null;
|
||||
isBannerDismissed: boolean;
|
||||
dismissBanner: () => void;
|
||||
}
|
||||
|
||||
const CronitorHealthContext = createContext<CronitorHealthContextType | undefined>(undefined);
|
||||
|
||||
const DISMISSAL_DURATION = 15 * 60 * 1000; // 15 minutes
|
||||
const DISMISSAL_KEY = 'cronitor-banner-dismissed';
|
||||
|
||||
export function CronitorHealthProvider({ children }: { children: ReactNode }) {
|
||||
const { data, isLoading, error } = useCronitorHealthQuery();
|
||||
const [dismissedUntil, setDismissedUntil] = useState<number | null>(() => {
|
||||
const stored = localStorage.getItem(DISMISSAL_KEY);
|
||||
return stored ? parseInt(stored) : null;
|
||||
});
|
||||
|
||||
const dismissBanner = () => {
|
||||
const until = Date.now() + DISMISSAL_DURATION;
|
||||
localStorage.setItem(DISMISSAL_KEY, until.toString());
|
||||
setDismissedUntil(until);
|
||||
};
|
||||
|
||||
const isBannerDismissed = dismissedUntil ? Date.now() < dismissedUntil : false;
|
||||
|
||||
// Auto-clear dismissal when API is healthy again
|
||||
useEffect(() => {
|
||||
if (data?.passing === true && dismissedUntil) {
|
||||
localStorage.removeItem(DISMISSAL_KEY);
|
||||
setDismissedUntil(null);
|
||||
}
|
||||
}, [data?.passing, dismissedUntil]);
|
||||
|
||||
return (
|
||||
<CronitorHealthContext.Provider
|
||||
value={{
|
||||
passing: data?.passing ?? null,
|
||||
isLoading,
|
||||
error: error as Error | null,
|
||||
lastChecked: data ? new Date() : null,
|
||||
isBannerDismissed,
|
||||
dismissBanner,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CronitorHealthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCronitorHealth() {
|
||||
const context = useContext(CronitorHealthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCronitorHealth must be used within CronitorHealthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user