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
This commit is contained in:
5
apps/web/src/lib/hooks/index.ts
Normal file
5
apps/web/src/lib/hooks/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Custom hooks
|
||||
*/
|
||||
|
||||
export { useLayout, useWorkspaceId } from "./useLayout";
|
||||
227
apps/web/src/lib/hooks/useLayout.ts
Normal file
227
apps/web/src/lib/hooks/useLayout.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user