import { Workspace, TodoEntity, DocEntity, Status, LayoutNode } from "./types"; export function createDefaultWorkspace(): Workspace { const now = Date.now(); const todoId = crypto.randomUUID(); const docId = crypto.randomUUID(); const defaultStatuses: Status[] = [ { id: "status_todo", name: "To Do", order: 0, color: "#808080" }, { id: "status_inprogress", name: "In Progress", order: 1, color: "#0ea5e9", }, { id: "status_done", name: "Done", order: 2, color: "#10b981" }, ]; const todoEntity: TodoEntity = { id: todoId, type: "todo", name: "Tasks", config: { statuses: defaultStatuses }, tags: {}, tasks: {}, createdAt: now, updatedAt: now, }; const docEntity: DocEntity = { id: docId, type: "doc", name: "Notes", content: { version: "2.28.0", time: now, blocks: [] }, createdAt: now, updatedAt: now, }; return { id: crypto.randomUUID(), name: "My Workspace", entities: { [todoId]: todoEntity, [docId]: docEntity, }, layout: [ { type: "group", id: crypto.randomUUID(), name: "Productivity", children: [ { entityId: todoId, type: "item" }, { entityId: docId, type: "item" }, ], }, ], createdAt: now, updatedAt: now, }; } export function loadWorkspaceFromLocalStorage(id: string): Workspace { try { const raw = localStorage.getItem(id); if (!raw) { return createDefaultWorkspace(); } const parsed = JSON.parse(raw); // Basic shape validation if ( typeof parsed !== "object" || parsed === null || typeof parsed.id !== "string" ) { console.warn("Invalid workspace shape. Resetting."); return createDefaultWorkspace(); } return parsed as Workspace; } catch (err) { console.error("Failed to load workspace:", err); return createDefaultWorkspace(); } } 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; } // layout helpers ------------------------------------------------------------------- type RemoveResult = { layout: LayoutNode[]; removedEntityIds: string[] }; function collectEntityIds(node: LayoutNode): string[] { if (node.type === "item") return [node.entityId]; const ids: string[] = []; for (const child of node.children) { ids.push(...collectEntityIds(child)); } return ids; } export function removeLayoutNode( layout: LayoutNode[], id: string, ): RemoveResult { let removed: string[] = []; function recurse(nodes: LayoutNode[]): LayoutNode[] { return nodes .map((node) => { if (node.type === "group") { if (node.id === id) { removed.push(...collectEntityIds(node)); return null; } const children = recurse(node.children); if (children.length !== node.children.length) { // some children removed } return { ...node, children } as LayoutNode; } else { if (node.entityId === id) { removed.push(node.entityId); return null; } return node; } }) .filter(Boolean) as LayoutNode[]; } const newLayout = recurse(layout); return { layout: newLayout, removedEntityIds: removed }; } export function renameLayoutGroup( layout: LayoutNode[], id: string, newName: string, ): LayoutNode[] { return layout.map((node) => { if (node.type === "group") { if (node.id === id) { return { ...node, name: newName }; } return { ...node, children: renameLayoutGroup(node.children, id, newName), }; } return node; }); } export function addLayoutGroup( layout: LayoutNode[], parentId: string | null, group: LayoutNode, ): LayoutNode[] { if (!parentId) { return [...layout, group]; } return layout.map((node) => { if (node.type === "group") { if (node.id === parentId) { return { ...node, children: [...node.children, group] }; } return { ...node, children: addLayoutGroup(node.children, parentId, group), }; } return node; }); } export function addLayoutItem( layout: LayoutNode[], parentId: string | null, item: LayoutNode, ): LayoutNode[] { if (!parentId) { return [...layout, item]; } return layout.map((node) => { if (node.type === "group") { if (node.id === parentId) { return { ...node, children: [...node.children, item] }; } return { ...node, children: addLayoutItem(node.children, parentId, item), }; } return node; }); } // entity creation helpers ---------------------------------------------------------- export function createTodoEntity(name = "New list"): TodoEntity { const id = crypto.randomUUID(); const now = Date.now(); return { id, type: "todo", name, config: { statuses: [] }, tags: {}, tasks: {}, createdAt: now, updatedAt: now, }; } export function createDocEntity(name = "New doc"): DocEntity { const id = crypto.randomUUID(); const now = Date.now(); return { id, type: "doc", name, content: { version: "2.28.0", time: now, blocks: [] }, createdAt: now, updatedAt: now, }; }