import { createContext, useContext, useReducer, useEffect } from 'react'; import * as db from '../lib/db'; import { pb } from '../lib/pb'; import { OIDC_PROVIDER } from '../lib/azureAuth'; import { getPersonalWeekNumber } from '../lib/curriculumService'; const AppContext = createContext(); const initialState = { currentUser: null, users: [], weekNumber: 0, isLoading: true }; function computeWeekNumber(user) { if (!user || !user.curriculum_started_at || user.enrollment_status !== 'active') return 0; return getPersonalWeekNumber(user.curriculum_started_at); } function appReducer(state, action) { switch (action.type) { case 'INIT_APP': return { ...state, users: action.payload.users, isLoading: false }; case 'LOGIN': return { ...state, currentUser: action.payload, weekNumber: computeWeekNumber(action.payload), }; case 'UPDATE_CURRENT_USER': return { ...state, currentUser: action.payload, weekNumber: computeWeekNumber(action.payload), users: state.users.map(u => u.id === action.payload.id ? action.payload : u), }; case 'LOGOUT': return { ...state, currentUser: null, weekNumber: 0 }; default: return state; } } export function AppProvider({ children }) { const [state, dispatch] = useReducer(appReducer, initialState); useEffect(() => { const loadState = async () => { // Restore the session from PocketBase's persistent auth store. The token // is kept in localStorage by the SDK; authRefresh verifies it is still // valid (and that the Entra account hasn't been revoked) and returns the // up-to-date record. if (pb.authStore.isValid && pb.authStore.record?.collectionName === 'team_members') { try { const { record } = await pb.collection('team_members').authRefresh(); dispatch({ type: 'LOGIN', payload: record }); } catch { // Token rejected (expired / account revoked) — drop the session. pb.authStore.clear(); } } // The team list (leaderboard, dashboard) requires authentication now, so // only load it once we have a valid session. const members = pb.authStore.isValid ? await db.getTeamMembers() : []; dispatch({ type: 'INIT_APP', payload: { users: members } }); }; loadState().catch(console.error); }, []); // Start the Entra (OIDC) login flow. PocketBase opens the provider popup, // handles the token exchange server-side, and auto-provisions the record on // first login (see pb_hooks/team_members.pb.js). const loginWithAzure = async () => { const authData = await pb.collection('team_members').authWithOAuth2({ provider: OIDC_PROVIDER }); dispatch({ type: 'LOGIN', payload: authData.record }); return authData.record; }; const logout = () => { pb.authStore.clear(); dispatch({ type: 'LOGOUT' }); }; /** * Start the curriculum for the currently logged-in user. * Records the start timestamp and flips enrollment_status to 'active'. */ const enrollCurrentUser = async () => { if (!state.currentUser) throw new Error('No user is logged in.'); const startedAt = new Date().toISOString(); const updated = await pb.collection('team_members').update(state.currentUser.id, { curriculum_started_at: startedAt, enrollment_status: 'active', }); dispatch({ type: 'UPDATE_CURRENT_USER', payload: updated }); return updated; }; return ( {children} ); } export function useApp() { return useContext(AppContext); }