From cef6da87f4349e2caa3c6f13f6670ecb7d8d2882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 20 Dec 2025 21:24:29 +0100 Subject: [PATCH] feat: implement dual-state system for temporary layout sessions - Add temporaryStateAtom to track in-memory sessions - Update useGrimoire to handle conditional state management - Previews and Direct links now use temporary state, preserving dashboard - Add Apply and Discard logic to persist or revert temporary sessions --- src/components/Home.tsx | 76 ++++++++++-------------- src/components/SpellbookDropdown.tsx | 6 +- src/core/state.ts | 89 +++++++++++++++++++++------- 3 files changed, 102 insertions(+), 69 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index cd8269f..e80d114 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -23,10 +23,16 @@ import { SpellbookEvent } from "@/types/spell"; import { toast } from "sonner"; import { Button } from "./ui/button"; -const PREVIEW_BACKUP_KEY = "grimoire-preview-backup"; - export default function Home() { - const { state, updateLayout, removeWindow, loadSpellbook } = useGrimoire(); + const { + state, + updateLayout, + removeWindow, + switchToTemporary, + applyTemporaryToPersistent, + discardTemporary, + isTemporary + } = useGrimoire(); const [commandLauncherOpen, setCommandLauncherOpen] = useState(false); const { actor, identifier } = useParams(); const navigate = useNavigate(); @@ -35,6 +41,7 @@ export default function Home() { // Preview state const [resolvedPubkey, setResolvedPubkey] = useState(null); const isPreviewPath = location.pathname.startsWith("/preview/"); + const isDirectPath = actor && identifier && !isPreviewPath; const [hasLoadedSpellbook, setHasLoadedSpellbook] = useState(false); // 1. Resolve actor to pubkey @@ -42,6 +49,8 @@ export default function Home() { if (!actor) { setResolvedPubkey(null); setHasLoadedSpellbook(false); + // If we were in temporary mode and navigated back to /, discard + if (isTemporary) discardTemporary(); return; } @@ -62,7 +71,7 @@ export default function Home() { }; resolve(); - }, [actor]); + }, [actor, isTemporary, discardTemporary]); // 2. Fetch the spellbook event const pointer = useMemo(() => { @@ -82,58 +91,35 @@ export default function Home() { try { const parsed = parseSpellbook(spellbookEvent as SpellbookEvent); + // Use the new temporary state system + switchToTemporary(parsed); + setHasLoadedSpellbook(true); + if (isPreviewPath) { - // In preview mode, save current state to sessionStorage for recovery - if (!sessionStorage.getItem(PREVIEW_BACKUP_KEY)) { - sessionStorage.setItem(PREVIEW_BACKUP_KEY, JSON.stringify(state)); - } - - loadSpellbook(parsed); - setHasLoadedSpellbook(true); toast.info(`Previewing layout: ${parsed.title}`, { - description: "You are in preview mode. Apply to keep this layout or discard to return.", + description: "You are in a temporary session. Apply to keep this layout permanently.", + }); + } else if (isDirectPath) { + toast.success(`Loaded temporary layout: ${parsed.title}`, { + description: "Visit / to return to your permanent dashboard, or click Apply Layout.", }); - } else { - // Direct mode: Just load it immediately - loadSpellbook(parsed); - setHasLoadedSpellbook(true); - // Update URL to home after loading to avoid re-loading on refresh if they start modifying - navigate("/", { replace: true }); - toast.success(`Loaded layout: ${parsed.title}`); } } catch (e) { console.error("Failed to parse spellbook:", e); toast.error("Failed to load spellbook"); } } - }, [spellbookEvent, hasLoadedSpellbook, isPreviewPath]); + }, [spellbookEvent, hasLoadedSpellbook, isPreviewPath, isDirectPath, switchToTemporary]); const handleApplyLayout = () => { - sessionStorage.removeItem(PREVIEW_BACKUP_KEY); + applyTemporaryToPersistent(); navigate("/", { replace: true }); - toast.success("Layout applied permanently"); + toast.success("Layout applied to your dashboard permanently"); }; const handleDiscardPreview = () => { - const backup = sessionStorage.getItem(PREVIEW_BACKUP_KEY); - if (backup) { - try { - JSON.parse(backup); - // We need a way to restore the whole state. - // For now, the easiest way to "restore" a persisted state from sessionStorage - // is to clear our local storage and reload, or manually call setters. - // But loadSpellbook already overwrote it in localStorage via Jotai. - - // Let's try to overwrite localStorage directly and reload for a clean restore - localStorage.setItem("grimoire-state", backup); - sessionStorage.removeItem(PREVIEW_BACKUP_KEY); - window.location.href = "/"; - return; - } catch (e) { - console.error("Failed to restore backup:", e); - } - } - navigate("/"); + discardTemporary(); + navigate("/", { replace: true }); }; // Sync active account and fetch relay lists @@ -206,11 +192,13 @@ export default function Home() { />
- {isPreviewPath && ( -
+ {isTemporary && ( +
- Preview Mode: {spellbookEvent?.tags.find(t => t[0] === 'title')?.[1] || 'Spellbook'} + + {isPreviewPath ? "Preview Mode" : "Temporary Layout"}: {spellbookEvent?.tags.find(t => t[0] === 'title')?.[1] || 'Spellbook'} +