From 9200e1cfadc736bef968ec3cba16c26c16370d98 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 28 Feb 2026 02:30:41 +0100 Subject: [PATCH] Sidebar --- src/app/workspace/page.tsx | 6 +- src/components/sidebar/AppSidebar.tsx | 95 ++++++++++++++++++++++++++- src/lib/workspace.ts | 34 ++++++++++ 3 files changed, 131 insertions(+), 4 deletions(-) diff --git a/src/app/workspace/page.tsx b/src/app/workspace/page.tsx index 4a512cb..d9a4302 100644 --- a/src/app/workspace/page.tsx +++ b/src/app/workspace/page.tsx @@ -116,7 +116,11 @@ export default function Workspace() { } return ( - +
diff --git a/src/components/sidebar/AppSidebar.tsx b/src/components/sidebar/AppSidebar.tsx index 200c40c..d0c07d8 100644 --- a/src/components/sidebar/AppSidebar.tsx +++ b/src/components/sidebar/AppSidebar.tsx @@ -1,5 +1,7 @@ "use client"; +import * as React from "react"; + import { Sidebar, SidebarContent, @@ -16,11 +18,13 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { ChevronDown } from "lucide-react"; import { Route, SetState, Workspace } from "@/lib/types"; +import { listWorkspaces, saveWorkspaceToLocalStorage } from "@/lib/workspace"; // Simple ID generator const id = (prefix = "t") => @@ -30,13 +34,77 @@ type AppSidebarProps = { children: React.ReactNode; workspace: Workspace; setRoute: SetState; + /** optional setter for the current workspace, used for rename */ + setWorkspace?: React.Dispatch>; }; export default function AppSidebar({ children, workspace, setRoute, + setWorkspace, }: AppSidebarProps) { + const [availableWorkspaces, setAvailableWorkspaces] = React.useState< + Workspace[] + >([]); + + // reload list when component mounts or whenever the current workspace changes + React.useEffect(() => { + setAvailableWorkspaces(listWorkspaces()); + }, [workspace]); + + const switchWorkspace = (id: string) => { + setRoute({ workspace: id }); + }; + + const importWorkspace = () => { + const json = window.prompt( + "Paste a workspace JSON to import (generated by export)", + ); + if (!json) return; + + try { + const parsed = JSON.parse(json); + if (parsed && typeof parsed.id === "string") { + saveWorkspaceToLocalStorage(parsed); + setAvailableWorkspaces(listWorkspaces()); + switchWorkspace(parsed.id); + } else { + throw new Error("invalid shape"); + } + } catch (err) { + console.error("Failed to import workspace:", err); + window.alert("Import failed: invalid JSON"); + } + }; + + const exportWorkspace = () => { + try { + const text = JSON.stringify(workspace, null, 2); + navigator.clipboard.writeText(text); + window.alert("Workspace JSON copied to clipboard"); + } catch (err) { + console.error("Export failed", err); + window.alert("Failed to copy workspace JSON"); + } + }; + + const renameWorkspace = () => { + const newName = window.prompt("New workspace name", workspace.name); + if (!newName || newName === workspace.name) return; + + const updated: Workspace = { + ...workspace, + name: newName, + updatedAt: Date.now(), + }; + saveWorkspaceToLocalStorage(updated); + setAvailableWorkspaces(listWorkspaces()); + if (setWorkspace) { + setWorkspace(updated); + } + }; + return ( @@ -56,9 +124,30 @@ export default function AppSidebar({ align="start" className="w-[--radix-popper-anchor-width]" > - {/* Future: real workspace switching */} - Rename Workspace - Export Workspace + {/* list all known workspaces */} + {availableWorkspaces.map((w) => ( + switchWorkspace(w.id)} + > + {w.name} + {w.id === workspace.id ? " (current)" : ""} + + ))} + + + Import Workspace + + + + Rename Workspace + + + Export Workspace + diff --git a/src/lib/workspace.ts b/src/lib/workspace.ts index 01ab95f..eee2c04 100644 --- a/src/lib/workspace.ts +++ b/src/lib/workspace.ts @@ -90,8 +90,42 @@ export function loadWorkspaceFromLocalStorage(id: string): Workspace { export function saveWorkspaceToLocalStorage(workspace: Workspace): void { try { const serialized = JSON.stringify(workspace); + // keep using raw id for backwards compatibility localStorage.setItem(workspace.id, serialized); } catch (err) { console.error("Failed to save workspace:", err); } } + +/** + * Scan localStorage and return all entries that appear to be workspaces. + * We ignore keys starting with "route_" which are used for routing state. + */ +export function listWorkspaces(): Workspace[] { + const workspaces: Workspace[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (!key) continue; + if (key.startsWith("route_")) continue; + + const raw = localStorage.getItem(key); + if (!raw) continue; + + try { + const parsed = JSON.parse(raw); + if ( + parsed && + typeof parsed === "object" && + typeof parsed.id === "string" && + typeof parsed.name === "string" && + parsed.entities && + parsed.layout + ) { + workspaces.push(parsed as Workspace); + } + } catch { + // ignore non-json values + } + } + return workspaces; +}