diff --git a/components/backlog/pbi-list.tsx b/components/backlog/pbi-list.tsx index c4b0171..cb6a721 100644 --- a/components/backlog/pbi-list.tsx +++ b/components/backlog/pbi-list.tsx @@ -197,29 +197,40 @@ function SortablePbiRow({ export function PbiList({ productId, pbis, isDemo }: PbiListProps) { const { selectedPbiId, selectPbi } = useSelectionStore() const { pbiOrder, pbiPriority, initPbis, reorderPbis, rollbackPbis, updatePbiPriority } = usePlannerStore() - const [filterPriority, setFilterPriority] = useState(() => { - if (typeof window === 'undefined') return 'all' - const saved = localStorage.getItem('scrum4me:pbi_filter_priority') - if (!saved || saved === 'all') return 'all' - const n = parseInt(saved, 10) - return Number.isInteger(n) && n >= 1 && n <= 4 ? n : 'all' - }) - const [filterStatus, setFilterStatus] = useState(() => { - if (typeof window === 'undefined') return 'all' - const saved = localStorage.getItem('scrum4me:pbi_filter_status') - return saved === 'ready' || saved === 'blocked' || saved === 'done' ? saved : 'all' - }) - const [sortMode, setSortMode] = useState(() => { - const saved = typeof window !== 'undefined' ? localStorage.getItem('scrum4me:pbi_sort') : null - return (saved === 'priority' || saved === 'code' || saved === 'date') ? saved : 'priority' - }) + // Defaults match SSR; persisted values applied post-mount in the loader effect below. + // This avoids hydration mismatch when localStorage holds non-default values. + const [filterPriority, setFilterPriority] = useState('all') + const [filterStatus, setFilterStatus] = useState('all') + const [sortMode, setSortMode] = useState('priority') + const [prefsLoaded, setPrefsLoaded] = useState(false) const [dialogState, setDialogState] = useState(null) const [activeDragId, setActiveDragId] = useState(null) const [, startTransition] = useTransition() - useEffect(() => { localStorage.setItem('scrum4me:pbi_sort', sortMode) }, [sortMode]) - useEffect(() => { localStorage.setItem('scrum4me:pbi_filter_priority', String(filterPriority)) }, [filterPriority]) - useEffect(() => { localStorage.setItem('scrum4me:pbi_filter_status', filterStatus) }, [filterStatus]) + // Load persisted preferences once after mount (client-only). + // setState calls here are intentional: hydrating from localStorage on first paint. + useEffect(() => { + const savedSort = localStorage.getItem('scrum4me:pbi_sort') + if (savedSort === 'priority' || savedSort === 'code' || savedSort === 'date') { + // eslint-disable-next-line react-hooks/set-state-in-effect + setSortMode(savedSort) + } + const savedPriority = localStorage.getItem('scrum4me:pbi_filter_priority') + if (savedPriority && savedPriority !== 'all') { + const n = parseInt(savedPriority, 10) + if (Number.isInteger(n) && n >= 1 && n <= 4) setFilterPriority(n) + } + const savedStatus = localStorage.getItem('scrum4me:pbi_filter_status') + if (savedStatus === 'ready' || savedStatus === 'blocked' || savedStatus === 'done') { + setFilterStatus(savedStatus) + } + setPrefsLoaded(true) + }, []) + + // Persist on change, but skip the initial render so we don't overwrite saved values with defaults. + useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_sort', sortMode) }, [sortMode, prefsLoaded]) + useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_filter_priority', String(filterPriority)) }, [filterPriority, prefsLoaded]) + useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:pbi_filter_status', filterStatus) }, [filterStatus, prefsLoaded]) // Sync server data into store — use stable string dep to avoid infinite loop const pbiIdKey = pbis.map(p => p.id).join(',')