@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,81 @@
1
+ /**
2
+ * API Key Confirmation Modal
3
+ * Shows the newly created API key (only time it's visible)
4
+ */
5
+
6
+ import { Copy } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { Button, Modal } from "@/components/ui";
9
+
10
+ interface ApiKeyConfirmationModalProps {
11
+ isOpen: boolean;
12
+ apiKey: string | null;
13
+ keyName: string;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export function ApiKeyConfirmationModal({
18
+ isOpen,
19
+ apiKey,
20
+ keyName,
21
+ onClose,
22
+ }: ApiKeyConfirmationModalProps) {
23
+ const handleCopy = () => {
24
+ if (!apiKey) return;
25
+ navigator.clipboard.writeText(apiKey);
26
+ toast.success("API key copied to clipboard");
27
+ };
28
+
29
+ return (
30
+ <Modal isOpen={isOpen} onClose={onClose}>
31
+ <div className="space-y-4">
32
+ <div>
33
+ <h2 className="text-xl font-bold text-foreground">API Key Created</h2>
34
+ <p className="text-sm text-muted-foreground mt-1">
35
+ Save your API key now. You won't be able to see it again!
36
+ </p>
37
+ </div>
38
+
39
+ <div className="bg-primary/10 border border-primary/20 rounded-lg p-4 space-y-3">
40
+ <div>
41
+ <p className="text-xs font-medium text-muted-foreground mb-1">
42
+ Key Name
43
+ </p>
44
+ <p className="font-medium">{keyName}</p>
45
+ </div>
46
+
47
+ <div>
48
+ <p className="text-xs font-medium text-muted-foreground mb-1">
49
+ API Key
50
+ </p>
51
+ <div className="flex items-center gap-2">
52
+ <code className="flex-1 bg-secondary/50 px-3 py-2 rounded font-mono text-sm break-all">
53
+ {apiKey}
54
+ </code>
55
+ <Button
56
+ variant="secondary"
57
+ size="sm"
58
+ onClick={handleCopy}
59
+ className="flex-shrink-0"
60
+ >
61
+ <Copy size={16} className="mr-1.5" />
62
+ Copy
63
+ </Button>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3">
69
+ <p className="text-xs text-amber-600 dark:text-amber-400">
70
+ ⚠️ <strong>Important:</strong> Keep this key secure. Never share it
71
+ publicly or commit it to version control.
72
+ </p>
73
+ </div>
74
+
75
+ <div className="flex gap-3 justify-end pt-4">
76
+ <Button onClick={onClose}>Done</Button>
77
+ </div>
78
+ </div>
79
+ </Modal>
80
+ );
81
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Create API Key Modal Component
3
+ * Modal form for creating new API keys
4
+ */
5
+
6
+ import { useState } from "react";
7
+ import { toast } from "sonner";
8
+ import { Button, Input, Modal } from "@/components/ui";
9
+
10
+ interface CreateApiKeyModalProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ onSubmit: (name: string) => Promise<void>;
14
+ isLoading?: boolean;
15
+ }
16
+
17
+ export function CreateApiKeyModal({
18
+ isOpen,
19
+ onClose,
20
+ onSubmit,
21
+ isLoading,
22
+ }: CreateApiKeyModalProps) {
23
+ const [name, setName] = useState("");
24
+ const [isSubmitting, setIsSubmitting] = useState(false);
25
+
26
+ const handleSubmit = async () => {
27
+ if (!name.trim()) {
28
+ toast.error("Please enter a name for the API key");
29
+ return;
30
+ }
31
+
32
+ setIsSubmitting(true);
33
+ try {
34
+ await onSubmit(name.trim());
35
+ setName("");
36
+ onClose();
37
+ } catch (error) {
38
+ toast.error(
39
+ error instanceof Error ? error.message : "Failed to create API key"
40
+ );
41
+ } finally {
42
+ setIsSubmitting(false);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <Modal isOpen={isOpen} onClose={onClose}>
48
+ <div className="space-y-4">
49
+ <div>
50
+ <h2 className="text-xl font-bold text-foreground">Create API Key</h2>
51
+ <p className="text-sm text-muted-foreground mt-1">
52
+ API keys allow external applications (like the CLI) to authenticate
53
+ with your workspace.
54
+ </p>
55
+ </div>
56
+
57
+ <div className="space-y-2">
58
+ <label className="text-sm font-medium text-foreground">
59
+ Key Name
60
+ </label>
61
+ <Input
62
+ placeholder="e.g., Production Agent, CI/CD"
63
+ value={name}
64
+ onChange={(e) => setName(e.target.value)}
65
+ disabled={isSubmitting || isLoading}
66
+ onKeyDown={(e) => {
67
+ if (e.key === "Enter" && name.trim()) {
68
+ handleSubmit();
69
+ }
70
+ }}
71
+ />
72
+ <p className="text-xs text-muted-foreground">
73
+ Give this key a meaningful name to remember its purpose.
74
+ </p>
75
+ </div>
76
+
77
+ <div className="flex gap-3 justify-end pt-4">
78
+ <Button
79
+ variant="secondary"
80
+ onClick={onClose}
81
+ disabled={isSubmitting || isLoading}
82
+ >
83
+ Cancel
84
+ </Button>
85
+ <Button
86
+ onClick={handleSubmit}
87
+ disabled={!name.trim() || isSubmitting || isLoading}
88
+ isLoading={isSubmitting || isLoading}
89
+ >
90
+ Create Key
91
+ </Button>
92
+ </div>
93
+ </div>
94
+ </Modal>
95
+ );
96
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * API Keys List Component
3
+ * Displays organization API keys with copy, view, and delete actions
4
+ */
5
+
6
+ import { Copy, Trash2 } from "lucide-react";
7
+ import { useState } from "react";
8
+ import { toast } from "sonner";
9
+ import { Badge, Button } from "@/components/ui";
10
+
11
+ export interface ApiKey {
12
+ id: string;
13
+ name: string;
14
+ key: string;
15
+ createdAt: Date | string;
16
+ lastUsedAt: Date | string | null;
17
+ active: boolean;
18
+ }
19
+
20
+ interface ApiKeysListProps {
21
+ apiKeys: ApiKey[];
22
+ isLoading: boolean;
23
+ onDelete: (id: string) => Promise<void>;
24
+ }
25
+
26
+ function formatDate(date: Date | string | null) {
27
+ if (!date) return "Never";
28
+ const d = new Date(date);
29
+ return d.toLocaleDateString("en-US", {
30
+ month: "short",
31
+ day: "numeric",
32
+ year: "numeric",
33
+ });
34
+ }
35
+
36
+ function maskApiKey(key: string): string {
37
+ if (!key) return "";
38
+ if (key.length <= 8) return key;
39
+ return `${key.slice(0, 4)}••••${key.slice(-4)}`;
40
+ }
41
+
42
+ export function ApiKeysList({
43
+ apiKeys,
44
+ isLoading,
45
+ onDelete,
46
+ }: ApiKeysListProps) {
47
+ const [deletingId, setDeletingId] = useState<string | null>(null);
48
+
49
+ const copyToClipboard = (key: string) => {
50
+ navigator.clipboard.writeText(key);
51
+ toast.success("API key copied to clipboard");
52
+ };
53
+
54
+ const handleDelete = async (id: string) => {
55
+ setDeletingId(id);
56
+ try {
57
+ await onDelete(id);
58
+ toast.success("API key deleted");
59
+ } catch (error) {
60
+ toast.error(
61
+ error instanceof Error ? error.message : "Failed to delete API key"
62
+ );
63
+ } finally {
64
+ setDeletingId(null);
65
+ }
66
+ };
67
+
68
+ if (isLoading) {
69
+ return (
70
+ <div className="p-8 text-center text-muted-foreground">Loading...</div>
71
+ );
72
+ }
73
+
74
+ if (apiKeys.length === 0) {
75
+ return (
76
+ <div className="p-8 text-center text-muted-foreground">
77
+ No API keys created yet. Create one to get started.
78
+ </div>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div className="divide-y divide-border/50">
84
+ {apiKeys.map((apiKey) => {
85
+ return (
86
+ <div
87
+ key={apiKey.id}
88
+ className="flex items-center justify-between p-4 hover:bg-secondary/10 transition-colors"
89
+ >
90
+ <div className="flex-1 min-w-0">
91
+ <div className="flex items-center gap-3">
92
+ <span className="font-medium truncate">{apiKey.name}</span>
93
+ {apiKey.active ? (
94
+ <Badge variant="success" size="sm" className="shrink-0">
95
+ Active
96
+ </Badge>
97
+ ) : (
98
+ <Badge variant="secondary" size="sm" className="shrink-0">
99
+ Inactive
100
+ </Badge>
101
+ )}
102
+ </div>
103
+
104
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-2 mt-2">
105
+ <div className="flex items-center gap-2 shrink-0">
106
+ <code className="text-xs bg-secondary/50 px-2 py-1 rounded font-mono whitespace-nowrap">
107
+ {maskApiKey(apiKey.key)}
108
+ </code>
109
+
110
+ <Button
111
+ variant="ghost"
112
+ size="icon"
113
+ className="h-6 w-6 shrink-0"
114
+ onClick={() => copyToClipboard(apiKey.key)}
115
+ title="Copy"
116
+ >
117
+ <Copy size={14} />
118
+ </Button>
119
+ </div>
120
+
121
+ <div className="flex items-center gap-4 text-xs text-muted-foreground whitespace-nowrap">
122
+ <span>Created {formatDate(apiKey.createdAt)}</span>
123
+ <span>Last used {formatDate(apiKey.lastUsedAt)}</span>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <Button
129
+ variant="ghost"
130
+ size="icon"
131
+ onClick={() => handleDelete(apiKey.id)}
132
+ disabled={deletingId === apiKey.id}
133
+ className="text-muted-foreground hover:text-red-500 hover:bg-red-500/10"
134
+ title="Delete"
135
+ >
136
+ <Trash2 size={18} />
137
+ </Button>
138
+ </div>
139
+ );
140
+ })}
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * API Keys Settings Section
3
+ * Container for API keys management UI with hooks integration
4
+ */
5
+
6
+ "use client";
7
+
8
+ import { Key } from "lucide-react";
9
+ import { useCallback, useEffect, useState } from "react";
10
+ import { toast } from "sonner";
11
+ import { Button } from "@/components/ui";
12
+ import { useAuthenticatedUserWithOrg, useWorkspaceIdOptional } from "@/hooks";
13
+ import { locusClient } from "@/lib/api-client";
14
+ import { ApiKeyConfirmationModal } from "./ApiKeyConfirmationModal";
15
+ import { CreateApiKeyModal } from "./ApiKeyCreatedModal";
16
+ import { ApiKeysList } from "./ApiKeysList";
17
+ import { ProjectSetupGuide } from "./ProjectSetupGuide";
18
+ import { SettingSection } from "./SettingSection";
19
+
20
+ interface ApiKey {
21
+ id: string;
22
+ name: string;
23
+ key: string;
24
+ createdAt: Date | string;
25
+ lastUsedAt: Date | string | null;
26
+ active: boolean;
27
+ }
28
+
29
+ export function ApiKeysSettings() {
30
+ const { orgId } = useAuthenticatedUserWithOrg();
31
+ const workspaceId = useWorkspaceIdOptional();
32
+ const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
33
+ const [isLoading, setIsLoading] = useState(true);
34
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
35
+ const [newApiKey, setNewApiKey] = useState<string | null>(null);
36
+ const [newKeyName, setNewKeyName] = useState("");
37
+ const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
38
+
39
+ // Fetch API keys
40
+ const fetchApiKeys = useCallback(async () => {
41
+ if (!orgId) return;
42
+
43
+ setIsLoading(true);
44
+ try {
45
+ const keys = await locusClient.organizations.listApiKeys(orgId);
46
+ setApiKeys(keys);
47
+ } catch (error) {
48
+ toast.error(
49
+ error instanceof Error ? error.message : "Failed to fetch API keys"
50
+ );
51
+ } finally {
52
+ setIsLoading(false);
53
+ }
54
+ }, [orgId]);
55
+
56
+ useEffect(() => {
57
+ fetchApiKeys();
58
+ }, [fetchApiKeys]);
59
+
60
+ const handleCreateApiKey = async (name: string) => {
61
+ if (!orgId) return;
62
+
63
+ try {
64
+ const response = await locusClient.organizations.createApiKey(
65
+ orgId,
66
+ name
67
+ );
68
+ setNewApiKey(response.key);
69
+ setNewKeyName(name);
70
+ setIsConfirmationModalOpen(true);
71
+ setIsCreateModalOpen(false);
72
+
73
+ // Add to list
74
+ setApiKeys([...apiKeys, response]);
75
+ } catch (error) {
76
+ toast.error(
77
+ error instanceof Error ? error.message : "Failed to create API key"
78
+ );
79
+ }
80
+ };
81
+
82
+ const handleDeleteApiKey = async (id: string) => {
83
+ if (!orgId) return;
84
+
85
+ try {
86
+ await locusClient.organizations.deleteApiKey(orgId, id);
87
+ setApiKeys(apiKeys.filter((key) => key.id !== id));
88
+ toast.success("API key deleted");
89
+ } catch (error) {
90
+ toast.error(
91
+ error instanceof Error ? error.message : "Failed to delete API key"
92
+ );
93
+ }
94
+ };
95
+
96
+ return (
97
+ <>
98
+ <SettingSection title="API Keys" className="mb-8">
99
+ <div className="flex items-center justify-between p-4 border-b border-border/50">
100
+ <div className="flex items-center gap-3">
101
+ <Key size={18} className="text-muted-foreground" />
102
+ <div>
103
+ <p className="font-medium text-sm">Manage API Keys</p>
104
+ <p className="text-xs text-muted-foreground mt-0.5">
105
+ Use API keys for CLI authentication and external integrations
106
+ </p>
107
+ </div>
108
+ </div>
109
+ <Button size="sm" onClick={() => setIsCreateModalOpen(true)}>
110
+ New Key
111
+ </Button>
112
+ </div>
113
+
114
+ <ApiKeysList
115
+ apiKeys={apiKeys}
116
+ isLoading={isLoading}
117
+ onDelete={handleDeleteApiKey}
118
+ />
119
+ </SettingSection>
120
+
121
+ <ProjectSetupGuide
122
+ hasApiKeys={apiKeys.length > 0}
123
+ workspaceId={workspaceId || undefined}
124
+ />
125
+
126
+ <CreateApiKeyModal
127
+ isOpen={isCreateModalOpen}
128
+ onClose={() => setIsCreateModalOpen(false)}
129
+ onSubmit={handleCreateApiKey}
130
+ />
131
+
132
+ <ApiKeyConfirmationModal
133
+ isOpen={isConfirmationModalOpen}
134
+ apiKey={newApiKey}
135
+ keyName={newKeyName}
136
+ onClose={() => {
137
+ setIsConfirmationModalOpen(false);
138
+ setNewApiKey(null);
139
+ setNewKeyName("");
140
+ }}
141
+ />
142
+ </>
143
+ );
144
+ }
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import { MembershipRole } from "@locusai/shared";
4
+ import { useState } from "react";
5
+ import { CreateModal } from "@/components/CreateModal";
6
+ import { Input } from "@/components/ui";
7
+ import { useAuthenticatedUserWithOrg } from "@/hooks";
8
+ import { useMutationWithToast } from "@/hooks/useMutationWithToast";
9
+ import { locusClient } from "@/lib/api-client";
10
+ import { queryKeys } from "@/lib/query-keys";
11
+ import { cn } from "@/lib/utils";
12
+
13
+ interface InviteMemberModalProps {
14
+ isOpen: boolean;
15
+ onClose: () => void;
16
+ }
17
+
18
+ export function InviteMemberModal({ isOpen, onClose }: InviteMemberModalProps) {
19
+ const [email, setEmail] = useState("");
20
+ const [role, setRole] = useState<MembershipRole>(MembershipRole.MEMBER);
21
+ const user = useAuthenticatedUserWithOrg();
22
+
23
+ const sendInvitation = useMutationWithToast({
24
+ mutationFn: (data: { email: string; role: MembershipRole }) =>
25
+ locusClient.invitations.create(user.orgId, {
26
+ email: data.email,
27
+ role: data.role,
28
+ orgId: user.orgId,
29
+ }),
30
+ successMessage: `Invitation sent to ${email}`,
31
+ invalidateKeys: [queryKeys.invitations.list(user.orgId)],
32
+ onSuccess: () => {
33
+ setEmail("");
34
+ setRole(MembershipRole.MEMBER);
35
+ onClose();
36
+ },
37
+ });
38
+
39
+ const handleSubmit = async (e: React.FormEvent) => {
40
+ e.preventDefault();
41
+ if (!user.orgId || !email) return;
42
+ sendInvitation.mutate({ email, role });
43
+ };
44
+
45
+ const roleDescription =
46
+ role === MembershipRole.ADMIN
47
+ ? "Admins can manage tasks, sprints, and other members."
48
+ : "Members can view and manage tasks and sprints.";
49
+
50
+ return (
51
+ <CreateModal
52
+ isOpen={isOpen}
53
+ title="Invite Team Member"
54
+ size="sm"
55
+ fields={[
56
+ {
57
+ name: "email",
58
+ label: "Email Address",
59
+ component: (
60
+ <Input
61
+ type="email"
62
+ placeholder="colleague@example.com"
63
+ value={email}
64
+ onChange={(e) => setEmail(e.target.value)}
65
+ autoFocus
66
+ />
67
+ ),
68
+ required: true,
69
+ },
70
+ {
71
+ name: "role",
72
+ label: "Role",
73
+ component: (
74
+ <div className="space-y-2">
75
+ <div className="grid grid-cols-2 gap-2">
76
+ {[MembershipRole.MEMBER, MembershipRole.ADMIN].map((r) => (
77
+ <button
78
+ key={r}
79
+ type="button"
80
+ onClick={() => setRole(r)}
81
+ className={cn(
82
+ "px-4 py-2 rounded-xl border text-sm transition-all",
83
+ role === r
84
+ ? "bg-primary/10 border-primary text-primary"
85
+ : "border-secondary hover:bg-secondary/50"
86
+ )}
87
+ >
88
+ {r.charAt(0) + r.slice(1).toLowerCase()}
89
+ </button>
90
+ ))}
91
+ </div>
92
+ <p className="text-xs text-muted-foreground mt-1 px-1">
93
+ {roleDescription}
94
+ </p>
95
+ </div>
96
+ ),
97
+ },
98
+ ]}
99
+ onSubmit={handleSubmit}
100
+ onClose={onClose}
101
+ submitText="Send Invitation"
102
+ isPending={sendInvitation.isPending}
103
+ submitDisabled={!email.trim() || !user.orgId}
104
+ />
105
+ );
106
+ }