@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,84 @@
1
+ /**
2
+ * Hook for login form management
3
+ * Handles OTP flow: email -> OTP verification
4
+ */
5
+ "use client";
6
+
7
+ import { type User } from "@locusai/shared";
8
+ import { useState } from "react";
9
+ import { toast } from "sonner";
10
+ import { useAuth } from "@/context/AuthContext";
11
+ import { locusClient } from "@/lib/api-client";
12
+
13
+ type LoginStep = "email" | "otp";
14
+
15
+ interface UseLoginFormReturn {
16
+ step: LoginStep;
17
+ email: string;
18
+ otp: string;
19
+ loading: boolean;
20
+ setEmail: (email: string) => void;
21
+ setOtp: (otp: string) => void;
22
+ handleSendOtp: (e: React.FormEvent) => Promise<void>;
23
+ handleVerify: (e: React.FormEvent) => Promise<void>;
24
+ goBackToEmail: () => void;
25
+ }
26
+
27
+ /**
28
+ * Custom hook for managing login form state and logic
29
+ * Separates form logic from UI
30
+ */
31
+ export function useLoginForm(): UseLoginFormReturn {
32
+ const { login } = useAuth();
33
+ const [step, setStep] = useState<LoginStep>("email");
34
+ const [email, setEmail] = useState("");
35
+ const [otp, setOtp] = useState("");
36
+ const [loading, setLoading] = useState(false);
37
+
38
+ const handleSendOtp = async (e: React.FormEvent) => {
39
+ e.preventDefault();
40
+ setLoading(true);
41
+ try {
42
+ await locusClient.auth.requestLoginOtp(email);
43
+ setStep("otp");
44
+ toast.success("Verification code sent to your email");
45
+ } catch (error) {
46
+ toast.error(
47
+ error instanceof Error ? error.message : "Failed to send OTP"
48
+ );
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ };
53
+
54
+ const handleVerify = async (e: React.FormEvent) => {
55
+ e.preventDefault();
56
+ setLoading(true);
57
+ try {
58
+ const response = await locusClient.auth.verifyLogin({ email, otp });
59
+ login(response.token, response.user as User);
60
+ toast.success("Welcome back!");
61
+ } catch (error) {
62
+ toast.error(error instanceof Error ? error.message : "Invalid code");
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+
68
+ const goBackToEmail = () => {
69
+ setStep("email");
70
+ setOtp("");
71
+ };
72
+
73
+ return {
74
+ step,
75
+ email,
76
+ otp,
77
+ loading,
78
+ setEmail,
79
+ setOtp,
80
+ handleSendOtp,
81
+ handleVerify,
82
+ goBackToEmail,
83
+ };
84
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import {
4
+ type QueryKey,
5
+ useMutation,
6
+ useQueryClient,
7
+ } from "@tanstack/react-query";
8
+ import { toast } from "sonner";
9
+
10
+ interface UseMutationWithToastConfig<T, DTO> {
11
+ mutationFn: (data: DTO) => Promise<T>;
12
+ successMessage?: string;
13
+ errorMessage?: string;
14
+ onSuccess?: (data: T) => void;
15
+ invalidateKeys?: QueryKey[];
16
+ }
17
+
18
+ /**
19
+ * Enhanced mutation hook with automatic toast notifications and query invalidation.
20
+ *
21
+ * Handles common mutation patterns:
22
+ * - Success/error toast notifications
23
+ * - Automatic query cache invalidation
24
+ * - Consistent error handling
25
+ *
26
+ * @example
27
+ * const mutation = useMutationWithToast({
28
+ * mutationFn: (data) => api.create(data),
29
+ * successMessage: "Created successfully",
30
+ * invalidateKeys: [queryKeys.items.all()],
31
+ * onSuccess: () => closeModal(),
32
+ * });
33
+ */
34
+ export function useMutationWithToast<T, DTO = void>({
35
+ mutationFn,
36
+ successMessage = "Success",
37
+ errorMessage,
38
+ onSuccess,
39
+ invalidateKeys = [],
40
+ }: UseMutationWithToastConfig<T, DTO>) {
41
+ const queryClient = useQueryClient();
42
+
43
+ return useMutation({
44
+ mutationFn,
45
+ onSuccess: (data) => {
46
+ toast.success(successMessage);
47
+ invalidateKeys.forEach((key) => {
48
+ queryClient.invalidateQueries({ queryKey: key });
49
+ });
50
+ onSuccess?.(data);
51
+ },
52
+ onError: (error: Error) => {
53
+ toast.error(errorMessage ?? error.message ?? "An error occurred");
54
+ },
55
+ });
56
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useAuth } from "@/context";
5
+ import { locusClient } from "@/lib/api-client";
6
+ import { queryKeys } from "@/lib/query-keys";
7
+
8
+ /**
9
+ * Organization Members Query Hook
10
+ */
11
+ export function useOrganizationMembersQuery() {
12
+ const { user } = useAuth();
13
+ const orgId = user?.orgId;
14
+
15
+ return useQuery({
16
+ queryKey: orgId ? queryKeys.organizations.members(orgId) : [],
17
+ queryFn: () =>
18
+ orgId
19
+ ? locusClient.organizations.listMembers(orgId)
20
+ : Promise.resolve([]),
21
+ enabled: !!orgId,
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Invitations Query Hook
27
+ */
28
+ export function useInvitationsQuery(options?: { enabled?: boolean }) {
29
+ const { user } = useAuth();
30
+ const orgId = user?.orgId;
31
+
32
+ return useQuery({
33
+ queryKey: orgId ? queryKeys.invitations.list(orgId) : [],
34
+ queryFn: () =>
35
+ orgId ? locusClient.invitations.list(orgId) : Promise.resolve([]),
36
+ enabled: !!orgId && (options?.enabled ?? true),
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Organization Detail Query Hook
42
+ */
43
+ export function useOrganizationQuery() {
44
+ const { user } = useAuth();
45
+ const orgId = user?.orgId;
46
+
47
+ return useQuery({
48
+ queryKey: orgId ? queryKeys.organizations.detail(orgId) : [],
49
+ queryFn: () =>
50
+ orgId
51
+ ? locusClient.organizations.getById(orgId)
52
+ : Promise.reject("No orgId"),
53
+ enabled: !!orgId,
54
+ });
55
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Hook for registration form management
3
+ * Handles 6-step registration flow with email, OTP, profile, organization, workspace, and invites
4
+ */
5
+ "use client";
6
+
7
+ import { type User } from "@locusai/shared";
8
+ import { useState } from "react";
9
+ import { toast } from "sonner";
10
+ import { useAuth } from "@/context/AuthContext";
11
+ import { locusClient } from "@/lib/api-client";
12
+ import { deduplicateEmails, validateInvitationEmails } from "@/lib/validation";
13
+
14
+ type RegisterStep =
15
+ | "email"
16
+ | "otp"
17
+ | "profile"
18
+ | "organization"
19
+ | "workspace"
20
+ | "invite";
21
+
22
+ interface UseRegisterFormReturn {
23
+ step: RegisterStep;
24
+ loading: boolean;
25
+ email: string;
26
+ otp: string;
27
+ name: string;
28
+ companyName: string;
29
+ teamSize: string;
30
+ userRole: string;
31
+ workspaceName: string;
32
+ invitedEmails: string[];
33
+ currentInviteEmail: string;
34
+ currentStepIndex: number;
35
+ totalSteps: number;
36
+ setEmail: (email: string) => void;
37
+ setOtp: (otp: string) => void;
38
+ setName: (name: string) => void;
39
+ setCompanyName: (name: string) => void;
40
+ setTeamSize: (size: string) => void;
41
+ setUserRole: (role: string) => void;
42
+ setWorkspaceName: (name: string) => void;
43
+ setCurrentInviteEmail: (email: string) => void;
44
+ goToStep: (step: RegisterStep) => void;
45
+ nextStep: () => void;
46
+ handleSendOtp: (e: React.FormEvent) => Promise<void>;
47
+ handleVerifyOtp: (e: React.FormEvent) => void;
48
+ handleAddInvite: () => void;
49
+ handleRemoveInvite: (email: string) => void;
50
+ handleCompleteRegistration: (e?: React.FormEvent) => Promise<void>;
51
+ }
52
+
53
+ /**
54
+ * Custom hook for managing multi-step registration form
55
+ * Separates form logic from UI components
56
+ */
57
+ export function useRegisterForm(): UseRegisterFormReturn {
58
+ const { login } = useAuth();
59
+ const [step, setStep] = useState<RegisterStep>("email");
60
+ const [loading, setLoading] = useState(false);
61
+
62
+ // Form state
63
+ const [email, setEmail] = useState("");
64
+ const [otp, setOtp] = useState("");
65
+ const [name, setName] = useState("");
66
+ const [companyName, setCompanyName] = useState("");
67
+ const [teamSize, setTeamSize] = useState<string>("");
68
+ const [userRole, setUserRole] = useState<string>("");
69
+ const [workspaceName, setWorkspaceName] = useState("");
70
+ const [invitedEmails, setInvitedEmails] = useState<string[]>([]);
71
+ const [currentInviteEmail, setCurrentInviteEmail] = useState("");
72
+
73
+ const steps: RegisterStep[] = [
74
+ "email",
75
+ "otp",
76
+ "profile",
77
+ "organization",
78
+ "workspace",
79
+ "invite",
80
+ ];
81
+ const currentStepIndex = steps.indexOf(step) + 1;
82
+ const totalSteps = steps.length;
83
+
84
+ const handleSendOtp = async (e: React.FormEvent) => {
85
+ e.preventDefault();
86
+ setLoading(true);
87
+ try {
88
+ await locusClient.auth.requestRegisterOtp(email);
89
+ setStep("otp");
90
+ toast.success("Verification code sent to your email");
91
+ } catch (error) {
92
+ toast.error(
93
+ error instanceof Error ? error.message : "Failed to send OTP"
94
+ );
95
+ } finally {
96
+ setLoading(false);
97
+ }
98
+ };
99
+
100
+ const handleVerifyOtp = (e: React.FormEvent) => {
101
+ e.preventDefault();
102
+ if (otp.length === 6) {
103
+ setStep("profile");
104
+ } else {
105
+ toast.error("Please enter a valid 6-digit code");
106
+ }
107
+ };
108
+
109
+ const handleAddInvite = () => {
110
+ if (!currentInviteEmail.trim()) {
111
+ toast.error("Please enter an email address");
112
+ return;
113
+ }
114
+
115
+ if (currentInviteEmail.includes("@")) {
116
+ const normalized = currentInviteEmail.toLowerCase();
117
+ if (invitedEmails.includes(normalized)) {
118
+ toast.error("Email already added");
119
+ return;
120
+ }
121
+ setInvitedEmails([...invitedEmails, normalized]);
122
+ setCurrentInviteEmail("");
123
+ } else {
124
+ toast.error("Invalid email address");
125
+ }
126
+ };
127
+
128
+ const handleRemoveInvite = (emailToRemove: string) => {
129
+ setInvitedEmails(invitedEmails.filter((e) => e !== emailToRemove));
130
+ };
131
+
132
+ const handleCompleteRegistration = async (e?: React.FormEvent) => {
133
+ if (e) e.preventDefault();
134
+
135
+ // Validate invites
136
+ const { valid: validEmails, invalid: invalidEmails } =
137
+ validateInvitationEmails(invitedEmails);
138
+
139
+ if (invalidEmails.length > 0) {
140
+ toast.error(`Invalid email addresses: ${invalidEmails.join(", ")}`);
141
+ return;
142
+ }
143
+
144
+ // Deduplicate emails
145
+ const deduped = deduplicateEmails(validEmails);
146
+
147
+ setLoading(true);
148
+ try {
149
+ const response = await locusClient.auth.completeRegistration({
150
+ email,
151
+ otp,
152
+ name,
153
+ companyName: companyName || undefined,
154
+ teamSize: teamSize as "solo" | "2-10" | "11-50" | "51-200" | "200+",
155
+ userRole: userRole as
156
+ | "developer"
157
+ | "designer"
158
+ | "product_manager"
159
+ | "other",
160
+ workspaceName: workspaceName || undefined,
161
+ invitedEmails: deduped.length > 0 ? deduped : undefined,
162
+ });
163
+
164
+ login(response.token, response.user as User);
165
+ toast.success("Account created successfully!");
166
+ } catch (error) {
167
+ toast.error(
168
+ error instanceof Error ? error.message : "Registration failed"
169
+ );
170
+ } finally {
171
+ setLoading(false);
172
+ }
173
+ };
174
+
175
+ const goToStep = (newStep: RegisterStep) => {
176
+ setStep(newStep);
177
+ };
178
+
179
+ const nextStep = () => {
180
+ const currentIndex = steps.indexOf(step);
181
+ if (currentIndex < steps.length - 1) {
182
+ setStep(steps[currentIndex + 1]);
183
+ }
184
+ };
185
+
186
+ return {
187
+ step,
188
+ loading,
189
+ email,
190
+ otp,
191
+ name,
192
+ companyName,
193
+ teamSize,
194
+ userRole,
195
+ workspaceName,
196
+ invitedEmails,
197
+ currentInviteEmail,
198
+ currentStepIndex,
199
+ totalSteps,
200
+ setEmail,
201
+ setOtp,
202
+ setName,
203
+ setCompanyName,
204
+ setTeamSize,
205
+ setUserRole,
206
+ setWorkspaceName,
207
+ setCurrentInviteEmail,
208
+ goToStep,
209
+ nextStep,
210
+ handleSendOtp,
211
+ handleVerifyOtp,
212
+ handleAddInvite,
213
+ handleRemoveInvite,
214
+ handleCompleteRegistration,
215
+ };
216
+ }
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { type Sprint } 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
+ * Sprints Query Hook
11
+ * Centralizes sprint fetching logic.
12
+ */
13
+ export function useSprintsQuery() {
14
+ const workspaceId = useWorkspaceIdOptional();
15
+
16
+ return useQuery<Sprint[]>({
17
+ queryKey: queryKeys.sprints.list(workspaceId),
18
+ queryFn: () =>
19
+ workspaceId ? locusClient.sprints.list(workspaceId) : Promise.resolve([]),
20
+ enabled: !!workspaceId,
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Active Sprint Query Hook
26
+ */
27
+ export function useActiveSprintQuery() {
28
+ const workspaceId = useWorkspaceIdOptional();
29
+
30
+ return useQuery<Sprint | null>({
31
+ queryKey: queryKeys.sprints.active(workspaceId),
32
+ queryFn: () =>
33
+ workspaceId
34
+ ? locusClient.sprints.getActive(workspaceId)
35
+ : Promise.resolve(null),
36
+ enabled: !!workspaceId,
37
+ });
38
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Task Description Hook
3
+ *
4
+ * Extracts state management for task description editing.
5
+ * Handles title and description editing with save functionality.
6
+ */
7
+
8
+ "use client";
9
+
10
+ import { type Task } from "@locusai/shared";
11
+ import { useCallback, useState } from "react";
12
+
13
+ interface UseTaskDescriptionProps {
14
+ task: Task;
15
+ onUpdate: (updates: Partial<Task>) => void;
16
+ }
17
+
18
+ interface UseTaskDescriptionReturn {
19
+ // Title state
20
+ isEditingTitle: boolean;
21
+ editTitle: string;
22
+
23
+ // Description state
24
+ editDesc: string;
25
+ descMode: "edit" | "preview";
26
+
27
+ // Handlers
28
+ setIsEditingTitle: (val: boolean) => void;
29
+ setEditTitle: (val: string) => void;
30
+ handleTitleSave: () => void;
31
+ setEditDesc: (val: string) => void;
32
+ handleDescSave: () => void;
33
+ setDescMode: (mode: "edit" | "preview") => void;
34
+
35
+ // Quick actions
36
+ toggleEditMode: () => void;
37
+ cancelEdit: () => void;
38
+ }
39
+
40
+ /**
41
+ * Hook for managing task description editing
42
+ *
43
+ * Handles state for:
44
+ * - Title editing mode and content
45
+ * - Description editing mode and content
46
+ * - Save operations for both
47
+ *
48
+ * @example
49
+ * const {
50
+ * isEditingTitle,
51
+ * editTitle,
52
+ * handleTitleSave,
53
+ * // ... other handlers
54
+ * } = useTaskDescription({ task, onUpdate });
55
+ */
56
+ export function useTaskDescription({
57
+ task,
58
+ onUpdate,
59
+ }: UseTaskDescriptionProps): UseTaskDescriptionReturn {
60
+ const [isEditingTitle, setIsEditingTitle] = useState(false);
61
+ const [editTitle, setEditTitle] = useState(task.title);
62
+ const [editDesc, setEditDesc] = useState(task.description || "");
63
+ const [descMode, setDescMode] = useState<"edit" | "preview">("preview");
64
+
65
+ const handleTitleSave = useCallback(() => {
66
+ if (editTitle !== task.title && editTitle.trim()) {
67
+ onUpdate({ title: editTitle });
68
+ }
69
+ setIsEditingTitle(false);
70
+ }, [editTitle, task.title, onUpdate]);
71
+
72
+ const handleDescSave = useCallback(() => {
73
+ if (editDesc !== task.description) {
74
+ onUpdate({ description: editDesc });
75
+ }
76
+ }, [editDesc, task.description, onUpdate]);
77
+
78
+ const toggleEditMode = useCallback(() => {
79
+ setDescMode(descMode === "edit" ? "preview" : "edit");
80
+ }, [descMode]);
81
+
82
+ const cancelEdit = useCallback(() => {
83
+ setEditTitle(task.title);
84
+ setEditDesc(task.description || "");
85
+ setIsEditingTitle(false);
86
+ }, [task]);
87
+
88
+ return {
89
+ isEditingTitle,
90
+ editTitle,
91
+ editDesc,
92
+ descMode,
93
+ setIsEditingTitle,
94
+ setEditTitle,
95
+ handleTitleSave,
96
+ setEditDesc,
97
+ handleDescSave,
98
+ setDescMode,
99
+ toggleEditMode,
100
+ cancelEdit,
101
+ };
102
+ }