@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,36 @@
1
+ "use client";
2
+
3
+ import { type User } from "@locusai/shared";
4
+ import { useRouter } from "next/navigation";
5
+ import { useEffect } from "react";
6
+ import { useAuth } from "@/context/AuthContext";
7
+ import { isCloudMode } from "@/utils/env.utils";
8
+
9
+ /**
10
+ * Hook for pages that require an authenticated user.
11
+ * Returns a guaranteed non-null User object.
12
+ * Redirects to login if user is not authenticated.
13
+ *
14
+ * Usage: Only use this in pages/components that are already protected
15
+ * by authentication middleware (wrapped with auth guards).
16
+ *
17
+ * @returns {User} The authenticated user (guaranteed non-null)
18
+ * @throws Redirects to login if not authenticated
19
+ */
20
+ export function useAuthenticatedUser(): User {
21
+ const { user, isLoading, isAuthenticated } = useAuth();
22
+ const router = useRouter();
23
+
24
+ useEffect(() => {
25
+ // Redirect to login if not authenticated (only in cloud mode)
26
+ if (!isLoading && !isAuthenticated && isCloudMode()) {
27
+ router.push("/login");
28
+ }
29
+ }, [isLoading, isAuthenticated, router]);
30
+
31
+ // Type assertion is safe here because:
32
+ // 1. This hook is only used in authenticated contexts
33
+ // 2. If user is null, we redirect to login above
34
+ // 3. The caller is responsible for ensuring this component is protected
35
+ return user as User;
36
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { type User } from "@locusai/shared";
4
+ import { useRouter } from "next/navigation";
5
+ import { useEffect } from "react";
6
+ import { useAuth } from "@/context/AuthContext";
7
+ import { isCloudMode } from "@/utils/env.utils";
8
+
9
+ /**
10
+ * Hook for pages that require an authenticated user WITH an organization.
11
+ * Returns a guaranteed non-null User object with a non-null orgId.
12
+ * Redirects to login if user is not authenticated or has no organization.
13
+ *
14
+ * Usage: Only use this in pages/components that are already protected
15
+ * by authentication middleware and require organization context.
16
+ *
17
+ * @returns {User} The authenticated user with orgId (both guaranteed non-null)
18
+ * @throws Redirects to login if not authenticated or no organization
19
+ */
20
+ export function useAuthenticatedUserWithOrg(): User & { orgId: string } {
21
+ const { user, isLoading, isAuthenticated } = useAuth();
22
+ const router = useRouter();
23
+
24
+ useEffect(() => {
25
+ // Redirect to login if not authenticated (only in cloud mode)
26
+ if (!isLoading && !isAuthenticated && isCloudMode()) {
27
+ router.push("/login");
28
+ }
29
+
30
+ // Redirect to onboarding if no organization
31
+ if (!isLoading && isAuthenticated && user && !user.orgId && isCloudMode()) {
32
+ router.push("/onboarding/workspace");
33
+ }
34
+ }, [isLoading, isAuthenticated, user, router]);
35
+
36
+ // Type assertion is safe here because:
37
+ // 1. This hook is only used in authenticated contexts with org
38
+ // 2. If user is null or has no orgId, we redirect above
39
+ // 3. The caller is responsible for ensuring this component is protected
40
+ return user as User & { orgId: string };
41
+ }
@@ -0,0 +1,303 @@
1
+ "use client";
2
+
3
+ import {
4
+ type DragEndEvent,
5
+ type DragStartEvent,
6
+ PointerSensor,
7
+ useSensor,
8
+ useSensors,
9
+ } from "@dnd-kit/core";
10
+ import { type Sprint, SprintStatus, type Task } from "@locusai/shared";
11
+ import { useQueryClient } from "@tanstack/react-query";
12
+ import { useRouter, useSearchParams } from "next/navigation";
13
+ import { useEffect, useMemo, useState } from "react";
14
+ import { toast } from "sonner";
15
+ import { useSprintsQuery, useTasksQuery } from "@/hooks";
16
+ import { useWorkspaceId } from "@/hooks/useWorkspaceId";
17
+ import { locusClient } from "@/lib/api-client";
18
+ import { queryKeys } from "@/lib/query-keys";
19
+
20
+ export function useBacklog() {
21
+ const workspaceId = useWorkspaceId();
22
+ const queryClient = useQueryClient();
23
+ const router = useRouter();
24
+ const searchParams = useSearchParams();
25
+
26
+ const {
27
+ data: tasks = [],
28
+ isLoading: tasksLoading,
29
+ refetch: refetchTasks,
30
+ } = useTasksQuery();
31
+ const {
32
+ data: sprints = [],
33
+ isLoading: sprintsLoading,
34
+ refetch: refetchSprints,
35
+ } = useSprintsQuery();
36
+
37
+ const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
38
+ const [isSprintModalOpen, setIsSprintModalOpen] = useState(false);
39
+ const [selectedTaskId, setSelectedTaskIdState] = useState<string | null>(
40
+ null
41
+ );
42
+
43
+ // Sync URL query param with state on mount
44
+ useEffect(() => {
45
+ const taskIdFromUrl = searchParams.get("taskId");
46
+ if (taskIdFromUrl) {
47
+ setSelectedTaskIdState(taskIdFromUrl);
48
+ }
49
+ }, [searchParams]);
50
+
51
+ const setSelectedTaskId = (id: string | null) => {
52
+ setSelectedTaskIdState(id);
53
+ if (id) {
54
+ router.push(`/backlog?taskId=${id}`, { scroll: false });
55
+ } else {
56
+ router.push("/backlog", { scroll: false });
57
+ }
58
+ };
59
+ const [activeTask, setActiveTask] = useState<Task | null>(null);
60
+ const [expandedSections, setExpandedSections] = useState<Set<string>>(
61
+ new Set(["active", "planned"])
62
+ );
63
+ const [isSubmitting, setIsSubmitting] = useState(false);
64
+
65
+ // Handle query parameters for new task/sprint
66
+ useEffect(() => {
67
+ const createTask = searchParams.get("createTask");
68
+ const createSprint = searchParams.get("createSprint");
69
+
70
+ if (createTask === "true") {
71
+ setIsTaskModalOpen(true);
72
+ router.replace("/backlog");
73
+ } else if (createSprint === "true") {
74
+ setIsSprintModalOpen(true);
75
+ router.replace("/backlog");
76
+ }
77
+ }, [searchParams, router]);
78
+
79
+ // Sensors for drag and drop
80
+ const sensors = useSensors(
81
+ useSensor(PointerSensor, {
82
+ activationConstraint: { distance: 8 },
83
+ })
84
+ );
85
+
86
+ // Group tasks efficiently
87
+ const { backlogTasks, activeSprint, plannedSprints, completedSprints } =
88
+ useMemo(() => {
89
+ return {
90
+ backlogTasks: tasks.filter((t) => !t.sprintId),
91
+ activeSprint: sprints.find((s) => s.status === SprintStatus.ACTIVE),
92
+ plannedSprints: sprints.filter(
93
+ (s) => s.status === SprintStatus.PLANNED
94
+ ),
95
+ completedSprints: sprints.filter(
96
+ (s) => s.status === SprintStatus.COMPLETED
97
+ ),
98
+ };
99
+ }, [tasks, sprints]);
100
+
101
+ const getSprintTasks = (sprintId: string) =>
102
+ tasks.filter((t) => t.sprintId === sprintId);
103
+
104
+ const toggleSection = (section: string) => {
105
+ const newExpanded = new Set(expandedSections);
106
+ if (newExpanded.has(section)) {
107
+ newExpanded.delete(section);
108
+ } else {
109
+ newExpanded.add(section);
110
+ }
111
+ setExpandedSections(newExpanded);
112
+ };
113
+
114
+ // Drag handlers
115
+ const handleDragStart = (event: DragStartEvent) => {
116
+ const task = tasks.find((t) => t.id === event.active.id);
117
+ setActiveTask(task || null);
118
+ };
119
+
120
+ const handleDragEnd = async (event: DragEndEvent) => {
121
+ const { active, over } = event;
122
+ setActiveTask(null);
123
+
124
+ if (!over) return;
125
+
126
+ const taskId = active.id as string;
127
+ const overId = over.id as string;
128
+
129
+ // Find the actual target container (sprint ID or "backlog")
130
+ let targetContainerId = overId;
131
+ const overTask = tasks.find((t) => t.id === overId);
132
+ if (overTask) {
133
+ targetContainerId = overTask.sprintId
134
+ ? `sprint-${overTask.sprintId}`
135
+ : "backlog";
136
+ }
137
+
138
+ let newSprintId: string | null = null;
139
+ if (targetContainerId === "backlog") {
140
+ newSprintId = null;
141
+ } else if (targetContainerId.startsWith("sprint-")) {
142
+ newSprintId = targetContainerId.replace("sprint-", "");
143
+ } else {
144
+ return;
145
+ }
146
+
147
+ const task = tasks.find((t) => t.id === taskId);
148
+ if (!task || task.sprintId === newSprintId) return;
149
+
150
+ // Optimistic update
151
+ const queryKey = queryKeys.tasks.list(workspaceId);
152
+ await queryClient.cancelQueries({ queryKey });
153
+
154
+ const previousTasks = queryClient.getQueryData<Task[]>(queryKey);
155
+
156
+ if (previousTasks) {
157
+ queryClient.setQueryData<Task[]>(
158
+ queryKey,
159
+ previousTasks.map((t) =>
160
+ t.id === taskId ? { ...t, sprintId: newSprintId } : t
161
+ )
162
+ );
163
+ }
164
+
165
+ try {
166
+ await locusClient.tasks.update(taskId, workspaceId, {
167
+ sprintId: newSprintId,
168
+ });
169
+ // Invalidate to ensure sync
170
+ queryClient.invalidateQueries({ queryKey });
171
+ } catch (error) {
172
+ // Rollback
173
+ if (previousTasks) {
174
+ queryClient.setQueryData(queryKey, previousTasks);
175
+ }
176
+ toast.error(
177
+ error instanceof Error ? error.message : "Failed to move task"
178
+ );
179
+ }
180
+ };
181
+
182
+ // Sprint actions
183
+ const handleCreateSprint = async (name: string) => {
184
+ try {
185
+ setIsSubmitting(true);
186
+ await locusClient.sprints.create(workspaceId, { name });
187
+ toast.success("Sprint created");
188
+ setIsSprintModalOpen(false);
189
+ refetchSprints();
190
+ queryClient.invalidateQueries({
191
+ queryKey: queryKeys.sprints.list(workspaceId),
192
+ });
193
+ } catch (error) {
194
+ toast.error(
195
+ error instanceof Error ? error.message : "Failed to create sprint"
196
+ );
197
+ } finally {
198
+ setIsSubmitting(false);
199
+ }
200
+ };
201
+
202
+ const handleStartSprint = async (sprintId: string) => {
203
+ try {
204
+ setIsSubmitting(true);
205
+ await locusClient.sprints.start(sprintId, workspaceId);
206
+ toast.success("Sprint started");
207
+ refetchSprints();
208
+ queryClient.invalidateQueries({
209
+ queryKey: queryKeys.sprints.list(workspaceId),
210
+ });
211
+ } catch (error) {
212
+ toast.error(
213
+ error instanceof Error ? error.message : "Failed to start sprint"
214
+ );
215
+ } finally {
216
+ setIsSubmitting(false);
217
+ }
218
+ };
219
+
220
+ const handleCompleteSprint = async (sprintId: string) => {
221
+ try {
222
+ setIsSubmitting(true);
223
+
224
+ // Optimistic update
225
+ const sprintKey = queryKeys.sprints.list(workspaceId);
226
+ const previousSprints = queryClient.getQueryData<Sprint[]>(sprintKey);
227
+
228
+ if (previousSprints) {
229
+ queryClient.setQueryData<Sprint[]>(
230
+ sprintKey,
231
+ previousSprints.map((s) =>
232
+ s.id === sprintId
233
+ ? {
234
+ ...s,
235
+ status: SprintStatus.COMPLETED,
236
+ endDate: Date.now(),
237
+ }
238
+ : s
239
+ )
240
+ );
241
+ }
242
+
243
+ await locusClient.sprints.complete(sprintId, workspaceId);
244
+ toast.success("Sprint completed");
245
+
246
+ // Full sync
247
+ queryClient.invalidateQueries({ queryKey: sprintKey });
248
+ queryClient.invalidateQueries({
249
+ queryKey: queryKeys.tasks.list(workspaceId),
250
+ });
251
+ } catch (error) {
252
+ toast.error(
253
+ error instanceof Error ? error.message : "Failed to complete sprint"
254
+ );
255
+ } finally {
256
+ setIsSubmitting(false);
257
+ }
258
+ };
259
+
260
+ const handleDeleteTask = async (taskId: string) => {
261
+ try {
262
+ await locusClient.tasks.delete(taskId, workspaceId);
263
+ toast.success("Task deleted");
264
+ refetchTasks();
265
+ } catch (error) {
266
+ toast.error(
267
+ error instanceof Error ? error.message : "Failed to delete task"
268
+ );
269
+ }
270
+ };
271
+
272
+ const isLoading = tasksLoading || sprintsLoading;
273
+
274
+ return {
275
+ tasks,
276
+ sprints,
277
+ backlogTasks,
278
+ activeSprint,
279
+ plannedSprints,
280
+ completedSprints,
281
+ getSprintTasks,
282
+ isLoading,
283
+ isTaskModalOpen,
284
+ setIsTaskModalOpen,
285
+ isSprintModalOpen,
286
+ setIsSprintModalOpen,
287
+ selectedTaskId,
288
+ setSelectedTaskId,
289
+ activeTask,
290
+ expandedSections,
291
+ isSubmitting,
292
+ sensors,
293
+ toggleSection,
294
+ handleDragStart,
295
+ handleDragEnd,
296
+ handleCreateSprint,
297
+ handleStartSprint,
298
+ handleCompleteSprint,
299
+ handleDeleteTask,
300
+ refetchTasks,
301
+ refetchSprints,
302
+ };
303
+ }
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ import {
4
+ type DragEndEvent,
5
+ type DragStartEvent,
6
+ PointerSensor,
7
+ useSensor,
8
+ useSensors,
9
+ } from "@dnd-kit/core";
10
+ import { SprintStatus, type Task, TaskStatus } from "@locusai/shared";
11
+ import { useQueryClient } from "@tanstack/react-query";
12
+ import { useRouter, useSearchParams } from "next/navigation";
13
+ import { useEffect, useMemo, useState } from "react";
14
+ import { toast } from "sonner";
15
+ import { BOARD_STATUSES } from "@/components/board/constants";
16
+ import { useSprintsQuery, useTasksQuery } from "@/hooks";
17
+ import { useWorkspaceId } from "@/hooks/useWorkspaceId";
18
+ import { locusClient } from "@/lib/api-client";
19
+ import { queryKeys } from "@/lib/query-keys";
20
+
21
+ export function useBoard() {
22
+ const workspaceId = useWorkspaceId();
23
+ const queryClient = useQueryClient();
24
+ const router = useRouter();
25
+ const searchParams = useSearchParams();
26
+
27
+ const {
28
+ data: tasks = [],
29
+ isLoading: tasksLoading,
30
+ refetch,
31
+ } = useTasksQuery();
32
+ const { data: sprints = [], isLoading: sprintsLoading } = useSprintsQuery();
33
+
34
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
35
+ const [selectedTaskId, setSelectedTaskIdState] = useState<string | null>(
36
+ null
37
+ );
38
+
39
+ // Sync URL query param with state on mount
40
+ useEffect(() => {
41
+ const taskIdFromUrl = searchParams.get("taskId");
42
+ if (taskIdFromUrl) {
43
+ setSelectedTaskIdState(taskIdFromUrl);
44
+ }
45
+ }, [searchParams]);
46
+
47
+ const setSelectedTaskId = (id: string | null) => {
48
+ setSelectedTaskIdState(id);
49
+ if (id) {
50
+ router.push(`/board?taskId=${id}`, { scroll: false });
51
+ } else {
52
+ router.push("/board", { scroll: false });
53
+ }
54
+ };
55
+ const [activeTask, setActiveTask] = useState<Task | null>(null);
56
+ const [searchQuery, setSearchQuery] = useState("");
57
+ const [priorityFilter, setPriorityFilter] = useState<string | null>(null);
58
+ const [roleFilter, setRoleFilter] = useState<string | null>(null);
59
+ const [view, setView] = useState<"board" | "mindmap">("board");
60
+
61
+ const sensors = useSensors(
62
+ useSensor(PointerSensor, {
63
+ activationConstraint: { distance: 8 },
64
+ })
65
+ );
66
+
67
+ // Get active sprint
68
+ const activeSprint = useMemo(
69
+ () => sprints.find((s) => s.status === SprintStatus.ACTIVE),
70
+ [sprints]
71
+ );
72
+
73
+ // Filter tasks
74
+ const filteredTasks = useMemo(() => {
75
+ return tasks.filter((task) => {
76
+ // Sprint filter (Always show active sprint only if it exists)
77
+ if (activeSprint) {
78
+ if (task.sprintId !== activeSprint.id) return false;
79
+ } else {
80
+ // If no active sprint, show nothing on board (it will hit empty state)
81
+ return false;
82
+ }
83
+ // Search filter
84
+ if (
85
+ searchQuery &&
86
+ !task.title.toLowerCase().includes(searchQuery.toLowerCase())
87
+ ) {
88
+ return false;
89
+ }
90
+ // Priority filter
91
+ if (priorityFilter && task.priority !== priorityFilter) {
92
+ return false;
93
+ }
94
+ // Role filter
95
+ if (roleFilter && task.assigneeRole !== roleFilter) {
96
+ return false;
97
+ }
98
+ return true;
99
+ });
100
+ }, [tasks, activeSprint, searchQuery, priorityFilter, roleFilter]);
101
+
102
+ // Group by status
103
+ const tasksByStatus = useMemo(() => {
104
+ return BOARD_STATUSES.reduce(
105
+ (acc, status) => {
106
+ acc[status.key] = filteredTasks.filter((t) => t.status === status.key);
107
+ return acc;
108
+ },
109
+ {} as Record<TaskStatus, Task[]>
110
+ );
111
+ }, [filteredTasks]);
112
+
113
+ const handleDragStart = (event: DragStartEvent) => {
114
+ const task = tasks.find((t) => t.id === event.active.id);
115
+ setActiveTask(task || null);
116
+ };
117
+
118
+ const handleDragEnd = async (event: DragEndEvent) => {
119
+ const { active, over } = event;
120
+ setActiveTask(null);
121
+
122
+ if (!over) return;
123
+
124
+ const taskId = active.id as string;
125
+ const overId = over.id as string;
126
+
127
+ // Resolve target status
128
+ const overTask = tasks.find((t) => t.id === overId);
129
+ const newStatus = overTask ? overTask.status : (overId as TaskStatus);
130
+
131
+ const task = tasks.find((t) => t.id === taskId);
132
+ if (!task || task.status === newStatus) return;
133
+
134
+ // Optimistic update
135
+ const previousTasks = queryClient.getQueryData<Task[]>(
136
+ queryKeys.tasks.list(workspaceId)
137
+ );
138
+
139
+ if (previousTasks) {
140
+ queryClient.setQueryData<Task[]>(
141
+ queryKeys.tasks.list(workspaceId),
142
+ previousTasks.map((t) =>
143
+ t.id === taskId ? { ...t, status: newStatus } : t
144
+ )
145
+ );
146
+ }
147
+
148
+ try {
149
+ await locusClient.tasks.update(taskId, workspaceId, {
150
+ status: newStatus,
151
+ });
152
+ // No need to refetch if optimistic update is successful,
153
+ // but we should eventually to stay in sync.
154
+ // queryClient.invalidateQueries({ queryKey: queryKeys.tasks.list(workspaceId) });
155
+ } catch (error) {
156
+ // Rollback
157
+ if (previousTasks) {
158
+ queryClient.setQueryData(
159
+ queryKeys.tasks.list(workspaceId),
160
+ previousTasks
161
+ );
162
+ }
163
+ toast.error(
164
+ error instanceof Error ? error.message : "Failed to update task"
165
+ );
166
+ }
167
+ };
168
+
169
+ const handleDeleteTask = async (taskId: string) => {
170
+ const previousTasks = queryClient.getQueryData<Task[]>(
171
+ queryKeys.tasks.list(workspaceId)
172
+ );
173
+
174
+ if (previousTasks) {
175
+ queryClient.setQueryData<Task[]>(
176
+ queryKeys.tasks.list(workspaceId),
177
+ previousTasks.filter((t) => t.id !== taskId)
178
+ );
179
+ }
180
+
181
+ try {
182
+ await locusClient.tasks.delete(taskId, workspaceId);
183
+ toast.success("Task deleted");
184
+ queryClient.invalidateQueries({
185
+ queryKey: queryKeys.tasks.list(workspaceId),
186
+ });
187
+ } catch (error) {
188
+ // Rollback
189
+ if (previousTasks) {
190
+ queryClient.setQueryData(
191
+ queryKeys.tasks.list(workspaceId),
192
+ previousTasks
193
+ );
194
+ }
195
+ toast.error(
196
+ error instanceof Error ? error.message : "Failed to delete task"
197
+ );
198
+ }
199
+ };
200
+
201
+ const isLoading = tasksLoading || sprintsLoading;
202
+ const shouldShowEmptyState = !activeSprint;
203
+
204
+ return {
205
+ tasks,
206
+ activeSprint,
207
+ filteredTasks,
208
+ tasksByStatus,
209
+ activeTask,
210
+ isLoading,
211
+ shouldShowEmptyState,
212
+ isCreateModalOpen,
213
+ setIsCreateModalOpen,
214
+ selectedTaskId,
215
+ setSelectedTaskId,
216
+ searchQuery,
217
+ setSearchQuery,
218
+ priorityFilter,
219
+ setPriorityFilter,
220
+ roleFilter,
221
+ setRoleFilter,
222
+ view,
223
+ setView,
224
+ sensors,
225
+ handleDragStart,
226
+ handleDragEnd,
227
+ handleDeleteTask,
228
+ refetch,
229
+ };
230
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Hook for dashboard layout routing and protection logic
3
+ *
4
+ * Handles redirecting unauthenticated users away from dashboard
5
+ * and managing the authenticated dashboard UI state
6
+ */
7
+ "use client";
8
+
9
+ import { useRouter } from "next/navigation";
10
+ import { useEffect } from "react";
11
+ import { useAuth } from "@/context/AuthContext";
12
+ import { isCloudMode } from "@/utils/env.utils";
13
+
14
+ interface UseDashboardLayoutReturn {
15
+ isLoading: boolean;
16
+ isAuthenticated: boolean;
17
+ shouldShowUI: boolean;
18
+ }
19
+
20
+ /**
21
+ * Manages dashboard layout routing and protection
22
+ * Returns flags to determine what to render
23
+ *
24
+ * @returns Dashboard layout state flags
25
+ */
26
+ export function useDashboardLayout(): UseDashboardLayoutReturn {
27
+ const { isAuthenticated, isLoading } = useAuth();
28
+ const router = useRouter();
29
+
30
+ useEffect(() => {
31
+ // Skip redirect if still loading or not in cloud mode
32
+ if (isLoading || !isCloudMode()) {
33
+ return;
34
+ }
35
+
36
+ // Redirect unauthenticated users to login
37
+ if (!isAuthenticated) {
38
+ router.push("/login");
39
+ }
40
+ }, [isLoading, isAuthenticated, router]);
41
+
42
+ return {
43
+ isLoading,
44
+ isAuthenticated,
45
+ // Show UI only if authenticated or still loading
46
+ // Hide UI if unauthenticated (to avoid flash before redirect)
47
+ shouldShowUI: isAuthenticated || isLoading,
48
+ };
49
+ }