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:
@@ -18,7 +18,7 @@ import { SchoolSearchModal } from './SchoolSearchModal';
|
|||||||
import { EmptyState } from './EmptyState';
|
import { EmptyState } from './EmptyState';
|
||||||
import { LoadingSkeleton } from './LoadingSkeleton';
|
import { LoadingSkeleton } from './LoadingSkeleton';
|
||||||
import type { ComparisonData, MetricDefinition, School } from '@/lib/types';
|
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 { fetchComparison } from '@/lib/api';
|
||||||
import { track } from '@/lib/analytics';
|
import { track } from '@/lib/analytics';
|
||||||
import styles from './ComparisonView.module.css';
|
import styles from './ComparisonView.module.css';
|
||||||
@@ -58,7 +58,7 @@ export function ComparisonView({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison();
|
const { selectedSchools, removeSchool, addSchool, replaceSchools, isInitialized } = useComparison();
|
||||||
|
|
||||||
const [selectedMetric, setSelectedMetric] = useState(initialMetric);
|
const [selectedMetric, setSelectedMetric] = useState(initialMetric);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
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.
|
// While true, auto-phase detection is suppressed so manual selections aren't overridden.
|
||||||
const phaseLockedByUser = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized) return;
|
if (!isInitialized) return;
|
||||||
if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) {
|
if (initialUrns.length > 0 && initialData) {
|
||||||
initialUrns.forEach(urn => {
|
const urlSchools = initialUrns
|
||||||
const data = initialData[String(urn)];
|
.map(urn => initialData[String(urn)]?.school_info)
|
||||||
if (data?.school_info) {
|
.filter((info): info is NonNullable<typeof info> => Boolean(info));
|
||||||
addSchool(data.school_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
|
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
@@ -378,7 +384,8 @@ export function ComparisonView({
|
|||||||
{activeComparisonData[school.urn] && (
|
{activeComparisonData[school.urn] && (
|
||||||
<div className={styles.latestValue}>
|
<div className={styles.latestValue}>
|
||||||
<div className={styles.latestLabel}>{metricLabel}</div>
|
<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
|
<span
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface ComparisonContextType {
|
|||||||
error: any;
|
error: any;
|
||||||
addSchool: (school: School) => void;
|
addSchool: (school: School) => void;
|
||||||
removeSchool: (urn: number) => void;
|
removeSchool: (urn: number) => void;
|
||||||
|
replaceSchools: (schools: School[]) => void;
|
||||||
clearAll: () => void;
|
clearAll: () => void;
|
||||||
isSelected: (urn: number) => boolean;
|
isSelected: (urn: number) => boolean;
|
||||||
canAddMore: boolean;
|
canAddMore: boolean;
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
|
|||||||
setSelectedSchools((prev) => prev.filter((s) => s.urn !== urn));
|
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(() => {
|
const clearAll = useCallback(() => {
|
||||||
setSelectedSchools([]);
|
setSelectedSchools([]);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -86,6 +92,7 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
|
|||||||
error: null,
|
error: null,
|
||||||
addSchool,
|
addSchool,
|
||||||
removeSchool,
|
removeSchool,
|
||||||
|
replaceSchools,
|
||||||
clearAll,
|
clearAll,
|
||||||
isSelected,
|
isSelected,
|
||||||
canAddMore: selectedSchools.length < MAX_SCHOOLS,
|
canAddMore: selectedSchools.length < MAX_SCHOOLS,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function useComparison() {
|
|||||||
selectedSchools,
|
selectedSchools,
|
||||||
addSchool,
|
addSchool,
|
||||||
removeSchool,
|
removeSchool,
|
||||||
|
replaceSchools,
|
||||||
clearAll,
|
clearAll,
|
||||||
isSelected,
|
isSelected,
|
||||||
canAddMore,
|
canAddMore,
|
||||||
@@ -39,6 +40,7 @@ export function useComparison() {
|
|||||||
error,
|
error,
|
||||||
addSchool,
|
addSchool,
|
||||||
removeSchool,
|
removeSchool,
|
||||||
|
replaceSchools,
|
||||||
clearAll,
|
clearAll,
|
||||||
isSelected,
|
isSelected,
|
||||||
canAddMore,
|
canAddMore,
|
||||||
|
|||||||
Reference in New Issue
Block a user