2026-02-02 20:34:35 +00:00
|
|
|
/**
|
|
|
|
|
* ComparisonProvider
|
2026-02-03 14:26:23 +00:00
|
|
|
* Provides shared comparison state to all components
|
2026-02-02 20:34:35 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
2026-02-03 14:26:23 +00:00
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { getFromLocalStorage, setToLocalStorage } from '@/lib/utils';
|
2026-09-30 11:56:54 +01:00
|
|
|
import { MAX_PER_GROUP, fitToGroupLimits, fullGroupFor } from '@/lib/compareLogic';
|
2026-02-03 14:26:23 +00:00
|
|
|
import type { School } from '@/lib/types';
|
2026-02-02 20:34:35 +00:00
|
|
|
import { ComparisonContext } from './ComparisonContext';
|
|
|
|
|
|
2026-02-03 14:26:23 +00:00
|
|
|
const STORAGE_KEY = 'selectedSchools';
|
|
|
|
|
|
2026-02-02 20:34:35 +00:00
|
|
|
export function ComparisonProvider({ children }: { children: React.ReactNode }) {
|
2026-02-03 14:26:23 +00:00
|
|
|
const [selectedSchools, setSelectedSchools] = useState<School[]>([]);
|
|
|
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Load from localStorage on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const stored = getFromLocalStorage<School[]>(STORAGE_KEY, []);
|
|
|
|
|
setSelectedSchools(stored);
|
|
|
|
|
setIsInitialized(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Save to localStorage when schools change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isInitialized) {
|
|
|
|
|
setToLocalStorage(STORAGE_KEY, selectedSchools);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedSchools, isInitialized]);
|
|
|
|
|
|
|
|
|
|
// Listen for storage changes from other tabs
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleStorageChange = (e: StorageEvent) => {
|
|
|
|
|
if (e.key === STORAGE_KEY && e.newValue) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(e.newValue);
|
|
|
|
|
setSelectedSchools(parsed);
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('storage', handleStorageChange);
|
|
|
|
|
return () => window.removeEventListener('storage', handleStorageChange);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const addSchool = useCallback((school: School) => {
|
|
|
|
|
setSelectedSchools((prev) => {
|
|
|
|
|
if (prev.some((s) => s.urn === school.urn)) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
2026-09-30 11:56:54 +01:00
|
|
|
const fullGroup = fullGroupFor(prev, school);
|
|
|
|
|
if (fullGroup) {
|
|
|
|
|
alert(`You can compare up to ${MAX_PER_GROUP} ${fullGroup} schools at once. Remove one to add another.`);
|
2026-02-03 14:26:23 +00:00
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
return [...prev, school];
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const removeSchool = useCallback((urn: number) => {
|
|
|
|
|
setSelectedSchools((prev) => prev.filter((s) => s.urn !== urn));
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-07-02 21:34:07 +01:00
|
|
|
// Replace the whole selection — used when a shared /compare?urns=… link
|
|
|
|
|
// must take precedence over whatever this visitor had stored.
|
|
|
|
|
const replaceSchools = useCallback((schools: School[]) => {
|
2026-09-30 11:56:54 +01:00
|
|
|
setSelectedSchools(fitToGroupLimits(schools));
|
2026-07-02 21:34:07 +01:00
|
|
|
}, []);
|
|
|
|
|
|
2026-09-30 12:04:39 +01:00
|
|
|
// Baskets saved before phases were recorded (or added from a path that
|
|
|
|
|
// lacked one) count against both groups. Fill the gaps once the compare
|
|
|
|
|
// page has fetched each school, so they stop holding a slot they don't need.
|
|
|
|
|
const backfillPhases = useCallback((phases: Record<number, string | null | undefined>) => {
|
|
|
|
|
setSelectedSchools((prev) => {
|
|
|
|
|
let changed = false;
|
|
|
|
|
const next = prev.map((s) => {
|
|
|
|
|
const phase = phases[s.urn];
|
|
|
|
|
if (s.phase || !phase) return s;
|
|
|
|
|
changed = true;
|
|
|
|
|
return { ...s, phase };
|
|
|
|
|
});
|
|
|
|
|
return changed ? next : prev;
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-02-03 14:26:23 +00:00
|
|
|
const clearAll = useCallback(() => {
|
|
|
|
|
setSelectedSchools([]);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const isSelected = useCallback(
|
|
|
|
|
(urn: number) => selectedSchools.some((s) => s.urn === urn),
|
|
|
|
|
[selectedSchools]
|
|
|
|
|
);
|
|
|
|
|
|
2026-09-30 11:56:54 +01:00
|
|
|
const fullGroupForSchool = useCallback(
|
|
|
|
|
(school: Pick<School, 'phase'>) => fullGroupFor(selectedSchools, school),
|
|
|
|
|
[selectedSchools]
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-03 14:26:23 +00:00
|
|
|
// Placeholder mutate - actual SWR mutate is in useComparison hook
|
|
|
|
|
const mutate = useCallback(() => {}, []);
|
2026-02-02 20:34:35 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-02-03 14:26:23 +00:00
|
|
|
<ComparisonContext.Provider
|
|
|
|
|
value={{
|
|
|
|
|
selectedSchools,
|
|
|
|
|
comparisonData: null,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
addSchool,
|
|
|
|
|
removeSchool,
|
2026-07-02 21:34:07 +01:00
|
|
|
replaceSchools,
|
2026-09-30 12:04:39 +01:00
|
|
|
backfillPhases,
|
2026-02-03 14:26:23 +00:00
|
|
|
clearAll,
|
|
|
|
|
isSelected,
|
2026-09-30 11:56:54 +01:00
|
|
|
fullGroupFor: fullGroupForSchool,
|
2026-02-03 14:26:23 +00:00
|
|
|
isInitialized,
|
|
|
|
|
mutate,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-02-02 20:34:35 +00:00
|
|
|
{children}
|
|
|
|
|
</ComparisonContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|