@locusai/web 0.1.7 → 0.2.2
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/CHANGELOG.md +26 -0
- package/next.config.js +15 -2
- package/package.json +26 -3
- package/src/app/(auth)/invite/page.tsx +109 -0
- package/src/app/(auth)/layout.tsx +19 -0
- package/src/app/(auth)/login/page.tsx +65 -0
- package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
- package/src/app/(auth)/register/page.tsx +165 -0
- package/src/app/(dashboard)/activity/page.tsx +7 -0
- package/src/app/(dashboard)/backlog/page.tsx +195 -0
- package/src/app/(dashboard)/board/page.tsx +141 -0
- package/src/app/(dashboard)/layout.tsx +32 -0
- package/src/app/(dashboard)/page.tsx +14 -0
- package/src/app/(dashboard)/settings/page.tsx +161 -0
- package/src/app/(dashboard)/settings/team/page.tsx +75 -0
- package/src/app/globals.css +259 -0
- package/src/app/layout.tsx +10 -20
- package/src/app/providers.tsx +26 -3
- package/src/components/AuthLayoutUI.tsx +53 -0
- package/src/components/BoardFilter.tsx +75 -74
- package/src/components/CreateModal/CreateModal.tsx +142 -0
- package/src/components/CreateModal/index.ts +1 -0
- package/src/components/Editor.tsx +279 -0
- package/src/components/Header.tsx +99 -12
- package/src/components/PageLayout.tsx +69 -0
- package/src/components/PropertyItem.tsx +55 -9
- package/src/components/Sidebar.tsx +280 -36
- package/src/components/SprintCreateModal.tsx +84 -0
- package/src/components/TaskCard.tsx +196 -78
- package/src/components/TaskCreateModal.tsx +181 -178
- package/src/components/TaskPanel.tsx +140 -692
- package/src/components/WorkspaceCreateModal.tsx +97 -0
- package/src/components/WorkspaceProtected.tsx +91 -0
- package/src/components/auth/InviteSteps.tsx +220 -0
- package/src/components/auth/LoginSteps.tsx +86 -0
- package/src/components/auth/RegisterSteps.tsx +371 -0
- package/src/components/auth/index.ts +3 -0
- package/src/components/backlog/BacklogList.tsx +92 -0
- package/src/components/backlog/BacklogSection.tsx +137 -0
- package/src/components/backlog/CompletedSprintItem.tsx +95 -0
- package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
- package/src/components/backlog/SprintSection.tsx +155 -0
- package/src/components/backlog/constants.ts +26 -0
- package/src/components/board/BoardColumn.tsx +97 -0
- package/src/components/board/BoardContent.tsx +84 -0
- package/src/components/board/BoardEmptyState.tsx +82 -0
- package/src/components/board/BoardHeader.tsx +47 -0
- package/src/components/board/SprintMindmap.tsx +65 -0
- package/src/components/board/constants.ts +40 -0
- package/src/components/board/index.ts +5 -0
- package/src/components/common/ErrorState.tsx +124 -0
- package/src/components/common/LoadingState.tsx +83 -0
- package/src/components/common/index.ts +40 -0
- package/src/components/dashboard/ActivityFeed.tsx +77 -0
- package/src/components/dashboard/ActivityItem.tsx +207 -0
- package/src/components/dashboard/QuickActions.tsx +50 -0
- package/src/components/dashboard/StatCard.tsx +79 -0
- package/src/components/dnd/index.tsx +51 -0
- package/src/components/docs/DocsEditorArea.tsx +87 -0
- package/src/components/docs/DocsHeaderActions.tsx +121 -0
- package/src/components/docs/DocsSidebar.tsx +351 -0
- package/src/components/index.ts +7 -0
- package/src/components/onboarding/StepProgress.tsx +21 -0
- package/src/components/onboarding/index.ts +1 -0
- package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
- package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
- package/src/components/settings/ApiKeysList.tsx +143 -0
- package/src/components/settings/ApiKeysSettings.tsx +144 -0
- package/src/components/settings/InviteMemberModal.tsx +106 -0
- package/src/components/settings/ProjectSetupGuide.tsx +147 -0
- package/src/components/settings/SettingItem.tsx +32 -0
- package/src/components/settings/SettingSection.tsx +50 -0
- package/src/components/settings/TeamInvitationsList.tsx +90 -0
- package/src/components/settings/TeamMembersList.tsx +95 -0
- package/src/components/settings/index.ts +8 -0
- package/src/components/task-panel/TaskActivity.tsx +127 -0
- package/src/components/task-panel/TaskChecklist.tsx +142 -0
- package/src/components/task-panel/TaskDescription.tsx +201 -0
- package/src/components/task-panel/TaskDocs.tsx +137 -0
- package/src/components/task-panel/TaskHeader.tsx +125 -0
- package/src/components/task-panel/TaskProperties.tsx +111 -0
- package/src/components/task-panel/index.ts +12 -0
- package/src/components/typography/EmptyStateText.tsx +59 -0
- package/src/components/typography/MetadataText.tsx +65 -0
- package/src/components/typography/SecondaryText.tsx +60 -0
- package/src/components/typography/SectionLabel.tsx +60 -0
- package/src/components/typography/index.ts +14 -0
- package/src/components/typography-scales.tsx +218 -0
- package/src/components/ui/Avatar.tsx +123 -0
- package/src/components/ui/Badge.tsx +69 -2
- package/src/components/ui/Button.tsx +71 -30
- package/src/components/ui/Checkbox.tsx +34 -0
- package/src/components/ui/Dropdown.tsx +67 -1
- package/src/components/ui/EmptyState.tsx +129 -0
- package/src/components/ui/Input.tsx +53 -6
- package/src/components/ui/Modal.tsx +45 -12
- package/src/components/ui/OtpInput.tsx +148 -0
- package/src/components/ui/Skeleton.tsx +36 -0
- package/src/components/ui/Spinner.tsx +112 -0
- package/src/components/ui/Textarea.tsx +28 -3
- package/src/components/ui/Toast.tsx +99 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/constants.ts +108 -0
- package/src/components/ui/index.ts +7 -0
- package/src/context/AuthContext.tsx +140 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/backlog/index.ts +13 -0
- package/src/hooks/backlog/useBacklogActions.ts +144 -0
- package/src/hooks/backlog/useBacklogComposite.ts +73 -0
- package/src/hooks/backlog/useBacklogData.ts +74 -0
- package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
- package/src/hooks/backlog/useBacklogUI.ts +74 -0
- package/src/hooks/index.ts +22 -0
- package/src/hooks/task-panel/index.ts +13 -0
- package/src/hooks/task-panel/useTaskActions.ts +200 -0
- package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
- package/src/hooks/task-panel/useTaskData.ts +78 -0
- package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
- package/src/hooks/task-panel/useTaskUIState.ts +80 -0
- package/src/hooks/useAuthLayoutLogic.ts +43 -0
- package/src/hooks/useAuthenticatedUser.ts +36 -0
- package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
- package/src/hooks/useBacklog.ts +303 -0
- package/src/hooks/useBoard.ts +230 -0
- package/src/hooks/useDashboardLayout.ts +49 -0
- package/src/hooks/useDocs.ts +279 -0
- package/src/hooks/useDocsQuery.ts +99 -0
- package/src/hooks/useDocsSidebarState.ts +104 -0
- package/src/hooks/useFormState.ts +40 -0
- package/src/hooks/useGlobalKeydowns.ts +52 -0
- package/src/hooks/useInviteForm.ts +122 -0
- package/src/hooks/useLoginForm.ts +84 -0
- package/src/hooks/useMutationWithToast.ts +56 -0
- package/src/hooks/useOrganizationQuery.ts +55 -0
- package/src/hooks/useRegisterForm.ts +216 -0
- package/src/hooks/useSprintsQuery.ts +38 -0
- package/src/hooks/useTaskDescription.ts +102 -0
- package/src/hooks/useTaskPanel.ts +341 -0
- package/src/hooks/useTasksQuery.ts +39 -0
- package/src/hooks/useTeamManagement.ts +92 -0
- package/src/hooks/useWorkspaceCreateForm.ts +70 -0
- package/src/hooks/useWorkspaceId.ts +29 -0
- package/src/lib/api-client.ts +40 -23
- package/src/lib/config.ts +25 -0
- package/src/lib/constants.ts +83 -0
- package/src/lib/options.ts +96 -0
- package/src/lib/query-keys.ts +91 -0
- package/src/lib/typography.ts +103 -0
- package/src/lib/utils.ts +4 -0
- package/src/lib/validation.ts +192 -0
- package/src/services/index.ts +7 -3
- package/src/services/notifications.ts +80 -0
- package/src/utils/env.utils.ts +15 -0
- package/src/views/ActivityView.tsx +123 -0
- package/src/views/Dashboard.tsx +126 -0
- package/src/views/Docs.tsx +98 -612
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/BUILD_ID +0 -1
- package/.next/app-build-manifest.json +0 -55
- package/.next/app-path-routes-manifest.json +0 -8
- package/.next/build-manifest.json +0 -33
- package/.next/cache/.previewinfo +0 -1
- package/.next/cache/.rscinfo +0 -1
- package/.next/cache/.tsbuildinfo +0 -1
- package/.next/cache/config.json +0 -7
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +0 -6
- package/.next/diagnostics/framework.json +0 -1
- package/.next/export-detail.json +0 -5
- package/.next/export-marker.json +0 -6
- package/.next/images-manifest.json +0 -57
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -137
- package/.next/react-loadable-manifest.json +0 -32
- package/.next/required-server-files.json +0 -324
- package/.next/routes-manifest.json +0 -77
- package/.next/server/app/_not-found/page.js +0 -2
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -21
- package/.next/server/app/backlog/page.js +0 -2
- package/.next/server/app/backlog/page.js.nft.json +0 -1
- package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
- package/.next/server/app/backlog.html +0 -1
- package/.next/server/app/backlog.meta +0 -7
- package/.next/server/app/backlog.rsc +0 -25
- package/.next/server/app/docs/page.js +0 -97
- package/.next/server/app/docs/page.js.nft.json +0 -1
- package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
- package/.next/server/app/docs.html +0 -1
- package/.next/server/app/docs.meta +0 -7
- package/.next/server/app/docs.rsc +0 -26
- package/.next/server/app/favicon.ico/route.js +0 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +0 -1
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -7
- package/.next/server/app/index.rsc +0 -25
- package/.next/server/app/page.js +0 -2
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings/page.js +0 -2
- package/.next/server/app/settings/page.js.nft.json +0 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings.html +0 -1
- package/.next/server/app/settings.meta +0 -7
- package/.next/server/app/settings.rsc +0 -25
- package/.next/server/app-paths-manifest.json +0 -8
- package/.next/server/chunks/496.js +0 -6
- package/.next/server/chunks/585.js +0 -1
- package/.next/server/chunks/665.js +0 -1
- package/.next/server/chunks/699.js +0 -22
- package/.next/server/chunks/852.js +0 -7
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -1
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -1
- package/.next/server/pages/_app.js +0 -1
- package/.next/server/pages/_app.js.nft.json +0 -1
- package/.next/server/pages/_document.js +0 -1
- package/.next/server/pages/_document.js.nft.json +0 -1
- package/.next/server/pages/_error.js +0 -19
- package/.next/server/pages/_error.js.nft.json +0 -1
- package/.next/server/pages-manifest.json +0 -6
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -1
- package/.next/server/webpack-runtime.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/.next/static/chunks/487-1808785ba665f784.js +0 -1
- package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/.next/static/css/13e8617b72f9d3aa.css +0 -1
- package/.next/static/css/8aea088cdc4338f0.css +0 -1
- package/.next/static/css/b301ab0424111664.css +0 -1
- package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/.next/trace +0 -46
- package/.next/types/app/backlog/page.ts +0 -84
- package/.next/types/app/docs/page.ts +0 -84
- package/.next/types/app/layout.ts +0 -84
- package/.next/types/app/page.ts +0 -84
- package/.next/types/app/settings/page.ts +0 -84
- package/.next/types/cache-life.d.ts +0 -141
- package/.next/types/package.json +0 -1
- package/next-env.d.ts +0 -5
- package/out/404.html +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
- package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
- package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
- package/out/_next/static/css/b301ab0424111664.css +0 -1
- package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/out/backlog.html +0 -1
- package/out/backlog.txt +0 -25
- package/out/docs.html +0 -1
- package/out/docs.txt +0 -26
- package/out/favicon.ico +0 -0
- package/out/index.html +0 -1
- package/out/index.txt +0 -25
- package/out/logo.png +0 -0
- package/out/settings.html +0 -1
- package/out/settings.txt +0 -25
- package/src/app/backlog/page.tsx +0 -19
- package/src/app/page.tsx +0 -16
- package/src/app/settings/page.tsx +0 -194
- package/src/hooks/useTasks.ts +0 -119
- package/src/services/doc.service.ts +0 -27
- package/src/services/sprint.service.ts +0 -24
- package/src/services/task.service.ts +0 -75
- package/src/views/Backlog.tsx +0 -691
- package/src/views/Board.tsx +0 -306
- /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Component Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized constants for UI components including sizes, variants, and styling
|
|
5
|
+
* This ensures consistency across all UI components and makes maintenance easier.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Button component sizes and their corresponding Tailwind classes
|
|
10
|
+
*/
|
|
11
|
+
export const BUTTON_SIZES = {
|
|
12
|
+
sm: "h-8 px-3 text-[10px] uppercase tracking-wider",
|
|
13
|
+
md: "h-10 px-5 text-sm",
|
|
14
|
+
lg: "h-12 px-8 text-base",
|
|
15
|
+
icon: "h-10 w-10 p-0",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Button component variants and their styling
|
|
20
|
+
*/
|
|
21
|
+
export const BUTTON_VARIANTS = {
|
|
22
|
+
primary:
|
|
23
|
+
"bg-primary text-primary-foreground shadow-sm hover:translate-y-[-1px] hover:shadow-md",
|
|
24
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
25
|
+
subtle: "bg-primary/10 text-primary hover:bg-primary/20",
|
|
26
|
+
outline: "border border-border bg-transparent hover:bg-secondary/50",
|
|
27
|
+
ghost: "hover:bg-secondary text-muted-foreground hover:text-foreground",
|
|
28
|
+
danger: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
|
|
29
|
+
success: "bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20",
|
|
30
|
+
emerald: "bg-emerald-500 text-white hover:bg-emerald-600",
|
|
31
|
+
amber: "bg-amber-500 text-black hover:bg-amber-600 font-bold",
|
|
32
|
+
"emerald-subtle":
|
|
33
|
+
"bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20 border border-emerald-500/20",
|
|
34
|
+
"amber-subtle":
|
|
35
|
+
"bg-amber-500/10 text-amber-600 hover:bg-amber-500/20 border border-amber-500/20",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Badge component sizes
|
|
40
|
+
*/
|
|
41
|
+
export const BADGE_SIZES = {
|
|
42
|
+
sm: "px-2 py-1 text-xs",
|
|
43
|
+
md: "px-3 py-1.5 text-sm",
|
|
44
|
+
lg: "px-4 py-2 text-base",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Badge component variants
|
|
49
|
+
*/
|
|
50
|
+
export const BADGE_VARIANTS = {
|
|
51
|
+
default: "bg-primary/10 text-primary border border-primary/20",
|
|
52
|
+
success: "bg-emerald-500/10 text-emerald-600 border border-emerald-500/20",
|
|
53
|
+
warning: "bg-amber-500/10 text-amber-600 border border-amber-500/20",
|
|
54
|
+
danger: "bg-red-500/10 text-red-500 border border-red-500/20",
|
|
55
|
+
info: "bg-blue-500/10 text-blue-600 border border-blue-500/20",
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Modal sizes
|
|
60
|
+
*/
|
|
61
|
+
export const MODAL_SIZES = {
|
|
62
|
+
sm: "w-[400px]",
|
|
63
|
+
md: "w-[520px]",
|
|
64
|
+
lg: "w-[680px]",
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Spinner sizes
|
|
69
|
+
*/
|
|
70
|
+
export const SPINNER_SIZES = {
|
|
71
|
+
sm: "w-4 h-4",
|
|
72
|
+
md: "w-6 h-6",
|
|
73
|
+
lg: "w-8 h-8",
|
|
74
|
+
xl: "w-12 h-12",
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Z-index scale for proper layering
|
|
79
|
+
*/
|
|
80
|
+
export const Z_INDEXES = {
|
|
81
|
+
base: "z-0",
|
|
82
|
+
dropdown: "z-100",
|
|
83
|
+
sticky: "z-500",
|
|
84
|
+
fixed: "z-900",
|
|
85
|
+
modalOverlay: "z-940",
|
|
86
|
+
modal: "z-950",
|
|
87
|
+
tooltip: "z-1000",
|
|
88
|
+
notification: "z-1100",
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Transition durations for consistent animations
|
|
93
|
+
*/
|
|
94
|
+
export const TRANSITIONS = {
|
|
95
|
+
fast: "duration-100",
|
|
96
|
+
normal: "duration-200",
|
|
97
|
+
slow: "duration-300",
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Border radius constants
|
|
102
|
+
*/
|
|
103
|
+
export const BORDER_RADIUS = {
|
|
104
|
+
sm: "rounded-md",
|
|
105
|
+
md: "rounded-lg",
|
|
106
|
+
lg: "rounded-xl",
|
|
107
|
+
full: "rounded-full",
|
|
108
|
+
} as const;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
export * from "./Avatar";
|
|
1
2
|
export * from "./Badge";
|
|
2
3
|
export * from "./Button";
|
|
3
4
|
export * from "./Checkbox";
|
|
4
5
|
export * from "./Dropdown";
|
|
6
|
+
export * from "./EmptyState";
|
|
5
7
|
export * from "./Input";
|
|
6
8
|
export * from "./Modal";
|
|
9
|
+
export * from "./OtpInput";
|
|
10
|
+
export * from "./Skeleton";
|
|
11
|
+
export * from "./Spinner";
|
|
7
12
|
export * from "./Textarea";
|
|
13
|
+
export * from "./Toast";
|
|
14
|
+
export * from "./Toggle";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type User, type Workspace } from "@locusai/shared";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { locusClient, setClientToken } from "@/lib/api-client";
|
|
13
|
+
|
|
14
|
+
interface AuthContextType {
|
|
15
|
+
user: User | null;
|
|
16
|
+
workspaces: Workspace[];
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isAuthenticated: boolean;
|
|
19
|
+
login: (token: string, user: User) => void;
|
|
20
|
+
logout: () => void;
|
|
21
|
+
refreshUser: () => Promise<void>;
|
|
22
|
+
switchWorkspace: (workspaceId: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
28
|
+
const [user, setUser] = useState<User | null>(null);
|
|
29
|
+
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
30
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
31
|
+
const router = useRouter();
|
|
32
|
+
|
|
33
|
+
const switchWorkspace = useCallback((workspaceId: string) => {
|
|
34
|
+
setUser((prev) => (prev ? { ...prev, workspaceId } : null));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const refreshUser = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
// Check if token exists first - skip API call if not
|
|
40
|
+
const token =
|
|
41
|
+
typeof window !== "undefined"
|
|
42
|
+
? localStorage.getItem("locus_token")
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
if (!token) {
|
|
46
|
+
// No token stored, immediately set loading to false
|
|
47
|
+
// Layout will redirect to login
|
|
48
|
+
setIsLoading(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const userData = await locusClient.auth.getMe();
|
|
53
|
+
const workspacesData = await locusClient.workspaces.listAll();
|
|
54
|
+
|
|
55
|
+
let effectiveWorkspaceId = userData.workspaceId;
|
|
56
|
+
|
|
57
|
+
// Validation: Ensure current workspaceId is valid
|
|
58
|
+
if (workspacesData.length > 0) {
|
|
59
|
+
const isValid = workspacesData.some(
|
|
60
|
+
(w) => w.id === effectiveWorkspaceId
|
|
61
|
+
);
|
|
62
|
+
if (!isValid) {
|
|
63
|
+
effectiveWorkspaceId = workspacesData[0].id;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setUser({
|
|
68
|
+
...userData,
|
|
69
|
+
workspaceId: effectiveWorkspaceId
|
|
70
|
+
? String(effectiveWorkspaceId)
|
|
71
|
+
: undefined,
|
|
72
|
+
});
|
|
73
|
+
setWorkspaces(workspacesData);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("Failed to fetch user:", error);
|
|
76
|
+
setClientToken(null);
|
|
77
|
+
setUser(null);
|
|
78
|
+
setWorkspaces([]);
|
|
79
|
+
} finally {
|
|
80
|
+
setIsLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
refreshUser();
|
|
86
|
+
}, [refreshUser]);
|
|
87
|
+
|
|
88
|
+
const login = (token: string, userData: User) => {
|
|
89
|
+
setClientToken(token);
|
|
90
|
+
setUser(userData);
|
|
91
|
+
|
|
92
|
+
// If user doesn't have a workspace, redirect to create one
|
|
93
|
+
if (!userData.workspaceId) {
|
|
94
|
+
router.push("/onboarding/workspace");
|
|
95
|
+
} else {
|
|
96
|
+
router.push("/");
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const logout = () => {
|
|
101
|
+
setClientToken(null);
|
|
102
|
+
setUser(null);
|
|
103
|
+
router.push("/login");
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<AuthContext.Provider
|
|
108
|
+
value={{
|
|
109
|
+
user,
|
|
110
|
+
workspaces,
|
|
111
|
+
isLoading,
|
|
112
|
+
isAuthenticated: !!user,
|
|
113
|
+
login,
|
|
114
|
+
logout,
|
|
115
|
+
refreshUser,
|
|
116
|
+
switchWorkspace,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</AuthContext.Provider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function useAuth() {
|
|
125
|
+
const context = useContext(AuthContext);
|
|
126
|
+
if (context === undefined) {
|
|
127
|
+
throw new Error("useAuth must be used within an AuthProvider");
|
|
128
|
+
}
|
|
129
|
+
return context;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useSafeAuth() {
|
|
133
|
+
const context = useContext(AuthContext);
|
|
134
|
+
if (context === undefined) {
|
|
135
|
+
throw new Error("useAuth must be used within an AuthProvider");
|
|
136
|
+
}
|
|
137
|
+
return context as AuthContextType & {
|
|
138
|
+
user: User;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./AuthContext";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog Hooks - Composite Export
|
|
3
|
+
*
|
|
4
|
+
* For backward compatibility, export all backlog hooks.
|
|
5
|
+
* Prefer using individual hooks for better tree-shaking and clarity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from "./useBacklogActions";
|
|
9
|
+
// Composite hook for components not yet refactored
|
|
10
|
+
export { useBacklogComposite as useBacklog } from "./useBacklogComposite";
|
|
11
|
+
export * from "./useBacklogData";
|
|
12
|
+
export * from "./useBacklogDragDrop";
|
|
13
|
+
export * from "./useBacklogUI";
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog Actions Hook
|
|
3
|
+
*
|
|
4
|
+
* Handles API operations for sprints and tasks (create, start, complete, delete).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use client";
|
|
8
|
+
|
|
9
|
+
import { type Sprint, SprintStatus } from "@locusai/shared";
|
|
10
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useWorkspaceId } from "@/hooks/useWorkspaceId";
|
|
13
|
+
import { locusClient } from "@/lib/api-client";
|
|
14
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
15
|
+
import { notifications } from "@/services/notifications";
|
|
16
|
+
|
|
17
|
+
export interface BacklogActionsState {
|
|
18
|
+
isSubmitting: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BacklogActionsHandlers {
|
|
22
|
+
handleCreateSprint: (name: string) => Promise<void>;
|
|
23
|
+
handleStartSprint: (sprintId: string) => Promise<void>;
|
|
24
|
+
handleCompleteSprint: (sprintId: string) => Promise<void>;
|
|
25
|
+
handleDeleteTask: (taskId: string) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface BacklogActionsOptions {
|
|
29
|
+
onSprintCreated?: () => void;
|
|
30
|
+
onSprintStarted?: () => void;
|
|
31
|
+
onSprintCompleted?: () => void;
|
|
32
|
+
onTaskDeleted?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Manage backlog operations (sprint and task actions)
|
|
37
|
+
*/
|
|
38
|
+
export function useBacklogActions(
|
|
39
|
+
options: BacklogActionsOptions = {}
|
|
40
|
+
): BacklogActionsState & BacklogActionsHandlers {
|
|
41
|
+
const workspaceId = useWorkspaceId();
|
|
42
|
+
const queryClient = useQueryClient();
|
|
43
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
44
|
+
|
|
45
|
+
const handleCreateSprint = async (name: string) => {
|
|
46
|
+
try {
|
|
47
|
+
setIsSubmitting(true);
|
|
48
|
+
await locusClient.sprints.create(workspaceId, { name });
|
|
49
|
+
notifications.created("Sprint");
|
|
50
|
+
options.onSprintCreated?.();
|
|
51
|
+
queryClient.invalidateQueries({
|
|
52
|
+
queryKey: queryKeys.sprints.list(workspaceId),
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
notifications.error(
|
|
56
|
+
error instanceof Error ? error.message : "Failed to create sprint"
|
|
57
|
+
);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsSubmitting(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleStartSprint = async (sprintId: string) => {
|
|
64
|
+
try {
|
|
65
|
+
setIsSubmitting(true);
|
|
66
|
+
await locusClient.sprints.start(sprintId, workspaceId);
|
|
67
|
+
notifications.success("Sprint started");
|
|
68
|
+
options.onSprintStarted?.();
|
|
69
|
+
queryClient.invalidateQueries({
|
|
70
|
+
queryKey: queryKeys.sprints.list(workspaceId),
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
notifications.error(
|
|
74
|
+
error instanceof Error ? error.message : "Failed to start sprint"
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
setIsSubmitting(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleCompleteSprint = async (sprintId: string) => {
|
|
82
|
+
try {
|
|
83
|
+
setIsSubmitting(true);
|
|
84
|
+
|
|
85
|
+
// Optimistic update
|
|
86
|
+
const sprintKey = queryKeys.sprints.list(workspaceId);
|
|
87
|
+
const previousSprints = queryClient.getQueryData<Sprint[]>(sprintKey);
|
|
88
|
+
|
|
89
|
+
if (previousSprints) {
|
|
90
|
+
queryClient.setQueryData<Sprint[]>(
|
|
91
|
+
sprintKey,
|
|
92
|
+
previousSprints.map((s) =>
|
|
93
|
+
s.id === sprintId
|
|
94
|
+
? {
|
|
95
|
+
...s,
|
|
96
|
+
status: SprintStatus.COMPLETED,
|
|
97
|
+
endDate: Date.now(),
|
|
98
|
+
}
|
|
99
|
+
: s
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await locusClient.sprints.complete(sprintId, workspaceId);
|
|
105
|
+
notifications.success("Sprint completed");
|
|
106
|
+
options.onSprintCompleted?.();
|
|
107
|
+
|
|
108
|
+
// Full sync
|
|
109
|
+
queryClient.invalidateQueries({ queryKey: sprintKey });
|
|
110
|
+
queryClient.invalidateQueries({
|
|
111
|
+
queryKey: queryKeys.tasks.list(workspaceId),
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
114
|
+
notifications.error(
|
|
115
|
+
error instanceof Error ? error.message : "Failed to complete sprint"
|
|
116
|
+
);
|
|
117
|
+
} finally {
|
|
118
|
+
setIsSubmitting(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleDeleteTask = async (taskId: string) => {
|
|
123
|
+
try {
|
|
124
|
+
await locusClient.tasks.delete(taskId, workspaceId);
|
|
125
|
+
notifications.deleted();
|
|
126
|
+
options.onTaskDeleted?.();
|
|
127
|
+
queryClient.invalidateQueries({
|
|
128
|
+
queryKey: queryKeys.tasks.list(workspaceId),
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
notifications.error(
|
|
132
|
+
error instanceof Error ? error.message : "Failed to delete task"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
isSubmitting,
|
|
139
|
+
handleCreateSprint,
|
|
140
|
+
handleStartSprint,
|
|
141
|
+
handleCompleteSprint,
|
|
142
|
+
handleDeleteTask,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog Composite Hook
|
|
3
|
+
*
|
|
4
|
+
* Combines all backlog hooks for backward compatibility.
|
|
5
|
+
* Prefer using individual hooks for new code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use client";
|
|
9
|
+
|
|
10
|
+
import { useBacklogActions } from "./useBacklogActions";
|
|
11
|
+
import { useBacklogData } from "./useBacklogData";
|
|
12
|
+
import { useBacklogDragDrop } from "./useBacklogDragDrop";
|
|
13
|
+
import { useBacklogUI } from "./useBacklogUI";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* All-in-one backlog hook (kept for backward compatibility)
|
|
17
|
+
*
|
|
18
|
+
* @deprecated Prefer using individual hooks:
|
|
19
|
+
* - useBacklogUI for UI state
|
|
20
|
+
* - useBacklogData for data
|
|
21
|
+
* - useBacklogDragDrop for drag operations
|
|
22
|
+
* - useBacklogActions for API calls
|
|
23
|
+
*/
|
|
24
|
+
export function useBacklogComposite() {
|
|
25
|
+
const ui = useBacklogUI();
|
|
26
|
+
const data = useBacklogData();
|
|
27
|
+
const dragDrop = useBacklogDragDrop(data.tasks);
|
|
28
|
+
const actions = useBacklogActions({
|
|
29
|
+
onSprintCreated: () => {
|
|
30
|
+
ui.setIsSprintModalOpen(false);
|
|
31
|
+
data.refetchSprints();
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
// Data
|
|
37
|
+
tasks: data.tasks,
|
|
38
|
+
sprints: data.sprints,
|
|
39
|
+
backlogTasks: data.backlogTasks,
|
|
40
|
+
activeSprint: data.activeSprint,
|
|
41
|
+
plannedSprints: data.plannedSprints,
|
|
42
|
+
completedSprints: data.completedSprints,
|
|
43
|
+
getSprintTasks: data.getSprintTasks,
|
|
44
|
+
isLoading: data.isLoading,
|
|
45
|
+
|
|
46
|
+
// UI State
|
|
47
|
+
isTaskModalOpen: ui.isTaskModalOpen,
|
|
48
|
+
setIsTaskModalOpen: ui.setIsTaskModalOpen,
|
|
49
|
+
isSprintModalOpen: ui.isSprintModalOpen,
|
|
50
|
+
setIsSprintModalOpen: ui.setIsSprintModalOpen,
|
|
51
|
+
selectedTaskId: ui.selectedTaskId,
|
|
52
|
+
setSelectedTaskId: ui.setSelectedTaskId,
|
|
53
|
+
expandedSections: ui.expandedSections,
|
|
54
|
+
toggleSection: ui.toggleSection,
|
|
55
|
+
|
|
56
|
+
// Drag & Drop
|
|
57
|
+
activeTask: dragDrop.activeTask,
|
|
58
|
+
sensors: dragDrop.sensors,
|
|
59
|
+
handleDragStart: dragDrop.handleDragStart,
|
|
60
|
+
handleDragEnd: dragDrop.handleDragEnd,
|
|
61
|
+
|
|
62
|
+
// Actions
|
|
63
|
+
isSubmitting: actions.isSubmitting,
|
|
64
|
+
handleCreateSprint: actions.handleCreateSprint,
|
|
65
|
+
handleStartSprint: actions.handleStartSprint,
|
|
66
|
+
handleCompleteSprint: actions.handleCompleteSprint,
|
|
67
|
+
handleDeleteTask: actions.handleDeleteTask,
|
|
68
|
+
|
|
69
|
+
// Data actions
|
|
70
|
+
refetchTasks: data.refetchTasks,
|
|
71
|
+
refetchSprints: data.refetchSprints,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog Data & Grouping Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages task and sprint data grouping and organization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use client";
|
|
8
|
+
|
|
9
|
+
import { type Sprint, SprintStatus, type Task } from "@locusai/shared";
|
|
10
|
+
import { useMemo } from "react";
|
|
11
|
+
import { useSprintsQuery, useTasksQuery } from "@/hooks";
|
|
12
|
+
|
|
13
|
+
export interface BacklogGroupedData {
|
|
14
|
+
tasks: Task[];
|
|
15
|
+
sprints: Sprint[];
|
|
16
|
+
backlogTasks: Task[];
|
|
17
|
+
activeSprint: Sprint | undefined;
|
|
18
|
+
plannedSprints: Sprint[];
|
|
19
|
+
completedSprints: Sprint[];
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BacklogDataActions {
|
|
24
|
+
getSprintTasks: (sprintId: string) => Task[];
|
|
25
|
+
refetchTasks: () => void;
|
|
26
|
+
refetchSprints: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Manage backlog data grouping and organization
|
|
31
|
+
*/
|
|
32
|
+
export function useBacklogData(): BacklogGroupedData & BacklogDataActions {
|
|
33
|
+
const {
|
|
34
|
+
data: tasks = [],
|
|
35
|
+
isLoading: tasksLoading,
|
|
36
|
+
refetch: refetchTasks,
|
|
37
|
+
} = useTasksQuery();
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
data: sprints = [],
|
|
41
|
+
isLoading: sprintsLoading,
|
|
42
|
+
refetch: refetchSprints,
|
|
43
|
+
} = useSprintsQuery();
|
|
44
|
+
|
|
45
|
+
const { backlogTasks, activeSprint, plannedSprints, completedSprints } =
|
|
46
|
+
useMemo(() => {
|
|
47
|
+
return {
|
|
48
|
+
backlogTasks: tasks.filter((t) => !t.sprintId),
|
|
49
|
+
activeSprint: sprints.find((s) => s.status === SprintStatus.ACTIVE),
|
|
50
|
+
plannedSprints: sprints.filter(
|
|
51
|
+
(s) => s.status === SprintStatus.PLANNED
|
|
52
|
+
),
|
|
53
|
+
completedSprints: sprints.filter(
|
|
54
|
+
(s) => s.status === SprintStatus.COMPLETED
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
}, [tasks, sprints]);
|
|
58
|
+
|
|
59
|
+
const getSprintTasks = (sprintId: string) =>
|
|
60
|
+
tasks.filter((t) => t.sprintId === sprintId);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
tasks,
|
|
64
|
+
sprints,
|
|
65
|
+
backlogTasks,
|
|
66
|
+
activeSprint,
|
|
67
|
+
plannedSprints,
|
|
68
|
+
completedSprints,
|
|
69
|
+
isLoading: tasksLoading || sprintsLoading,
|
|
70
|
+
getSprintTasks,
|
|
71
|
+
refetchTasks,
|
|
72
|
+
refetchSprints,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backlog Drag & Drop Hook
|
|
3
|
+
*
|
|
4
|
+
* Handles drag and drop logic for task moving between sprints/backlog.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use client";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type DragEndEvent,
|
|
11
|
+
type DragStartEvent,
|
|
12
|
+
PointerSensor,
|
|
13
|
+
useSensor,
|
|
14
|
+
useSensors,
|
|
15
|
+
} from "@dnd-kit/core";
|
|
16
|
+
import { type Task } from "@locusai/shared";
|
|
17
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { useWorkspaceId } from "@/hooks/useWorkspaceId";
|
|
20
|
+
import { locusClient } from "@/lib/api-client";
|
|
21
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
22
|
+
import { notifications } from "@/services/notifications";
|
|
23
|
+
|
|
24
|
+
export interface BacklogDragDropActions {
|
|
25
|
+
activeTask: Task | null;
|
|
26
|
+
sensors: ReturnType<typeof useSensors>;
|
|
27
|
+
handleDragStart: (event: DragStartEvent) => void;
|
|
28
|
+
handleDragEnd: (event: DragEndEvent) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Manage drag and drop operations for backlog tasks
|
|
33
|
+
*/
|
|
34
|
+
export function useBacklogDragDrop(tasks: Task[]): BacklogDragDropActions {
|
|
35
|
+
const workspaceId = useWorkspaceId();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
37
|
+
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
38
|
+
|
|
39
|
+
const sensors = useSensors(
|
|
40
|
+
useSensor(PointerSensor, {
|
|
41
|
+
activationConstraint: { distance: 8 },
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
46
|
+
const task = tasks.find((t) => t.id === event.active.id);
|
|
47
|
+
setActiveTask(task || null);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleDragEnd = async (event: DragEndEvent) => {
|
|
51
|
+
const { active, over } = event;
|
|
52
|
+
setActiveTask(null);
|
|
53
|
+
|
|
54
|
+
if (!over) return;
|
|
55
|
+
|
|
56
|
+
const taskId = active.id as string;
|
|
57
|
+
const overId = over.id as string;
|
|
58
|
+
|
|
59
|
+
// Find the actual target container (sprint ID or "backlog")
|
|
60
|
+
let targetContainerId = overId;
|
|
61
|
+
const overTask = tasks.find((t) => t.id === overId);
|
|
62
|
+
if (overTask) {
|
|
63
|
+
targetContainerId = overTask.sprintId
|
|
64
|
+
? `sprint-${overTask.sprintId}`
|
|
65
|
+
: "backlog";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let newSprintId: string | null = null;
|
|
69
|
+
if (targetContainerId === "backlog") {
|
|
70
|
+
newSprintId = null;
|
|
71
|
+
} else if (targetContainerId.startsWith("sprint-")) {
|
|
72
|
+
newSprintId = targetContainerId.replace("sprint-", "");
|
|
73
|
+
} else {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
78
|
+
if (!task || task.sprintId === newSprintId) return;
|
|
79
|
+
|
|
80
|
+
// Optimistic update
|
|
81
|
+
const queryKey = queryKeys.tasks.list(workspaceId);
|
|
82
|
+
await queryClient.cancelQueries({ queryKey });
|
|
83
|
+
|
|
84
|
+
const previousTasks = queryClient.getQueryData<Task[]>(queryKey);
|
|
85
|
+
|
|
86
|
+
if (previousTasks) {
|
|
87
|
+
queryClient.setQueryData<Task[]>(
|
|
88
|
+
queryKey,
|
|
89
|
+
previousTasks.map((t) =>
|
|
90
|
+
t.id === taskId ? { ...t, sprintId: newSprintId } : t
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await locusClient.tasks.update(taskId, workspaceId, {
|
|
97
|
+
sprintId: newSprintId,
|
|
98
|
+
});
|
|
99
|
+
// Invalidate to ensure sync
|
|
100
|
+
queryClient.invalidateQueries({ queryKey });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Rollback
|
|
103
|
+
if (previousTasks) {
|
|
104
|
+
queryClient.setQueryData(queryKey, previousTasks);
|
|
105
|
+
}
|
|
106
|
+
notifications.error(
|
|
107
|
+
error instanceof Error ? error.message : "Failed to move task"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
activeTask,
|
|
114
|
+
sensors,
|
|
115
|
+
handleDragStart,
|
|
116
|
+
handleDragEnd,
|
|
117
|
+
};
|
|
118
|
+
}
|