fix: scope selectedEncounter + scrollY localStorage per campaign

- selectedEncounter key: ttrpg.selectedEncounter.{campaignId}
- scrollY key: ttrpg.scrollY.{campaignId}
- Restore scoped encounter selection on campaign switch
- Global keys unchanged (selectedCampaign, wakeLock, collapses)
- Prevents same-browser tabs on different campaigns from fighting
- Tests: 3 scoping cases (scoped encounter, scoped scroll, global intact)
This commit is contained in:
david raistrick
2026-07-08 20:00:13 -04:00
parent 2c6dfdafc8
commit eef11c3b6e
2 changed files with 58 additions and 8 deletions
+19 -8
View File
@@ -2349,7 +2349,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const [encounters, setEncounters] = useState([]);
const [selectedEncounterId, setSelectedEncounterId] = useState(() => {
try { return localStorage.getItem('ttrpg.selectedEncounter') || null; }
try { return localStorage.getItem(`ttrpg.selectedEncounter.${campaignId}`) || null; }
catch { return null; }
});
const [showCreateModal, setShowCreateModal] = useState(false);
@@ -2360,6 +2360,17 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const selectedEncounterIdRef = useRef(selectedEncounterId);
// Restore scoped encounter selection when campaign changes.
useEffect(() => {
if (!campaignId) { setSelectedEncounterId(null); return; }
try {
const saved = localStorage.getItem(`ttrpg.selectedEncounter.${campaignId}`) || null;
setSelectedEncounterId(saved);
} catch {
setSelectedEncounterId(null);
}
}, [campaignId]);
useEffect(() => {
if (encountersData) setEncounters(encountersData);
}, [encountersData]);
@@ -2406,10 +2417,10 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
useEffect(() => {
selectedEncounterIdRef.current = selectedEncounterId;
try {
if (selectedEncounterId) localStorage.setItem('ttrpg.selectedEncounter', selectedEncounterId);
else localStorage.removeItem('ttrpg.selectedEncounter');
if (selectedEncounterId) localStorage.setItem(`ttrpg.selectedEncounter.${campaignId}`, selectedEncounterId);
else localStorage.removeItem(`ttrpg.selectedEncounter.${campaignId}`);
} catch {}
}, [selectedEncounterId]);
}, [selectedEncounterId, campaignId]);
useEffect(() => {
if (!campaignId) {
@@ -2752,7 +2763,7 @@ function AdminView({ userId }) {
// Save scroll on unload + visibility change + interval.
useEffect(() => {
const onSave = () => {
try { localStorage.setItem('ttrpg.scrollY', String(window.scrollY)); } catch {}
try { localStorage.setItem(`ttrpg.scrollY.${selectedCampaignId}`, String(window.scrollY)); } catch {}
};
window.addEventListener('beforeunload', onSave);
window.addEventListener('pagehide', onSave);
@@ -2764,20 +2775,20 @@ function AdminView({ userId }) {
document.removeEventListener('visibilitychange', onSave);
clearInterval(interval);
};
}, []);
}, [selectedCampaignId]);
// Restore scroll once data loaded.
useEffect(() => {
if (scrollRestoredRef.current) return;
if (campaignsWithDetails.length === 0) return;
try {
const y = parseInt(localStorage.getItem('ttrpg.scrollY') || '0', 10);
const y = parseInt(localStorage.getItem(`ttrpg.scrollY.${selectedCampaignId}`) || '0', 10);
if (y > 0) {
scrollRestoredRef.current = true;
setTimeout(() => window.scrollTo(0, y), 300);
}
} catch {}
}, [campaignsWithDetails]);
}, [campaignsWithDetails, selectedCampaignId]);
// Persist selections across reload.
useEffect(() => {