@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.
Files changed (346) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/next.config.js +15 -2
  3. package/package.json +26 -3
  4. package/src/app/(auth)/invite/page.tsx +109 -0
  5. package/src/app/(auth)/layout.tsx +19 -0
  6. package/src/app/(auth)/login/page.tsx +65 -0
  7. package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
  8. package/src/app/(auth)/register/page.tsx +165 -0
  9. package/src/app/(dashboard)/activity/page.tsx +7 -0
  10. package/src/app/(dashboard)/backlog/page.tsx +195 -0
  11. package/src/app/(dashboard)/board/page.tsx +141 -0
  12. package/src/app/(dashboard)/layout.tsx +32 -0
  13. package/src/app/(dashboard)/page.tsx +14 -0
  14. package/src/app/(dashboard)/settings/page.tsx +161 -0
  15. package/src/app/(dashboard)/settings/team/page.tsx +75 -0
  16. package/src/app/globals.css +259 -0
  17. package/src/app/layout.tsx +10 -20
  18. package/src/app/providers.tsx +26 -3
  19. package/src/components/AuthLayoutUI.tsx +53 -0
  20. package/src/components/BoardFilter.tsx +75 -74
  21. package/src/components/CreateModal/CreateModal.tsx +142 -0
  22. package/src/components/CreateModal/index.ts +1 -0
  23. package/src/components/Editor.tsx +279 -0
  24. package/src/components/Header.tsx +99 -12
  25. package/src/components/PageLayout.tsx +69 -0
  26. package/src/components/PropertyItem.tsx +55 -9
  27. package/src/components/Sidebar.tsx +280 -36
  28. package/src/components/SprintCreateModal.tsx +84 -0
  29. package/src/components/TaskCard.tsx +196 -78
  30. package/src/components/TaskCreateModal.tsx +181 -178
  31. package/src/components/TaskPanel.tsx +140 -692
  32. package/src/components/WorkspaceCreateModal.tsx +97 -0
  33. package/src/components/WorkspaceProtected.tsx +91 -0
  34. package/src/components/auth/InviteSteps.tsx +220 -0
  35. package/src/components/auth/LoginSteps.tsx +86 -0
  36. package/src/components/auth/RegisterSteps.tsx +371 -0
  37. package/src/components/auth/index.ts +3 -0
  38. package/src/components/backlog/BacklogList.tsx +92 -0
  39. package/src/components/backlog/BacklogSection.tsx +137 -0
  40. package/src/components/backlog/CompletedSprintItem.tsx +95 -0
  41. package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
  42. package/src/components/backlog/SprintSection.tsx +155 -0
  43. package/src/components/backlog/constants.ts +26 -0
  44. package/src/components/board/BoardColumn.tsx +97 -0
  45. package/src/components/board/BoardContent.tsx +84 -0
  46. package/src/components/board/BoardEmptyState.tsx +82 -0
  47. package/src/components/board/BoardHeader.tsx +47 -0
  48. package/src/components/board/SprintMindmap.tsx +65 -0
  49. package/src/components/board/constants.ts +40 -0
  50. package/src/components/board/index.ts +5 -0
  51. package/src/components/common/ErrorState.tsx +124 -0
  52. package/src/components/common/LoadingState.tsx +83 -0
  53. package/src/components/common/index.ts +40 -0
  54. package/src/components/dashboard/ActivityFeed.tsx +77 -0
  55. package/src/components/dashboard/ActivityItem.tsx +207 -0
  56. package/src/components/dashboard/QuickActions.tsx +50 -0
  57. package/src/components/dashboard/StatCard.tsx +79 -0
  58. package/src/components/dnd/index.tsx +51 -0
  59. package/src/components/docs/DocsEditorArea.tsx +87 -0
  60. package/src/components/docs/DocsHeaderActions.tsx +121 -0
  61. package/src/components/docs/DocsSidebar.tsx +351 -0
  62. package/src/components/index.ts +7 -0
  63. package/src/components/onboarding/StepProgress.tsx +21 -0
  64. package/src/components/onboarding/index.ts +1 -0
  65. package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
  66. package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
  67. package/src/components/settings/ApiKeysList.tsx +143 -0
  68. package/src/components/settings/ApiKeysSettings.tsx +144 -0
  69. package/src/components/settings/InviteMemberModal.tsx +106 -0
  70. package/src/components/settings/ProjectSetupGuide.tsx +147 -0
  71. package/src/components/settings/SettingItem.tsx +32 -0
  72. package/src/components/settings/SettingSection.tsx +50 -0
  73. package/src/components/settings/TeamInvitationsList.tsx +90 -0
  74. package/src/components/settings/TeamMembersList.tsx +95 -0
  75. package/src/components/settings/index.ts +8 -0
  76. package/src/components/task-panel/TaskActivity.tsx +127 -0
  77. package/src/components/task-panel/TaskChecklist.tsx +142 -0
  78. package/src/components/task-panel/TaskDescription.tsx +201 -0
  79. package/src/components/task-panel/TaskDocs.tsx +137 -0
  80. package/src/components/task-panel/TaskHeader.tsx +125 -0
  81. package/src/components/task-panel/TaskProperties.tsx +111 -0
  82. package/src/components/task-panel/index.ts +12 -0
  83. package/src/components/typography/EmptyStateText.tsx +59 -0
  84. package/src/components/typography/MetadataText.tsx +65 -0
  85. package/src/components/typography/SecondaryText.tsx +60 -0
  86. package/src/components/typography/SectionLabel.tsx +60 -0
  87. package/src/components/typography/index.ts +14 -0
  88. package/src/components/typography-scales.tsx +218 -0
  89. package/src/components/ui/Avatar.tsx +123 -0
  90. package/src/components/ui/Badge.tsx +69 -2
  91. package/src/components/ui/Button.tsx +71 -30
  92. package/src/components/ui/Checkbox.tsx +34 -0
  93. package/src/components/ui/Dropdown.tsx +67 -1
  94. package/src/components/ui/EmptyState.tsx +129 -0
  95. package/src/components/ui/Input.tsx +53 -6
  96. package/src/components/ui/Modal.tsx +45 -12
  97. package/src/components/ui/OtpInput.tsx +148 -0
  98. package/src/components/ui/Skeleton.tsx +36 -0
  99. package/src/components/ui/Spinner.tsx +112 -0
  100. package/src/components/ui/Textarea.tsx +28 -3
  101. package/src/components/ui/Toast.tsx +99 -0
  102. package/src/components/ui/Toggle.tsx +63 -0
  103. package/src/components/ui/constants.ts +108 -0
  104. package/src/components/ui/index.ts +7 -0
  105. package/src/context/AuthContext.tsx +140 -0
  106. package/src/context/index.ts +1 -0
  107. package/src/hooks/backlog/index.ts +13 -0
  108. package/src/hooks/backlog/useBacklogActions.ts +144 -0
  109. package/src/hooks/backlog/useBacklogComposite.ts +73 -0
  110. package/src/hooks/backlog/useBacklogData.ts +74 -0
  111. package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
  112. package/src/hooks/backlog/useBacklogUI.ts +74 -0
  113. package/src/hooks/index.ts +22 -0
  114. package/src/hooks/task-panel/index.ts +13 -0
  115. package/src/hooks/task-panel/useTaskActions.ts +200 -0
  116. package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
  117. package/src/hooks/task-panel/useTaskData.ts +78 -0
  118. package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
  119. package/src/hooks/task-panel/useTaskUIState.ts +80 -0
  120. package/src/hooks/useAuthLayoutLogic.ts +43 -0
  121. package/src/hooks/useAuthenticatedUser.ts +36 -0
  122. package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
  123. package/src/hooks/useBacklog.ts +303 -0
  124. package/src/hooks/useBoard.ts +230 -0
  125. package/src/hooks/useDashboardLayout.ts +49 -0
  126. package/src/hooks/useDocs.ts +279 -0
  127. package/src/hooks/useDocsQuery.ts +99 -0
  128. package/src/hooks/useDocsSidebarState.ts +104 -0
  129. package/src/hooks/useFormState.ts +40 -0
  130. package/src/hooks/useGlobalKeydowns.ts +52 -0
  131. package/src/hooks/useInviteForm.ts +122 -0
  132. package/src/hooks/useLoginForm.ts +84 -0
  133. package/src/hooks/useMutationWithToast.ts +56 -0
  134. package/src/hooks/useOrganizationQuery.ts +55 -0
  135. package/src/hooks/useRegisterForm.ts +216 -0
  136. package/src/hooks/useSprintsQuery.ts +38 -0
  137. package/src/hooks/useTaskDescription.ts +102 -0
  138. package/src/hooks/useTaskPanel.ts +341 -0
  139. package/src/hooks/useTasksQuery.ts +39 -0
  140. package/src/hooks/useTeamManagement.ts +92 -0
  141. package/src/hooks/useWorkspaceCreateForm.ts +70 -0
  142. package/src/hooks/useWorkspaceId.ts +29 -0
  143. package/src/lib/api-client.ts +40 -23
  144. package/src/lib/config.ts +25 -0
  145. package/src/lib/constants.ts +83 -0
  146. package/src/lib/options.ts +96 -0
  147. package/src/lib/query-keys.ts +91 -0
  148. package/src/lib/typography.ts +103 -0
  149. package/src/lib/utils.ts +4 -0
  150. package/src/lib/validation.ts +192 -0
  151. package/src/services/index.ts +7 -3
  152. package/src/services/notifications.ts +80 -0
  153. package/src/utils/env.utils.ts +15 -0
  154. package/src/views/ActivityView.tsx +123 -0
  155. package/src/views/Dashboard.tsx +126 -0
  156. package/src/views/Docs.tsx +98 -612
  157. package/tsconfig.tsbuildinfo +1 -1
  158. package/.next/BUILD_ID +0 -1
  159. package/.next/app-build-manifest.json +0 -55
  160. package/.next/app-path-routes-manifest.json +0 -8
  161. package/.next/build-manifest.json +0 -33
  162. package/.next/cache/.previewinfo +0 -1
  163. package/.next/cache/.rscinfo +0 -1
  164. package/.next/cache/.tsbuildinfo +0 -1
  165. package/.next/cache/config.json +0 -7
  166. package/.next/cache/webpack/client-production/0.pack +0 -0
  167. package/.next/cache/webpack/client-production/index.pack +0 -0
  168. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  169. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  170. package/.next/cache/webpack/server-production/0.pack +0 -0
  171. package/.next/cache/webpack/server-production/index.pack +0 -0
  172. package/.next/diagnostics/build-diagnostics.json +0 -6
  173. package/.next/diagnostics/framework.json +0 -1
  174. package/.next/export-detail.json +0 -5
  175. package/.next/export-marker.json +0 -6
  176. package/.next/images-manifest.json +0 -57
  177. package/.next/next-minimal-server.js.nft.json +0 -1
  178. package/.next/next-server.js.nft.json +0 -1
  179. package/.next/package.json +0 -1
  180. package/.next/prerender-manifest.json +0 -137
  181. package/.next/react-loadable-manifest.json +0 -32
  182. package/.next/required-server-files.json +0 -324
  183. package/.next/routes-manifest.json +0 -77
  184. package/.next/server/app/_not-found/page.js +0 -2
  185. package/.next/server/app/_not-found/page.js.nft.json +0 -1
  186. package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  187. package/.next/server/app/_not-found.html +0 -1
  188. package/.next/server/app/_not-found.meta +0 -8
  189. package/.next/server/app/_not-found.rsc +0 -21
  190. package/.next/server/app/backlog/page.js +0 -2
  191. package/.next/server/app/backlog/page.js.nft.json +0 -1
  192. package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
  193. package/.next/server/app/backlog.html +0 -1
  194. package/.next/server/app/backlog.meta +0 -7
  195. package/.next/server/app/backlog.rsc +0 -25
  196. package/.next/server/app/docs/page.js +0 -97
  197. package/.next/server/app/docs/page.js.nft.json +0 -1
  198. package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
  199. package/.next/server/app/docs.html +0 -1
  200. package/.next/server/app/docs.meta +0 -7
  201. package/.next/server/app/docs.rsc +0 -26
  202. package/.next/server/app/favicon.ico/route.js +0 -1
  203. package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  204. package/.next/server/app/favicon.ico.body +0 -0
  205. package/.next/server/app/favicon.ico.meta +0 -1
  206. package/.next/server/app/index.html +0 -1
  207. package/.next/server/app/index.meta +0 -7
  208. package/.next/server/app/index.rsc +0 -25
  209. package/.next/server/app/page.js +0 -2
  210. package/.next/server/app/page.js.nft.json +0 -1
  211. package/.next/server/app/page_client-reference-manifest.js +0 -1
  212. package/.next/server/app/settings/page.js +0 -2
  213. package/.next/server/app/settings/page.js.nft.json +0 -1
  214. package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
  215. package/.next/server/app/settings.html +0 -1
  216. package/.next/server/app/settings.meta +0 -7
  217. package/.next/server/app/settings.rsc +0 -25
  218. package/.next/server/app-paths-manifest.json +0 -8
  219. package/.next/server/chunks/496.js +0 -6
  220. package/.next/server/chunks/585.js +0 -1
  221. package/.next/server/chunks/665.js +0 -1
  222. package/.next/server/chunks/699.js +0 -22
  223. package/.next/server/chunks/852.js +0 -7
  224. package/.next/server/functions-config-manifest.json +0 -4
  225. package/.next/server/interception-route-rewrite-manifest.js +0 -1
  226. package/.next/server/middleware-build-manifest.js +0 -1
  227. package/.next/server/middleware-manifest.json +0 -6
  228. package/.next/server/middleware-react-loadable-manifest.js +0 -1
  229. package/.next/server/next-font-manifest.js +0 -1
  230. package/.next/server/next-font-manifest.json +0 -1
  231. package/.next/server/pages/404.html +0 -1
  232. package/.next/server/pages/500.html +0 -1
  233. package/.next/server/pages/_app.js +0 -1
  234. package/.next/server/pages/_app.js.nft.json +0 -1
  235. package/.next/server/pages/_document.js +0 -1
  236. package/.next/server/pages/_document.js.nft.json +0 -1
  237. package/.next/server/pages/_error.js +0 -19
  238. package/.next/server/pages/_error.js.nft.json +0 -1
  239. package/.next/server/pages-manifest.json +0 -6
  240. package/.next/server/server-reference-manifest.js +0 -1
  241. package/.next/server/server-reference-manifest.json +0 -1
  242. package/.next/server/webpack-runtime.js +0 -1
  243. package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
  244. package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
  245. package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
  246. package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
  247. package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
  248. package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
  249. package/.next/static/chunks/487-1808785ba665f784.js +0 -1
  250. package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
  251. package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
  252. package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
  253. package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
  254. package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
  255. package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
  256. package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
  257. package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
  258. package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
  259. package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
  260. package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
  261. package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
  262. package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
  263. package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
  264. package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
  265. package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
  266. package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  267. package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
  268. package/.next/static/css/13e8617b72f9d3aa.css +0 -1
  269. package/.next/static/css/8aea088cdc4338f0.css +0 -1
  270. package/.next/static/css/b301ab0424111664.css +0 -1
  271. package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
  272. package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
  273. package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
  274. package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
  275. package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
  276. package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
  277. package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
  278. package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
  279. package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
  280. package/.next/trace +0 -46
  281. package/.next/types/app/backlog/page.ts +0 -84
  282. package/.next/types/app/docs/page.ts +0 -84
  283. package/.next/types/app/layout.ts +0 -84
  284. package/.next/types/app/page.ts +0 -84
  285. package/.next/types/app/settings/page.ts +0 -84
  286. package/.next/types/cache-life.d.ts +0 -141
  287. package/.next/types/package.json +0 -1
  288. package/next-env.d.ts +0 -5
  289. package/out/404.html +0 -1
  290. package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
  291. package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
  292. package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
  293. package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
  294. package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
  295. package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
  296. package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
  297. package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
  298. package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
  299. package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
  300. package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
  301. package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
  302. package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
  303. package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
  304. package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
  305. package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
  306. package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
  307. package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
  308. package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
  309. package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
  310. package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
  311. package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
  312. package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
  313. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  314. package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
  315. package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
  316. package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
  317. package/out/_next/static/css/b301ab0424111664.css +0 -1
  318. package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
  319. package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
  320. package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
  321. package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
  322. package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
  323. package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
  324. package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
  325. package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
  326. package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
  327. package/out/backlog.html +0 -1
  328. package/out/backlog.txt +0 -25
  329. package/out/docs.html +0 -1
  330. package/out/docs.txt +0 -26
  331. package/out/favicon.ico +0 -0
  332. package/out/index.html +0 -1
  333. package/out/index.txt +0 -25
  334. package/out/logo.png +0 -0
  335. package/out/settings.html +0 -1
  336. package/out/settings.txt +0 -25
  337. package/src/app/backlog/page.tsx +0 -19
  338. package/src/app/page.tsx +0 -16
  339. package/src/app/settings/page.tsx +0 -194
  340. package/src/hooks/useTasks.ts +0 -119
  341. package/src/services/doc.service.ts +0 -27
  342. package/src/services/sprint.service.ts +0 -24
  343. package/src/services/task.service.ts +0 -75
  344. package/src/views/Backlog.tsx +0 -691
  345. package/src/views/Board.tsx +0 -306
  346. /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
@@ -0,0 +1,341 @@
1
+ "use client";
2
+
3
+ import { type AcceptanceItem, type Task, TaskStatus } from "@locusai/shared";
4
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { useEffect, useState } from "react";
6
+ import { toast } from "sonner";
7
+ import { useAuth } from "@/context/AuthContext";
8
+ import { useWorkspaceId } from "@/hooks/useWorkspaceId";
9
+ import { locusClient } from "@/lib/api-client";
10
+ import { queryKeys } from "@/lib/query-keys";
11
+
12
+ interface UseTaskPanelProps {
13
+ taskId: string;
14
+ onUpdated: () => void;
15
+ onDeleted: () => void;
16
+ onClose: () => void;
17
+ }
18
+
19
+ /**
20
+ * Task Panel Hook
21
+ * Handles detailed task view, checklist logic, and operations.
22
+ *
23
+ * Features:
24
+ * - Optimistic updates for instant UI feedback
25
+ * - Support for assignedTo and dueDate fields
26
+ * - Loading states for all mutations
27
+ */
28
+ export function useTaskPanel({
29
+ taskId,
30
+ onUpdated,
31
+ onDeleted,
32
+ onClose,
33
+ }: UseTaskPanelProps) {
34
+ const { user } = useAuth();
35
+ const workspaceId = useWorkspaceId();
36
+ const queryClient = useQueryClient();
37
+
38
+ // Fetch task details
39
+ const { data: task, refetch: fetchTask } = useQuery({
40
+ queryKey: queryKeys.tasks.detail(taskId),
41
+ queryFn: () => locusClient.tasks.getById(taskId, workspaceId),
42
+ enabled: !!workspaceId,
43
+ });
44
+
45
+ // UI State
46
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
47
+ const [editTitle, setEditTitle] = useState("");
48
+ const [editDesc, setEditDesc] = useState("");
49
+ const [editAssignedTo, setEditAssignedTo] = useState("");
50
+ const [editDueDate, setEditDueDate] = useState("");
51
+ const [newComment, setNewComment] = useState("");
52
+ const [newChecklistItem, setNewChecklistItem] = useState("");
53
+ const [descMode, setDescMode] = useState<"edit" | "preview">("preview");
54
+ const [showRejectModal, setShowRejectModal] = useState(false);
55
+ const [rejectReason, setRejectReason] = useState("");
56
+
57
+ useEffect(() => {
58
+ if (task) {
59
+ setEditTitle(task.title);
60
+ setEditDesc(task.description || "");
61
+ setEditAssignedTo(task.assignedTo || "");
62
+ setEditDueDate(
63
+ task.dueDate ? new Date(task.dueDate).toISOString().split("T")[0] : ""
64
+ );
65
+ }
66
+ }, [task]);
67
+
68
+ // Mutations with Optimistic Updates
69
+ const updateTaskMutation = useMutation({
70
+ mutationFn: (updates: Partial<Task> & { docIds?: string[] }) =>
71
+ locusClient.tasks.update(taskId, workspaceId as string, updates),
72
+ // Optimistic update: update cache immediately
73
+ onMutate: async (updates) => {
74
+ // Cancel ongoing queries
75
+ await queryClient.cancelQueries({
76
+ queryKey: queryKeys.tasks.detail(taskId),
77
+ });
78
+
79
+ // Snapshot previous data
80
+ const previousTask = queryClient.getQueryData<Task>(
81
+ queryKeys.tasks.detail(taskId)
82
+ );
83
+
84
+ // Update cache optimistically
85
+ if (previousTask) {
86
+ queryClient.setQueryData(queryKeys.tasks.detail(taskId), {
87
+ ...previousTask,
88
+ ...updates,
89
+ dueDate: updates.dueDate
90
+ ? new Date(updates.dueDate)
91
+ : previousTask.dueDate,
92
+ });
93
+ }
94
+
95
+ return { previousTask };
96
+ },
97
+ // On error, rollback to previous data
98
+ onError: (_err, _variables, context) => {
99
+ if (context?.previousTask) {
100
+ queryClient.setQueryData(
101
+ queryKeys.tasks.detail(taskId),
102
+ context.previousTask
103
+ );
104
+ }
105
+ toast.error("Failed to update task");
106
+ },
107
+ // On success, refetch to confirm
108
+ onSuccess: () => {
109
+ fetchTask();
110
+ onUpdated();
111
+ },
112
+ });
113
+
114
+ const handleLinkDoc = async (docId: string) => {
115
+ if (!task) return;
116
+ const currentDocIds = task.docs?.map((d) => d.id) || [];
117
+ if (currentDocIds.includes(docId)) return;
118
+
119
+ try {
120
+ await updateTaskMutation.mutateAsync({
121
+ docIds: [...currentDocIds, docId],
122
+ });
123
+ toast.success("Document linked");
124
+ } catch {
125
+ toast.error("Failed to link document");
126
+ }
127
+ };
128
+
129
+ const handleUnlinkDoc = async (docId: string) => {
130
+ if (!task) return;
131
+ const currentDocIds = task.docs?.map((d) => d.id) || [];
132
+
133
+ try {
134
+ await updateTaskMutation.mutateAsync({
135
+ docIds: currentDocIds.filter((id) => id !== docId),
136
+ });
137
+ toast.success("Document unlinked");
138
+ } catch {
139
+ toast.error("Failed to unlink document");
140
+ }
141
+ };
142
+
143
+ const deleteTaskMutation = useMutation({
144
+ mutationFn: () => locusClient.tasks.delete(taskId, workspaceId as string),
145
+ onSuccess: () => {
146
+ queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all() });
147
+ toast.success("Task deleted");
148
+ onDeleted();
149
+ onClose();
150
+ },
151
+ onError: () => {
152
+ toast.error("Failed to delete task");
153
+ },
154
+ });
155
+
156
+ const addCommentMutation = useMutation({
157
+ mutationFn: (data: { author: string; text: string }) =>
158
+ locusClient.tasks.addComment(taskId, workspaceId as string, data),
159
+ onSuccess: () => {
160
+ fetchTask();
161
+ },
162
+ onError: () => {
163
+ toast.error("Failed to add comment");
164
+ },
165
+ });
166
+
167
+ const handleUpdateTask = async (updates: Partial<Task>) => {
168
+ try {
169
+ await updateTaskMutation.mutateAsync(updates);
170
+ } catch (err) {
171
+ console.error("Failed to update task:", err);
172
+ }
173
+ };
174
+
175
+ const handleDelete = async () => {
176
+ if (
177
+ !confirm(
178
+ "Are you sure you want to delete this task? This action cannot be undone."
179
+ )
180
+ ) {
181
+ return;
182
+ }
183
+ try {
184
+ await deleteTaskMutation.mutateAsync();
185
+ } catch (err) {
186
+ console.error("Failed to delete task:", err);
187
+ }
188
+ };
189
+
190
+ const handleTitleSave = () => {
191
+ if (editTitle.trim() && editTitle !== task?.title) {
192
+ handleUpdateTask({ title: editTitle.trim() });
193
+ }
194
+ setIsEditingTitle(false);
195
+ };
196
+
197
+ const handleDescSave = () => {
198
+ if (editDesc !== task?.description) {
199
+ handleUpdateTask({ description: editDesc });
200
+ }
201
+ };
202
+
203
+ const handleAssignedToSave = () => {
204
+ if (editAssignedTo !== (task?.assignedTo || "")) {
205
+ handleUpdateTask({ assignedTo: editAssignedTo || null });
206
+ }
207
+ };
208
+
209
+ const handleDueDateSave = () => {
210
+ if (editDueDate) {
211
+ const newDate = new Date(editDueDate);
212
+ handleUpdateTask({ dueDate: newDate });
213
+ } else if (task?.dueDate) {
214
+ handleUpdateTask({ dueDate: null });
215
+ }
216
+ };
217
+
218
+ const handleAddChecklistItem = () => {
219
+ if (!newChecklistItem.trim() || !task) return;
220
+ const newItem: AcceptanceItem = {
221
+ id: crypto.randomUUID(),
222
+ text: newChecklistItem.trim(),
223
+ done: false,
224
+ };
225
+ handleUpdateTask({
226
+ acceptanceChecklist: [...(task.acceptanceChecklist || []), newItem],
227
+ });
228
+ setNewChecklistItem("");
229
+ };
230
+
231
+ const handleToggleChecklistItem = (itemId: string) => {
232
+ if (!task?.acceptanceChecklist) return;
233
+ const updated = task.acceptanceChecklist.map((item) =>
234
+ item.id === itemId ? { ...item, done: !item.done } : item
235
+ );
236
+ handleUpdateTask({ acceptanceChecklist: updated });
237
+ };
238
+
239
+ const handleRemoveChecklistItem = (itemId: string) => {
240
+ if (!task?.acceptanceChecklist) return;
241
+ const updated = task.acceptanceChecklist.filter(
242
+ (item) => item.id !== itemId
243
+ );
244
+ handleUpdateTask({ acceptanceChecklist: updated });
245
+ };
246
+
247
+ const handleAddComment = async () => {
248
+ if (!newComment.trim()) return;
249
+ try {
250
+ await addCommentMutation.mutateAsync({
251
+ author: user?.name || "Anonymous",
252
+ text: newComment,
253
+ });
254
+ setNewComment("");
255
+ toast.success("Comment added");
256
+ } catch (err) {
257
+ console.error("Failed to add comment:", err);
258
+ }
259
+ };
260
+
261
+ const handleReject = async () => {
262
+ if (!rejectReason.trim()) return;
263
+ try {
264
+ await updateTaskMutation.mutateAsync({
265
+ status: TaskStatus.IN_PROGRESS,
266
+ });
267
+ await addCommentMutation.mutateAsync({
268
+ author: user?.name || "Manager",
269
+ text: `❌ **Rejected**: ${rejectReason}`,
270
+ });
271
+ setShowRejectModal(false);
272
+ setRejectReason("");
273
+ toast.success("Task rejected and moved back to in progress");
274
+ onUpdated();
275
+ } catch (err) {
276
+ console.error("Failed to reject task:", err);
277
+ toast.error("Failed to reject task");
278
+ }
279
+ };
280
+
281
+ const handleApprove = async () => {
282
+ try {
283
+ await updateTaskMutation.mutateAsync({ status: TaskStatus.DONE });
284
+ toast.success("Task approved and marked as done");
285
+ onUpdated();
286
+ } catch (err) {
287
+ console.error("Failed to approve task:", err);
288
+ toast.error("Failed to approve task");
289
+ }
290
+ };
291
+
292
+ const checklistProgress = task?.acceptanceChecklist?.length
293
+ ? Math.round(
294
+ (task.acceptanceChecklist.filter((i) => i.done).length /
295
+ task.acceptanceChecklist.length) *
296
+ 100
297
+ )
298
+ : 0;
299
+
300
+ return {
301
+ task,
302
+ isEditingTitle,
303
+ setIsEditingTitle,
304
+ editTitle,
305
+ setEditTitle,
306
+ editDesc,
307
+ setEditDesc,
308
+ editAssignedTo,
309
+ setEditAssignedTo,
310
+ editDueDate,
311
+ setEditDueDate,
312
+ newComment,
313
+ setNewComment,
314
+ newChecklistItem,
315
+ setNewChecklistItem,
316
+ descMode,
317
+ setDescMode,
318
+ showRejectModal,
319
+ setShowRejectModal,
320
+ rejectReason,
321
+ setRejectReason,
322
+ checklistProgress,
323
+ isLoading: updateTaskMutation.isPending || addCommentMutation.isPending,
324
+ isDeleting: deleteTaskMutation.isPending,
325
+ handleUpdateTask,
326
+ handleLinkDoc,
327
+ handleUnlinkDoc,
328
+ handleDelete,
329
+ handleTitleSave,
330
+ handleDescSave,
331
+ handleAssignedToSave,
332
+ handleDueDateSave,
333
+ handleAddChecklistItem,
334
+ handleToggleChecklistItem,
335
+ handleRemoveChecklistItem,
336
+ handleAddComment,
337
+ handleReject,
338
+ handleApprove,
339
+ refresh: fetchTask,
340
+ };
341
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { type Task } from "@locusai/shared";
4
+ import { useQuery } from "@tanstack/react-query";
5
+ import { locusClient } from "@/lib/api-client";
6
+ import { queryKeys } from "@/lib/query-keys";
7
+ import { useWorkspaceIdOptional } from "./useWorkspaceId";
8
+
9
+ /**
10
+ * Tasks Query Hook
11
+ * Centralizes task fetching logic.
12
+ */
13
+ export function useTasksQuery() {
14
+ const workspaceId = useWorkspaceIdOptional();
15
+
16
+ return useQuery<Task[]>({
17
+ queryKey: queryKeys.tasks.list(workspaceId),
18
+ queryFn: () =>
19
+ workspaceId ? locusClient.tasks.list(workspaceId) : Promise.resolve([]),
20
+ enabled: !!workspaceId,
21
+ refetchInterval: 10_000,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Backlog Query Hook
27
+ */
28
+ export function useBacklogQuery() {
29
+ const workspaceId = useWorkspaceIdOptional();
30
+
31
+ return useQuery<Task[]>({
32
+ queryKey: queryKeys.tasks.backlog(workspaceId),
33
+ queryFn: () =>
34
+ workspaceId
35
+ ? locusClient.tasks.getBacklog(workspaceId)
36
+ : Promise.resolve([]),
37
+ enabled: !!workspaceId,
38
+ });
39
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Hook for team management logic
3
+ * Handles members, invitations, and member operations
4
+ */
5
+ "use client";
6
+
7
+ import { MembershipRole } from "@locusai/shared";
8
+ import { useQueryClient } from "@tanstack/react-query";
9
+ import { useState } from "react";
10
+ import { toast } from "sonner";
11
+ import {
12
+ useAuthenticatedUserWithOrg,
13
+ useInvitationsQuery,
14
+ useOrganizationMembersQuery,
15
+ } from "@/hooks";
16
+ import { locusClient } from "@/lib/api-client";
17
+ import { queryKeys } from "@/lib/query-keys";
18
+
19
+ export function useTeamManagement() {
20
+ const currentUser = useAuthenticatedUserWithOrg();
21
+ const { data: members = [], isLoading: membersLoading } =
22
+ useOrganizationMembersQuery();
23
+ const queryClient = useQueryClient();
24
+ const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
25
+
26
+ // Determine user's role
27
+ const userMembership = members.find((m) => m.userId === currentUser?.id);
28
+ const isOwner = userMembership?.role === MembershipRole.OWNER;
29
+ const isAdmin = userMembership?.role === MembershipRole.ADMIN;
30
+ const canManage = isOwner || isAdmin;
31
+
32
+ const { data: invitations = [], isLoading: invitationsLoading } =
33
+ useInvitationsQuery({ enabled: canManage });
34
+
35
+ const handleRemoveMember = async (userId: string) => {
36
+ if (!confirm("Are you sure you want to remove this member?")) return;
37
+
38
+ try {
39
+ await locusClient.organizations.removeMember(currentUser.orgId, userId);
40
+ toast.success("Member removed");
41
+ queryClient.invalidateQueries({
42
+ queryKey: queryKeys.organizations.members(currentUser.orgId),
43
+ });
44
+ } catch (error) {
45
+ toast.error(
46
+ error instanceof Error ? error.message : "Failed to remove member"
47
+ );
48
+ }
49
+ };
50
+
51
+ const handleRevokeInvitation = async (invitationId: string) => {
52
+ try {
53
+ await locusClient.invitations.revoke(currentUser.orgId, invitationId);
54
+ toast.success("Invitation revoked");
55
+ queryClient.invalidateQueries({
56
+ queryKey: queryKeys.invitations.list(currentUser.orgId),
57
+ });
58
+ } catch (error) {
59
+ toast.error(
60
+ error instanceof Error ? error.message : "Failed to revoke invitation"
61
+ );
62
+ }
63
+ };
64
+
65
+ const handleCopyLink = (token: string) => {
66
+ const baseUrl =
67
+ typeof window !== "undefined"
68
+ ? window.location.origin
69
+ : "https://app.locusai.dev";
70
+ const inviteUrl = `${baseUrl}/invite?token=${token}`;
71
+
72
+ navigator.clipboard.writeText(inviteUrl);
73
+ toast.success("Invitation link copied to clipboard");
74
+ };
75
+
76
+ const isLoading = membersLoading || invitationsLoading;
77
+
78
+ return {
79
+ currentUser,
80
+ members,
81
+ invitations,
82
+ isLoading,
83
+ isOwner,
84
+ isAdmin,
85
+ canManage,
86
+ isInviteModalOpen,
87
+ setIsInviteModalOpen,
88
+ handleRemoveMember,
89
+ handleRevokeInvitation,
90
+ handleCopyLink,
91
+ };
92
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Hook for workspace creation form management
3
+ * Handles workspace creation with optional auto-organization creation
4
+ */
5
+ "use client";
6
+
7
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
8
+ import { useRouter } from "next/navigation";
9
+ import { useState } from "react";
10
+ import { toast } from "sonner";
11
+ import { useAuth } from "@/context/AuthContext";
12
+ import { locusClient } from "@/lib/api-client";
13
+ import { queryKeys } from "@/lib/query-keys";
14
+ import { useAuthenticatedUser } from "./useAuthenticatedUser";
15
+
16
+ interface UseWorkspaceCreateFormReturn {
17
+ name: string;
18
+ isLoading: boolean;
19
+ setName: (name: string) => void;
20
+ handleSubmit: (e: React.FormEvent) => void;
21
+ }
22
+
23
+ /**
24
+ * Custom hook for workspace creation
25
+ * Handles both standard and auto-org workspace creation
26
+ */
27
+ export function useWorkspaceCreateForm(): UseWorkspaceCreateFormReturn {
28
+ const [name, setName] = useState("");
29
+ const router = useRouter();
30
+ const queryClient = useQueryClient();
31
+ const user = useAuthenticatedUser();
32
+ const { refreshUser } = useAuth();
33
+
34
+ const createWorkspaceMutation = useMutation({
35
+ mutationFn: (workspaceName: string) => {
36
+ // If user has orgId, use the standard create method
37
+ if (user?.orgId) {
38
+ return locusClient.workspaces.create({
39
+ name: workspaceName,
40
+ orgId: user.orgId,
41
+ });
42
+ }
43
+ // Otherwise, use createWithAutoOrg which creates organization if needed
44
+ return locusClient.workspaces.createWithAutoOrg({ name: workspaceName });
45
+ },
46
+ onSuccess: async () => {
47
+ queryClient.invalidateQueries({ queryKey: queryKeys.workspaces.all() });
48
+ // Refresh user data to get the new workspaceId
49
+ await refreshUser();
50
+ toast.success("Workspace created!");
51
+ router.push("/");
52
+ },
53
+ onError: (error: Error) => {
54
+ toast.error(error.message || "Failed to create workspace");
55
+ },
56
+ });
57
+
58
+ const handleSubmit = (e: React.FormEvent) => {
59
+ e.preventDefault();
60
+ if (!name.trim()) return;
61
+ createWorkspaceMutation.mutate(name.trim());
62
+ };
63
+
64
+ return {
65
+ name,
66
+ isLoading: createWorkspaceMutation.isPending,
67
+ setName,
68
+ handleSubmit,
69
+ };
70
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useAuth } from "@/context";
4
+
5
+ /**
6
+ * Hook to get the current workspace ID
7
+ * Ensures consistent handling of workspace context across the app
8
+ */
9
+ export function useWorkspaceId(): string {
10
+ const { user } = useAuth();
11
+ const workspaceId = user?.workspaceId;
12
+
13
+ if (!workspaceId) {
14
+ throw new Error(
15
+ "No workspace ID available. User must be authenticated with a workspace."
16
+ );
17
+ }
18
+
19
+ return workspaceId;
20
+ }
21
+
22
+ /**
23
+ * Hook to safely get workspace ID or null
24
+ * Useful for conditional queries that shouldn't execute without a workspace
25
+ */
26
+ export function useWorkspaceIdOptional(): string | null {
27
+ const { user } = useAuth();
28
+ return user?.workspaceId ?? null;
29
+ }
@@ -1,28 +1,45 @@
1
- import axios from "axios";
1
+ import { LocusClient, LocusEvent } from "@locusai/sdk";
2
+ import { config } from "./config";
2
3
 
3
- // In dev mode, API runs on port 3080. In production (static export), relative paths work.
4
- const API_BASE_URL =
5
- process.env.NEXT_PUBLIC_API_URL || "http://localhost:3080/api";
4
+ const tokenKey = "locus_token";
6
5
 
7
- const apiClient = axios.create({
8
- baseURL: API_BASE_URL,
9
- headers: {
10
- "Content-Type": "application/json",
11
- },
6
+ // Get initial token from localStorage if available
7
+ const initialToken =
8
+ typeof window !== "undefined" ? localStorage.getItem(tokenKey) : null;
9
+
10
+ export const locusClient = new LocusClient({
11
+ baseUrl: config.NEXT_PUBLIC_API_URL,
12
+ token: initialToken,
12
13
  });
13
14
 
14
- // Response interceptor for consistent error handling
15
- apiClient.interceptors.response.use(
16
- (response) => response,
17
- (error) => {
18
- // We can add global error handling here (e.g., logging, toast notifications)
19
- const message =
20
- error.response?.data?.message ||
21
- error.message ||
22
- "An unexpected error occurred";
23
- console.error(`[API Error] ${message}`, error);
24
- return Promise.reject(error);
25
- }
26
- );
15
+ // Setup event listeners for the web app
16
+ if (typeof window !== "undefined") {
17
+ locusClient.emitter.on(LocusEvent.TOKEN_EXPIRED, () => {
18
+ localStorage.removeItem(tokenKey);
19
+ // Only redirect if not already on login/register/invite pages
20
+ if (!window.location.pathname.match(/\/(login|register|invite)/)) {
21
+ window.location.href = "/login";
22
+ }
23
+ });
24
+
25
+ locusClient.emitter.on(LocusEvent.AUTH_ERROR, (error) => {
26
+ console.error("[Auth Error]", error);
27
+ });
27
28
 
28
- export default apiClient;
29
+ locusClient.emitter.on(LocusEvent.REQUEST_ERROR, (error) => {
30
+ console.error("[Request Error]", error);
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Helper to update token in client and localStorage
36
+ */
37
+ export const setClientToken = (token: string | null) => {
38
+ if (token) {
39
+ localStorage.setItem(tokenKey, token);
40
+ locusClient.setToken(token);
41
+ } else {
42
+ localStorage.removeItem(tokenKey);
43
+ locusClient.setToken(null);
44
+ }
45
+ };
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+
3
+ const ConfigSchema = z.object({
4
+ NEXT_PUBLIC_API_URL: z.string().url().default("http://localhost:3080/api"),
5
+ });
6
+
7
+ export type Config = z.infer<typeof ConfigSchema>;
8
+
9
+ function loadConfig(): Config {
10
+ // Safe parse for Next.js environment variables
11
+ const result = ConfigSchema.safeParse({
12
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
13
+ });
14
+
15
+ if (!result.success) {
16
+ console.error("❌ Invalid environment variables:", result.error.format());
17
+ return {
18
+ NEXT_PUBLIC_API_URL: "http://localhost:3080/api",
19
+ } as Config;
20
+ }
21
+
22
+ return result.data;
23
+ }
24
+
25
+ export const config = loadConfig();