Files
school_compare/nextjs-app/context/ComparisonProvider.tsx
TudorandClaude Fable 5 0294038fd3 fix(compare): shared ?urns= links win over the visitor's stored selection (P1.3)
The seed effect only adopted the URL's schools when localStorage was
empty, so a recipient who had ever used compare silently saw their own
old shortlist instead of the shared one. Explicit URL urns now replace
the stored selection on load (then persist as usual); bare /compare
still restores the visitor's own selection. Adds replaceSchools() to
the comparison context. Card values also switch to CHART_TEXT_COLORS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:34:07 +01:00

107 lines
2.9 KiB
TypeScript

/**
* ComparisonProvider
* Provides shared comparison state to all components
*/
'use client';
import { useState, useEffect, useCallback } from 'react';
import { getFromLocalStorage, setToLocalStorage } from '@/lib/utils';
import type { School } from '@/lib/types';
import { ComparisonContext } from './ComparisonContext';
const STORAGE_KEY = 'selectedSchools';
const MAX_SCHOOLS = 5;
export function ComparisonProvider({ children }: { children: React.ReactNode }) {
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;
}
if (prev.length >= MAX_SCHOOLS) {
alert(`Maximum ${MAX_SCHOOLS} schools can be compared`);
return prev;
}
return [...prev, school];
});
}, []);
const removeSchool = useCallback((urn: number) => {
setSelectedSchools((prev) => prev.filter((s) => s.urn !== urn));
}, []);
// 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[]) => {
setSelectedSchools(schools.slice(0, MAX_SCHOOLS));
}, []);
const clearAll = useCallback(() => {
setSelectedSchools([]);
}, []);
const isSelected = useCallback(
(urn: number) => selectedSchools.some((s) => s.urn === urn),
[selectedSchools]
);
// Placeholder mutate - actual SWR mutate is in useComparison hook
const mutate = useCallback(() => {}, []);
return (
<ComparisonContext.Provider
value={{
selectedSchools,
comparisonData: null,
isLoading: false,
error: null,
addSchool,
removeSchool,
replaceSchools,
clearAll,
isSelected,
canAddMore: selectedSchools.length < MAX_SCHOOLS,
isInitialized,
mutate,
}}
>
{children}
</ComparisonContext.Provider>
);
}