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>
This commit is contained in:
Tudor
2026-07-02 21:34:07 +01:00
co-authored by Claude Fable 5
parent a1fa4fe874
commit 0294038fd3
4 changed files with 28 additions and 11 deletions
+18 -11
View File
@@ -18,7 +18,7 @@ import { SchoolSearchModal } from './SchoolSearchModal';
import { EmptyState } from './EmptyState';
import { LoadingSkeleton } from './LoadingSkeleton';
import type { ComparisonData, MetricDefinition, School } from '@/lib/types';
import { formatPercentage, formatProgress, formatAcademicYear, CHART_COLORS, schoolUrl } from '@/lib/utils';
import { formatPercentage, formatProgress, formatAcademicYear, CHART_COLORS, CHART_TEXT_COLORS, schoolUrl } from '@/lib/utils';
import { fetchComparison } from '@/lib/api';
import { track } from '@/lib/analytics';
import styles from './ComparisonView.module.css';
@@ -58,7 +58,7 @@ export function ComparisonView({
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison();
const { selectedSchools, removeSchool, addSchool, replaceSchools, isInitialized } = useComparison();
const [selectedMetric, setSelectedMetric] = useState(initialMetric);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -69,16 +69,22 @@ export function ComparisonView({
// While true, auto-phase detection is suppressed so manual selections aren't overridden.
const phaseLockedByUser = useRef(false);
// Seed context from initialData when component mounts and localStorage is empty
// Seed context from the URL on mount. An explicit ?urns=… (e.g. a link a
// parent shared with their partner) always wins over this visitor's stored
// selection — otherwise the recipient silently sees their own old schools.
// The replacement is then persisted like any other selection change.
useEffect(() => {
if (!isInitialized) return;
if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) {
initialUrns.forEach(urn => {
const data = initialData[String(urn)];
if (data?.school_info) {
addSchool(data.school_info);
}
});
if (initialUrns.length > 0 && initialData) {
const urlSchools = initialUrns
.map(urn => initialData[String(urn)]?.school_info)
.filter((info): info is NonNullable<typeof info> => Boolean(info));
const sameSet =
urlSchools.length === selectedSchools.length &&
urlSchools.every(s => selectedSchools.some(sel => sel.urn === s.urn));
if (urlSchools.length > 0 && !sameSet) {
replaceSchools(urlSchools);
}
}
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -378,7 +384,8 @@ export function ComparisonView({
{activeComparisonData[school.urn] && (
<div className={styles.latestValue}>
<div className={styles.latestLabel}>{metricLabel}</div>
<div className={styles.latestNumber} style={{ color: CHART_COLORS[index % CHART_COLORS.length] }}>
{/* Text uses the AA-dark variant; the swatch dot keeps the true series colour */}
<div className={styles.latestNumber} style={{ color: CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length] }}>
<span
style={{
display: 'inline-block',
+1
View File
@@ -15,6 +15,7 @@ interface ComparisonContextType {
error: any;
addSchool: (school: School) => void;
removeSchool: (urn: number) => void;
replaceSchools: (schools: School[]) => void;
clearAll: () => void;
isSelected: (urn: number) => boolean;
canAddMore: boolean;
@@ -65,6 +65,12 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
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([]);
}, []);
@@ -86,6 +92,7 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
error: null,
addSchool,
removeSchool,
replaceSchools,
clearAll,
isSelected,
canAddMore: selectedSchools.length < MAX_SCHOOLS,
+2
View File
@@ -15,6 +15,7 @@ export function useComparison() {
selectedSchools,
addSchool,
removeSchool,
replaceSchools,
clearAll,
isSelected,
canAddMore,
@@ -39,6 +40,7 @@ export function useComparison() {
error,
addSchool,
removeSchool,
replaceSchools,
clearAll,
isSelected,
canAddMore,