Files
stack/apps/web/src/lib/hooks/useLayout.ts
Jason Woltje f47dd8bc92 feat: add domains, ideas, layouts, widgets API modules
- Add DomainsModule with full CRUD, search, and activity logging
- Add IdeasModule with quick capture endpoint
- Add LayoutsModule for user dashboard layouts
- Add WidgetsModule for widget definitions (read-only)
- Update ActivityService with domain/idea logging methods
- Register all new modules in AppModule
2026-01-29 13:47:03 -06:00

228 lines
5.4 KiB
TypeScript

/**
* Hook for managing widget layouts
*/
import { useCallback, useState, useEffect } from "react";
import type { WidgetPlacement, LayoutConfig } from "@mosaic/shared";
const STORAGE_KEY = "mosaic-layout";
const DEFAULT_LAYOUT_NAME = "default";
/**
* Local storage key for user's workspace preference
*/
const WORKSPACE_KEY = "mosaic-workspace-id";
/**
* Hook to manage widget layout state
*/
export function useLayout() {
const [layouts, setLayouts] = useState<Record<string, LayoutConfig>>({});
const [currentLayoutId, setCurrentLayoutId] = useState<string>(DEFAULT_LAYOUT_NAME);
const [isLoading, setIsLoading] = useState(true);
// Load layouts from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
setLayouts(parsed);
}
// Load current layout ID preference
const storedLayoutId = localStorage.getItem(`${STORAGE_KEY}-current`);
if (storedLayoutId) {
setCurrentLayoutId(storedLayoutId);
}
} catch (error) {
console.error("Failed to load layouts from localStorage:", error);
} finally {
setIsLoading(false);
}
}, []);
// Save layouts to localStorage whenever they change
useEffect(() => {
if (!isLoading) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layouts));
localStorage.setItem(`${STORAGE_KEY}-current`, currentLayoutId);
} catch (error) {
console.error("Failed to save layouts to localStorage:", error);
}
}
}, [layouts, currentLayoutId, isLoading]);
const currentLayout = layouts[currentLayoutId];
const updateLayout = useCallback(
(layoutItems: WidgetPlacement[]) => {
setLayouts((prev) => ({
...prev,
[currentLayoutId]: {
id: currentLayoutId,
name: prev[currentLayoutId]?.name || "My Layout",
layout: layoutItems,
},
}));
},
[currentLayoutId]
);
const addWidget = useCallback(
(widget: WidgetPlacement) => {
setLayouts((prev) => {
const currentLayoutData = prev[currentLayoutId];
if (!currentLayoutData) {
return prev;
}
return {
...prev,
[currentLayoutId]: {
...currentLayoutData,
layout: [...currentLayoutData.layout, widget],
},
};
});
},
[currentLayoutId]
);
const removeWidget = useCallback(
(widgetId: string) => {
setLayouts((prev) => {
const currentLayoutData = prev[currentLayoutId];
if (!currentLayoutData) {
return prev;
}
return {
...prev,
[currentLayoutId]: {
...currentLayoutData,
layout: currentLayoutData.layout.filter((w) => w.i !== widgetId),
},
};
});
},
[currentLayoutId]
);
const updateWidget = useCallback(
(widgetId: string, updates: Partial<WidgetPlacement>) => {
setLayouts((prev) => {
const currentLayoutData = prev[currentLayoutId];
if (!currentLayoutData) {
return prev;
}
return {
...prev,
[currentLayoutId]: {
...currentLayoutData,
layout: currentLayoutData.layout.map((w) =>
w.i === widgetId ? { ...w, ...updates } : w
),
},
};
});
},
[currentLayoutId]
);
const createLayout = useCallback(
(name: string) => {
const id = `layout-${Date.now()}`;
setLayouts((prev) => ({
...prev,
[id]: {
id,
name,
layout: [],
},
}));
setCurrentLayoutId(id);
return id;
},
[]
);
const deleteLayout = useCallback(
(layoutId: string) => {
setLayouts((prev) => {
const { [layoutId]: deleted, ...rest } = prev;
// If we deleted the current layout, switch to default
if (layoutId === currentLayoutId) {
const remainingIds = Object.keys(rest);
setCurrentLayoutId(remainingIds[0] || DEFAULT_LAYOUT_NAME);
}
return rest;
});
},
[currentLayoutId]
);
const renameLayout = useCallback((layoutId: string, name: string) => {
setLayouts((prev) => {
const existing = prev[layoutId];
if (!existing) return prev;
return {
...prev,
[layoutId]: {
...existing,
name,
},
};
});
}, []);
const resetLayout = useCallback(() => {
setLayouts({
[DEFAULT_LAYOUT_NAME]: {
id: DEFAULT_LAYOUT_NAME,
name: "Default Layout",
layout: [],
},
});
setCurrentLayoutId(DEFAULT_LAYOUT_NAME);
}, []);
return {
layouts,
currentLayout,
currentLayoutId,
isLoading,
updateLayout,
addWidget,
removeWidget,
updateWidget,
createLayout,
deleteLayout,
renameLayout,
switchLayout: setCurrentLayoutId,
resetLayout,
};
}
/**
* Hook to get the current workspace ID
*/
export function useWorkspaceId(): string | null {
const [workspaceId, setWorkspaceId] = useState<string | null>(null);
useEffect(() => {
try {
const stored = localStorage.getItem(WORKSPACE_KEY);
if (stored) {
setWorkspaceId(stored);
}
} catch (error) {
console.error("Failed to load workspace ID from localStorage:", error);
}
}, []);
return workspaceId;
}