From 0294038fd393cfe80ec7aaaeb28363c32a1a8ae7 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 2 Jul 2026 21:34:07 +0100 Subject: [PATCH] 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 --- nextjs-app/components/ComparisonView.tsx | 29 ++++++++++++++--------- nextjs-app/context/ComparisonContext.tsx | 1 + nextjs-app/context/ComparisonProvider.tsx | 7 ++++++ nextjs-app/hooks/useComparison.ts | 2 ++ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/nextjs-app/components/ComparisonView.tsx b/nextjs-app/components/ComparisonView.tsx index 14399b4..3ecb5f5 100644 --- a/nextjs-app/components/ComparisonView.tsx +++ b/nextjs-app/components/ComparisonView.tsx @@ -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 => 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] && (
{metricLabel}
-
+ {/* Text uses the AA-dark variant; the swatch dot keeps the true series colour */} +
void; removeSchool: (urn: number) => void; + replaceSchools: (schools: School[]) => void; clearAll: () => void; isSelected: (urn: number) => boolean; canAddMore: boolean; diff --git a/nextjs-app/context/ComparisonProvider.tsx b/nextjs-app/context/ComparisonProvider.tsx index ea5cee5..27f569e 100644 --- a/nextjs-app/context/ComparisonProvider.tsx +++ b/nextjs-app/context/ComparisonProvider.tsx @@ -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, diff --git a/nextjs-app/hooks/useComparison.ts b/nextjs-app/hooks/useComparison.ts index 7a165bc..f7ed256 100644 --- a/nextjs-app/hooks/useComparison.ts +++ b/nextjs-app/hooks/useComparison.ts @@ -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,