From 96770099f604f53fec28b55be67ae4f3d4bfcf44 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:12:35 -0400 Subject: [PATCH] Persist campaign/encounter selection + scroll position across reload UI reset to top on every reload/code update. Now: - selectedCampaignId persisted via localStorage - selectedEncounterId persisted via localStorage - scrollY saved (beforeunload + pagehide + visibilitychange + 2s interval for Android Chrome reliability) - scroll restored after campaigns data loads (300ms delay for mobile render) --- src/App.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index 576fe10..bc08cc8 100644 --- a/src/App.js +++ b/src/App.js @@ -1,3 +1,4 @@ +// import React, { useState, useEffect, useRef, useMemo, createContext, useContext, useCallback } from 'react'; import * as shared from '@ttrpg/shared'; import { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, where, orderBy, limit, offset, getStorage, getStorageMode } from './storage'; @@ -2261,7 +2262,10 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac const { data: campaignDoc } = useFirestoreDocument(campaignId ? getPath.campaign(campaignId) : null); const [encounters, setEncounters] = useState([]); - const [selectedEncounterId, setSelectedEncounterId] = useState(null); + const [selectedEncounterId, setSelectedEncounterId] = useState(() => { + try { return localStorage.getItem('ttrpg.selectedEncounter') || null; } + catch { return null; } + }); const [showCreateModal, setShowCreateModal] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); @@ -2314,6 +2318,10 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac useEffect(() => { selectedEncounterIdRef.current = selectedEncounterId; + try { + if (selectedEncounterId) localStorage.setItem('ttrpg.selectedEncounter', selectedEncounterId); + else localStorage.removeItem('ttrpg.selectedEncounter'); + } catch {} }, [selectedEncounterId]); useEffect(() => { @@ -2636,11 +2644,53 @@ function AdminView({ userId }) { const [campaignsWithDetails, setCampaignsWithDetails] = useState([]); const [draggedCampaignId, setDraggedCampaignId] = useState(null); - const [selectedCampaignId, setSelectedCampaignId] = useState(null); + const [selectedCampaignId, setSelectedCampaignId] = useState(() => { + try { return localStorage.getItem('ttrpg.selectedCampaign') || null; } + catch { return null; } + }); const encounterStartedRef = useRef(false); const encounterActiveRef = useRef(false); const manualSelectRef = useRef(false); const prevDisplayCampaignRef = useRef(null); + const scrollRestoredRef = useRef(false); + + // Save scroll on unload + visibility change + interval. + useEffect(() => { + const onSave = () => { + try { localStorage.setItem('ttrpg.scrollY', String(window.scrollY)); } catch {} + }; + window.addEventListener('beforeunload', onSave); + window.addEventListener('pagehide', onSave); + document.addEventListener('visibilitychange', onSave); + const interval = setInterval(onSave, 2000); + return () => { + window.removeEventListener('beforeunload', onSave); + window.removeEventListener('pagehide', onSave); + document.removeEventListener('visibilitychange', onSave); + clearInterval(interval); + }; + }, []); + + // 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); + if (y > 0) { + scrollRestoredRef.current = true; + setTimeout(() => window.scrollTo(0, y), 300); + } + } catch {} + }, [campaignsWithDetails]); + + // Persist selections across reload. + useEffect(() => { + try { + if (selectedCampaignId) localStorage.setItem('ttrpg.selectedCampaign', selectedCampaignId); + else localStorage.removeItem('ttrpg.selectedCampaign'); + } catch {} + }, [selectedCampaignId]); // External display change (replay/other DM) = clear manual override, allow follow. useEffect(() => {