508 lines
15 KiB
TypeScript
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;
|
|
}
|