@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
@@ -1,691 +0,0 @@
1
- "use client";
2
-
3
- import { type Sprint, SprintStatus, type Task } from "@locusai/shared";
4
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
- import { format } from "date-fns";
6
- import { AnimatePresence, motion } from "framer-motion";
7
- import {
8
- Archive,
9
- CheckCircle,
10
- ChevronRight,
11
- GripVertical,
12
- Layers,
13
- Play,
14
- Plus,
15
- Sparkles,
16
- Target,
17
- Trash2,
18
- Zap,
19
- } from "lucide-react";
20
- import { useState } from "react";
21
- import { TaskCreateModal } from "@/components/TaskCreateModal";
22
- import { TaskPanel } from "@/components/TaskPanel";
23
- import { Button, Input, PriorityBadge, StatusBadge } from "@/components/ui";
24
- import { cn } from "@/lib/utils";
25
- import { sprintService } from "@/services/sprint.service";
26
- import { taskService } from "@/services/task.service";
27
-
28
- export function Backlog() {
29
- const queryClient = useQueryClient();
30
- const [selectedTask, setSelectedTask] = useState<number | null>(null);
31
- const [isCreatingSprint, setIsCreatingSprint] = useState(false);
32
- const [newSprintName, setNewSprintName] = useState("");
33
- const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false);
34
- const [showCompletedSprints, setShowCompletedSprints] = useState(false);
35
- const [expandedSprints, setExpandedSprints] = useState<Set<number>>(
36
- new Set()
37
- );
38
- const [backlogExpanded, setBacklogExpanded] = useState(true);
39
-
40
- const { data: sprints = [] } = useQuery({
41
- queryKey: ["sprints"],
42
- queryFn: sprintService.getAll,
43
- });
44
-
45
- const { data: tasks = [] } = useQuery({
46
- queryKey: ["tasks"],
47
- queryFn: taskService.getAll,
48
- });
49
-
50
- const createSprint = useMutation({
51
- mutationFn: sprintService.create,
52
- onSuccess: () => {
53
- queryClient.invalidateQueries({ queryKey: ["sprints"] });
54
- setIsCreatingSprint(false);
55
- setNewSprintName("");
56
- },
57
- });
58
-
59
- const updateTask = useMutation({
60
- mutationFn: ({ id, updates }: { id: number; updates: Partial<Task> }) =>
61
- taskService.update(id, updates),
62
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["tasks"] }),
63
- });
64
-
65
- const updateSprint = useMutation({
66
- mutationFn: ({ id, updates }: { id: number; updates: Partial<Sprint> }) =>
67
- sprintService.update(id, updates),
68
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["sprints"] }),
69
- });
70
-
71
- const deleteSprint = useMutation({
72
- mutationFn: (id: number) => sprintService.delete(id),
73
- onSuccess: () => {
74
- queryClient.invalidateQueries({ queryKey: ["sprints"] });
75
- queryClient.invalidateQueries({ queryKey: ["tasks"] });
76
- },
77
- });
78
-
79
- const handleCreateSprint = () => {
80
- if (!newSprintName.trim()) return;
81
- createSprint.mutate({ name: newSprintName });
82
- };
83
-
84
- const handleStartSprint = (sprint: Sprint) => {
85
- updateSprint.mutate({
86
- id: sprint.id,
87
- updates: {
88
- status: SprintStatus.ACTIVE,
89
- startDate: Date.now(),
90
- },
91
- });
92
- };
93
-
94
- const handleCompleteSprint = (sprint: Sprint) => {
95
- updateSprint.mutate({
96
- id: sprint.id,
97
- updates: {
98
- status: SprintStatus.COMPLETED,
99
- endDate: Date.now(),
100
- },
101
- });
102
- };
103
-
104
- const toggleSprintExpand = (sprintId: number) => {
105
- setExpandedSprints((prev) => {
106
- const next = new Set(prev);
107
- if (next.has(sprintId)) next.delete(sprintId);
108
- else next.add(sprintId);
109
- return next;
110
- });
111
- };
112
-
113
- const backlogTasks = tasks.filter((t: Task) => !t.sprintId);
114
- const activeSprints = sprints.filter(
115
- (s: Sprint) => s.status === SprintStatus.ACTIVE
116
- );
117
- const plannedSprints = sprints.filter(
118
- (s: Sprint) => s.status === SprintStatus.PLANNED
119
- );
120
- const completedSprints = sprints.filter(
121
- (s: Sprint) => s.status === SprintStatus.COMPLETED
122
- );
123
-
124
- const handleDeleteSprint = (sprint: Sprint) => {
125
- if (
126
- confirm(
127
- `Delete "${sprint.name}" and all its tasks? This cannot be undone.`
128
- )
129
- ) {
130
- deleteSprint.mutate(sprint.id);
131
- }
132
- };
133
-
134
- const getTasksForSprint = (sprintId: number) =>
135
- tasks.filter((t: Task) => t.sprintId === sprintId);
136
-
137
- return (
138
- <div className="h-full flex flex-col bg-linear-to-br from-background via-background to-secondary/5">
139
- {/* Header */}
140
- <header className="px-8 py-6 border-b border-border/30 bg-background/80 backdrop-blur-xl sticky top-0 z-20">
141
- <div className="flex items-center justify-between">
142
- <div className="flex items-center gap-4">
143
- <div className="p-2.5 rounded-xl bg-linear-to-br from-primary/20 to-primary/5 border border-primary/20">
144
- <Layers className="w-5 h-5 text-primary" />
145
- </div>
146
- <div>
147
- <h1 className="text-xl font-bold text-foreground">
148
- Product Backlog
149
- </h1>
150
- <p className="text-sm text-muted-foreground">
151
- {tasks.length} items · {sprints.length} sprints
152
- </p>
153
- </div>
154
- </div>
155
-
156
- <div className="flex items-center gap-3">
157
- <Button
158
- variant="secondary"
159
- onClick={() => setIsCreatingSprint(true)}
160
- disabled={isCreatingSprint}
161
- className="gap-2"
162
- >
163
- <Target size={16} />
164
- New Sprint
165
- </Button>
166
- <Button
167
- onClick={() => setIsCreateTaskOpen(true)}
168
- className="gap-2 shadow-lg shadow-primary/20"
169
- >
170
- <Plus size={16} />
171
- Create Issue
172
- </Button>
173
- </div>
174
- </div>
175
-
176
- {/* Sprint Creation Inline */}
177
- <AnimatePresence>
178
- {isCreatingSprint && (
179
- <motion.div
180
- initial={{ height: 0, opacity: 0 }}
181
- animate={{ height: "auto", opacity: 1 }}
182
- exit={{ height: 0, opacity: 0 }}
183
- className="overflow-hidden"
184
- >
185
- <div className="flex gap-3 mt-4 pt-4 border-t border-border/30 max-w-lg">
186
- <Input
187
- value={newSprintName}
188
- onChange={(e) => setNewSprintName(e.target.value)}
189
- placeholder="Sprint name (e.g. Sprint 24)"
190
- autoFocus
191
- onKeyDown={(e) => e.key === "Enter" && handleCreateSprint()}
192
- className="flex-1"
193
- />
194
- <Button
195
- onClick={handleCreateSprint}
196
- disabled={!newSprintName.trim()}
197
- size="sm"
198
- >
199
- Create
200
- </Button>
201
- <Button
202
- variant="ghost"
203
- size="sm"
204
- onClick={() => {
205
- setIsCreatingSprint(false);
206
- setNewSprintName("");
207
- }}
208
- >
209
- Cancel
210
- </Button>
211
- </div>
212
- </motion.div>
213
- )}
214
- </AnimatePresence>
215
- </header>
216
-
217
- {/* Content */}
218
- <div className="flex-1 overflow-y-auto">
219
- <div className="max-w-5xl mx-auto px-8 py-6 space-y-4">
220
- {/* Active Sprints */}
221
- {activeSprints.map((sprint) => (
222
- <SprintSection
223
- key={sprint.id}
224
- sprint={sprint}
225
- tasks={getTasksForSprint(sprint.id)}
226
- isExpanded={expandedSprints.has(sprint.id)}
227
- onToggle={() => toggleSprintExpand(sprint.id)}
228
- onTaskClick={setSelectedTask}
229
- onMoveTask={(taskId, targetSprintId) =>
230
- updateTask.mutate({
231
- id: taskId,
232
- updates: { sprintId: targetSprintId },
233
- })
234
- }
235
- availableSprints={sprints}
236
- variant="active"
237
- onAction={() => handleCompleteSprint(sprint)}
238
- actionLabel="Complete"
239
- actionIcon={<CheckCircle size={14} />}
240
- />
241
- ))}
242
-
243
- {/* Planned Sprints */}
244
- {plannedSprints.map((sprint) => (
245
- <SprintSection
246
- key={sprint.id}
247
- sprint={sprint}
248
- tasks={getTasksForSprint(sprint.id)}
249
- isExpanded={expandedSprints.has(sprint.id)}
250
- onToggle={() => toggleSprintExpand(sprint.id)}
251
- onTaskClick={setSelectedTask}
252
- onMoveTask={(taskId, targetSprintId) =>
253
- updateTask.mutate({
254
- id: taskId,
255
- updates: { sprintId: targetSprintId },
256
- })
257
- }
258
- availableSprints={sprints}
259
- variant="planned"
260
- onAction={() => handleStartSprint(sprint)}
261
- actionLabel="Start Sprint"
262
- actionIcon={<Play size={14} />}
263
- />
264
- ))}
265
-
266
- {/* Backlog Section */}
267
- <motion.div
268
- layout
269
- className="rounded-2xl border border-border/40 bg-card/30 backdrop-blur-sm overflow-hidden"
270
- >
271
- <div className="flex items-center justify-between p-4 hover:bg-secondary/30 transition-colors">
272
- <div
273
- className="flex items-center gap-3 flex-1 cursor-pointer"
274
- onClick={() => setBacklogExpanded(!backlogExpanded)}
275
- >
276
- <motion.div
277
- animate={{ rotate: backlogExpanded ? 90 : 0 }}
278
- transition={{ duration: 0.2 }}
279
- >
280
- <ChevronRight size={18} className="text-muted-foreground" />
281
- </motion.div>
282
- <div className="flex items-center gap-2">
283
- <Sparkles size={16} className="text-amber-500" />
284
- <span className="font-semibold text-foreground">Backlog</span>
285
- </div>
286
- <span className="px-2 py-0.5 rounded-full bg-secondary text-[11px] font-bold text-muted-foreground">
287
- {backlogTasks.length}
288
- </span>
289
- </div>
290
- <Button
291
- variant="ghost"
292
- size="sm"
293
- className="text-xs gap-1.5 text-muted-foreground hover:text-primary"
294
- onClick={() => setIsCreateTaskOpen(true)}
295
- >
296
- <Plus size={14} />
297
- Add
298
- </Button>
299
- </div>
300
-
301
- <AnimatePresence>
302
- {backlogExpanded && (
303
- <motion.div
304
- initial={{ height: 0 }}
305
- animate={{ height: "auto" }}
306
- exit={{ height: 0 }}
307
- className="overflow-hidden"
308
- >
309
- <div className="border-t border-border/30">
310
- {backlogTasks.length === 0 ? (
311
- <div className="py-12 text-center">
312
- <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-secondary/50 mb-3">
313
- <Zap className="w-5 h-5 text-muted-foreground/50" />
314
- </div>
315
- <p className="text-sm text-muted-foreground">
316
- No items in backlog
317
- </p>
318
- <Button
319
- variant="ghost"
320
- size="sm"
321
- className="mt-2 text-primary"
322
- onClick={() => setIsCreateTaskOpen(true)}
323
- >
324
- Create your first issue
325
- </Button>
326
- </div>
327
- ) : (
328
- <div className="divide-y divide-border/20">
329
- {backlogTasks.map((task) => (
330
- <TaskRow
331
- key={task.id}
332
- task={task}
333
- onClick={() => setSelectedTask(task.id)}
334
- onMoveTask={(targetSprintId) =>
335
- updateTask.mutate({
336
- id: task.id,
337
- updates: { sprintId: targetSprintId },
338
- })
339
- }
340
- availableSprints={sprints.filter(
341
- (s) => s.status !== SprintStatus.COMPLETED
342
- )}
343
- currentSprintId={null}
344
- />
345
- ))}
346
- </div>
347
- )}
348
- </div>
349
- </motion.div>
350
- )}
351
- </AnimatePresence>
352
- </motion.div>
353
-
354
- {/* Completed Sprints */}
355
- {completedSprints.length > 0 && (
356
- <motion.div
357
- layout
358
- className="rounded-2xl border border-border/30 bg-background/50 overflow-hidden"
359
- >
360
- <button
361
- onClick={() => setShowCompletedSprints(!showCompletedSprints)}
362
- className="w-full flex items-center justify-between p-4 hover:bg-secondary/20 transition-colors"
363
- >
364
- <div className="flex items-center gap-3">
365
- <motion.div
366
- animate={{ rotate: showCompletedSprints ? 90 : 0 }}
367
- transition={{ duration: 0.2 }}
368
- >
369
- <ChevronRight
370
- size={18}
371
- className="text-muted-foreground/50"
372
- />
373
- </motion.div>
374
- <div className="flex items-center gap-2">
375
- <Archive size={16} className="text-muted-foreground/50" />
376
- <span className="font-medium text-muted-foreground">
377
- Completed Sprints
378
- </span>
379
- </div>
380
- <span className="px-2 py-0.5 rounded-full bg-secondary/50 text-[11px] font-bold text-muted-foreground/60">
381
- {completedSprints.length}
382
- </span>
383
- </div>
384
- </button>
385
-
386
- <AnimatePresence>
387
- {showCompletedSprints && (
388
- <motion.div
389
- initial={{ height: 0 }}
390
- animate={{ height: "auto" }}
391
- exit={{ height: 0 }}
392
- className="overflow-hidden"
393
- >
394
- <div className="border-t border-border/30 divide-y divide-border/20">
395
- {completedSprints.map((sprint) => (
396
- <div
397
- key={sprint.id}
398
- className="flex items-center justify-between px-4 py-3 hover:bg-secondary/10 transition-colors"
399
- >
400
- <div className="flex items-center gap-3">
401
- <CheckCircle
402
- size={14}
403
- className="text-emerald-500/60"
404
- />
405
- <span className="text-sm font-medium text-muted-foreground">
406
- {sprint.name}
407
- </span>
408
- <span className="text-xs text-muted-foreground/50 font-mono">
409
- {getTasksForSprint(sprint.id).length} tasks
410
- </span>
411
- {sprint.endDate && (
412
- <span
413
- className="text-xs text-muted-foreground/40"
414
- suppressHydrationWarning
415
- >
416
- Completed{" "}
417
- {format(sprint.endDate, "MMM d, yyyy")}
418
- </span>
419
- )}
420
- </div>
421
- <Button
422
- variant="ghost"
423
- size="sm"
424
- className="text-xs text-muted-foreground hover:text-destructive hover:bg-destructive/10"
425
- onClick={() => handleDeleteSprint(sprint)}
426
- >
427
- <Trash2 size={14} className="mr-1" />
428
- Delete
429
- </Button>
430
- </div>
431
- ))}
432
- </div>
433
- </motion.div>
434
- )}
435
- </AnimatePresence>
436
- </motion.div>
437
- )}
438
- </div>
439
- </div>
440
-
441
- {/* Task Create Modal */}
442
- <TaskCreateModal
443
- isOpen={isCreateTaskOpen}
444
- onClose={() => setIsCreateTaskOpen(false)}
445
- onCreated={() => {
446
- queryClient.invalidateQueries({ queryKey: ["tasks"] });
447
- setIsCreateTaskOpen(false);
448
- }}
449
- />
450
-
451
- {/* Task Panel */}
452
- <AnimatePresence>
453
- {selectedTask && (
454
- <TaskPanel
455
- taskId={selectedTask}
456
- onClose={() => setSelectedTask(null)}
457
- onDeleted={() => {
458
- setSelectedTask(null);
459
- queryClient.invalidateQueries({ queryKey: ["tasks"] });
460
- }}
461
- onUpdated={() =>
462
- queryClient.invalidateQueries({ queryKey: ["tasks"] })
463
- }
464
- />
465
- )}
466
- </AnimatePresence>
467
- </div>
468
- );
469
- }
470
-
471
- // Sprint Section Component
472
- function SprintSection({
473
- sprint,
474
- tasks,
475
- isExpanded,
476
- onToggle,
477
- onTaskClick,
478
- onMoveTask,
479
- availableSprints,
480
- variant,
481
- onAction,
482
- actionLabel,
483
- actionIcon,
484
- }: {
485
- sprint: Sprint;
486
- tasks: Task[];
487
- isExpanded: boolean;
488
- onToggle: () => void;
489
- onTaskClick: (id: number) => void;
490
- onMoveTask: (taskId: number, targetSprintId: number | null) => void;
491
- availableSprints: Sprint[];
492
- variant: "active" | "planned";
493
- onAction: () => void;
494
- actionLabel: string;
495
- actionIcon: React.ReactNode;
496
- }) {
497
- const isActive = variant === "active";
498
-
499
- return (
500
- <motion.div
501
- layout
502
- className={cn(
503
- "rounded-2xl border overflow-hidden transition-all",
504
- isActive
505
- ? "border-primary/30 bg-primary/2 shadow-lg shadow-primary/5"
506
- : "border-border/40 bg-card/30"
507
- )}
508
- >
509
- <div
510
- onClick={onToggle}
511
- className={cn(
512
- "w-full flex items-center justify-between p-4 transition-colors",
513
- isActive ? "hover:bg-primary/5" : "hover:bg-secondary/30"
514
- )}
515
- >
516
- <div className="flex items-center gap-3">
517
- <motion.div
518
- animate={{ rotate: isExpanded ? 90 : 0 }}
519
- transition={{ duration: 0.2 }}
520
- >
521
- <ChevronRight
522
- size={18}
523
- className={isActive ? "text-primary" : "text-muted-foreground"}
524
- />
525
- </motion.div>
526
-
527
- <div className="flex items-center gap-3">
528
- {isActive && (
529
- <div className="relative">
530
- <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
531
- <div className="absolute inset-0 w-2 h-2 rounded-full bg-primary animate-ping" />
532
- </div>
533
- )}
534
- <span
535
- className={cn(
536
- "font-semibold",
537
- isActive ? "text-primary" : "text-foreground"
538
- )}
539
- >
540
- {sprint.name}
541
- </span>
542
- <span className="px-2 py-0.5 rounded-full bg-secondary text-[11px] font-bold text-muted-foreground">
543
- {tasks.length} issues
544
- </span>
545
- {sprint.startDate && isActive && (
546
- <span
547
- className="text-xs text-muted-foreground font-mono"
548
- suppressHydrationWarning
549
- >
550
- Started {format(sprint.startDate, "MMM d")}
551
- </span>
552
- )}
553
- </div>
554
- </div>
555
-
556
- <Button
557
- variant={isActive ? "secondary" : "ghost"}
558
- size="sm"
559
- className={cn(
560
- "gap-1.5 text-xs",
561
- isActive
562
- ? "hover:bg-emerald-500/10 hover:text-emerald-600"
563
- : "hover:bg-primary/10 hover:text-primary"
564
- )}
565
- onClick={(e) => {
566
- e.stopPropagation();
567
- onAction();
568
- }}
569
- >
570
- {actionIcon}
571
- {actionLabel}
572
- </Button>
573
- </div>
574
-
575
- <AnimatePresence>
576
- {isExpanded && (
577
- <motion.div
578
- initial={{ height: 0 }}
579
- animate={{ height: "auto" }}
580
- exit={{ height: 0 }}
581
- className="overflow-hidden"
582
- >
583
- <div className="border-t border-border/30">
584
- {tasks.length === 0 ? (
585
- <div className="py-8 text-center text-sm text-muted-foreground/60">
586
- Drag issues here or move from backlog
587
- </div>
588
- ) : (
589
- <div className="divide-y divide-border/20">
590
- {tasks.map((task) => (
591
- <TaskRow
592
- key={task.id}
593
- task={task}
594
- onClick={() => onTaskClick(task.id)}
595
- onMoveTask={(targetSprintId) =>
596
- onMoveTask(task.id, targetSprintId)
597
- }
598
- availableSprints={availableSprints.filter(
599
- (s) =>
600
- s.id !== sprint.id &&
601
- s.status !== SprintStatus.COMPLETED
602
- )}
603
- currentSprintId={sprint.id}
604
- />
605
- ))}
606
- </div>
607
- )}
608
- </div>
609
- </motion.div>
610
- )}
611
- </AnimatePresence>
612
- </motion.div>
613
- );
614
- }
615
-
616
- // Task Row Component
617
- function TaskRow({
618
- task,
619
- onClick,
620
- onMoveTask,
621
- availableSprints,
622
- currentSprintId,
623
- }: {
624
- task: Task;
625
- onClick: () => void;
626
- onMoveTask: (targetSprintId: number | null) => void;
627
- availableSprints: Sprint[];
628
- currentSprintId: number | null;
629
- }) {
630
- return (
631
- <motion.div
632
- layout
633
- className="group flex items-center gap-3 px-4 py-3 hover:bg-secondary/20 transition-all cursor-pointer"
634
- onClick={onClick}
635
- >
636
- <div className="opacity-0 group-hover:opacity-40 transition-opacity cursor-grab">
637
- <GripVertical size={14} />
638
- </div>
639
-
640
- <PriorityBadge priority={task.priority} />
641
-
642
- <div className="flex-1 min-w-0">
643
- <div className="flex items-center gap-2">
644
- <span className="text-xs font-mono text-muted-foreground/60 group-hover:text-primary transition-colors">
645
- LCS-{task.id}
646
- </span>
647
- <span className="text-sm font-medium text-foreground/90 truncate group-hover:text-foreground transition-colors">
648
- {task.title}
649
- </span>
650
- </div>
651
- </div>
652
-
653
- <div className="flex items-center gap-2">
654
- <StatusBadge status={task.status} />
655
- {task.assigneeRole && (
656
- <span className="px-1.5 py-0.5 rounded text-[10px] font-bold bg-secondary border border-border/50 text-muted-foreground">
657
- {task.assigneeRole}
658
- </span>
659
- )}
660
- </div>
661
-
662
- {/* Move dropdown */}
663
- <div
664
- className="opacity-0 group-hover:opacity-100 transition-opacity"
665
- onClick={(e) => e.stopPropagation()}
666
- >
667
- <select
668
- className="appearance-none bg-secondary/50 hover:bg-secondary text-xs font-medium text-muted-foreground px-2 py-1 rounded-md border border-border/50 cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary/20"
669
- value=""
670
- onChange={(e) => {
671
- const val = e.target.value;
672
- if (val === "backlog") onMoveTask(null);
673
- else if (val) onMoveTask(Number(val));
674
- }}
675
- >
676
- <option value="" disabled>
677
- Move →
678
- </option>
679
- {currentSprintId !== null && (
680
- <option value="backlog">→ Backlog</option>
681
- )}
682
- {availableSprints.map((s) => (
683
- <option key={s.id} value={s.id}>
684
- → {s.name}
685
- </option>
686
- ))}
687
- </select>
688
- </div>
689
- </motion.div>
690
- );
691
- }