From 108d330efd1f181006f7fe769a35e66851bc489c Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Fri, 17 Jul 2026 06:27:28 +0200 Subject: [PATCH] feat(editor): preserve in-progress workspace across a login redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/pages/EditorPage.tsx | 8 +++ frontend/src/utils/workspaceDraft.ts | 76 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 frontend/src/utils/workspaceDraft.ts diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 106f03c9..1715e38d 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -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 diff --git a/frontend/src/utils/workspaceDraft.ts b/frontend/src/utils/workspaceDraft.ts new file mode 100644 index 00000000..6b4ce9f3 --- /dev/null +++ b/frontend/src/utils/workspaceDraft.ts @@ -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; + } +}