@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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error State Component
|
|
3
|
+
*
|
|
4
|
+
* Unified error display with recovery options.
|
|
5
|
+
* Use to display errors in a consistent, user-friendly way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use client";
|
|
9
|
+
|
|
10
|
+
import { AlertTriangle, RotateCcw } from "lucide-react";
|
|
11
|
+
import { Button } from "@/components/ui";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error state component props
|
|
16
|
+
*
|
|
17
|
+
* @property variant - Display variant (page, section, or inline)
|
|
18
|
+
* @property title - Error title text
|
|
19
|
+
* @property message - Error description
|
|
20
|
+
* @property onRetry - Callback for retry button
|
|
21
|
+
*/
|
|
22
|
+
interface ErrorStateProps {
|
|
23
|
+
/** Display variant */
|
|
24
|
+
variant?: "page" | "section" | "inline";
|
|
25
|
+
/** Error title */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Error description message */
|
|
28
|
+
message?: string;
|
|
29
|
+
/** Callback when user clicks retry */
|
|
30
|
+
onRetry?: () => void;
|
|
31
|
+
/** Additional CSS classes */
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Error state component
|
|
37
|
+
*
|
|
38
|
+
* Displays error messages with optional retry functionality.
|
|
39
|
+
* Three variants for different contexts.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Page-level error
|
|
43
|
+
* <ErrorState
|
|
44
|
+
* title="Failed to load data"
|
|
45
|
+
* message="Please try again"
|
|
46
|
+
* onRetry={handleRetry}
|
|
47
|
+
* />
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Inline error (e.g., in a form)
|
|
51
|
+
* <ErrorState
|
|
52
|
+
* variant="inline"
|
|
53
|
+
* message="Invalid email address"
|
|
54
|
+
* />
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Section error (e.g., in a card)
|
|
58
|
+
* <ErrorState
|
|
59
|
+
* variant="section"
|
|
60
|
+
* title="Failed to load"
|
|
61
|
+
* onRetry={refetch}
|
|
62
|
+
* />
|
|
63
|
+
*/
|
|
64
|
+
export function ErrorState({
|
|
65
|
+
variant = "page",
|
|
66
|
+
title = "Something went wrong",
|
|
67
|
+
message = "An error occurred. Please try again.",
|
|
68
|
+
onRetry,
|
|
69
|
+
className,
|
|
70
|
+
}: ErrorStateProps) {
|
|
71
|
+
const variantClasses = {
|
|
72
|
+
page: "flex flex-col items-center justify-center min-h-[400px] w-full gap-4 py-12",
|
|
73
|
+
section:
|
|
74
|
+
"flex flex-col items-center justify-center py-12 w-full gap-3 px-4",
|
|
75
|
+
inline:
|
|
76
|
+
"flex items-center gap-2 p-3 bg-destructive/10 rounded-lg border border-destructive/20",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const iconSize = {
|
|
80
|
+
page: 48,
|
|
81
|
+
section: 32,
|
|
82
|
+
inline: 16,
|
|
83
|
+
} as const;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
className={cn(variantClasses[variant], className)}
|
|
88
|
+
role="alert"
|
|
89
|
+
aria-live="polite"
|
|
90
|
+
aria-atomic="true"
|
|
91
|
+
>
|
|
92
|
+
<div className="flex flex-col items-center gap-2">
|
|
93
|
+
<AlertTriangle
|
|
94
|
+
size={iconSize[variant]}
|
|
95
|
+
className="text-destructive/70"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
/>
|
|
98
|
+
{variant !== "inline" && (
|
|
99
|
+
<>
|
|
100
|
+
<h3 className="text-lg font-semibold text-destructive">{title}</h3>
|
|
101
|
+
<p className="text-sm text-muted-foreground text-center max-w-sm">
|
|
102
|
+
{message}
|
|
103
|
+
</p>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
{variant === "inline" && (
|
|
107
|
+
<p className="text-sm text-destructive font-medium">{message}</p>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
{onRetry && (
|
|
111
|
+
<Button
|
|
112
|
+
variant="outline"
|
|
113
|
+
size={variant === "inline" ? "sm" : "md"}
|
|
114
|
+
onClick={onRetry}
|
|
115
|
+
className="gap-2"
|
|
116
|
+
aria-label="Retry the failed action"
|
|
117
|
+
>
|
|
118
|
+
<RotateCcw size={16} aria-hidden="true" />
|
|
119
|
+
Try Again
|
|
120
|
+
</Button>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading State Component
|
|
3
|
+
*
|
|
4
|
+
* Unified loading state display with consistent sizing.
|
|
5
|
+
* Use for data fetching, form submission, and async operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use client";
|
|
9
|
+
|
|
10
|
+
import { Spinner } from "@/components/ui";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Loading state component props
|
|
15
|
+
*
|
|
16
|
+
* @property variant - Display variant (page, section, or inline)
|
|
17
|
+
* @property message - Optional loading message
|
|
18
|
+
*/
|
|
19
|
+
interface LoadingStateProps {
|
|
20
|
+
/** Display variant */
|
|
21
|
+
variant?: "page" | "section" | "inline";
|
|
22
|
+
/** Optional loading message */
|
|
23
|
+
message?: string;
|
|
24
|
+
/** Additional CSS classes */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loading state component
|
|
30
|
+
*
|
|
31
|
+
* Displays loading indicator with optional message.
|
|
32
|
+
* Three variants for different contexts.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Page-level loading
|
|
36
|
+
* <LoadingState
|
|
37
|
+
* variant="page"
|
|
38
|
+
* message="Loading data..."
|
|
39
|
+
* />
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Inline loading (e.g., in a button)
|
|
43
|
+
* <LoadingState
|
|
44
|
+
* variant="inline"
|
|
45
|
+
* />
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Section loading (e.g., in a card)
|
|
49
|
+
* <LoadingState
|
|
50
|
+
* variant="section"
|
|
51
|
+
* message="Fetching results..."
|
|
52
|
+
* />
|
|
53
|
+
*/
|
|
54
|
+
export function LoadingState({
|
|
55
|
+
variant = "page",
|
|
56
|
+
message,
|
|
57
|
+
className,
|
|
58
|
+
}: LoadingStateProps) {
|
|
59
|
+
const variantClasses = {
|
|
60
|
+
page: "flex flex-col items-center justify-center min-h-[400px] w-full gap-4",
|
|
61
|
+
section: "flex flex-col items-center justify-center py-12 w-full gap-3",
|
|
62
|
+
inline: "flex items-center gap-2",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const spinnerSize = {
|
|
66
|
+
page: "lg" as const,
|
|
67
|
+
section: "md" as const,
|
|
68
|
+
inline: "sm" as const,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<output
|
|
73
|
+
className={cn(variantClasses[variant], className)}
|
|
74
|
+
aria-live="polite"
|
|
75
|
+
aria-label={message || "Loading"}
|
|
76
|
+
>
|
|
77
|
+
<Spinner size={spinnerSize[variant]} />
|
|
78
|
+
{message && (
|
|
79
|
+
<p className="text-muted-foreground text-sm font-medium">{message}</p>
|
|
80
|
+
)}
|
|
81
|
+
</output>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Components
|
|
3
|
+
*
|
|
4
|
+
* Reusable components for standardized UI patterns across the application.
|
|
5
|
+
*
|
|
6
|
+
* ## Error State
|
|
7
|
+
* Use `ErrorState` to display error messages with optional retry functionality.
|
|
8
|
+
* Supports three variants: page, section, and inline.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <ErrorState
|
|
12
|
+
* title="Failed to load"
|
|
13
|
+
* message="Please try again"
|
|
14
|
+
* onRetry={handleRetry}
|
|
15
|
+
* />
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Loading State
|
|
19
|
+
* Use `LoadingState` to display loading indicators while data is being fetched.
|
|
20
|
+
* Supports three variants: page, section, and inline.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <LoadingState
|
|
24
|
+
* variant="section"
|
|
25
|
+
* message="Loading data..."
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* ## Usage Pattern
|
|
30
|
+
* Use these components for:
|
|
31
|
+
* - Data fetching states
|
|
32
|
+
* - Form submission feedback
|
|
33
|
+
* - API error handling
|
|
34
|
+
* - Async operation feedback
|
|
35
|
+
*
|
|
36
|
+
* Keep error and loading states visible and accessible.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export * from "./ErrorState";
|
|
40
|
+
export * from "./LoadingState";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Feed Component
|
|
3
|
+
*
|
|
4
|
+
* Displays recent workspace activity and events.
|
|
5
|
+
* Shows recent actions performed by team members.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <ActivityFeed activity={workspaceEvents} />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use client";
|
|
12
|
+
|
|
13
|
+
import { type Event as WorkspaceEvent } from "@locusai/shared";
|
|
14
|
+
import { Activity } from "lucide-react";
|
|
15
|
+
import { useRouter } from "next/navigation";
|
|
16
|
+
import { Button } from "@/components/ui";
|
|
17
|
+
import { ActivityItem } from "./ActivityItem";
|
|
18
|
+
|
|
19
|
+
interface ActivityFeedProps {
|
|
20
|
+
/** Array of workspace events to display */
|
|
21
|
+
activity: WorkspaceEvent[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Activity Feed Component
|
|
26
|
+
*
|
|
27
|
+
* Features:
|
|
28
|
+
* - Displays recent workspace events
|
|
29
|
+
* - Shows activity items with timestamps
|
|
30
|
+
* - Empty state when no activity
|
|
31
|
+
* - View all button for navigation
|
|
32
|
+
*
|
|
33
|
+
* @component
|
|
34
|
+
*/
|
|
35
|
+
export function ActivityFeed({ activity }: ActivityFeedProps) {
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="bg-card border border-border/50 rounded-2xl p-6 shadow-sm h-full">
|
|
40
|
+
<div className="flex items-center justify-between mb-6">
|
|
41
|
+
<h3 className="font-semibold text-lg flex items-center gap-2">
|
|
42
|
+
<Activity size={20} className="text-primary" />
|
|
43
|
+
Recent Activity
|
|
44
|
+
</h3>
|
|
45
|
+
<Button
|
|
46
|
+
variant="ghost"
|
|
47
|
+
size="sm"
|
|
48
|
+
className="text-xs"
|
|
49
|
+
onClick={() => router.push("/activity")}
|
|
50
|
+
>
|
|
51
|
+
View All
|
|
52
|
+
</Button>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
{activity.length > 0 ? (
|
|
56
|
+
activity
|
|
57
|
+
.slice(0, 10)
|
|
58
|
+
.map((event) => <ActivityItem key={event.id} event={event} />)
|
|
59
|
+
) : (
|
|
60
|
+
<div className="py-12 flex flex-col items-center justify-center text-center space-y-3">
|
|
61
|
+
<div className="p-3 bg-secondary/30 rounded-full">
|
|
62
|
+
<Activity size={24} className="text-muted-foreground/50" />
|
|
63
|
+
</div>
|
|
64
|
+
<div className="max-w-[200px]">
|
|
65
|
+
<p className="text-sm font-medium text-foreground">
|
|
66
|
+
No activity yet
|
|
67
|
+
</p>
|
|
68
|
+
<p className="text-xs text-muted-foreground">
|
|
69
|
+
When you or your team take actions, they will appear here.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Item Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single activity event from the workspace.
|
|
5
|
+
* Shows event type, details, and relative timestamp.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use client";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type EventPayload,
|
|
12
|
+
type Event as WorkspaceEvent,
|
|
13
|
+
} from "@locusai/shared";
|
|
14
|
+
import { formatDistanceToNow } from "date-fns";
|
|
15
|
+
import {
|
|
16
|
+
Activity,
|
|
17
|
+
CheckCircle2,
|
|
18
|
+
Clock,
|
|
19
|
+
MessageSquare,
|
|
20
|
+
Plus,
|
|
21
|
+
Rocket,
|
|
22
|
+
ShieldCheck,
|
|
23
|
+
UserPlus,
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import { useAuth } from "@/context/AuthContext";
|
|
26
|
+
|
|
27
|
+
interface ActivityItemProps {
|
|
28
|
+
/** Workspace event to display */
|
|
29
|
+
event: WorkspaceEvent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const formatStatus = (status: string) => {
|
|
33
|
+
return status
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.split("_")
|
|
36
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
37
|
+
.join(" ");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function ActivityItem({ event }: ActivityItemProps) {
|
|
41
|
+
const { user } = useAuth();
|
|
42
|
+
const { type, payload } = event;
|
|
43
|
+
|
|
44
|
+
const getEventConfig = () => {
|
|
45
|
+
switch (type) {
|
|
46
|
+
case "TASK_CREATED": {
|
|
47
|
+
const p = payload as Extract<
|
|
48
|
+
EventPayload,
|
|
49
|
+
{ type: "TASK_CREATED" }
|
|
50
|
+
>["payload"];
|
|
51
|
+
return {
|
|
52
|
+
icon: Plus,
|
|
53
|
+
color: "text-emerald-500",
|
|
54
|
+
action: "created task",
|
|
55
|
+
target: p.title,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case "STATUS_CHANGED": {
|
|
59
|
+
const p = payload as Extract<
|
|
60
|
+
EventPayload,
|
|
61
|
+
{ type: "STATUS_CHANGED" }
|
|
62
|
+
>["payload"];
|
|
63
|
+
return {
|
|
64
|
+
icon: Activity,
|
|
65
|
+
color: "text-primary",
|
|
66
|
+
action: `moved task to ${formatStatus(p.newStatus)}`,
|
|
67
|
+
target: p.title,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
case "COMMENT_ADDED": {
|
|
71
|
+
const p = payload as Extract<
|
|
72
|
+
EventPayload,
|
|
73
|
+
{ type: "COMMENT_ADDED" }
|
|
74
|
+
>["payload"];
|
|
75
|
+
return {
|
|
76
|
+
icon: MessageSquare,
|
|
77
|
+
color: "text-blue-500",
|
|
78
|
+
action: "commented on",
|
|
79
|
+
target: p.title,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "WORKSPACE_CREATED": {
|
|
83
|
+
const p = payload as Extract<
|
|
84
|
+
EventPayload,
|
|
85
|
+
{ type: "WORKSPACE_CREATED" }
|
|
86
|
+
>["payload"];
|
|
87
|
+
return {
|
|
88
|
+
icon: Rocket,
|
|
89
|
+
color: "text-purple-500",
|
|
90
|
+
action: "created the workspace",
|
|
91
|
+
target: p.name,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case "MEMBER_INVITED": {
|
|
95
|
+
const p = payload as Extract<
|
|
96
|
+
EventPayload,
|
|
97
|
+
{ type: "MEMBER_INVITED" }
|
|
98
|
+
>["payload"];
|
|
99
|
+
return {
|
|
100
|
+
icon: UserPlus,
|
|
101
|
+
color: "text-amber-500",
|
|
102
|
+
action: `invited ${p.email} to the team`,
|
|
103
|
+
target: "",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "SPRINT_CREATED": {
|
|
107
|
+
const p = payload as Extract<
|
|
108
|
+
EventPayload,
|
|
109
|
+
{ type: "SPRINT_CREATED" }
|
|
110
|
+
>["payload"];
|
|
111
|
+
return {
|
|
112
|
+
icon: Clock,
|
|
113
|
+
color: "text-primary",
|
|
114
|
+
action: "created sprint",
|
|
115
|
+
target: p.name,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
case "SPRINT_STATUS_CHANGED": {
|
|
119
|
+
const p = payload as Extract<
|
|
120
|
+
EventPayload,
|
|
121
|
+
{ type: "SPRINT_STATUS_CHANGED" }
|
|
122
|
+
>["payload"];
|
|
123
|
+
return {
|
|
124
|
+
icon: Rocket,
|
|
125
|
+
color: "text-orange-500",
|
|
126
|
+
action: `moved sprint to ${formatStatus(p.newStatus)}`,
|
|
127
|
+
target: p.name,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "TASK_DELETED": {
|
|
131
|
+
const p = payload as Extract<
|
|
132
|
+
EventPayload,
|
|
133
|
+
{ type: "TASK_DELETED" }
|
|
134
|
+
>["payload"];
|
|
135
|
+
return {
|
|
136
|
+
icon: Activity,
|
|
137
|
+
color: "text-red-500",
|
|
138
|
+
action: "deleted task",
|
|
139
|
+
target: p.title,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "CI_RAN": {
|
|
143
|
+
const p = payload as Extract<
|
|
144
|
+
EventPayload,
|
|
145
|
+
{ type: "CI_RAN" }
|
|
146
|
+
>["payload"];
|
|
147
|
+
return {
|
|
148
|
+
icon: ShieldCheck,
|
|
149
|
+
color: p.ok ? "text-emerald-500" : "text-rose-500",
|
|
150
|
+
action: `ran verification: ${p.ok ? "Passed" : "Failed"}`,
|
|
151
|
+
target: p.preset,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "CHECKLIST_INITIALIZED": {
|
|
155
|
+
const p = payload as Extract<
|
|
156
|
+
EventPayload,
|
|
157
|
+
{ type: "CHECKLIST_INITIALIZED" }
|
|
158
|
+
>["payload"];
|
|
159
|
+
return {
|
|
160
|
+
icon: CheckCircle2,
|
|
161
|
+
color: "text-blue-500",
|
|
162
|
+
action: `initialized ${p.itemCount} checklist items`,
|
|
163
|
+
target: "",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return {
|
|
168
|
+
icon: Activity,
|
|
169
|
+
color: "text-muted-foreground",
|
|
170
|
+
action: type.toLowerCase().replace(/_/g, " "),
|
|
171
|
+
target: "",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const config = getEventConfig();
|
|
177
|
+
const Icon = config.icon;
|
|
178
|
+
|
|
179
|
+
const parseDate = (date: string | number | Date | null | undefined) => {
|
|
180
|
+
if (!date) return new Date();
|
|
181
|
+
return new Date(date);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const eventDate = parseDate(event.createdAt);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="flex items-start gap-4 p-3 hover:bg-secondary/20 rounded-xl transition-colors group">
|
|
188
|
+
<div className={`p-2 rounded-lg bg-secondary/50 ${config.color}`}>
|
|
189
|
+
<Icon size={16} />
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex-1 min-w-0">
|
|
192
|
+
<p className="text-sm text-foreground leading-relaxed">
|
|
193
|
+
<span className="font-semibold text-foreground/90">
|
|
194
|
+
{event.userId === user?.id ? "You" : "Team member"}
|
|
195
|
+
</span>{" "}
|
|
196
|
+
<span className="text-muted-foreground">{config.action}</span>{" "}
|
|
197
|
+
{config.target && (
|
|
198
|
+
<span className="font-medium text-primary">"{config.target}"</span>
|
|
199
|
+
)}
|
|
200
|
+
</p>
|
|
201
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">
|
|
202
|
+
{formatDistanceToNow(eventDate, { addSuffix: true })}
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Clock, Plus, Users } from "lucide-react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui";
|
|
6
|
+
|
|
7
|
+
export function QuickActions() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="bg-card border border-border/50 rounded-2xl p-6 shadow-sm">
|
|
12
|
+
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
|
13
|
+
<Plus size={20} className="text-primary" />
|
|
14
|
+
Quick Actions
|
|
15
|
+
</h3>
|
|
16
|
+
<div className="grid grid-cols-1 gap-3">
|
|
17
|
+
<Button
|
|
18
|
+
variant="outline"
|
|
19
|
+
className="justify-start gap-3 h-12 rounded-xl group"
|
|
20
|
+
onClick={() => router.push("/backlog?createTask=true")}
|
|
21
|
+
>
|
|
22
|
+
<div className="p-1.5 bg-primary/10 rounded-lg group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
|
23
|
+
<Plus size={16} />
|
|
24
|
+
</div>
|
|
25
|
+
New Task
|
|
26
|
+
</Button>
|
|
27
|
+
<Button
|
|
28
|
+
variant="outline"
|
|
29
|
+
className="justify-start gap-3 h-12 rounded-xl group"
|
|
30
|
+
onClick={() => router.push("/backlog?createSprint=true")}
|
|
31
|
+
>
|
|
32
|
+
<div className="p-1.5 bg-amber-500/10 text-amber-500 rounded-lg group-hover:bg-amber-500 group-hover:text-white transition-colors">
|
|
33
|
+
<Clock size={16} />
|
|
34
|
+
</div>
|
|
35
|
+
Start Sprint
|
|
36
|
+
</Button>
|
|
37
|
+
<Button
|
|
38
|
+
variant="outline"
|
|
39
|
+
className="justify-start gap-3 h-12 rounded-xl group"
|
|
40
|
+
onClick={() => router.push("/settings/team")}
|
|
41
|
+
>
|
|
42
|
+
<div className="p-1.5 bg-purple-500/10 text-purple-500 rounded-lg group-hover:bg-purple-500 group-hover:text-white transition-colors">
|
|
43
|
+
<Users size={16} />
|
|
44
|
+
</div>
|
|
45
|
+
Invite Team
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stat Card Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a statistic with icon, value, title, and trend.
|
|
5
|
+
* Used on dashboard to show key metrics.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <StatCard
|
|
9
|
+
* title="Active Tasks"
|
|
10
|
+
* value={24}
|
|
11
|
+
* icon={CheckCircle}
|
|
12
|
+
* trend="+5% this week"
|
|
13
|
+
* color="success"
|
|
14
|
+
* />
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use client";
|
|
18
|
+
|
|
19
|
+
import { type LucideIcon } from "lucide-react";
|
|
20
|
+
import { Badge } from "@/components/ui";
|
|
21
|
+
|
|
22
|
+
export interface StatCardProps {
|
|
23
|
+
/** Display title */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Statistic value */
|
|
26
|
+
value: number | string;
|
|
27
|
+
/** Icon component to display */
|
|
28
|
+
icon: LucideIcon;
|
|
29
|
+
/** Trend or additional info text */
|
|
30
|
+
trend: string;
|
|
31
|
+
/** Color theme */
|
|
32
|
+
color: "primary" | "warning" | "success" | "purple";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const colors = {
|
|
36
|
+
primary: "text-primary bg-primary/10",
|
|
37
|
+
warning: "text-amber-500 bg-amber-500/10",
|
|
38
|
+
success: "text-emerald-500 bg-emerald-500/10",
|
|
39
|
+
purple: "text-purple-500 bg-purple-500/10",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stat Card Component
|
|
44
|
+
*
|
|
45
|
+
* Features:
|
|
46
|
+
* - Icon display with color theming
|
|
47
|
+
* - Value and title display
|
|
48
|
+
* - Trend badge
|
|
49
|
+
* - Hover effect for interactivity
|
|
50
|
+
*
|
|
51
|
+
* @component
|
|
52
|
+
*/
|
|
53
|
+
export function StatCard({
|
|
54
|
+
title,
|
|
55
|
+
value,
|
|
56
|
+
icon: Icon,
|
|
57
|
+
trend,
|
|
58
|
+
color,
|
|
59
|
+
}: StatCardProps) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="bg-card border border-border/50 rounded-2xl p-6 shadow-sm hover:shadow-md transition-shadow">
|
|
62
|
+
<div className="flex items-center justify-between mb-4">
|
|
63
|
+
<div className={`p-2 rounded-xl ${colors[color]}`}>
|
|
64
|
+
<Icon size={20} />
|
|
65
|
+
</div>
|
|
66
|
+
<Badge
|
|
67
|
+
variant="outline"
|
|
68
|
+
className="text-[10px] font-medium border-border/50"
|
|
69
|
+
>
|
|
70
|
+
{trend}
|
|
71
|
+
</Badge>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<div className="text-3xl font-bold text-foreground">{value}</div>
|
|
75
|
+
<div className="text-sm text-muted-foreground mt-1">{title}</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|