feat(editor): preserve in-progress workspace across a login redirect

The Sign-in links navigate with a full page load (they mount in a
separate React root without Router context), which wipes the in-memory
Zustand workspace. New utils/workspaceDraft stashes the whole workspace
(reusing the lossless .vlx serialisation) to sessionStorage before that
navigation and restores it once when the editor remounts after login —
so a user who was building a circuit and signs in lands back on their
work instead of the empty starter board.

Strictly scoped to the login round-trip by a one-shot restore flag (not
a general autosave), and skipped when a named project is already loaded
so it never clobbers one. EditorPage calls restoreStashedWorkspace() on
mount; the pro overlay's auth links call stashWorkspaceForAuth() before
navigating.
This commit is contained in:
David Montero Crespo 2026-07-17 06:27:28 +02:00
parent 9826737831
commit 108d330efd
2 changed files with 84 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import React, { useRef, useState, useCallback, useEffect, lazy, Suspense } from
import { useTranslation } from 'react-i18next';
import { startSimulation } from '../simulation/spice/start';
import { useSEO } from '../utils/useSEO';
import { restoreStashedWorkspace } from '../utils/workspaceDraft';
import { CodeEditor } from '../components/editor/CodeEditor';
import { EditorToolbar } from '../components/editor/EditorToolbar';
import { FileExplorer } from '../components/editor/FileExplorer';
@ -100,6 +101,13 @@ export const EditorPage: React.FC = () => {
return startSimulation();
}, []);
// Restore an in-progress workspace stashed before a login redirect, so a
// user who was building something and signed in lands back on their circuit
// (not the empty starter board). One-shot; see utils/workspaceDraft.
useEffect(() => {
restoreStashedWorkspace();
}, []);
// ── GitHub star prompt (show twice at most: 2nd visit OR after 3 min) ──────
// Three localStorage flags drive this:
// velxio_star_prompted → dismissed the first ask

View File

@ -0,0 +1,76 @@
/**
* Workspace draft keep an anonymous user's in-progress circuit + code across
* a full-page navigation to /login and back.
*
* The Sign-in links mount in a separate React root without Router context, so
* they navigate with `window.location.assign` (full page load), which wipes
* the in-memory Zustand stores. Before that navigation we stash the whole
* workspace (reusing the lossless `.vlx` serialisation) into sessionStorage;
* when the editor remounts after login it restores the stash. sessionStorage
* is same-tab and survives the reload, and is discarded when the tab closes.
*
* Scoped strictly to the login round-trip via a one-shot restore flag it is
* NOT a general autosave, so a normal reload never resurrects an old draft.
*/
import type { BoardInstance } from '../types/board';
import { useProjectStore } from '../store/useProjectStore';
import { useSimulatorStore } from '../store/useSimulatorStore';
import { buildVlxPayload } from './vlxFile';
const DRAFT_KEY = 'velxio_ws_draft';
const RESTORE_FLAG = 'velxio_ws_restore';
/** True when the canvas holds something worth preserving (a real build, not
* just the empty starter board). */
export function workspaceHasWork(): boolean {
const sim = useSimulatorStore.getState();
return sim.components.length > 0 || sim.wires.length > 0;
}
/**
* Snapshot the current workspace and mark it for restore on the next editor
* mount. Call right before a full-page navigation to /login. No-op when there
* is nothing worth keeping or storage is unavailable.
*/
export function stashWorkspaceForAuth(): void {
try {
if (!workspaceHasWork()) return;
sessionStorage.setItem(DRAFT_KEY, JSON.stringify(buildVlxPayload()));
sessionStorage.setItem(RESTORE_FLAG, '1');
} catch {
// storage full / disabled — degrade to losing the draft, never throw.
}
}
/**
* If a stash is pending (set before the login redirect), load it into the
* stores and clear it. Runs once on editor mount. Skips when a named project
* is already loaded so it never clobbers one. Returns whether it restored.
*/
export function restoreStashedWorkspace(): boolean {
let raw: string | null = null;
try {
if (sessionStorage.getItem(RESTORE_FLAG) !== '1') return false;
sessionStorage.removeItem(RESTORE_FLAG);
raw = sessionStorage.getItem(DRAFT_KEY);
sessionStorage.removeItem(DRAFT_KEY);
} catch {
return false;
}
if (!raw) return false;
if (useProjectStore.getState().currentProject) return false;
try {
const payload = JSON.parse(raw);
useSimulatorStore.getState().loadProjectState({
boards: payload.boards as unknown as BoardInstance[],
fileGroups: payload.fileGroups,
components: payload.components,
wires: payload.wires,
activeBoardId: payload.activeBoardId,
});
return true;
} catch {
return false;
}
}