feat(#41): implement Widget/HUD system
- BaseWidget wrapper with loading/error states - WidgetRegistry for central widget management - WidgetGrid with react-grid-layout integration - TasksWidget, CalendarWidget, QuickCaptureWidget - useLayouts hooks for layout persistence - Comprehensive test suite (TDD approach)
This commit is contained in:
176
apps/web/src/hooks/useLayouts.ts
Normal file
176
apps/web/src/hooks/useLayouts.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* React Query hooks for layout management
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { UserLayout, WidgetPlacement } from "@mosaic/shared";
|
||||
|
||||
const LAYOUTS_KEY = ["layouts"];
|
||||
|
||||
interface CreateLayoutData {
|
||||
name: string;
|
||||
layout: WidgetPlacement[];
|
||||
isDefault?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface UpdateLayoutData {
|
||||
id: string;
|
||||
name?: string;
|
||||
layout?: WidgetPlacement[];
|
||||
isDefault?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all layouts for the current user
|
||||
*/
|
||||
export function useLayouts() {
|
||||
return useQuery<UserLayout[]>({
|
||||
queryKey: LAYOUTS_KEY,
|
||||
queryFn: async () => {
|
||||
const response = await fetch("/api/layouts");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch layouts");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single layout by ID
|
||||
*/
|
||||
export function useLayout(id: string) {
|
||||
return useQuery<UserLayout>({
|
||||
queryKey: [...LAYOUTS_KEY, id],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`/api/layouts/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch layout");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the default layout
|
||||
*/
|
||||
export function useDefaultLayout() {
|
||||
return useQuery<UserLayout>({
|
||||
queryKey: [...LAYOUTS_KEY, "default"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch("/api/layouts/default");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch default layout");
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new layout
|
||||
*/
|
||||
export function useCreateLayout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: CreateLayoutData) => {
|
||||
const response = await fetch("/api/layouts", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create layout");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate layouts cache to refetch
|
||||
queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing layout
|
||||
*/
|
||||
export function useUpdateLayout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...data }: UpdateLayoutData) => {
|
||||
const response = await fetch(`/api/layouts/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update layout");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
// Invalidate affected queries
|
||||
queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
||||
queryClient.invalidateQueries({ queryKey: [...LAYOUTS_KEY, variables.id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a layout
|
||||
*/
|
||||
export function useDeleteLayout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await fetch(`/api/layouts/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete layout");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate layouts cache to refetch
|
||||
queryClient.invalidateQueries({ queryKey: LAYOUTS_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper hook to save layout changes with debouncing
|
||||
*/
|
||||
export function useSaveLayout(layoutId: string) {
|
||||
const updateLayout = useUpdateLayout();
|
||||
|
||||
const saveLayout = (layout: WidgetPlacement[]) => {
|
||||
updateLayout.mutate({
|
||||
id: layoutId,
|
||||
layout,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
saveLayout,
|
||||
isSaving: updateLayout.isPending,
|
||||
error: updateLayout.error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user