backstory/frontend/src/hooks/GlobalContext.tsx

508 lines
15 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
JSX,
} from 'react';
import * as Types from 'types/types';
// Assuming you're using React Router
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/AuthContext';
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
// ============================
// Storage Keys
// ============================
const STORAGE_KEYS = {
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
SELECTED_JOB_ID: 'selectedJobId',
SELECTED_EMPLOYER_ID: 'selectedEmployerId',
LAST_ROUTE: 'lastVisitedRoute',
ROUTE_STATE: 'routeState',
ACTIVE_TAB: 'activeTab',
APPLIED_FILTERS: 'appliedFilters',
SIDEBAR_COLLAPSED: 'sidebarCollapsed',
} as const;
// ============================
// Route State Interface
// ============================
interface RouteState {
lastRoute: string | null;
activeTab: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
appliedFilters: Record<string, any>;
sidebarCollapsed: boolean;
}
// ============================
// Enhanced App State Interface
// ============================
export interface AppState {
selectedCandidate: Types.Candidate | null;
selectedJob: Types.Job | null;
selectedEmployer: Types.Employer | null;
selectedResume: Types.Resume | null;
setSelectedResume: (resume: Types.Resume | null) => void;
routeState: RouteState;
isInitializing: boolean;
}
export interface AppStateActions {
// Snackbar
setSnack: SetSnackType;
//
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
setSelectedJob: (job: Types.Job | null) => void;
setSelectedEmployer: (employer: Types.Employer | null) => void;
clearSelections: () => void;
// Route management
saveCurrentRoute: () => void;
restoreLastRoute: () => void;
setActiveTab: (tab: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setFilters: (filters: Record<string, any>) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
clearRouteState: () => void;
}
export type AppStateContextType = AppState & AppStateActions;
// ============================
// Storage Utilities
// ============================
function getStoredId(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.warn('Failed to read from localStorage:', error);
return null;
}
}
function setStoredId(key: string, id: string | null): void {
try {
if (id) {
localStorage.setItem(key, id);
} else {
localStorage.removeItem(key);
}
} catch (error) {
console.warn('Failed to write to localStorage:', error);
}
}
function getStoredObject<T>(key: string, defaultValue: T): T {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
} catch (error) {
console.warn(`Failed to read ${key} from localStorage:`, error);
return defaultValue;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setStoredObject(key: string, value: any): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Failed to write ${key} to localStorage:`, error);
}
}
// ============================
// Route State Management
// ============================
function getInitialRouteState(): RouteState {
return {
lastRoute: getStoredId(STORAGE_KEYS.LAST_ROUTE),
activeTab: getStoredId(STORAGE_KEYS.ACTIVE_TAB),
appliedFilters: getStoredObject(STORAGE_KEYS.APPLIED_FILTERS, {}),
sidebarCollapsed: getStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, false),
};
}
function persistRouteState(routeState: RouteState): void {
setStoredId(STORAGE_KEYS.LAST_ROUTE, routeState.lastRoute);
setStoredId(STORAGE_KEYS.ACTIVE_TAB, routeState.activeTab);
setStoredObject(STORAGE_KEYS.APPLIED_FILTERS, routeState.appliedFilters);
setStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, routeState.sidebarCollapsed);
}
// ============================
// Enhanced App State Hook
// ============================
export function useAppStateLogic(): AppStateContextType {
const location = useLocation();
const navigate = useNavigate();
const { apiClient } = useAuth();
// Entity state
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
const [selectedResume, setSelectedResume] = useState<Types.Resume | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(true);
// Route state
const [routeState, setRouteStateState] = useState<RouteState>(getInitialRouteState);
// ============================
// Initialization Effect
// ============================
useEffect(() => {
const initializeFromStorage = async (): Promise<void> => {
setIsInitializing(true);
try {
// Get stored entity IDs
const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID);
const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID);
const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID);
// Restore entities in parallel if IDs exist
const promises: Promise<void>[] = [];
if (candidateId) {
promises.push(
(async (): Promise<void> => {
try {
const candidate = await apiClient.getCandidate(candidateId);
if (candidate) {
setSelectedCandidateState(candidate);
console.log('Restored candidate from storage:', candidate);
} else {
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
console.log('Candidate not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore candidate:', error);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
}
})()
);
}
if (jobId) {
promises.push(
(async (): Promise<void> => {
try {
const job = await apiClient.getJob(jobId);
if (job) {
setSelectedJobState(job);
console.log('Restored job from storage:', job);
} else {
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
console.log('Job not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore job:', error);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
}
})()
);
}
if (employerId) {
promises.push(
(async (): Promise<void> => {
try {
const employer = await apiClient.getEmployer(employerId);
if (employer) {
setSelectedEmployerState(employer);
console.log('Restored employer from storage:', employer);
} else {
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log('Employer not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore employer:', error);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
}
})()
);
}
// Wait for all restoration attempts to complete
await Promise.all(promises);
} catch (error) {
console.error('Error during app state initialization:', error);
} finally {
setIsInitializing(false);
}
};
initializeFromStorage();
}, [apiClient]);
// ============================
// Auto-save current route
// ============================
useEffect(() => {
// Don't save certain routes (login, register, etc.)
const excludedRoutes = ['/login', '/register', '/verify-email', '/reset-password'];
const shouldSaveRoute = !excludedRoutes.some(route => location.pathname.startsWith(route));
if (shouldSaveRoute && !isInitializing) {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
});
}
}, [location.pathname, isInitializing]);
// ============================
// Entity State Setters with Persistence
// ============================
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
setSelectedCandidateState(candidate);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
if (candidate) {
console.log('Selected candidate:', candidate);
} else {
console.log('Cleared selected candidate');
}
}, []);
const setSelectedJob = useCallback((job: Types.Job | null) => {
setSelectedJobState(job);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
if (job) {
console.log('Selected job:', job);
} else {
console.log('Cleared selected job');
}
}, []);
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
setSelectedEmployerState(employer);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
if (employer) {
console.log('Selected employer:', employer);
} else {
console.log('Cleared selected employer');
}
}, []);
const clearSelections = useCallback(() => {
setSelectedCandidateState(null);
setSelectedJobState(null);
setSelectedEmployerState(null);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log('Cleared all selections');
}, []);
// ============================
// Route State Actions
// ============================
const saveCurrentRoute = useCallback(() => {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
});
}, [location.pathname]);
const restoreLastRoute = useCallback(() => {
if (routeState.lastRoute && routeState.lastRoute !== location.pathname) {
navigate(routeState.lastRoute);
}
}, [routeState.lastRoute, location.pathname, navigate]);
const setActiveTab = useCallback((tab: string) => {
setRouteStateState(prev => {
const newState = { ...prev, activeTab: tab };
persistRouteState(newState);
return newState;
});
}, []);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setFilters = useCallback((filters: Record<string, any>) => {
setRouteStateState(prev => {
const newState = { ...prev, appliedFilters: filters };
persistRouteState(newState);
return newState;
});
}, []);
const setSidebarCollapsed = useCallback((collapsed: boolean) => {
setRouteStateState(prev => {
const newState = { ...prev, sidebarCollapsed: collapsed };
persistRouteState(newState);
return newState;
});
}, []);
const clearRouteState = useCallback(() => {
const clearedState: RouteState = {
lastRoute: null,
activeTab: null,
appliedFilters: {},
sidebarCollapsed: false,
};
setRouteStateState(clearedState);
// Clear from localStorage
localStorage.removeItem(STORAGE_KEYS.LAST_ROUTE);
localStorage.removeItem(STORAGE_KEYS.ACTIVE_TAB);
localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS);
localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED);
console.log('Cleared all route state');
}, []);
const emptySetSnack: SetSnackType = (_message: string, _severity?: SeverityType) => {
return;
};
return {
setSnack: emptySetSnack,
selectedCandidate,
selectedJob,
selectedEmployer,
routeState,
selectedResume,
setSelectedResume,
isInitializing,
setSelectedCandidate,
setSelectedJob,
setSelectedEmployer,
clearSelections,
saveCurrentRoute,
restoreLastRoute,
setActiveTab,
setFilters,
setSidebarCollapsed,
clearRouteState,
};
}
// ============================
// Context Provider
// ============================
const AppStateContext = createContext<AppStateContextType | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }): JSX.Element {
const appState = useAppStateLogic();
const snackRef = useRef<{ setSnack: SetSnackType }>(null);
// Global UI components
appState.setSnack = useCallback(
(message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
},
[snackRef]
);
return (
<AppStateContext.Provider value={appState}>
{children}
<Snack ref={snackRef} />
</AppStateContext.Provider>
);
}
export function useAppState(): AppStateContextType {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within an AppStateProvider');
}
return context;
}
// ============================
// Convenience Hooks
// ============================
export function useSelectedCandidate(): {
selectedCandidate: Types.Candidate | null;
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
} {
const { selectedCandidate, setSelectedCandidate } = useAppState();
return { selectedCandidate, setSelectedCandidate };
}
export function useSelectedJob(): {
selectedJob: Types.Job | null;
setSelectedJob: (job: Types.Job | null) => void;
} {
const { selectedJob, setSelectedJob } = useAppState();
return { selectedJob, setSelectedJob };
}
export function useSelectedEmployer(): {
selectedEmployer: Types.Employer | null;
setSelectedEmployer: (employer: Types.Employer | null) => void;
} {
const { selectedEmployer, setSelectedEmployer } = useAppState();
return { selectedEmployer, setSelectedEmployer };
}
export const useSelectedResume = (): {
selectedResume: Types.Resume | null;
setSelectedResume: (resume: Types.Resume | null) => void;
} => {
const { selectedResume, setSelectedResume } = useAppState();
return { selectedResume, setSelectedResume };
};
export function useRouteState(): {
routeState: RouteState;
setActiveTab: (tab: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setFilters: (filters: Record<string, any>) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
restoreLastRoute: () => void;
clearRouteState: () => void;
} {
const {
routeState,
setActiveTab,
setFilters,
setSidebarCollapsed,
restoreLastRoute,
clearRouteState,
} = useAppState();
return {
routeState,
setActiveTab,
setFilters,
setSidebarCollapsed,
restoreLastRoute,
clearRouteState,
};
}
export function useAppInitializing(): boolean {
const { isInitializing } = useAppState();
return isInitializing;
}