Auto-restore projects when signing in on a new device
Projects that exist on the server but not in this browser's local cache (e.g. a fresh device) are now pulled down automatically, instead of requiring a manual "Restore" click per project. Shows a brief "synced N projects" toast instead. Conflict handling for projects that exist locally but are stale is unchanged (still a manual "Pull latest" action), since silently overwriting local edits there would risk data loss. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+26
-34
@@ -1003,8 +1003,8 @@ const App = () => {
|
|||||||
const [typedChunkBulk, setTypedChunkBulk] = useState('');
|
const [typedChunkBulk, setTypedChunkBulk] = useState('');
|
||||||
const saveTimerRef = useRef(null);
|
const saveTimerRef = useRef(null);
|
||||||
const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error'
|
const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error'
|
||||||
const [remoteOnlyProjects, setRemoteOnlyProjects] = useState([]); // projects on server not in localStorage
|
|
||||||
const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
|
const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
|
||||||
|
const [autoRestoredCount, setAutoRestoredCount] = useState(null); // brief "synced from another device" toast
|
||||||
const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
|
const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
|
||||||
const [suggestingHebrewForChunkId, setSuggestingHebrewForChunkId] = useState(null);
|
const [suggestingHebrewForChunkId, setSuggestingHebrewForChunkId] = useState(null);
|
||||||
// suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
|
// suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
|
||||||
@@ -1389,25 +1389,40 @@ const App = () => {
|
|||||||
|
|
||||||
// Once we know who's signed in, reconcile the local project index against the server.
|
// Once we know who's signed in, reconcile the local project index against the server.
|
||||||
// Runs on every authUser change (including logout -> different login) so a previous
|
// Runs on every authUser change (including logout -> different login) so a previous
|
||||||
// account's remote-only/stale suggestions never linger after switching users.
|
// account's stale suggestions never linger after switching users. Projects that exist
|
||||||
|
// on the server but not on this device (e.g. a brand new browser) are pulled down
|
||||||
|
// automatically instead of requiring a manual "Restore" click per project.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
setRemoteOnlyProjects([]);
|
|
||||||
setStaleLocalProjects([]);
|
setStaleLocalProjects([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let cancelled = false;
|
||||||
const localIndex = loadProjectIndex();
|
const localIndex = loadProjectIndex();
|
||||||
listRemoteProjects().then((result) => {
|
listRemoteProjects().then(async (result) => {
|
||||||
if (!result.ok) return;
|
if (!result.ok || cancelled) return;
|
||||||
const localMap = new Map(localIndex.map((e) => [e.id, e]));
|
const localMap = new Map(localIndex.map((e) => [e.id, e]));
|
||||||
|
|
||||||
const missing = result.data.filter((e) => !localMap.has(e.id));
|
const missing = result.data.filter((e) => !localMap.has(e.id));
|
||||||
setRemoteOnlyProjects(missing);
|
if (missing.length > 0) {
|
||||||
|
const fetched = await Promise.all(missing.map((e) => loadRemoteProject(e.id)));
|
||||||
|
fetched.filter((r) => r.ok).forEach((r) => saveProjectToStorage(r.data));
|
||||||
|
if (cancelled) return;
|
||||||
|
const restoredCount = fetched.filter((r) => r.ok).length;
|
||||||
|
if (restoredCount > 0) {
|
||||||
|
setProjectIndex(loadProjectIndex());
|
||||||
|
setAutoRestoredCount(restoredCount);
|
||||||
|
window.setTimeout(() => setAutoRestoredCount(null), 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stale = result.data.filter((e) => {
|
const stale = result.data.filter((e) => {
|
||||||
const local = localMap.get(e.id);
|
const local = localMap.get(e.id);
|
||||||
return local && (e.lastEdited ?? 0) > (local.lastEdited ?? 0);
|
return local && (e.lastEdited ?? 0) > (local.lastEdited ?? 0);
|
||||||
});
|
});
|
||||||
setStaleLocalProjects(stale);
|
if (!cancelled) setStaleLocalProjects(stale);
|
||||||
});
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
}, [authUser]);
|
}, [authUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -3088,17 +3103,6 @@ const deleteProject = (id) => {
|
|||||||
deleteRemoteProject(id); // fire-and-forget
|
deleteRemoteProject(id); // fire-and-forget
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreRemoteProject = async (id) => {
|
|
||||||
const result = await loadRemoteProject(id);
|
|
||||||
if (!result.ok) {
|
|
||||||
alert('Could not restore project from server.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveProjectToStorage(result.data);
|
|
||||||
setProjectIndex(loadProjectIndex());
|
|
||||||
setRemoteOnlyProjects((prev) => prev.filter((e) => e.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const pullLatestFromServer = async (id) => {
|
const pullLatestFromServer = async (id) => {
|
||||||
const result = await loadRemoteProject(id);
|
const result = await loadRemoteProject(id);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@@ -3635,23 +3639,11 @@ const restoreRemoteProject = async (id) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||||
{remoteOnlyProjects.length > 0 && (
|
{autoRestoredCount !== null && (
|
||||||
<div className="mb-6 rounded-2xl border border-sky-200 bg-sky-50 p-4">
|
<div className="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
|
||||||
<p className="mb-3 text-sm font-semibold text-sky-800">
|
<p className="text-sm font-semibold text-emerald-800">
|
||||||
📥 {remoteOnlyProjects.length} project{remoteOnlyProjects.length > 1 ? 's' : ''} found on the server that aren't saved locally:
|
📥 Synced {autoRestoredCount} project{autoRestoredCount > 1 ? 's' : ''} from another device.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{remoteOnlyProjects.map((entry) => (
|
|
||||||
<button
|
|
||||||
key={entry.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => restoreRemoteProject(entry.id)}
|
|
||||||
className="rounded-xl bg-sky-700 px-4 py-2 text-sm font-semibold text-white transition hover:bg-sky-600"
|
|
||||||
>
|
|
||||||
Restore "{entry.title}"
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{staleLocalProjects.length > 0 && (
|
{staleLocalProjects.length > 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user