@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.
- package/CHANGELOG.md +26 -0
- package/next.config.js +15 -2
- package/package.json +26 -3
- package/src/app/(auth)/invite/page.tsx +109 -0
- package/src/app/(auth)/layout.tsx +19 -0
- package/src/app/(auth)/login/page.tsx +65 -0
- package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
- package/src/app/(auth)/register/page.tsx +165 -0
- package/src/app/(dashboard)/activity/page.tsx +7 -0
- package/src/app/(dashboard)/backlog/page.tsx +195 -0
- package/src/app/(dashboard)/board/page.tsx +141 -0
- package/src/app/(dashboard)/layout.tsx +32 -0
- package/src/app/(dashboard)/page.tsx +14 -0
- package/src/app/(dashboard)/settings/page.tsx +161 -0
- package/src/app/(dashboard)/settings/team/page.tsx +75 -0
- package/src/app/globals.css +259 -0
- package/src/app/layout.tsx +10 -20
- package/src/app/providers.tsx +26 -3
- package/src/components/AuthLayoutUI.tsx +53 -0
- package/src/components/BoardFilter.tsx +75 -74
- package/src/components/CreateModal/CreateModal.tsx +142 -0
- package/src/components/CreateModal/index.ts +1 -0
- package/src/components/Editor.tsx +279 -0
- package/src/components/Header.tsx +99 -12
- package/src/components/PageLayout.tsx +69 -0
- package/src/components/PropertyItem.tsx +55 -9
- package/src/components/Sidebar.tsx +280 -36
- package/src/components/SprintCreateModal.tsx +84 -0
- package/src/components/TaskCard.tsx +196 -78
- package/src/components/TaskCreateModal.tsx +181 -178
- package/src/components/TaskPanel.tsx +140 -692
- package/src/components/WorkspaceCreateModal.tsx +97 -0
- package/src/components/WorkspaceProtected.tsx +91 -0
- package/src/components/auth/InviteSteps.tsx +220 -0
- package/src/components/auth/LoginSteps.tsx +86 -0
- package/src/components/auth/RegisterSteps.tsx +371 -0
- package/src/components/auth/index.ts +3 -0
- package/src/components/backlog/BacklogList.tsx +92 -0
- package/src/components/backlog/BacklogSection.tsx +137 -0
- package/src/components/backlog/CompletedSprintItem.tsx +95 -0
- package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
- package/src/components/backlog/SprintSection.tsx +155 -0
- package/src/components/backlog/constants.ts +26 -0
- package/src/components/board/BoardColumn.tsx +97 -0
- package/src/components/board/BoardContent.tsx +84 -0
- package/src/components/board/BoardEmptyState.tsx +82 -0
- package/src/components/board/BoardHeader.tsx +47 -0
- package/src/components/board/SprintMindmap.tsx +65 -0
- package/src/components/board/constants.ts +40 -0
- package/src/components/board/index.ts +5 -0
- package/src/components/common/ErrorState.tsx +124 -0
- package/src/components/common/LoadingState.tsx +83 -0
- package/src/components/common/index.ts +40 -0
- package/src/components/dashboard/ActivityFeed.tsx +77 -0
- package/src/components/dashboard/ActivityItem.tsx +207 -0
- package/src/components/dashboard/QuickActions.tsx +50 -0
- package/src/components/dashboard/StatCard.tsx +79 -0
- package/src/components/dnd/index.tsx +51 -0
- package/src/components/docs/DocsEditorArea.tsx +87 -0
- package/src/components/docs/DocsHeaderActions.tsx +121 -0
- package/src/components/docs/DocsSidebar.tsx +351 -0
- package/src/components/index.ts +7 -0
- package/src/components/onboarding/StepProgress.tsx +21 -0
- package/src/components/onboarding/index.ts +1 -0
- package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
- package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
- package/src/components/settings/ApiKeysList.tsx +143 -0
- package/src/components/settings/ApiKeysSettings.tsx +144 -0
- package/src/components/settings/InviteMemberModal.tsx +106 -0
- package/src/components/settings/ProjectSetupGuide.tsx +147 -0
- package/src/components/settings/SettingItem.tsx +32 -0
- package/src/components/settings/SettingSection.tsx +50 -0
- package/src/components/settings/TeamInvitationsList.tsx +90 -0
- package/src/components/settings/TeamMembersList.tsx +95 -0
- package/src/components/settings/index.ts +8 -0
- package/src/components/task-panel/TaskActivity.tsx +127 -0
- package/src/components/task-panel/TaskChecklist.tsx +142 -0
- package/src/components/task-panel/TaskDescription.tsx +201 -0
- package/src/components/task-panel/TaskDocs.tsx +137 -0
- package/src/components/task-panel/TaskHeader.tsx +125 -0
- package/src/components/task-panel/TaskProperties.tsx +111 -0
- package/src/components/task-panel/index.ts +12 -0
- package/src/components/typography/EmptyStateText.tsx +59 -0
- package/src/components/typography/MetadataText.tsx +65 -0
- package/src/components/typography/SecondaryText.tsx +60 -0
- package/src/components/typography/SectionLabel.tsx +60 -0
- package/src/components/typography/index.ts +14 -0
- package/src/components/typography-scales.tsx +218 -0
- package/src/components/ui/Avatar.tsx +123 -0
- package/src/components/ui/Badge.tsx +69 -2
- package/src/components/ui/Button.tsx +71 -30
- package/src/components/ui/Checkbox.tsx +34 -0
- package/src/components/ui/Dropdown.tsx +67 -1
- package/src/components/ui/EmptyState.tsx +129 -0
- package/src/components/ui/Input.tsx +53 -6
- package/src/components/ui/Modal.tsx +45 -12
- package/src/components/ui/OtpInput.tsx +148 -0
- package/src/components/ui/Skeleton.tsx +36 -0
- package/src/components/ui/Spinner.tsx +112 -0
- package/src/components/ui/Textarea.tsx +28 -3
- package/src/components/ui/Toast.tsx +99 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/constants.ts +108 -0
- package/src/components/ui/index.ts +7 -0
- package/src/context/AuthContext.tsx +140 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/backlog/index.ts +13 -0
- package/src/hooks/backlog/useBacklogActions.ts +144 -0
- package/src/hooks/backlog/useBacklogComposite.ts +73 -0
- package/src/hooks/backlog/useBacklogData.ts +74 -0
- package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
- package/src/hooks/backlog/useBacklogUI.ts +74 -0
- package/src/hooks/index.ts +22 -0
- package/src/hooks/task-panel/index.ts +13 -0
- package/src/hooks/task-panel/useTaskActions.ts +200 -0
- package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
- package/src/hooks/task-panel/useTaskData.ts +78 -0
- package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
- package/src/hooks/task-panel/useTaskUIState.ts +80 -0
- package/src/hooks/useAuthLayoutLogic.ts +43 -0
- package/src/hooks/useAuthenticatedUser.ts +36 -0
- package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
- package/src/hooks/useBacklog.ts +303 -0
- package/src/hooks/useBoard.ts +230 -0
- package/src/hooks/useDashboardLayout.ts +49 -0
- package/src/hooks/useDocs.ts +279 -0
- package/src/hooks/useDocsQuery.ts +99 -0
- package/src/hooks/useDocsSidebarState.ts +104 -0
- package/src/hooks/useFormState.ts +40 -0
- package/src/hooks/useGlobalKeydowns.ts +52 -0
- package/src/hooks/useInviteForm.ts +122 -0
- package/src/hooks/useLoginForm.ts +84 -0
- package/src/hooks/useMutationWithToast.ts +56 -0
- package/src/hooks/useOrganizationQuery.ts +55 -0
- package/src/hooks/useRegisterForm.ts +216 -0
- package/src/hooks/useSprintsQuery.ts +38 -0
- package/src/hooks/useTaskDescription.ts +102 -0
- package/src/hooks/useTaskPanel.ts +341 -0
- package/src/hooks/useTasksQuery.ts +39 -0
- package/src/hooks/useTeamManagement.ts +92 -0
- package/src/hooks/useWorkspaceCreateForm.ts +70 -0
- package/src/hooks/useWorkspaceId.ts +29 -0
- package/src/lib/api-client.ts +40 -23
- package/src/lib/config.ts +25 -0
- package/src/lib/constants.ts +83 -0
- package/src/lib/options.ts +96 -0
- package/src/lib/query-keys.ts +91 -0
- package/src/lib/typography.ts +103 -0
- package/src/lib/utils.ts +4 -0
- package/src/lib/validation.ts +192 -0
- package/src/services/index.ts +7 -3
- package/src/services/notifications.ts +80 -0
- package/src/utils/env.utils.ts +15 -0
- package/src/views/ActivityView.tsx +123 -0
- package/src/views/Dashboard.tsx +126 -0
- package/src/views/Docs.tsx +98 -612
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/BUILD_ID +0 -1
- package/.next/app-build-manifest.json +0 -55
- package/.next/app-path-routes-manifest.json +0 -8
- package/.next/build-manifest.json +0 -33
- package/.next/cache/.previewinfo +0 -1
- package/.next/cache/.rscinfo +0 -1
- package/.next/cache/.tsbuildinfo +0 -1
- package/.next/cache/config.json +0 -7
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +0 -6
- package/.next/diagnostics/framework.json +0 -1
- package/.next/export-detail.json +0 -5
- package/.next/export-marker.json +0 -6
- package/.next/images-manifest.json +0 -57
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -137
- package/.next/react-loadable-manifest.json +0 -32
- package/.next/required-server-files.json +0 -324
- package/.next/routes-manifest.json +0 -77
- package/.next/server/app/_not-found/page.js +0 -2
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -21
- package/.next/server/app/backlog/page.js +0 -2
- package/.next/server/app/backlog/page.js.nft.json +0 -1
- package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
- package/.next/server/app/backlog.html +0 -1
- package/.next/server/app/backlog.meta +0 -7
- package/.next/server/app/backlog.rsc +0 -25
- package/.next/server/app/docs/page.js +0 -97
- package/.next/server/app/docs/page.js.nft.json +0 -1
- package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
- package/.next/server/app/docs.html +0 -1
- package/.next/server/app/docs.meta +0 -7
- package/.next/server/app/docs.rsc +0 -26
- package/.next/server/app/favicon.ico/route.js +0 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +0 -1
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -7
- package/.next/server/app/index.rsc +0 -25
- package/.next/server/app/page.js +0 -2
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings/page.js +0 -2
- package/.next/server/app/settings/page.js.nft.json +0 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings.html +0 -1
- package/.next/server/app/settings.meta +0 -7
- package/.next/server/app/settings.rsc +0 -25
- package/.next/server/app-paths-manifest.json +0 -8
- package/.next/server/chunks/496.js +0 -6
- package/.next/server/chunks/585.js +0 -1
- package/.next/server/chunks/665.js +0 -1
- package/.next/server/chunks/699.js +0 -22
- package/.next/server/chunks/852.js +0 -7
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -1
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -1
- package/.next/server/pages/_app.js +0 -1
- package/.next/server/pages/_app.js.nft.json +0 -1
- package/.next/server/pages/_document.js +0 -1
- package/.next/server/pages/_document.js.nft.json +0 -1
- package/.next/server/pages/_error.js +0 -19
- package/.next/server/pages/_error.js.nft.json +0 -1
- package/.next/server/pages-manifest.json +0 -6
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -1
- package/.next/server/webpack-runtime.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/.next/static/chunks/487-1808785ba665f784.js +0 -1
- package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/.next/static/css/13e8617b72f9d3aa.css +0 -1
- package/.next/static/css/8aea088cdc4338f0.css +0 -1
- package/.next/static/css/b301ab0424111664.css +0 -1
- package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/.next/trace +0 -46
- package/.next/types/app/backlog/page.ts +0 -84
- package/.next/types/app/docs/page.ts +0 -84
- package/.next/types/app/layout.ts +0 -84
- package/.next/types/app/page.ts +0 -84
- package/.next/types/app/settings/page.ts +0 -84
- package/.next/types/cache-life.d.ts +0 -141
- package/.next/types/package.json +0 -1
- package/next-env.d.ts +0 -5
- package/out/404.html +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
- package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
- package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
- package/out/_next/static/css/b301ab0424111664.css +0 -1
- package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/out/backlog.html +0 -1
- package/out/backlog.txt +0 -25
- package/out/docs.html +0 -1
- package/out/docs.txt +0 -26
- package/out/favicon.ico +0 -0
- package/out/index.html +0 -1
- package/out/index.txt +0 -25
- package/out/logo.png +0 -0
- package/out/settings.html +0 -1
- package/out/settings.txt +0 -25
- package/src/app/backlog/page.tsx +0 -19
- package/src/app/page.tsx +0 -16
- package/src/app/settings/page.tsx +0 -194
- package/src/hooks/useTasks.ts +0 -119
- package/src/services/doc.service.ts +0 -27
- package/src/services/sprint.service.ts +0 -24
- package/src/services/task.service.ts +0 -75
- package/src/views/Backlog.tsx +0 -691
- package/src/views/Board.tsx +0 -306
- /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized configuration and constants.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loading timeouts and delays
|
|
9
|
+
*/
|
|
10
|
+
export const TIMING = {
|
|
11
|
+
DEBOUNCE_MS: 300,
|
|
12
|
+
THROTTLE_MS: 500,
|
|
13
|
+
ANIMATION_DURATION_MS: 300,
|
|
14
|
+
TOAST_DURATION_MS: 3000,
|
|
15
|
+
LONG_TOAST_DURATION_MS: 5000,
|
|
16
|
+
SKELETON_LOAD_TIME_MS: 2000,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Z-index layers for proper stacking
|
|
21
|
+
*/
|
|
22
|
+
export const Z_INDEX = {
|
|
23
|
+
BASE: 0,
|
|
24
|
+
DROPDOWN: 100,
|
|
25
|
+
STICKY: 500,
|
|
26
|
+
FIXED: 900,
|
|
27
|
+
MODAL_OVERLAY: 940,
|
|
28
|
+
MODAL: 950,
|
|
29
|
+
TOOLTIP: 1000,
|
|
30
|
+
NOTIFICATION: 1100,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keyboard shortcuts
|
|
35
|
+
*/
|
|
36
|
+
export const SHORTCUTS = {
|
|
37
|
+
CREATE_TASK: "Alt+N",
|
|
38
|
+
CREATE_SPRINT: "Alt+S",
|
|
39
|
+
CLOSE_MODAL: "Escape",
|
|
40
|
+
SAVE: "Cmd+S",
|
|
41
|
+
SEARCH: "Cmd+K",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* API rate limiting
|
|
46
|
+
*/
|
|
47
|
+
export const RATE_LIMITS = {
|
|
48
|
+
CREATE_SPRINT_MS: 1000,
|
|
49
|
+
CREATE_TASK_MS: 500,
|
|
50
|
+
UPDATE_TASK_MS: 500,
|
|
51
|
+
DELETE_TASK_MS: 500,
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pagination and limits
|
|
56
|
+
*/
|
|
57
|
+
export const PAGINATION = {
|
|
58
|
+
DEFAULT_LIMIT: 50,
|
|
59
|
+
DEFAULT_OFFSET: 0,
|
|
60
|
+
MAX_LIMIT: 100,
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validation rules
|
|
65
|
+
*/
|
|
66
|
+
export const VALIDATION = {
|
|
67
|
+
MIN_NAME_LENGTH: 1,
|
|
68
|
+
MAX_NAME_LENGTH: 255,
|
|
69
|
+
MIN_DESC_LENGTH: 0,
|
|
70
|
+
MAX_DESC_LENGTH: 10000,
|
|
71
|
+
MAX_COMMENT_LENGTH: 5000,
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* React Query configuration
|
|
76
|
+
*/
|
|
77
|
+
export const QUERY_CONFIG = {
|
|
78
|
+
DEFAULT_STALE_TIME: 0,
|
|
79
|
+
DEFAULT_GC_TIME: 0,
|
|
80
|
+
DEFAULT_RETRY: 1,
|
|
81
|
+
REFETCH_ON_WINDOW_FOCUS: false,
|
|
82
|
+
MUTATION_RETRY: 1,
|
|
83
|
+
} as const;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AssigneeRole, TaskPriority, TaskStatus } from "@locusai/shared";
|
|
2
|
+
import { capitalize } from "./utils";
|
|
3
|
+
|
|
4
|
+
export interface OptionItem<T extends string = string> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Status color mapping for UI consistency.
|
|
12
|
+
*/
|
|
13
|
+
const STATUS_COLOR_MAP: Record<TaskStatus, string> = {
|
|
14
|
+
[TaskStatus.BACKLOG]: "#64748b",
|
|
15
|
+
[TaskStatus.IN_PROGRESS]: "#f59e0b",
|
|
16
|
+
[TaskStatus.REVIEW]: "#a855f7",
|
|
17
|
+
[TaskStatus.VERIFICATION]: "#38bdf8",
|
|
18
|
+
[TaskStatus.DONE]: "#10b981",
|
|
19
|
+
[TaskStatus.BLOCKED]: "#ef4444",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Priority color mapping for UI consistency.
|
|
24
|
+
*/
|
|
25
|
+
const PRIORITY_COLOR_MAP: Record<TaskPriority, string> = {
|
|
26
|
+
[TaskPriority.LOW]: "#64748b",
|
|
27
|
+
[TaskPriority.MEDIUM]: "#38bdf8",
|
|
28
|
+
[TaskPriority.HIGH]: "#f59e0b",
|
|
29
|
+
[TaskPriority.CRITICAL]: "#ef4444",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all available task status options.
|
|
34
|
+
*/
|
|
35
|
+
export function getStatusOptions(): OptionItem<TaskStatus>[] {
|
|
36
|
+
return Object.values(TaskStatus).map((status) => ({
|
|
37
|
+
value: status,
|
|
38
|
+
label: status.replace(/_/g, " "),
|
|
39
|
+
color: STATUS_COLOR_MAP[status],
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get status color for a specific status value.
|
|
45
|
+
*/
|
|
46
|
+
export function getStatusColor(status: TaskStatus): string {
|
|
47
|
+
return STATUS_COLOR_MAP[status];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all available priority options.
|
|
52
|
+
*/
|
|
53
|
+
export function getPriorityOptions(): OptionItem<TaskPriority>[] {
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
value: TaskPriority.LOW,
|
|
57
|
+
label: "Low",
|
|
58
|
+
color: PRIORITY_COLOR_MAP[TaskPriority.LOW],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: TaskPriority.MEDIUM,
|
|
62
|
+
label: "Medium",
|
|
63
|
+
color: PRIORITY_COLOR_MAP[TaskPriority.MEDIUM],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: TaskPriority.HIGH,
|
|
67
|
+
label: "High",
|
|
68
|
+
color: PRIORITY_COLOR_MAP[TaskPriority.HIGH],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
value: TaskPriority.CRITICAL,
|
|
72
|
+
label: "Critical",
|
|
73
|
+
color: PRIORITY_COLOR_MAP[TaskPriority.CRITICAL],
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get all available assignee role options.
|
|
80
|
+
*/
|
|
81
|
+
export function getAssigneeOptions(): OptionItem<AssigneeRole>[] {
|
|
82
|
+
return Object.values(AssigneeRole).map((role) => ({
|
|
83
|
+
value: role,
|
|
84
|
+
label: capitalize(role),
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get all available membership role options.
|
|
90
|
+
*/
|
|
91
|
+
export function getMembershipRoleOptions(): OptionItem[] {
|
|
92
|
+
return [
|
|
93
|
+
{ value: "MEMBER", label: "Member" },
|
|
94
|
+
{ value: "ADMIN", label: "Admin" },
|
|
95
|
+
];
|
|
96
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Key Factory
|
|
3
|
+
*
|
|
4
|
+
* Centralizes all React Query keys to ensure consistent caching and invalidation.
|
|
5
|
+
* Follows the pattern: [entity, scope, ...params]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const queryKeys = {
|
|
9
|
+
workspaces: {
|
|
10
|
+
all: () => ["workspaces"] as const,
|
|
11
|
+
list: () => [...queryKeys.workspaces.all(), "list"] as const,
|
|
12
|
+
detail: (id: string) =>
|
|
13
|
+
[...queryKeys.workspaces.all(), "detail", id] as const,
|
|
14
|
+
stats: (id: string) =>
|
|
15
|
+
[...queryKeys.workspaces.all(), "stats", id] as const,
|
|
16
|
+
activity: (id: string) =>
|
|
17
|
+
[...queryKeys.workspaces.all(), "activity", id] as const,
|
|
18
|
+
},
|
|
19
|
+
tasks: {
|
|
20
|
+
all: () => ["tasks"] as const,
|
|
21
|
+
list: (workspaceId?: string | null) =>
|
|
22
|
+
[
|
|
23
|
+
...queryKeys.tasks.all(),
|
|
24
|
+
"list",
|
|
25
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
26
|
+
] as const,
|
|
27
|
+
detail: (id: string, workspaceId?: string) =>
|
|
28
|
+
[...queryKeys.tasks.all(), "detail", id, { workspaceId }] as const,
|
|
29
|
+
backlog: (workspaceId?: string | null) =>
|
|
30
|
+
[
|
|
31
|
+
...queryKeys.tasks.all(),
|
|
32
|
+
"backlog",
|
|
33
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
34
|
+
] as const,
|
|
35
|
+
},
|
|
36
|
+
sprints: {
|
|
37
|
+
all: () => ["sprints"] as const,
|
|
38
|
+
list: (workspaceId?: string | null) =>
|
|
39
|
+
[
|
|
40
|
+
...queryKeys.sprints.all(),
|
|
41
|
+
"list",
|
|
42
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
43
|
+
] as const,
|
|
44
|
+
active: (workspaceId?: string | null) =>
|
|
45
|
+
[
|
|
46
|
+
...queryKeys.sprints.all(),
|
|
47
|
+
"active",
|
|
48
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
49
|
+
] as const,
|
|
50
|
+
detail: (id: string, workspaceId?: string) =>
|
|
51
|
+
[...queryKeys.sprints.all(), "detail", id, { workspaceId }] as const,
|
|
52
|
+
},
|
|
53
|
+
docs: {
|
|
54
|
+
all: () => ["docs"] as const,
|
|
55
|
+
list: (workspaceId?: string | null) =>
|
|
56
|
+
[
|
|
57
|
+
...queryKeys.docs.all(),
|
|
58
|
+
"list",
|
|
59
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
60
|
+
] as const,
|
|
61
|
+
detail: (id: string, workspaceId?: string | null) =>
|
|
62
|
+
[
|
|
63
|
+
...queryKeys.docs.all(),
|
|
64
|
+
"detail",
|
|
65
|
+
id,
|
|
66
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
67
|
+
] as const,
|
|
68
|
+
},
|
|
69
|
+
docGroups: {
|
|
70
|
+
all: () => ["doc-groups"] as const,
|
|
71
|
+
list: (workspaceId?: string | null) =>
|
|
72
|
+
[
|
|
73
|
+
...queryKeys.docGroups.all(),
|
|
74
|
+
"list",
|
|
75
|
+
{ workspaceId: workspaceId ?? undefined },
|
|
76
|
+
] as const,
|
|
77
|
+
},
|
|
78
|
+
organizations: {
|
|
79
|
+
all: () => ["organizations"] as const,
|
|
80
|
+
list: () => [...queryKeys.organizations.all(), "list"] as const,
|
|
81
|
+
detail: (id: string) =>
|
|
82
|
+
[...queryKeys.organizations.all(), "detail", id] as const,
|
|
83
|
+
members: (id: string) =>
|
|
84
|
+
[...queryKeys.organizations.all(), "members", id] as const,
|
|
85
|
+
},
|
|
86
|
+
invitations: {
|
|
87
|
+
all: () => ["invitations"] as const,
|
|
88
|
+
list: (orgId: string) =>
|
|
89
|
+
[...queryKeys.invitations.all(), "list", orgId] as const,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typography System
|
|
3
|
+
*
|
|
4
|
+
* Centralized typography scales and variants for brand-consistent design.
|
|
5
|
+
* All text components should consume from this system.
|
|
6
|
+
*
|
|
7
|
+
* Readability Standards:
|
|
8
|
+
* - Primary text: text-foreground (100%)
|
|
9
|
+
* - Secondary text: text-foreground/60-70% (better contrast than text-muted-foreground)
|
|
10
|
+
* - Tertiary text: text-foreground/40-50%
|
|
11
|
+
* - Labels: text-foreground/60% with uppercase
|
|
12
|
+
* - Disabled: text-muted-foreground
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const typographyConfig = {
|
|
16
|
+
// Heading scales
|
|
17
|
+
h1: {
|
|
18
|
+
className: "text-4xl font-black tracking-tight text-foreground",
|
|
19
|
+
description: "Page titles, major sections",
|
|
20
|
+
},
|
|
21
|
+
h2: {
|
|
22
|
+
className: "text-3xl font-bold tracking-tight text-foreground",
|
|
23
|
+
description: "Section headers",
|
|
24
|
+
},
|
|
25
|
+
h3: {
|
|
26
|
+
className: "text-2xl font-bold tracking-tight text-foreground",
|
|
27
|
+
description: "Subsection headers",
|
|
28
|
+
},
|
|
29
|
+
h4: {
|
|
30
|
+
className: "text-xl font-semibold tracking-tight text-foreground",
|
|
31
|
+
description: "Component titles",
|
|
32
|
+
},
|
|
33
|
+
h5: {
|
|
34
|
+
className: "text-lg font-semibold tracking-tight text-foreground",
|
|
35
|
+
description: "Card titles, labels",
|
|
36
|
+
},
|
|
37
|
+
h6: {
|
|
38
|
+
className: "text-base font-semibold tracking-tight text-foreground",
|
|
39
|
+
description: "Minor titles",
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Body text
|
|
43
|
+
bodyLg: {
|
|
44
|
+
className: "text-base font-normal leading-relaxed text-foreground",
|
|
45
|
+
description: "Large body text",
|
|
46
|
+
},
|
|
47
|
+
body: {
|
|
48
|
+
className: "text-sm font-normal leading-relaxed text-foreground",
|
|
49
|
+
description: "Standard body text",
|
|
50
|
+
},
|
|
51
|
+
bodySm: {
|
|
52
|
+
className: "text-xs font-normal leading-relaxed text-foreground/70",
|
|
53
|
+
description: "Small body text with better secondary contrast",
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Labels and captions - standardized for readability
|
|
57
|
+
label: {
|
|
58
|
+
className:
|
|
59
|
+
"text-[10px] font-bold uppercase tracking-widest text-foreground/60",
|
|
60
|
+
description: "Form labels, section labels - readable secondary text",
|
|
61
|
+
},
|
|
62
|
+
caption: {
|
|
63
|
+
className: "text-xs font-medium text-foreground/65",
|
|
64
|
+
description:
|
|
65
|
+
"Captions, helper text - improved contrast over muted-foreground",
|
|
66
|
+
},
|
|
67
|
+
captionSm: {
|
|
68
|
+
className: "text-[10px] font-medium text-foreground/60",
|
|
69
|
+
description: "Small captions - readable secondary text",
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Code and monospace
|
|
73
|
+
code: {
|
|
74
|
+
className:
|
|
75
|
+
"text-sm font-mono bg-secondary/30 px-2 py-1 rounded text-foreground",
|
|
76
|
+
description: "Inline code",
|
|
77
|
+
},
|
|
78
|
+
codeBlock: {
|
|
79
|
+
className: "text-sm font-mono leading-relaxed text-foreground",
|
|
80
|
+
description: "Code blocks",
|
|
81
|
+
},
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
// Export for use in components
|
|
85
|
+
export type TypographyVariant = keyof typeof typographyConfig;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get typography class name by variant
|
|
89
|
+
* @example
|
|
90
|
+
* const titleClass = getTypographyClass('h1');
|
|
91
|
+
*/
|
|
92
|
+
export function getTypographyClass(variant: TypographyVariant): string {
|
|
93
|
+
return typographyConfig[variant].className;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Typography component props helper
|
|
98
|
+
*/
|
|
99
|
+
export interface TypographyProps {
|
|
100
|
+
variant: TypographyVariant;
|
|
101
|
+
className?: string;
|
|
102
|
+
children: React.ReactNode;
|
|
103
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication validation schemas and utilities
|
|
3
|
+
* Centralized validation for login, register, and invite flows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Email validation - used across all auth flows
|
|
10
|
+
*/
|
|
11
|
+
export const EmailSchema = z
|
|
12
|
+
.string()
|
|
13
|
+
.email("Invalid email address")
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* OTP validation - 6-digit code
|
|
18
|
+
*/
|
|
19
|
+
export const OtpSchema = z.string().regex(/^\d{6}$/, "OTP must be 6 digits");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* User name validation
|
|
23
|
+
*/
|
|
24
|
+
export const NameSchema = z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1, "Name is required")
|
|
27
|
+
.max(255, "Name must be less than 255 characters");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Company/organization name validation
|
|
31
|
+
*/
|
|
32
|
+
export const CompanyNameSchema = z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1, "Company name is required")
|
|
35
|
+
.max(255, "Company name must be less than 255 characters");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Workspace name validation
|
|
39
|
+
*/
|
|
40
|
+
export const WorkspaceNameSchema = z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1, "Workspace name is required")
|
|
43
|
+
.max(255, "Workspace name must be less than 255 characters");
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Team size options
|
|
47
|
+
*/
|
|
48
|
+
export const TeamSizeSchema = z.enum([
|
|
49
|
+
"solo",
|
|
50
|
+
"2-10",
|
|
51
|
+
"11-50",
|
|
52
|
+
"51-200",
|
|
53
|
+
"200+",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* User role options
|
|
58
|
+
*/
|
|
59
|
+
export const UserRoleSchema = z.enum([
|
|
60
|
+
"developer",
|
|
61
|
+
"designer",
|
|
62
|
+
"product_manager",
|
|
63
|
+
"other",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Login flow validation
|
|
68
|
+
*/
|
|
69
|
+
export const LoginEmailSchema = z.object({
|
|
70
|
+
email: EmailSchema,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const LoginOtpSchema = z.object({
|
|
74
|
+
email: EmailSchema,
|
|
75
|
+
otp: OtpSchema,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type LoginEmail = z.infer<typeof LoginEmailSchema>;
|
|
79
|
+
export type LoginOtp = z.infer<typeof LoginOtpSchema>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Registration flow validation
|
|
83
|
+
*/
|
|
84
|
+
export const RegisterEmailSchema = z.object({
|
|
85
|
+
email: EmailSchema,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const RegisterOtpSchema = z.object({
|
|
89
|
+
otp: OtpSchema,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const RegisterProfileSchema = z.object({
|
|
93
|
+
name: NameSchema,
|
|
94
|
+
userRole: UserRoleSchema,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const RegisterOrganizationSchema = z.object({
|
|
98
|
+
companyName: CompanyNameSchema,
|
|
99
|
+
teamSize: TeamSizeSchema,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const RegisterWorkspaceSchema = z.object({
|
|
103
|
+
workspaceName: WorkspaceNameSchema,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const RegisterInviteSchema = z.object({
|
|
107
|
+
invitedEmails: z.array(EmailSchema),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const CompleteRegistrationSchema = z.object({
|
|
111
|
+
email: EmailSchema,
|
|
112
|
+
otp: OtpSchema,
|
|
113
|
+
name: NameSchema,
|
|
114
|
+
companyName: CompanyNameSchema.optional(),
|
|
115
|
+
teamSize: TeamSizeSchema,
|
|
116
|
+
userRole: UserRoleSchema,
|
|
117
|
+
workspaceName: WorkspaceNameSchema.optional(),
|
|
118
|
+
invitedEmails: z.array(EmailSchema).optional(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export type RegisterProfile = z.infer<typeof RegisterProfileSchema>;
|
|
122
|
+
export type RegisterOrganization = z.infer<typeof RegisterOrganizationSchema>;
|
|
123
|
+
export type RegisterWorkspace = z.infer<typeof RegisterWorkspaceSchema>;
|
|
124
|
+
export type RegisterInvite = z.infer<typeof RegisterInviteSchema>;
|
|
125
|
+
export type CompleteRegistration = z.infer<typeof CompleteRegistrationSchema>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Invitation acceptance validation
|
|
129
|
+
*/
|
|
130
|
+
export const AcceptInvitationSchema = z.object({
|
|
131
|
+
email: EmailSchema,
|
|
132
|
+
name: NameSchema.optional(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export type AcceptInvitation = z.infer<typeof AcceptInvitationSchema>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Helper functions for validation
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validates email - simple utility
|
|
143
|
+
*/
|
|
144
|
+
export function isValidEmail(email: string): boolean {
|
|
145
|
+
try {
|
|
146
|
+
EmailSchema.parse(email);
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validates OTP format
|
|
155
|
+
*/
|
|
156
|
+
export function isValidOtp(otp: string): boolean {
|
|
157
|
+
return /^\d{6}$/.test(otp);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validates email array for invitations
|
|
162
|
+
*/
|
|
163
|
+
export function validateInvitationEmails(emails: string[]): {
|
|
164
|
+
valid: string[];
|
|
165
|
+
invalid: string[];
|
|
166
|
+
} {
|
|
167
|
+
const valid: string[] = [];
|
|
168
|
+
const invalid: string[] = [];
|
|
169
|
+
|
|
170
|
+
emails.forEach((email) => {
|
|
171
|
+
if (isValidEmail(email)) {
|
|
172
|
+
valid.push(email);
|
|
173
|
+
} else {
|
|
174
|
+
invalid.push(email);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return { valid, invalid };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Removes duplicate emails (case-insensitive)
|
|
183
|
+
*/
|
|
184
|
+
export function deduplicateEmails(emails: string[]): string[] {
|
|
185
|
+
const seen = new Set<string>();
|
|
186
|
+
return emails.filter((email) => {
|
|
187
|
+
const lower = email.toLowerCase();
|
|
188
|
+
if (seen.has(lower)) return false;
|
|
189
|
+
seen.add(lower);
|
|
190
|
+
return true;
|
|
191
|
+
});
|
|
192
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification Service
|
|
3
|
+
*
|
|
4
|
+
* Centralized notification/toast handling using Sonner.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { TIMING } from "@/lib/constants";
|
|
9
|
+
|
|
10
|
+
export type NotificationType =
|
|
11
|
+
| "default"
|
|
12
|
+
| "success"
|
|
13
|
+
| "error"
|
|
14
|
+
| "warning"
|
|
15
|
+
| "info";
|
|
16
|
+
|
|
17
|
+
interface NotificationOptions {
|
|
18
|
+
duration?: number;
|
|
19
|
+
action?: {
|
|
20
|
+
label: string;
|
|
21
|
+
onClick: () => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Show a notification
|
|
27
|
+
*/
|
|
28
|
+
export function notify(
|
|
29
|
+
message: string,
|
|
30
|
+
type: NotificationType = "default",
|
|
31
|
+
options?: NotificationOptions
|
|
32
|
+
) {
|
|
33
|
+
const duration = options?.duration || TIMING.TOAST_DURATION_MS;
|
|
34
|
+
|
|
35
|
+
const notificationProps = {
|
|
36
|
+
duration,
|
|
37
|
+
action: options?.action,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
switch (type) {
|
|
41
|
+
case "success":
|
|
42
|
+
toast.success(message, notificationProps);
|
|
43
|
+
break;
|
|
44
|
+
case "error":
|
|
45
|
+
toast.error(message, notificationProps);
|
|
46
|
+
break;
|
|
47
|
+
case "warning":
|
|
48
|
+
toast(message, { ...notificationProps, icon: "⚠️" });
|
|
49
|
+
break;
|
|
50
|
+
case "info":
|
|
51
|
+
toast.info?.(message, notificationProps) ||
|
|
52
|
+
toast(message, notificationProps);
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
toast(message, notificationProps);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Specific notification helpers
|
|
61
|
+
*/
|
|
62
|
+
export const notifications = {
|
|
63
|
+
success: (message: string, options?: NotificationOptions) =>
|
|
64
|
+
notify(message, "success", options),
|
|
65
|
+
error: (message: string, options?: NotificationOptions) =>
|
|
66
|
+
notify(message, "error", options),
|
|
67
|
+
warning: (message: string, options?: NotificationOptions) =>
|
|
68
|
+
notify(message, "warning", options),
|
|
69
|
+
info: (message: string, options?: NotificationOptions) =>
|
|
70
|
+
notify(message, "info", options),
|
|
71
|
+
|
|
72
|
+
// Common use cases
|
|
73
|
+
saved: () => notifications.success("Changes saved"),
|
|
74
|
+
deleted: () => notifications.success("Deleted successfully"),
|
|
75
|
+
created: (item: string) =>
|
|
76
|
+
notifications.success(`${item} created successfully`),
|
|
77
|
+
failed: (action: string) =>
|
|
78
|
+
notifications.error(`Failed to ${action}. Please try again.`),
|
|
79
|
+
loading: (message: string) => notify(message, "info"),
|
|
80
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Utility Functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const isLocalMode = () => {
|
|
6
|
+
return process.env.NEXT_PUBLIC_LOCUS_MODE === "local";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const isCloudMode = () => {
|
|
10
|
+
return process.env.NEXT_PUBLIC_LOCUS_MODE === "cloud";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getApiUrl = () => {
|
|
14
|
+
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3080/api";
|
|
15
|
+
};
|