@btst/stack 1.11.0 → 1.12.0
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.
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +1 -1
- package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +1 -1
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +77 -0
- package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +79 -3
- package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.cjs +140 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-avatar@1.1.11_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react_850cfbef1935a6e49a6ad6c93c7ca70d/node_modules/@radix-ui/react-avatar/dist/index.mjs +119 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.cjs +80 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-context@1.1.3_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-context/dist/index.mjs +64 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.cjs +18 -0
- package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.2.6_react@19.2.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.mjs +16 -0
- package/dist/packages/better-stack/src/plugins/kanban/api/plugin.cjs +846 -0
- package/dist/packages/better-stack/src/plugins/kanban/api/plugin.mjs +844 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.cjs +85 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/board-form.mjs +83 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.cjs +72 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/column-form.mjs +70 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.cjs +200 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/forms/task-form.mjs +198 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.cjs +47 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/board-skeleton.mjs +45 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/loading/boards-list-skeleton.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/404-page.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.cjs +31 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +458 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +456 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/board-page.mjs +29 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.cjs +72 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.internal.mjs +70 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/boards-list-page.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.cjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.cjs +51 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.internal.mjs +49 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/pages/new-board-page.mjs +28 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.cjs +76 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/column-content.mjs +74 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.cjs +27 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/default-error.mjs +25 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/empty-state.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.cjs +78 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/kanban-board.mjs +76 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.cjs +15 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/page-wrapper.mjs +13 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.cjs +68 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/task-card.mjs +66 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.cjs +32 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/components/shared/user-avatar.mjs +30 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.cjs +391 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/hooks/kanban-hooks.mjs +381 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/plugin.cjs +290 -0
- package/dist/packages/better-stack/src/plugins/kanban/client/plugin.mjs +288 -0
- package/dist/packages/better-stack/src/plugins/kanban/db.cjs +125 -0
- package/dist/packages/better-stack/src/plugins/kanban/db.mjs +123 -0
- package/dist/packages/better-stack/src/plugins/kanban/schemas.cjs +117 -0
- package/dist/packages/better-stack/src/plugins/kanban/schemas.mjs +102 -0
- package/dist/packages/better-stack/src/plugins/kanban/utils.cjs +49 -0
- package/dist/packages/better-stack/src/plugins/kanban/utils.mjs +45 -0
- package/dist/packages/ui/src/components/avatar.cjs +58 -0
- package/dist/packages/ui/src/components/avatar.mjs +54 -0
- package/dist/packages/ui/src/components/command.cjs +3 -3
- package/dist/packages/ui/src/components/command.mjs +3 -3
- package/dist/packages/ui/src/components/form-builder/index.mjs +2 -2
- package/dist/packages/ui/src/components/kanban.cjs +835 -0
- package/dist/packages/ui/src/components/kanban.mjs +805 -0
- package/dist/packages/ui/src/components/popover.cjs +8 -3
- package/dist/packages/ui/src/components/popover.mjs +9 -4
- package/dist/packages/ui/src/components/search-select.cjs +75 -0
- package/dist/packages/ui/src/components/search-select.mjs +73 -0
- package/dist/packages/ui/src/lib/compose-refs.cjs +56 -0
- package/dist/packages/ui/src/lib/compose-refs.mjs +39 -0
- package/dist/plugins/blog/api/index.d.cts +1 -1
- package/dist/plugins/blog/api/index.d.mts +1 -1
- package/dist/plugins/blog/api/index.d.ts +1 -1
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- package/dist/plugins/kanban/api/index.cjs +7 -0
- package/dist/plugins/kanban/api/index.d.cts +403 -0
- package/dist/plugins/kanban/api/index.d.mts +403 -0
- package/dist/plugins/kanban/api/index.d.ts +403 -0
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/client/components/index.cjs +35 -0
- package/dist/plugins/kanban/client/components/index.d.cts +102 -0
- package/dist/plugins/kanban/client/components/index.d.mts +102 -0
- package/dist/plugins/kanban/client/components/index.d.ts +102 -0
- package/dist/plugins/kanban/client/components/index.mjs +15 -0
- package/dist/plugins/kanban/client/hooks/index.cjs +15 -0
- package/dist/plugins/kanban/client/hooks/index.d.cts +143 -0
- package/dist/plugins/kanban/client/hooks/index.d.mts +143 -0
- package/dist/plugins/kanban/client/hooks/index.d.ts +143 -0
- package/dist/plugins/kanban/client/hooks/index.mjs +1 -0
- package/dist/plugins/kanban/client/index.cjs +7 -0
- package/dist/plugins/kanban/client/index.d.cts +196 -0
- package/dist/plugins/kanban/client/index.d.mts +196 -0
- package/dist/plugins/kanban/client/index.d.ts +196 -0
- package/dist/plugins/kanban/client/index.mjs +1 -0
- package/dist/plugins/kanban/client.css +68 -0
- package/dist/plugins/kanban/query-keys.cjs +105 -0
- package/dist/plugins/kanban/query-keys.d.cts +59 -0
- package/dist/plugins/kanban/query-keys.d.mts +59 -0
- package/dist/plugins/kanban/query-keys.d.ts +59 -0
- package/dist/plugins/kanban/query-keys.mjs +103 -0
- package/dist/plugins/kanban/style.css +7 -0
- package/dist/plugins/ui-builder/style.css +6 -0
- package/dist/shared/stack.DKDMI-QO.d.cts +70 -0
- package/dist/shared/stack.DKDMI-QO.d.mts +70 -0
- package/dist/shared/stack.DKDMI-QO.d.ts +70 -0
- package/dist/shared/stack.FeaWkglm.d.cts +190 -0
- package/dist/shared/stack.FeaWkglm.d.mts +190 -0
- package/dist/shared/stack.FeaWkglm.d.ts +190 -0
- package/package.json +56 -2
- package/src/plugins/kanban/api/index.ts +6 -0
- package/src/plugins/kanban/api/plugin.ts +1245 -0
- package/src/plugins/kanban/client/components/forms/board-form.tsx +108 -0
- package/src/plugins/kanban/client/components/forms/column-form.tsx +97 -0
- package/src/plugins/kanban/client/components/forms/task-form.tsx +274 -0
- package/src/plugins/kanban/client/components/index.tsx +21 -0
- package/src/plugins/kanban/client/components/loading/board-skeleton.tsx +49 -0
- package/src/plugins/kanban/client/components/loading/boards-list-skeleton.tsx +34 -0
- package/src/plugins/kanban/client/components/loading/index.tsx +2 -0
- package/src/plugins/kanban/client/components/pages/404-page.tsx +28 -0
- package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +575 -0
- package/src/plugins/kanban/client/components/pages/board-page.tsx +31 -0
- package/src/plugins/kanban/client/components/pages/boards-list-page.internal.tsx +101 -0
- package/src/plugins/kanban/client/components/pages/boards-list-page.tsx +26 -0
- package/src/plugins/kanban/client/components/pages/new-board-page.internal.tsx +65 -0
- package/src/plugins/kanban/client/components/pages/new-board-page.tsx +26 -0
- package/src/plugins/kanban/client/components/shared/column-content.tsx +108 -0
- package/src/plugins/kanban/client/components/shared/default-error.tsx +32 -0
- package/src/plugins/kanban/client/components/shared/empty-state.tsx +37 -0
- package/src/plugins/kanban/client/components/shared/kanban-board.tsx +87 -0
- package/src/plugins/kanban/client/components/shared/page-wrapper.tsx +20 -0
- package/src/plugins/kanban/client/components/shared/task-card.tsx +79 -0
- package/src/plugins/kanban/client/components/shared/user-avatar.tsx +63 -0
- package/src/plugins/kanban/client/hooks/index.tsx +11 -0
- package/src/plugins/kanban/client/hooks/kanban-hooks.tsx +560 -0
- package/src/plugins/kanban/client/index.ts +8 -0
- package/src/plugins/kanban/client/localization/index.ts +28 -0
- package/src/plugins/kanban/client/localization/kanban-common.ts +69 -0
- package/src/plugins/kanban/client/localization/kanban-forms.ts +70 -0
- package/src/plugins/kanban/client/localization/kanban-list.ts +36 -0
- package/src/plugins/kanban/client/overrides.ts +145 -0
- package/src/plugins/kanban/client/plugin.tsx +463 -0
- package/src/plugins/kanban/client.css +68 -0
- package/src/plugins/kanban/db.ts +125 -0
- package/src/plugins/kanban/query-keys.ts +154 -0
- package/src/plugins/kanban/schemas.ts +143 -0
- package/src/plugins/kanban/style.css +7 -0
- package/src/plugins/kanban/types.ts +106 -0
- package/src/plugins/kanban/utils.ts +107 -0
- package/src/plugins/ui-builder/style.css +6 -0
- package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.cts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
- package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface KanbanFormsLocalization {
|
|
2
|
+
// Board form
|
|
3
|
+
boardName: string;
|
|
4
|
+
boardNamePlaceholder: string;
|
|
5
|
+
boardDescription: string;
|
|
6
|
+
boardDescriptionPlaceholder: string;
|
|
7
|
+
createBoard: string;
|
|
8
|
+
updateBoard: string;
|
|
9
|
+
deleteBoard: string;
|
|
10
|
+
deleteBoardConfirm: string;
|
|
11
|
+
// Column form
|
|
12
|
+
columnTitle: string;
|
|
13
|
+
columnTitlePlaceholder: string;
|
|
14
|
+
createColumn: string;
|
|
15
|
+
updateColumn: string;
|
|
16
|
+
deleteColumn: string;
|
|
17
|
+
deleteColumnConfirm: string;
|
|
18
|
+
// Task form
|
|
19
|
+
taskTitle: string;
|
|
20
|
+
taskTitlePlaceholder: string;
|
|
21
|
+
taskDescription: string;
|
|
22
|
+
taskDescriptionPlaceholder: string;
|
|
23
|
+
taskPriority: string;
|
|
24
|
+
taskColumn: string;
|
|
25
|
+
taskAssignee: string;
|
|
26
|
+
createTask: string;
|
|
27
|
+
updateTask: string;
|
|
28
|
+
deleteTask: string;
|
|
29
|
+
deleteTaskConfirm: string;
|
|
30
|
+
// Validation
|
|
31
|
+
nameRequired: string;
|
|
32
|
+
titleRequired: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const defaultKanbanFormsLocalization: KanbanFormsLocalization = {
|
|
36
|
+
// Board form
|
|
37
|
+
boardName: "Name",
|
|
38
|
+
boardNamePlaceholder: "e.g., Project Alpha",
|
|
39
|
+
boardDescription: "Description",
|
|
40
|
+
boardDescriptionPlaceholder: "Describe your board...",
|
|
41
|
+
createBoard: "Create Board",
|
|
42
|
+
updateBoard: "Update Board",
|
|
43
|
+
deleteBoard: "Delete Board",
|
|
44
|
+
deleteBoardConfirm:
|
|
45
|
+
"Are you sure you want to delete this board? This action cannot be undone. All columns and tasks will be permanently removed.",
|
|
46
|
+
// Column form
|
|
47
|
+
columnTitle: "Title",
|
|
48
|
+
columnTitlePlaceholder: "e.g., To Do",
|
|
49
|
+
createColumn: "Create Column",
|
|
50
|
+
updateColumn: "Update Column",
|
|
51
|
+
deleteColumn: "Delete Column",
|
|
52
|
+
deleteColumnConfirm:
|
|
53
|
+
"Are you sure you want to delete this column? All tasks in this column will be permanently removed.",
|
|
54
|
+
// Task form
|
|
55
|
+
taskTitle: "Title",
|
|
56
|
+
taskTitlePlaceholder: "e.g., Fix login bug",
|
|
57
|
+
taskDescription: "Description",
|
|
58
|
+
taskDescriptionPlaceholder: "Describe the task...",
|
|
59
|
+
taskPriority: "Priority",
|
|
60
|
+
taskColumn: "Column",
|
|
61
|
+
taskAssignee: "Assignee",
|
|
62
|
+
createTask: "Create Task",
|
|
63
|
+
updateTask: "Update Task",
|
|
64
|
+
deleteTask: "Delete Task",
|
|
65
|
+
deleteTaskConfirm:
|
|
66
|
+
"Are you sure you want to delete this task? This action cannot be undone.",
|
|
67
|
+
// Validation
|
|
68
|
+
nameRequired: "Name is required",
|
|
69
|
+
titleRequired: "Title is required",
|
|
70
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface KanbanListLocalization {
|
|
2
|
+
// Page titles
|
|
3
|
+
kanbanBoards: string;
|
|
4
|
+
manageProjects: string;
|
|
5
|
+
createNewBoard: string;
|
|
6
|
+
boardDetails: string;
|
|
7
|
+
// Actions
|
|
8
|
+
newBoard: string;
|
|
9
|
+
addColumn: string;
|
|
10
|
+
addTask: string;
|
|
11
|
+
// List
|
|
12
|
+
columnsCount: string;
|
|
13
|
+
// Empty states
|
|
14
|
+
noBoardsDescription: string;
|
|
15
|
+
noColumnsDescription: string;
|
|
16
|
+
noTasksDescription: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const defaultKanbanListLocalization: KanbanListLocalization = {
|
|
20
|
+
// Page titles
|
|
21
|
+
kanbanBoards: "Kanban Boards",
|
|
22
|
+
manageProjects: "Manage your projects and tasks",
|
|
23
|
+
createNewBoard: "Create New Board",
|
|
24
|
+
boardDetails: "Board Details",
|
|
25
|
+
// Actions
|
|
26
|
+
newBoard: "New Board",
|
|
27
|
+
addColumn: "Add Column",
|
|
28
|
+
addTask: "Add Task",
|
|
29
|
+
// List
|
|
30
|
+
columnsCount: "columns",
|
|
31
|
+
// Empty states
|
|
32
|
+
noBoardsDescription:
|
|
33
|
+
"Create your first kanban board to start organizing your tasks.",
|
|
34
|
+
noColumnsDescription: "Create your first column to start organizing tasks.",
|
|
35
|
+
noTasksDescription: "Add a task to get started",
|
|
36
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { KanbanLocalization } from "./localization";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User information for assignee display/selection
|
|
6
|
+
* Framework-agnostic - consumers map their auth system to this shape
|
|
7
|
+
*/
|
|
8
|
+
export interface KanbanUser {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
avatarUrl?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Context passed to lifecycle hooks
|
|
17
|
+
*/
|
|
18
|
+
export interface RouteContext {
|
|
19
|
+
/** Current route path */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Route parameters (e.g., { boardId: "abc123" }) */
|
|
22
|
+
params?: Record<string, string>;
|
|
23
|
+
/** Whether rendering on server (true) or client (false) */
|
|
24
|
+
isSSR: boolean;
|
|
25
|
+
/** Additional context properties */
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Overridable components and functions for the Kanban plugin
|
|
31
|
+
*
|
|
32
|
+
* External consumers can provide their own implementations of these
|
|
33
|
+
* to customize the behavior for their framework (Next.js, React Router, etc.)
|
|
34
|
+
*/
|
|
35
|
+
export interface KanbanPluginOverrides {
|
|
36
|
+
/**
|
|
37
|
+
* Link component for navigation
|
|
38
|
+
*/
|
|
39
|
+
Link?: ComponentType<React.ComponentProps<"a"> & Record<string, unknown>>;
|
|
40
|
+
/**
|
|
41
|
+
* Navigation function for programmatic navigation
|
|
42
|
+
*/
|
|
43
|
+
navigate: (path: string) => void | Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())
|
|
46
|
+
*/
|
|
47
|
+
refresh?: () => void | Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Image component for displaying images
|
|
50
|
+
*/
|
|
51
|
+
Image?: ComponentType<
|
|
52
|
+
React.ImgHTMLAttributes<HTMLImageElement> & Record<string, unknown>
|
|
53
|
+
>;
|
|
54
|
+
/**
|
|
55
|
+
* Localization object for the kanban plugin
|
|
56
|
+
*/
|
|
57
|
+
localization?: KanbanLocalization;
|
|
58
|
+
/**
|
|
59
|
+
* API base URL
|
|
60
|
+
*/
|
|
61
|
+
apiBaseURL: string;
|
|
62
|
+
/**
|
|
63
|
+
* API base path
|
|
64
|
+
*/
|
|
65
|
+
apiBasePath: string;
|
|
66
|
+
/**
|
|
67
|
+
* Whether to show the attribution
|
|
68
|
+
*/
|
|
69
|
+
showAttribution?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Optional headers to pass with API requests (e.g., for SSR auth)
|
|
72
|
+
*/
|
|
73
|
+
headers?: HeadersInit;
|
|
74
|
+
|
|
75
|
+
// ============ User Resolution (required for assignee features) ============
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve user info from an assigneeId
|
|
79
|
+
* Called when rendering task cards/forms that have an assignee
|
|
80
|
+
* Return null for unknown users (will show fallback UI)
|
|
81
|
+
*/
|
|
82
|
+
resolveUser: (
|
|
83
|
+
userId: string,
|
|
84
|
+
) => Promise<KanbanUser | null> | KanbanUser | null;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Search/list users available for assignment
|
|
88
|
+
* Called when user opens the assignee picker
|
|
89
|
+
* @param query - Search query (empty string for initial load)
|
|
90
|
+
* @param boardId - Optional board context for scoped user lists
|
|
91
|
+
*/
|
|
92
|
+
searchUsers: (
|
|
93
|
+
query: string,
|
|
94
|
+
boardId?: string,
|
|
95
|
+
) => Promise<KanbanUser[]> | KanbanUser[];
|
|
96
|
+
|
|
97
|
+
// ============ Lifecycle Hooks (optional) ============
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Called when a route is rendered
|
|
101
|
+
* @param routeName - Name of the route (e.g., 'boards', 'board', 'newBoard')
|
|
102
|
+
* @param context - Route context with path, params, etc.
|
|
103
|
+
*/
|
|
104
|
+
onRouteRender?: (
|
|
105
|
+
routeName: string,
|
|
106
|
+
context: RouteContext,
|
|
107
|
+
) => void | Promise<void>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Called when a route encounters an error
|
|
111
|
+
* @param routeName - Name of the route
|
|
112
|
+
* @param error - The error that occurred
|
|
113
|
+
* @param context - Route context
|
|
114
|
+
*/
|
|
115
|
+
onRouteError?: (
|
|
116
|
+
routeName: string,
|
|
117
|
+
error: Error,
|
|
118
|
+
context: RouteContext,
|
|
119
|
+
) => void | Promise<void>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Called before the boards list page is rendered
|
|
123
|
+
* Return false to prevent rendering (e.g., for authorization)
|
|
124
|
+
* @param context - Route context
|
|
125
|
+
*/
|
|
126
|
+
onBeforeBoardsPageRendered?: (context: RouteContext) => boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Called before a single board page is rendered
|
|
130
|
+
* Return false to prevent rendering (e.g., for authorization)
|
|
131
|
+
* @param boardId - The board ID
|
|
132
|
+
* @param context - Route context
|
|
133
|
+
*/
|
|
134
|
+
onBeforeBoardPageRendered?: (
|
|
135
|
+
boardId: string,
|
|
136
|
+
context: RouteContext,
|
|
137
|
+
) => boolean;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Called before the new board page is rendered
|
|
141
|
+
* Return false to prevent rendering (e.g., for authorization)
|
|
142
|
+
* @param context - Route context
|
|
143
|
+
*/
|
|
144
|
+
onBeforeNewBoardPageRendered?: (context: RouteContext) => boolean;
|
|
145
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineClientPlugin,
|
|
3
|
+
createApiClient,
|
|
4
|
+
} from "@btst/stack/plugins/client";
|
|
5
|
+
import { createRoute } from "@btst/yar";
|
|
6
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
7
|
+
import type { KanbanApiRouter } from "../api";
|
|
8
|
+
import { createKanbanQueryKeys } from "../query-keys";
|
|
9
|
+
import type { SerializedBoardWithColumns } from "../types";
|
|
10
|
+
import { BoardsListPageComponent } from "./components/pages/boards-list-page";
|
|
11
|
+
import { NewBoardPageComponent } from "./components/pages/new-board-page";
|
|
12
|
+
import { BoardPageComponent } from "./components/pages/board-page";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Context passed to route hooks
|
|
16
|
+
*/
|
|
17
|
+
export interface RouteContext {
|
|
18
|
+
/** Current route path */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Route parameters (e.g., { boardId: "abc123" }) */
|
|
21
|
+
params?: Record<string, string>;
|
|
22
|
+
/** Whether rendering on server (true) or client (false) */
|
|
23
|
+
isSSR: boolean;
|
|
24
|
+
/** Additional context properties */
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Context passed to loader hooks
|
|
30
|
+
*/
|
|
31
|
+
export interface LoaderContext {
|
|
32
|
+
/** Current route path */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Route parameters */
|
|
35
|
+
params?: Record<string, string>;
|
|
36
|
+
/** Whether rendering on server (true) or client (false) */
|
|
37
|
+
isSSR: boolean;
|
|
38
|
+
/** Base URL for API calls */
|
|
39
|
+
apiBaseURL: string;
|
|
40
|
+
/** Path where the API is mounted */
|
|
41
|
+
apiBasePath: string;
|
|
42
|
+
/** Optional headers for the request */
|
|
43
|
+
headers?: Headers;
|
|
44
|
+
/** Additional context properties */
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configuration for kanban client plugin
|
|
50
|
+
*/
|
|
51
|
+
export interface KanbanClientConfig {
|
|
52
|
+
/** Base URL for API calls (e.g., "http://localhost:3000") */
|
|
53
|
+
apiBaseURL: string;
|
|
54
|
+
/** Path where the API is mounted (e.g., "/api/data") */
|
|
55
|
+
apiBasePath: string;
|
|
56
|
+
/** Base URL of your site for SEO meta tags */
|
|
57
|
+
siteBaseURL: string;
|
|
58
|
+
/** Path where pages are mounted (e.g., "/pages") */
|
|
59
|
+
siteBasePath: string;
|
|
60
|
+
/** React Query client instance for caching */
|
|
61
|
+
queryClient: QueryClient;
|
|
62
|
+
|
|
63
|
+
/** Optional SEO configuration for meta tags */
|
|
64
|
+
seo?: {
|
|
65
|
+
/** Site name for Open Graph tags */
|
|
66
|
+
siteName?: string;
|
|
67
|
+
/** Default description */
|
|
68
|
+
description?: string;
|
|
69
|
+
/** Locale for Open Graph (e.g., "en_US") */
|
|
70
|
+
locale?: string;
|
|
71
|
+
/** Default image URL for social sharing */
|
|
72
|
+
defaultImage?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Optional hooks for customizing behavior */
|
|
76
|
+
hooks?: KanbanClientHooks;
|
|
77
|
+
|
|
78
|
+
/** Optional headers for SSR (e.g., forwarding cookies) */
|
|
79
|
+
headers?: Headers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Hooks for kanban client plugin
|
|
84
|
+
* All hooks are optional and allow consumers to customize behavior
|
|
85
|
+
*/
|
|
86
|
+
export interface KanbanClientHooks {
|
|
87
|
+
/**
|
|
88
|
+
* Called before loading boards list. Return false to cancel loading.
|
|
89
|
+
*/
|
|
90
|
+
beforeLoadBoards?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Called after boards are loaded. Return false to cancel further processing.
|
|
93
|
+
*/
|
|
94
|
+
afterLoadBoards?: (
|
|
95
|
+
boards: SerializedBoardWithColumns[] | null,
|
|
96
|
+
context: LoaderContext,
|
|
97
|
+
) => Promise<boolean> | boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Called before loading a single board. Return false to cancel loading.
|
|
100
|
+
*/
|
|
101
|
+
beforeLoadBoard?: (
|
|
102
|
+
boardId: string,
|
|
103
|
+
context: LoaderContext,
|
|
104
|
+
) => Promise<boolean> | boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Called after a board is loaded. Return false to cancel further processing.
|
|
107
|
+
*/
|
|
108
|
+
afterLoadBoard?: (
|
|
109
|
+
board: SerializedBoardWithColumns | null,
|
|
110
|
+
boardId: string,
|
|
111
|
+
context: LoaderContext,
|
|
112
|
+
) => Promise<boolean> | boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Called before loading the new board page. Return false to cancel.
|
|
115
|
+
*/
|
|
116
|
+
beforeLoadNewBoard?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Called after the new board page is loaded. Return false to cancel.
|
|
119
|
+
*/
|
|
120
|
+
afterLoadNewBoard?: (context: LoaderContext) => Promise<boolean> | boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Called when a loading error occurs
|
|
123
|
+
*/
|
|
124
|
+
onLoadError?: (error: Error, context: LoaderContext) => Promise<void> | void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Loader for SSR prefetching - boards list
|
|
128
|
+
function createBoardsLoader(config: KanbanClientConfig) {
|
|
129
|
+
return async () => {
|
|
130
|
+
if (typeof window === "undefined") {
|
|
131
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
132
|
+
|
|
133
|
+
const context: LoaderContext = {
|
|
134
|
+
path: "/kanban",
|
|
135
|
+
isSSR: true,
|
|
136
|
+
apiBaseURL,
|
|
137
|
+
apiBasePath,
|
|
138
|
+
headers,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (hooks?.beforeLoadBoards) {
|
|
143
|
+
const canLoad = await hooks.beforeLoadBoards(context);
|
|
144
|
+
if (!canLoad) {
|
|
145
|
+
throw new Error("Load prevented by beforeLoadBoards hook");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const client = createApiClient<KanbanApiRouter>({
|
|
150
|
+
baseURL: apiBaseURL,
|
|
151
|
+
basePath: apiBasePath,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const queries = createKanbanQueryKeys(client, headers);
|
|
155
|
+
const listQuery = queries.boards.list({});
|
|
156
|
+
|
|
157
|
+
await queryClient.prefetchQuery(listQuery);
|
|
158
|
+
|
|
159
|
+
if (hooks?.afterLoadBoards) {
|
|
160
|
+
const boards = queryClient.getQueryData<SerializedBoardWithColumns[]>(
|
|
161
|
+
listQuery.queryKey,
|
|
162
|
+
);
|
|
163
|
+
const canContinue = await hooks.afterLoadBoards(
|
|
164
|
+
boards || null,
|
|
165
|
+
context,
|
|
166
|
+
);
|
|
167
|
+
if (canContinue === false) {
|
|
168
|
+
throw new Error("Load prevented by afterLoadBoards hook");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const queryState = queryClient.getQueryState(listQuery.queryKey);
|
|
173
|
+
if (queryState?.error && hooks?.onLoadError) {
|
|
174
|
+
const error =
|
|
175
|
+
queryState.error instanceof Error
|
|
176
|
+
? queryState.error
|
|
177
|
+
: new Error(String(queryState.error));
|
|
178
|
+
await hooks.onLoadError(error, context);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (hooks?.onLoadError) {
|
|
182
|
+
await hooks.onLoadError(error as Error, context);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Loader for SSR prefetching - single board
|
|
190
|
+
function createBoardLoader(boardId: string, config: KanbanClientConfig) {
|
|
191
|
+
return async () => {
|
|
192
|
+
if (typeof window === "undefined") {
|
|
193
|
+
const { queryClient, apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
194
|
+
|
|
195
|
+
const context: LoaderContext = {
|
|
196
|
+
path: `/kanban/${boardId}`,
|
|
197
|
+
params: { boardId },
|
|
198
|
+
isSSR: true,
|
|
199
|
+
apiBaseURL,
|
|
200
|
+
apiBasePath,
|
|
201
|
+
headers,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
if (hooks?.beforeLoadBoard) {
|
|
206
|
+
const canLoad = await hooks.beforeLoadBoard(boardId, context);
|
|
207
|
+
if (!canLoad) {
|
|
208
|
+
throw new Error("Load prevented by beforeLoadBoard hook");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const client = createApiClient<KanbanApiRouter>({
|
|
213
|
+
baseURL: apiBaseURL,
|
|
214
|
+
basePath: apiBasePath,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const queries = createKanbanQueryKeys(client, headers);
|
|
218
|
+
const boardQuery = queries.boards.detail(boardId);
|
|
219
|
+
await queryClient.prefetchQuery(boardQuery);
|
|
220
|
+
|
|
221
|
+
if (hooks?.afterLoadBoard) {
|
|
222
|
+
const board = queryClient.getQueryData<SerializedBoardWithColumns>(
|
|
223
|
+
boardQuery.queryKey,
|
|
224
|
+
);
|
|
225
|
+
const canContinue = await hooks.afterLoadBoard(
|
|
226
|
+
board || null,
|
|
227
|
+
boardId,
|
|
228
|
+
context,
|
|
229
|
+
);
|
|
230
|
+
if (canContinue === false) {
|
|
231
|
+
throw new Error("Load prevented by afterLoadBoard hook");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const queryState = queryClient.getQueryState(boardQuery.queryKey);
|
|
236
|
+
if (queryState?.error && hooks?.onLoadError) {
|
|
237
|
+
const error =
|
|
238
|
+
queryState.error instanceof Error
|
|
239
|
+
? queryState.error
|
|
240
|
+
: new Error(String(queryState.error));
|
|
241
|
+
await hooks.onLoadError(error, context);
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (hooks?.onLoadError) {
|
|
245
|
+
await hooks.onLoadError(error as Error, context);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Loader for new board page
|
|
253
|
+
function createNewBoardLoader(config: KanbanClientConfig) {
|
|
254
|
+
return async () => {
|
|
255
|
+
if (typeof window === "undefined") {
|
|
256
|
+
const { apiBasePath, apiBaseURL, hooks, headers } = config;
|
|
257
|
+
|
|
258
|
+
const context: LoaderContext = {
|
|
259
|
+
path: "/kanban/new",
|
|
260
|
+
isSSR: true,
|
|
261
|
+
apiBaseURL,
|
|
262
|
+
apiBasePath,
|
|
263
|
+
headers,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (hooks?.beforeLoadNewBoard) {
|
|
268
|
+
const canLoad = await hooks.beforeLoadNewBoard(context);
|
|
269
|
+
if (!canLoad) {
|
|
270
|
+
throw new Error("Load prevented by beforeLoadNewBoard hook");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (hooks?.afterLoadNewBoard) {
|
|
275
|
+
const canContinue = await hooks.afterLoadNewBoard(context);
|
|
276
|
+
if (canContinue === false) {
|
|
277
|
+
throw new Error("Load prevented by afterLoadNewBoard hook");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (hooks?.onLoadError) {
|
|
282
|
+
await hooks.onLoadError(error as Error, context);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Meta generators
|
|
290
|
+
function createBoardsListMeta(config: KanbanClientConfig) {
|
|
291
|
+
return () => {
|
|
292
|
+
const { siteBaseURL, siteBasePath, seo } = config;
|
|
293
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/kanban`;
|
|
294
|
+
const title = "Kanban Boards";
|
|
295
|
+
const description =
|
|
296
|
+
seo?.description || "Manage your projects with kanban boards";
|
|
297
|
+
|
|
298
|
+
return [
|
|
299
|
+
{ title },
|
|
300
|
+
{ name: "title", content: title },
|
|
301
|
+
{ name: "description", content: description },
|
|
302
|
+
{ name: "robots", content: "index, follow" },
|
|
303
|
+
{ property: "og:type", content: "website" },
|
|
304
|
+
{ property: "og:title", content: title },
|
|
305
|
+
{ property: "og:description", content: description },
|
|
306
|
+
{ property: "og:url", content: fullUrl },
|
|
307
|
+
...(seo?.siteName
|
|
308
|
+
? [{ property: "og:site_name", content: seo.siteName }]
|
|
309
|
+
: []),
|
|
310
|
+
...(seo?.locale ? [{ property: "og:locale", content: seo.locale }] : []),
|
|
311
|
+
...(seo?.defaultImage
|
|
312
|
+
? [{ property: "og:image", content: seo.defaultImage }]
|
|
313
|
+
: []),
|
|
314
|
+
{ name: "twitter:card", content: "summary" },
|
|
315
|
+
{ name: "twitter:title", content: title },
|
|
316
|
+
{ name: "twitter:description", content: description },
|
|
317
|
+
];
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createBoardMeta(boardId: string, config: KanbanClientConfig) {
|
|
322
|
+
return () => {
|
|
323
|
+
const {
|
|
324
|
+
queryClient,
|
|
325
|
+
apiBaseURL,
|
|
326
|
+
apiBasePath,
|
|
327
|
+
siteBaseURL,
|
|
328
|
+
siteBasePath,
|
|
329
|
+
seo,
|
|
330
|
+
} = config;
|
|
331
|
+
const queries = createKanbanQueryKeys(
|
|
332
|
+
createApiClient<KanbanApiRouter>({
|
|
333
|
+
baseURL: apiBaseURL,
|
|
334
|
+
basePath: apiBasePath,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
const board = queryClient.getQueryData<SerializedBoardWithColumns>(
|
|
338
|
+
queries.boards.detail(boardId).queryKey,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (!board) {
|
|
342
|
+
return [
|
|
343
|
+
{ title: "Board Not Found" },
|
|
344
|
+
{ name: "robots", content: "noindex" },
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/kanban/${board.id}`;
|
|
349
|
+
const title = board.name;
|
|
350
|
+
const description = board.description || `Kanban board: ${board.name}`;
|
|
351
|
+
|
|
352
|
+
return [
|
|
353
|
+
{ title },
|
|
354
|
+
{ name: "title", content: title },
|
|
355
|
+
{ name: "description", content: description },
|
|
356
|
+
{ name: "robots", content: "index, follow" },
|
|
357
|
+
{ property: "og:type", content: "website" },
|
|
358
|
+
{ property: "og:title", content: title },
|
|
359
|
+
{ property: "og:description", content: description },
|
|
360
|
+
{ property: "og:url", content: fullUrl },
|
|
361
|
+
...(seo?.siteName
|
|
362
|
+
? [{ property: "og:site_name", content: seo.siteName }]
|
|
363
|
+
: []),
|
|
364
|
+
...(seo?.defaultImage
|
|
365
|
+
? [{ property: "og:image", content: seo.defaultImage }]
|
|
366
|
+
: []),
|
|
367
|
+
{ name: "twitter:card", content: "summary" },
|
|
368
|
+
{ name: "twitter:title", content: title },
|
|
369
|
+
];
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createNewBoardMeta(config: KanbanClientConfig) {
|
|
374
|
+
return () => {
|
|
375
|
+
const { siteBaseURL, siteBasePath } = config;
|
|
376
|
+
const fullUrl = `${siteBaseURL}${siteBasePath}/kanban/new`;
|
|
377
|
+
const title = "Create New Board";
|
|
378
|
+
|
|
379
|
+
return [
|
|
380
|
+
{ title },
|
|
381
|
+
{ name: "title", content: title },
|
|
382
|
+
{ name: "description", content: "Create a new kanban board" },
|
|
383
|
+
{ name: "robots", content: "noindex, nofollow" },
|
|
384
|
+
{ property: "og:type", content: "website" },
|
|
385
|
+
{ property: "og:title", content: title },
|
|
386
|
+
{ property: "og:url", content: fullUrl },
|
|
387
|
+
{ name: "twitter:card", content: "summary" },
|
|
388
|
+
{ name: "twitter:title", content: title },
|
|
389
|
+
];
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Kanban client plugin
|
|
395
|
+
* Provides routes, components, and React Query hooks for kanban boards
|
|
396
|
+
*/
|
|
397
|
+
export const kanbanClientPlugin = (config: KanbanClientConfig) =>
|
|
398
|
+
defineClientPlugin({
|
|
399
|
+
name: "kanban",
|
|
400
|
+
|
|
401
|
+
routes: () => ({
|
|
402
|
+
boards: createRoute("/kanban", () => {
|
|
403
|
+
return {
|
|
404
|
+
PageComponent: () => <BoardsListPageComponent />,
|
|
405
|
+
loader: createBoardsLoader(config),
|
|
406
|
+
meta: createBoardsListMeta(config),
|
|
407
|
+
};
|
|
408
|
+
}),
|
|
409
|
+
newBoard: createRoute("/kanban/new", () => {
|
|
410
|
+
return {
|
|
411
|
+
PageComponent: NewBoardPageComponent,
|
|
412
|
+
loader: createNewBoardLoader(config),
|
|
413
|
+
meta: createNewBoardMeta(config),
|
|
414
|
+
};
|
|
415
|
+
}),
|
|
416
|
+
board: createRoute("/kanban/:boardId", ({ params: { boardId } }) => {
|
|
417
|
+
return {
|
|
418
|
+
PageComponent: () => <BoardPageComponent boardId={boardId} />,
|
|
419
|
+
loader: createBoardLoader(boardId, config),
|
|
420
|
+
meta: createBoardMeta(boardId, config),
|
|
421
|
+
};
|
|
422
|
+
}),
|
|
423
|
+
}),
|
|
424
|
+
|
|
425
|
+
sitemap: async () => {
|
|
426
|
+
const origin = `${config.siteBaseURL}${config.siteBasePath}`;
|
|
427
|
+
const indexUrl = `${origin}/kanban`;
|
|
428
|
+
|
|
429
|
+
const client = createApiClient<KanbanApiRouter>({
|
|
430
|
+
baseURL: config.apiBaseURL,
|
|
431
|
+
basePath: config.apiBasePath,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let boards: SerializedBoardWithColumns[] = [];
|
|
435
|
+
try {
|
|
436
|
+
const res = await client("/boards", {
|
|
437
|
+
method: "GET",
|
|
438
|
+
query: { limit: 100 },
|
|
439
|
+
});
|
|
440
|
+
boards = ((res as { data?: unknown }).data ??
|
|
441
|
+
[]) as SerializedBoardWithColumns[];
|
|
442
|
+
} catch {
|
|
443
|
+
// Ignore errors for sitemap
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const entries = [
|
|
447
|
+
{
|
|
448
|
+
url: indexUrl,
|
|
449
|
+
lastModified: new Date(),
|
|
450
|
+
changeFrequency: "daily" as const,
|
|
451
|
+
priority: 0.7,
|
|
452
|
+
},
|
|
453
|
+
...boards.map((b) => ({
|
|
454
|
+
url: `${origin}/kanban/${b.id}`,
|
|
455
|
+
lastModified: b.updatedAt ? new Date(b.updatedAt) : undefined,
|
|
456
|
+
changeFrequency: "weekly" as const,
|
|
457
|
+
priority: 0.6,
|
|
458
|
+
})),
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
return entries;
|
|
462
|
+
},
|
|
463
|
+
});
|