Fix comparison badge to update in real-time across components
- Move comparison state from hook to shared context provider - All components now share the same state instance - Badge count updates immediately when schools are added/removed - Add key prop to badge to re-trigger animation on count change - Add storage event listener for cross-tab synchronization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,9 @@ export function Navigation() {
|
|||||||
>
|
>
|
||||||
Compare
|
Compare
|
||||||
{selectedSchools.length > 0 && (
|
{selectedSchools.length > 0 && (
|
||||||
<span className={styles.badge}>{selectedSchools.length}</span>
|
<span key={selectedSchools.length} className={styles.badge}>
|
||||||
|
{selectedSchools.length}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface ComparisonContextType {
|
|||||||
clearAll: () => void;
|
clearAll: () => void;
|
||||||
isSelected: (urn: number) => boolean;
|
isSelected: (urn: number) => boolean;
|
||||||
canAddMore: boolean;
|
canAddMore: boolean;
|
||||||
|
isInitialized: boolean;
|
||||||
mutate: () => void;
|
mutate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,98 @@
|
|||||||
/**
|
/**
|
||||||
* ComparisonProvider
|
* ComparisonProvider
|
||||||
* Provides comparison state to all components
|
* Provides shared comparison state to all components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useComparison } from '@/hooks/useComparison';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { getFromLocalStorage, setToLocalStorage } from '@/lib/utils';
|
||||||
|
import type { School } from '@/lib/types';
|
||||||
import { ComparisonContext } from './ComparisonContext';
|
import { ComparisonContext } from './ComparisonContext';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'selectedSchools';
|
||||||
|
const MAX_SCHOOLS = 5;
|
||||||
|
|
||||||
export function ComparisonProvider({ children }: { children: React.ReactNode }) {
|
export function ComparisonProvider({ children }: { children: React.ReactNode }) {
|
||||||
const comparisonState = useComparison();
|
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));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<ComparisonContext.Provider value={comparisonState}>
|
<ComparisonContext.Provider
|
||||||
|
value={{
|
||||||
|
selectedSchools,
|
||||||
|
comparisonData: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
addSchool,
|
||||||
|
removeSchool,
|
||||||
|
clearAll,
|
||||||
|
isSelected,
|
||||||
|
canAddMore: selectedSchools.length < MAX_SCHOOLS,
|
||||||
|
isInitialized,
|
||||||
|
mutate,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ComparisonContext.Provider>
|
</ComparisonContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Custom hook for managing school comparison state
|
* Custom hook for managing school comparison state
|
||||||
|
* Uses shared context for real-time updates across components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { fetcher } from '@/lib/api';
|
import { fetcher } from '@/lib/api';
|
||||||
import { getFromLocalStorage, setToLocalStorage } from '@/lib/utils';
|
import { useComparisonContext } from '@/context/ComparisonContext';
|
||||||
import type { School, ComparisonResponse } from '@/lib/types';
|
import type { ComparisonResponse } from '@/lib/types';
|
||||||
|
|
||||||
const STORAGE_KEY = 'selectedSchools';
|
|
||||||
const MAX_SCHOOLS = 5;
|
|
||||||
|
|
||||||
export function useComparison() {
|
export function useComparison() {
|
||||||
const [selectedSchools, setSelectedSchools] = useState<School[]>([]);
|
const {
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
selectedSchools,
|
||||||
|
addSchool,
|
||||||
// Load from localStorage on mount
|
removeSchool,
|
||||||
useEffect(() => {
|
clearAll,
|
||||||
const stored = getFromLocalStorage<School[]>(STORAGE_KEY, []);
|
isSelected,
|
||||||
setSelectedSchools(stored);
|
canAddMore,
|
||||||
setIsInitialized(true);
|
isInitialized,
|
||||||
}, []);
|
} = useComparisonContext();
|
||||||
|
|
||||||
// Save to localStorage when schools change
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialized) {
|
|
||||||
setToLocalStorage(STORAGE_KEY, selectedSchools);
|
|
||||||
}
|
|
||||||
}, [selectedSchools, isInitialized]);
|
|
||||||
|
|
||||||
// Fetch comparison data for selected schools
|
// Fetch comparison data for selected schools
|
||||||
const urns = selectedSchools.map((s) => s.urn).join(',');
|
const urns = selectedSchools.map((s) => s.urn).join(',');
|
||||||
@@ -38,40 +28,10 @@ export function useComparison() {
|
|||||||
fetcher,
|
fetcher,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
dedupingInterval: 10000, // 10 seconds
|
dedupingInterval: 10000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const addSchool = useCallback((school: School) => {
|
|
||||||
setSelectedSchools((prev) => {
|
|
||||||
// Check if already selected
|
|
||||||
if (prev.some((s) => s.urn === school.urn)) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check max limit
|
|
||||||
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));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearAll = useCallback(() => {
|
|
||||||
setSelectedSchools([]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isSelected = useCallback(
|
|
||||||
(urn: number) => selectedSchools.some((s) => s.urn === urn),
|
|
||||||
[selectedSchools]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedSchools,
|
selectedSchools,
|
||||||
comparisonData: data?.comparison,
|
comparisonData: data?.comparison,
|
||||||
@@ -81,7 +41,8 @@ export function useComparison() {
|
|||||||
removeSchool,
|
removeSchool,
|
||||||
clearAll,
|
clearAll,
|
||||||
isSelected,
|
isSelected,
|
||||||
canAddMore: selectedSchools.length < MAX_SCHOOLS,
|
canAddMore,
|
||||||
|
isInitialized,
|
||||||
mutate,
|
mutate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user