@weirdfingers/boards 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Hook for managing a single board.
3
+ */
4
+
5
+ import { useCallback, useMemo } from "react";
6
+ import { useQuery, useMutation } from "urql";
7
+ import { useAuth } from "../auth/hooks/useAuth";
8
+ import {
9
+ GET_BOARD,
10
+ UPDATE_BOARD,
11
+ DELETE_BOARD,
12
+ ADD_BOARD_MEMBER,
13
+ UPDATE_BOARD_MEMBER_ROLE,
14
+ REMOVE_BOARD_MEMBER,
15
+ UpdateBoardInput,
16
+ BoardRole,
17
+ } from "../graphql/operations";
18
+
19
+ interface User {
20
+ id: string;
21
+ email: string;
22
+ displayName: string;
23
+ avatarUrl?: string;
24
+ createdAt: string;
25
+ }
26
+
27
+ interface BoardMember {
28
+ id: string;
29
+ boardId: string;
30
+ userId: string;
31
+ role: BoardRole;
32
+ invitedBy?: string;
33
+ joinedAt: string;
34
+ user: User;
35
+ inviter?: User;
36
+ }
37
+
38
+ interface Generation {
39
+ id: string;
40
+ boardId: string;
41
+ userId: string;
42
+ generatorName: string;
43
+ artifactType: string;
44
+ status: string;
45
+ progress: number;
46
+ storageUrl?: string | null;
47
+ thumbnailUrl?: string | null;
48
+ inputParams: Record<string, unknown>;
49
+ outputMetadata: Record<string, unknown>;
50
+ errorMessage?: string | null;
51
+ createdAt: string;
52
+ updatedAt: string;
53
+ completedAt?: string | null;
54
+ }
55
+
56
+ interface Board {
57
+ id: string;
58
+ tenantId: string;
59
+ ownerId: string;
60
+ title: string;
61
+ description?: string;
62
+ isPublic: boolean;
63
+ settings: Record<string, unknown>;
64
+ metadata: Record<string, unknown>;
65
+ createdAt: string;
66
+ updatedAt: string;
67
+ generationCount: number;
68
+ owner: User;
69
+ members: BoardMember[];
70
+ generations: Generation[];
71
+ }
72
+
73
+ type MemberRole = BoardRole;
74
+
75
+ interface BoardPermissions {
76
+ canEdit: boolean;
77
+ canDelete: boolean;
78
+ canAddMembers: boolean;
79
+ canRemoveMembers: boolean;
80
+ canGenerate: boolean;
81
+ canExport: boolean;
82
+ }
83
+
84
+ interface ShareLinkOptions {
85
+ expiresIn?: number;
86
+ permissions?: string[];
87
+ }
88
+
89
+ interface ShareLink {
90
+ id: string;
91
+ url: string;
92
+ expiresAt?: string;
93
+ permissions: string[];
94
+ }
95
+
96
+ interface BoardHook {
97
+ board: Board | null;
98
+ members: BoardMember[];
99
+ permissions: BoardPermissions;
100
+ loading: boolean;
101
+ error: Error | null;
102
+
103
+ // Board operations
104
+ updateBoard: (updates: Partial<UpdateBoardInput>) => Promise<Board>;
105
+ deleteBoard: () => Promise<void>;
106
+ refresh: () => Promise<void>;
107
+
108
+ // Member management
109
+ addMember: (email: string, role: MemberRole) => Promise<BoardMember>;
110
+ removeMember: (memberId: string) => Promise<void>;
111
+ updateMemberRole: (
112
+ memberId: string,
113
+ role: MemberRole
114
+ ) => Promise<BoardMember>;
115
+
116
+ // Sharing (placeholder - would need backend implementation)
117
+ generateShareLink: (options: ShareLinkOptions) => Promise<ShareLink>;
118
+ revokeShareLink: (linkId: string) => Promise<void>;
119
+ }
120
+
121
+ export function useBoard(boardId: string): BoardHook {
122
+ const { user } = useAuth();
123
+
124
+ // Query for board data
125
+ const [{ data, fetching, error }, reexecuteQuery] = useQuery({
126
+ query: GET_BOARD,
127
+ variables: { id: boardId },
128
+ pause: !boardId,
129
+ requestPolicy: "cache-and-network", // Always fetch fresh data while showing cached data
130
+ });
131
+
132
+ // Mutations
133
+ const [, updateBoardMutation] = useMutation(UPDATE_BOARD);
134
+ const [, deleteBoardMutation] = useMutation(DELETE_BOARD);
135
+ const [, addMemberMutation] = useMutation(ADD_BOARD_MEMBER);
136
+ const [, updateMemberRoleMutation] = useMutation(UPDATE_BOARD_MEMBER_ROLE);
137
+ const [, removeMemberMutation] = useMutation(REMOVE_BOARD_MEMBER);
138
+
139
+ const board = useMemo(() => data?.board || null, [data?.board]);
140
+ const members = useMemo(() => board?.members || [], [board?.members]);
141
+
142
+ // Calculate permissions based on user role
143
+ const permissions = useMemo((): BoardPermissions => {
144
+ if (!board || !user) {
145
+ return {
146
+ canEdit: false,
147
+ canDelete: false,
148
+ canAddMembers: false,
149
+ canRemoveMembers: false,
150
+ canGenerate: false,
151
+ canExport: false,
152
+ };
153
+ }
154
+
155
+ // Check if user is the board owner
156
+ const isOwner = board.ownerId === user.id;
157
+
158
+ // Find user's role in board members
159
+ const userMember = members.find(
160
+ (member: BoardMember) => member.userId === user.id
161
+ );
162
+ const userRole = userMember?.role;
163
+
164
+ const isAdmin = userRole === BoardRole.ADMIN;
165
+ const isEditor = userRole === BoardRole.EDITOR || isAdmin;
166
+ const isViewer = userRole === BoardRole.VIEWER || isEditor;
167
+
168
+ return {
169
+ canEdit: isOwner || isAdmin || isEditor,
170
+ canDelete: isOwner,
171
+ canAddMembers: isOwner || isAdmin,
172
+ canRemoveMembers: isOwner || isAdmin,
173
+ canGenerate: isOwner || isAdmin || isEditor,
174
+ canExport: isViewer, // Even viewers can export
175
+ };
176
+ }, [board, user, members]);
177
+
178
+ const updateBoard = useCallback(
179
+ async (updates: Partial<UpdateBoardInput>): Promise<Board> => {
180
+ if (!boardId) {
181
+ throw new Error("Board ID is required");
182
+ }
183
+
184
+ const result = await updateBoardMutation({
185
+ id: boardId,
186
+ input: updates,
187
+ });
188
+
189
+ if (result.error) {
190
+ throw new Error(result.error.message);
191
+ }
192
+
193
+ if (!result.data?.updateBoard) {
194
+ throw new Error("Failed to update board");
195
+ }
196
+
197
+ return result.data.updateBoard;
198
+ },
199
+ [boardId, updateBoardMutation]
200
+ );
201
+
202
+ const deleteBoard = useCallback(async (): Promise<void> => {
203
+ if (!boardId) {
204
+ throw new Error("Board ID is required");
205
+ }
206
+
207
+ const result = await deleteBoardMutation({ id: boardId });
208
+
209
+ if (result.error) {
210
+ throw new Error(result.error.message);
211
+ }
212
+
213
+ if (!result.data?.deleteBoard?.success) {
214
+ throw new Error("Failed to delete board");
215
+ }
216
+ }, [boardId, deleteBoardMutation]);
217
+
218
+ const addMember = useCallback(
219
+ async (email: string, role: MemberRole): Promise<BoardMember> => {
220
+ if (!boardId) {
221
+ throw new Error("Board ID is required");
222
+ }
223
+
224
+ const result = await addMemberMutation({
225
+ boardId,
226
+ email,
227
+ role,
228
+ });
229
+
230
+ if (result.error) {
231
+ throw new Error(result.error.message);
232
+ }
233
+
234
+ if (!result.data?.addBoardMember) {
235
+ throw new Error("Failed to add member");
236
+ }
237
+
238
+ // Refresh board data to get updated members list
239
+ reexecuteQuery({ requestPolicy: "network-only" });
240
+
241
+ return result.data.addBoardMember;
242
+ },
243
+ [boardId, addMemberMutation, reexecuteQuery]
244
+ );
245
+
246
+ const removeMember = useCallback(
247
+ async (memberId: string): Promise<void> => {
248
+ const result = await removeMemberMutation({ id: memberId });
249
+
250
+ if (result.error) {
251
+ throw new Error(result.error.message);
252
+ }
253
+
254
+ if (!result.data?.removeBoardMember?.success) {
255
+ throw new Error("Failed to remove member");
256
+ }
257
+
258
+ // Refresh board data to get updated members list
259
+ reexecuteQuery({ requestPolicy: "network-only" });
260
+ },
261
+ [removeMemberMutation, reexecuteQuery]
262
+ );
263
+
264
+ const updateMemberRole = useCallback(
265
+ async (memberId: string, role: MemberRole): Promise<BoardMember> => {
266
+ const result = await updateMemberRoleMutation({
267
+ id: memberId,
268
+ role,
269
+ });
270
+
271
+ if (result.error) {
272
+ throw new Error(result.error.message);
273
+ }
274
+
275
+ if (!result.data?.updateBoardMemberRole) {
276
+ throw new Error("Failed to update member role");
277
+ }
278
+
279
+ // Refresh board data to get updated members list
280
+ reexecuteQuery({ requestPolicy: "network-only" });
281
+
282
+ return result.data.updateBoardMemberRole;
283
+ },
284
+ [updateMemberRoleMutation, reexecuteQuery]
285
+ );
286
+
287
+ // Placeholder implementations for sharing features
288
+ const generateShareLink = useCallback(
289
+ async (_options: ShareLinkOptions): Promise<ShareLink> => {
290
+ // TODO: Implement share link generation
291
+ throw new Error("Share links not implemented yet");
292
+ },
293
+ []
294
+ );
295
+
296
+ const revokeShareLink = useCallback(
297
+ async (_linkId: string): Promise<void> => {
298
+ // TODO: Implement share link revocation
299
+ throw new Error("Share link revocation not implemented yet");
300
+ },
301
+ []
302
+ );
303
+
304
+ const refresh = useCallback(async (): Promise<void> => {
305
+ await reexecuteQuery({ requestPolicy: "network-only" });
306
+ }, [reexecuteQuery]);
307
+
308
+ return {
309
+ board,
310
+ members,
311
+ permissions,
312
+ loading: fetching,
313
+ error: error ? new Error(error.message) : null,
314
+ updateBoard,
315
+ deleteBoard,
316
+ refresh,
317
+ addMember,
318
+ removeMember,
319
+ updateMemberRole,
320
+ generateShareLink,
321
+ revokeShareLink,
322
+ };
323
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Hook for managing multiple boards.
3
+ */
4
+
5
+ import { useCallback, useMemo, useState } from "react";
6
+ import { useQuery, useMutation } from "urql";
7
+ // import { Cache } from '@urql/core';
8
+ import {
9
+ GET_BOARDS,
10
+ CREATE_BOARD,
11
+ DELETE_BOARD,
12
+ CreateBoardInput,
13
+ } from "../graphql/operations";
14
+
15
+ interface Board {
16
+ id: string;
17
+ tenantId: string;
18
+ ownerId: string;
19
+ title: string;
20
+ description?: string;
21
+ isPublic: boolean;
22
+ settings: Record<string, unknown>;
23
+ metadata: Record<string, unknown>;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ generationCount: number;
27
+ owner: {
28
+ id: string;
29
+ email: string;
30
+ displayName: string;
31
+ avatarUrl?: string;
32
+ createdAt: string;
33
+ };
34
+ }
35
+
36
+ interface UseBoardsOptions {
37
+ limit?: number;
38
+ offset?: number;
39
+ }
40
+
41
+ interface BoardsHook {
42
+ boards: Board[];
43
+ loading: boolean;
44
+ error: Error | null;
45
+ createBoard: (data: CreateBoardInput) => Promise<Board>;
46
+ deleteBoard: (boardId: string) => Promise<void>;
47
+ searchBoards: (query: string) => Promise<Board[]>;
48
+ refresh: () => Promise<void>;
49
+ setSearchQuery: (query: string) => void;
50
+ searchQuery: string;
51
+ }
52
+
53
+ export function useBoards(options: UseBoardsOptions = {}): BoardsHook {
54
+ const { limit = 50, offset = 0 } = options;
55
+ const [searchQuery, setSearchQuery] = useState("");
56
+
57
+ // Query for boards
58
+ const [{ data, fetching, error }, reexecuteQuery] = useQuery({
59
+ query: GET_BOARDS,
60
+ variables: { limit, offset },
61
+ });
62
+
63
+ // Mutations
64
+ const [, createBoardMutation] = useMutation(CREATE_BOARD);
65
+ const [, deleteBoardMutation] = useMutation(DELETE_BOARD);
66
+
67
+ const boards = useMemo(() => data?.myBoards || [], [data?.myBoards]);
68
+
69
+ const createBoard = useCallback(
70
+ async (input: CreateBoardInput): Promise<Board> => {
71
+ const result = await createBoardMutation({ input });
72
+ if (result.error) {
73
+ throw new Error(result.error.message);
74
+ }
75
+ if (!result.data?.createBoard) {
76
+ throw new Error("Failed to create board");
77
+ }
78
+ return result.data.createBoard;
79
+ },
80
+ [createBoardMutation]
81
+ );
82
+
83
+ const deleteBoard = useCallback(
84
+ async (boardId: string): Promise<void> => {
85
+ const result = await deleteBoardMutation({ id: boardId });
86
+
87
+ if (result.error) {
88
+ throw new Error(result.error.message);
89
+ }
90
+
91
+ if (!result.data?.deleteBoard?.success) {
92
+ throw new Error("Failed to delete board");
93
+ }
94
+ reexecuteQuery({ requestPolicy: "network-only" });
95
+ },
96
+ [deleteBoardMutation, reexecuteQuery]
97
+ );
98
+
99
+ const searchBoards = useCallback(
100
+ async (query: string): Promise<Board[]> => {
101
+ // Set search query which will trigger debounced search via useEffect
102
+ setSearchQuery(query);
103
+
104
+ // Return promise that resolves when search completes
105
+ // This is a simplified implementation - in a real app you might want
106
+ // to return the actual search results from the API
107
+ return new Promise((resolve) => {
108
+ // Wait for debounce delay plus a bit more for API response
109
+ setTimeout(() => {
110
+ resolve(
111
+ boards.filter(
112
+ (board: Board) =>
113
+ board.title.toLowerCase().includes(query.toLowerCase()) ||
114
+ board.description?.toLowerCase().includes(query.toLowerCase())
115
+ )
116
+ );
117
+ }, 350);
118
+ });
119
+ },
120
+ [boards]
121
+ );
122
+
123
+ const refresh = useCallback(async (): Promise<void> => {
124
+ await reexecuteQuery({ requestPolicy: "network-only" });
125
+ }, [reexecuteQuery]);
126
+
127
+ return {
128
+ boards,
129
+ loading: fetching,
130
+ error: error ? new Error(error.message) : null,
131
+ createBoard,
132
+ deleteBoard,
133
+ searchBoards,
134
+ refresh,
135
+ setSearchQuery,
136
+ searchQuery,
137
+ };
138
+ }