@devxcrew/react-ui 0.0.0-stage → 0.1.7
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/README.md +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { CloudUpload, FileCheck } from 'lucide-react'
|
|
2
|
+
import { useRef, useState } from 'react'
|
|
3
|
+
import { Button } from '../../components/button'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { DropzoneFileItem, formatBytes } from './dropzone-file-item'
|
|
6
|
+
import type { DropzoneProps } from './dropzone-types'
|
|
7
|
+
|
|
8
|
+
function validateFile(
|
|
9
|
+
file: File,
|
|
10
|
+
accept?: readonly string[],
|
|
11
|
+
maxSizeBytes?: number,
|
|
12
|
+
): string | null {
|
|
13
|
+
if (maxSizeBytes && file.size > maxSizeBytes) {
|
|
14
|
+
return `File exceeds max size of ${formatBytes(maxSizeBytes)}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (accept && accept.length > 0) {
|
|
18
|
+
const ext = '.' + file.name.split('.').pop()?.toLowerCase()
|
|
19
|
+
const mime = file.type.toLowerCase()
|
|
20
|
+
const isAccepted = accept.some((pattern) => {
|
|
21
|
+
const p = pattern.toLowerCase().trim()
|
|
22
|
+
if (p.startsWith('.')) return ext === p
|
|
23
|
+
if (p.endsWith('/*')) return mime.startsWith(p.replace('/*', '/'))
|
|
24
|
+
return mime === p
|
|
25
|
+
})
|
|
26
|
+
if (!isAccepted) {
|
|
27
|
+
return `File type not supported (expected ${accept.join(', ')})`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Dropzone({
|
|
35
|
+
accept,
|
|
36
|
+
allowMultiple = true,
|
|
37
|
+
ariaLabel = 'File upload dropzone',
|
|
38
|
+
className,
|
|
39
|
+
disabled = false,
|
|
40
|
+
emptyMessage = 'No files staged for upload',
|
|
41
|
+
files = [],
|
|
42
|
+
hint,
|
|
43
|
+
maxFiles,
|
|
44
|
+
maxSizeBytes,
|
|
45
|
+
onClearFiles,
|
|
46
|
+
onFilesDrop,
|
|
47
|
+
onRemoveFile,
|
|
48
|
+
onRetryFile,
|
|
49
|
+
showFileList = true,
|
|
50
|
+
title = 'Drag and drop files here, or click to browse',
|
|
51
|
+
}: DropzoneProps) {
|
|
52
|
+
const [isDragActive, setIsDragActive] = useState(false)
|
|
53
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
54
|
+
|
|
55
|
+
function handleProcessFiles(incomingFiles: FileList | File[]) {
|
|
56
|
+
if (disabled) return
|
|
57
|
+
const fileArray = Array.from(incomingFiles)
|
|
58
|
+
const accepted: File[] = []
|
|
59
|
+
const rejected: { error: string; file: File }[] = []
|
|
60
|
+
|
|
61
|
+
for (const f of fileArray) {
|
|
62
|
+
if (maxFiles && files.length + accepted.length >= maxFiles) {
|
|
63
|
+
rejected.push({ error: `Exceeds max allowed file limit of ${maxFiles}`, file: f })
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
const err = validateFile(f, accept, maxSizeBytes)
|
|
67
|
+
if (err) {
|
|
68
|
+
rejected.push({ error: err, file: f })
|
|
69
|
+
} else {
|
|
70
|
+
accepted.push(f)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
onFilesDrop?.(accepted, rejected)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleDragOver(e: React.DragEvent) {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
e.stopPropagation()
|
|
80
|
+
if (!disabled && !isDragActive) {
|
|
81
|
+
setIsDragActive(true)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleDragLeave(e: React.DragEvent) {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
setIsDragActive(false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleDrop(e: React.DragEvent) {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
setIsDragActive(false)
|
|
95
|
+
if (disabled || !e.dataTransfer.files) return
|
|
96
|
+
handleProcessFiles(e.dataTransfer.files)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
100
|
+
if (e.target.files) {
|
|
101
|
+
handleProcessFiles(e.target.files)
|
|
102
|
+
}
|
|
103
|
+
if (inputRef.current) {
|
|
104
|
+
inputRef.current.value = ''
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleBrowseClick() {
|
|
109
|
+
if (!disabled) {
|
|
110
|
+
inputRef.current?.click()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className={cn('flex flex-col gap-4', className)}>
|
|
116
|
+
<div
|
|
117
|
+
aria-disabled={disabled}
|
|
118
|
+
aria-label={ariaLabel}
|
|
119
|
+
className={cn(
|
|
120
|
+
'group relative flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-border/80 bg-muted/20 p-8 text-center transition-all cursor-pointer select-none',
|
|
121
|
+
isDragActive && 'border-primary bg-primary/5 ring-4 ring-primary/10',
|
|
122
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
123
|
+
!disabled && 'hover:border-foreground/30 hover:bg-muted/30',
|
|
124
|
+
)}
|
|
125
|
+
role="region"
|
|
126
|
+
tabIndex={disabled ? -1 : 0}
|
|
127
|
+
onClick={handleBrowseClick}
|
|
128
|
+
onDragLeave={handleDragLeave}
|
|
129
|
+
onDragOver={handleDragOver}
|
|
130
|
+
onDrop={handleDrop}
|
|
131
|
+
onKeyDown={(e) => {
|
|
132
|
+
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
|
|
133
|
+
e.preventDefault()
|
|
134
|
+
handleBrowseClick()
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<input
|
|
139
|
+
accept={accept?.join(',')}
|
|
140
|
+
className="sr-only"
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
multiple={allowMultiple}
|
|
143
|
+
ref={inputRef}
|
|
144
|
+
type="file"
|
|
145
|
+
onChange={handleInputChange}
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<div
|
|
149
|
+
className={cn(
|
|
150
|
+
'flex size-12 items-center justify-center rounded-2xl bg-muted/80 text-muted-foreground transition-transform duration-200 group-hover:scale-105',
|
|
151
|
+
isDragActive && 'scale-110 bg-primary/15 text-primary',
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
<CloudUpload className="size-6" />
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<h4 className="mt-3 text-sm font-semibold tracking-tight text-foreground">{title}</h4>
|
|
158
|
+
|
|
159
|
+
<p className="mt-1 text-xs text-muted-foreground max-w-sm">
|
|
160
|
+
{hint ??
|
|
161
|
+
`Supports ${accept ? accept.join(', ') : 'any files'}${
|
|
162
|
+
maxSizeBytes ? ` up to ${formatBytes(maxSizeBytes)}` : ''
|
|
163
|
+
}`}
|
|
164
|
+
</p>
|
|
165
|
+
|
|
166
|
+
<Button
|
|
167
|
+
className="mt-4 pointer-events-none"
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
size="sm"
|
|
170
|
+
variant="secondary"
|
|
171
|
+
>
|
|
172
|
+
Select from device
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{showFileList && (
|
|
177
|
+
<div className="flex flex-col gap-2">
|
|
178
|
+
<div className="flex items-center justify-between px-1">
|
|
179
|
+
<span className="flex items-center gap-1.5 text-xs font-semibold text-foreground">
|
|
180
|
+
<FileCheck className="size-3.5 text-muted-foreground" />
|
|
181
|
+
Staged files ({files.length}
|
|
182
|
+
{maxFiles ? ` / ${maxFiles}` : ''})
|
|
183
|
+
</span>
|
|
184
|
+
|
|
185
|
+
{files.length > 0 && onClearFiles && (
|
|
186
|
+
<button
|
|
187
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={onClearFiles}
|
|
190
|
+
>
|
|
191
|
+
Clear all
|
|
192
|
+
</button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{files.length > 0 ? (
|
|
197
|
+
<div className="flex flex-col gap-2">
|
|
198
|
+
{files.map((file) => (
|
|
199
|
+
<DropzoneFileItem
|
|
200
|
+
file={file}
|
|
201
|
+
key={file.id}
|
|
202
|
+
onRemove={onRemoveFile}
|
|
203
|
+
onRetry={onRetryFile}
|
|
204
|
+
/>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
) : (
|
|
208
|
+
<div className="rounded-xl border border-dashed border-border/60 py-4 text-center text-xs text-muted-foreground/70">
|
|
209
|
+
{emptyMessage}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowRightIcon,
|
|
3
|
+
MinusIcon,
|
|
4
|
+
PlusIcon,
|
|
5
|
+
ShoppingBagIcon,
|
|
6
|
+
Trash2Icon,
|
|
7
|
+
TruckIcon,
|
|
8
|
+
XIcon,
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import { cn } from '../../../lib/utils'
|
|
11
|
+
import { Button } from '../../../components/button'
|
|
12
|
+
import type { StorefrontCartProps } from './cart-types'
|
|
13
|
+
|
|
14
|
+
export function StorefrontCart({
|
|
15
|
+
className,
|
|
16
|
+
currencySymbol = '$',
|
|
17
|
+
freeShippingThreshold = 75,
|
|
18
|
+
isDrawer = true,
|
|
19
|
+
items,
|
|
20
|
+
onCheckout,
|
|
21
|
+
onClose,
|
|
22
|
+
onQuantityChange,
|
|
23
|
+
onRemoveItem,
|
|
24
|
+
open = true,
|
|
25
|
+
promoDiscountValue = 0,
|
|
26
|
+
}: StorefrontCartProps) {
|
|
27
|
+
if (isDrawer && !open) return null
|
|
28
|
+
|
|
29
|
+
const subtotal = items.reduce((acc, item) => acc + item.priceValue * item.quantity, 0)
|
|
30
|
+
const total = Math.max(0, subtotal - promoDiscountValue)
|
|
31
|
+
const freeShippingProgress = Math.min(100, Math.round((subtotal / freeShippingThreshold) * 100))
|
|
32
|
+
const remainingForFreeShipping = Math.max(0, freeShippingThreshold - subtotal)
|
|
33
|
+
|
|
34
|
+
const content = (
|
|
35
|
+
<div className="flex h-full flex-col justify-between bg-background">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<div className="flex items-center justify-between border-b border-border/80 px-5 py-4">
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<ShoppingBagIcon className="size-5 text-primary" />
|
|
40
|
+
<h2 className="text-base font-bold text-foreground">Shopping Cart</h2>
|
|
41
|
+
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-semibold text-muted-foreground">
|
|
42
|
+
{items.reduce((acc, it) => acc + it.quantity, 0)}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
{isDrawer && onClose && (
|
|
46
|
+
<Button
|
|
47
|
+
size="icon"
|
|
48
|
+
variant="ghost"
|
|
49
|
+
className="size-8"
|
|
50
|
+
onClick={onClose}
|
|
51
|
+
aria-label="Close cart"
|
|
52
|
+
>
|
|
53
|
+
<XIcon className="size-4" />
|
|
54
|
+
</Button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Free Shipping Progress Meter */}
|
|
59
|
+
<div className="border-b border-border/40 bg-muted/30 px-5 py-3 text-xs">
|
|
60
|
+
<div className="flex items-center gap-2 text-foreground font-medium mb-1.5">
|
|
61
|
+
<TruckIcon className="size-4 text-primary" />
|
|
62
|
+
{remainingForFreeShipping > 0 ? (
|
|
63
|
+
<span>
|
|
64
|
+
Add{' '}
|
|
65
|
+
<strong className="text-primary">
|
|
66
|
+
{currencySymbol}
|
|
67
|
+
{remainingForFreeShipping.toFixed(2)}
|
|
68
|
+
</strong>{' '}
|
|
69
|
+
more to get Free Shipping!
|
|
70
|
+
</span>
|
|
71
|
+
) : (
|
|
72
|
+
<span className="font-semibold text-emerald-600 dark:text-emerald-400">
|
|
73
|
+
🎉 Congratulations! You have unlocked Free Shipping!
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<div className="h-2 w-full rounded-full bg-border overflow-hidden">
|
|
78
|
+
<div
|
|
79
|
+
className="h-full bg-primary transition-all duration-300"
|
|
80
|
+
style={{ width: `${freeShippingProgress}%` }}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Item List */}
|
|
86
|
+
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
|
87
|
+
{items.length === 0 ? (
|
|
88
|
+
<div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground">
|
|
89
|
+
<ShoppingBagIcon className="size-12 stroke-[1.2] mb-3 opacity-40" />
|
|
90
|
+
<p className="text-sm font-semibold text-foreground">Your cart is empty</p>
|
|
91
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
92
|
+
Add products to your cart to checkout.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
items.map((item) => (
|
|
97
|
+
<div
|
|
98
|
+
key={item.id}
|
|
99
|
+
className="flex items-center gap-3 rounded-xl border border-border/60 bg-card p-3 shadow-xs transition-colors hover:border-border"
|
|
100
|
+
>
|
|
101
|
+
<img
|
|
102
|
+
src={item.image}
|
|
103
|
+
alt={item.title}
|
|
104
|
+
className="size-18 rounded-lg object-cover bg-muted shrink-0 border border-border/40"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<div className="flex-1 min-w-0">
|
|
108
|
+
<h4 className="text-xs font-semibold text-foreground truncate">{item.title}</h4>
|
|
109
|
+
{item.variant && (
|
|
110
|
+
<p className="text-[11px] text-muted-foreground truncate">{item.variant}</p>
|
|
111
|
+
)}
|
|
112
|
+
<div className="flex items-baseline gap-1.5 mt-1">
|
|
113
|
+
<span className="text-xs font-bold text-foreground">{item.price}</span>
|
|
114
|
+
{item.originalPrice && (
|
|
115
|
+
<span className="text-[10px] text-muted-foreground line-through">
|
|
116
|
+
{item.originalPrice}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Quantity Stepper */}
|
|
122
|
+
<div className="flex items-center gap-2 mt-2">
|
|
123
|
+
<div className="flex items-center rounded-lg border border-border/80 bg-muted/40">
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
disabled={item.quantity <= 1}
|
|
127
|
+
onClick={() => onQuantityChange?.(item.id, item.quantity - 1)}
|
|
128
|
+
className="flex size-6 items-center justify-center text-muted-foreground hover:text-foreground disabled:opacity-30"
|
|
129
|
+
>
|
|
130
|
+
<MinusIcon className="size-3" />
|
|
131
|
+
</button>
|
|
132
|
+
<span className="w-6 text-center text-xs font-bold text-foreground">
|
|
133
|
+
{item.quantity}
|
|
134
|
+
</span>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => onQuantityChange?.(item.id, item.quantity + 1)}
|
|
138
|
+
className="flex size-6 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
139
|
+
>
|
|
140
|
+
<PlusIcon className="size-3" />
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => onRemoveItem?.(item.id)}
|
|
147
|
+
className="text-muted-foreground hover:text-destructive p-1 transition-colors"
|
|
148
|
+
aria-label={`Remove ${item.title}`}
|
|
149
|
+
>
|
|
150
|
+
<Trash2Icon className="size-3.5" />
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
))
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Order Summary & Checkout */}
|
|
160
|
+
{items.length > 0 && (
|
|
161
|
+
<div className="border-t border-border/80 bg-muted/20 p-5 space-y-3">
|
|
162
|
+
<div className="space-y-1.5 text-xs">
|
|
163
|
+
<div className="flex justify-between text-muted-foreground">
|
|
164
|
+
<span>Subtotal</span>
|
|
165
|
+
<span className="font-semibold text-foreground">
|
|
166
|
+
{currencySymbol}
|
|
167
|
+
{subtotal.toFixed(2)}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
{promoDiscountValue > 0 && (
|
|
171
|
+
<div className="flex justify-between text-emerald-600 dark:text-emerald-400">
|
|
172
|
+
<span>Discount</span>
|
|
173
|
+
<span className="font-semibold">
|
|
174
|
+
-{currencySymbol}
|
|
175
|
+
{promoDiscountValue.toFixed(2)}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
<div className="flex justify-between text-muted-foreground">
|
|
180
|
+
<span>Shipping</span>
|
|
181
|
+
<span className="font-semibold text-foreground">
|
|
182
|
+
{remainingForFreeShipping === 0 ? 'FREE' : `${currencySymbol}9.99`}
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="border-t border-border/60 pt-2 flex justify-between text-sm font-bold text-foreground">
|
|
186
|
+
<span>Estimated Total</span>
|
|
187
|
+
<span>
|
|
188
|
+
{currencySymbol}
|
|
189
|
+
{total.toFixed(2)}
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<Button className="w-full gap-2 shadow-xs" onClick={onCheckout}>
|
|
195
|
+
<span>Proceed to Checkout</span>
|
|
196
|
+
<ArrowRightIcon className="size-4" />
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if (!isDrawer) {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className={cn(
|
|
207
|
+
'rounded-2xl border border-border/80 bg-card overflow-hidden shadow-xs',
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
{content}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="fixed inset-0 z-50 flex justify-end bg-background/80 backdrop-blur-sm animate-in fade-in-0 duration-200">
|
|
218
|
+
<div
|
|
219
|
+
className={cn(
|
|
220
|
+
'w-full max-w-md border-l border-border/80 shadow-2xl animate-in slide-in-from-right duration-300',
|
|
221
|
+
className,
|
|
222
|
+
)}
|
|
223
|
+
>
|
|
224
|
+
{content}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type CartItem = {
|
|
2
|
+
readonly discount?: string
|
|
3
|
+
readonly id: string
|
|
4
|
+
readonly image: string
|
|
5
|
+
readonly inStock?: boolean
|
|
6
|
+
readonly originalPrice?: string
|
|
7
|
+
readonly price: string
|
|
8
|
+
readonly priceValue: number
|
|
9
|
+
readonly quantity: number
|
|
10
|
+
readonly title: string
|
|
11
|
+
readonly variant?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type StorefrontCartProps = {
|
|
15
|
+
readonly className?: string
|
|
16
|
+
readonly currencySymbol?: string
|
|
17
|
+
readonly freeShippingThreshold?: number
|
|
18
|
+
readonly isDrawer?: boolean
|
|
19
|
+
readonly items: readonly CartItem[]
|
|
20
|
+
readonly onCheckout?: () => void
|
|
21
|
+
readonly onClose?: () => void
|
|
22
|
+
readonly onQuantityChange?: (itemId: string, newQuantity: number) => void
|
|
23
|
+
readonly onRemoveItem?: (itemId: string) => void
|
|
24
|
+
readonly open?: boolean
|
|
25
|
+
readonly promoCode?: string
|
|
26
|
+
readonly promoDiscountValue?: number
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type StoreCategoryCard = {
|
|
4
|
+
readonly badge?: string
|
|
5
|
+
readonly href: string
|
|
6
|
+
readonly icon?: ReactNode
|
|
7
|
+
readonly id: string
|
|
8
|
+
readonly image?: string
|
|
9
|
+
readonly itemCount?: number
|
|
10
|
+
readonly subtitle?: string
|
|
11
|
+
readonly tags?: readonly string[]
|
|
12
|
+
readonly title: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CategoryShowcaseProps = {
|
|
16
|
+
readonly categories: readonly StoreCategoryCard[]
|
|
17
|
+
readonly className?: string
|
|
18
|
+
readonly columns?: 2 | 3 | 4 | 6
|
|
19
|
+
readonly description?: string
|
|
20
|
+
readonly onSelectCategory?: (categoryId: string) => void
|
|
21
|
+
readonly title?: string
|
|
22
|
+
readonly variant?: 'cards' | 'minimal' | 'pills'
|
|
23
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ArrowRightIcon } from 'lucide-react'
|
|
2
|
+
import { cn } from '../../../lib/utils'
|
|
3
|
+
import { Badge } from '../../../components/badge'
|
|
4
|
+
import type { CategoryShowcaseProps } from './categories-types'
|
|
5
|
+
|
|
6
|
+
export function CategoryShowcase({
|
|
7
|
+
categories,
|
|
8
|
+
className,
|
|
9
|
+
columns = 4,
|
|
10
|
+
description = 'Browse by catalog department and find curated essentials.',
|
|
11
|
+
onSelectCategory,
|
|
12
|
+
title = 'Shop by Category',
|
|
13
|
+
variant = 'cards',
|
|
14
|
+
}: CategoryShowcaseProps) {
|
|
15
|
+
const colClass = {
|
|
16
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
17
|
+
3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
18
|
+
4: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4',
|
|
19
|
+
6: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6',
|
|
20
|
+
}[columns]
|
|
21
|
+
|
|
22
|
+
if (variant === 'pills') {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('flex flex-wrap items-center gap-2', className)}>
|
|
25
|
+
{categories.map((cat) => (
|
|
26
|
+
<button
|
|
27
|
+
key={cat.id}
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={() => onSelectCategory?.(cat.id)}
|
|
30
|
+
className="group flex items-center gap-2 rounded-full border border-border/80 bg-card px-3.5 py-1.5 text-xs font-semibold text-foreground shadow-xs transition-all hover:border-primary hover:bg-primary/5"
|
|
31
|
+
>
|
|
32
|
+
{cat.icon}
|
|
33
|
+
<span>{cat.title}</span>
|
|
34
|
+
{cat.itemCount !== undefined && (
|
|
35
|
+
<span className="rounded-full bg-muted px-1.5 py-0.2 text-[10px] text-muted-foreground group-hover:text-primary">
|
|
36
|
+
{cat.itemCount}
|
|
37
|
+
</span>
|
|
38
|
+
)}
|
|
39
|
+
</button>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn('space-y-6', className)}>
|
|
47
|
+
{(title || description) && (
|
|
48
|
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
|
|
49
|
+
<div>
|
|
50
|
+
{title && <h3 className="text-xl font-bold tracking-tight text-foreground">{title}</h3>}
|
|
51
|
+
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<div className={cn('grid gap-4', colClass)}>
|
|
57
|
+
{categories.map((cat) => (
|
|
58
|
+
<div
|
|
59
|
+
key={cat.id}
|
|
60
|
+
onClick={() => onSelectCategory?.(cat.id)}
|
|
61
|
+
className="group relative cursor-pointer overflow-hidden rounded-2xl border border-border/80 bg-card p-4 shadow-xs transition-all duration-200 hover:-translate-y-1 hover:border-primary/50 hover:shadow-md"
|
|
62
|
+
>
|
|
63
|
+
{cat.image ? (
|
|
64
|
+
<div className="aspect-4/3 w-full overflow-hidden rounded-xl bg-muted mb-3 relative">
|
|
65
|
+
<img
|
|
66
|
+
src={cat.image}
|
|
67
|
+
alt={cat.title}
|
|
68
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
69
|
+
/>
|
|
70
|
+
{cat.badge && (
|
|
71
|
+
<Badge variant="default" className="absolute top-2 left-2 text-[10px] font-bold">
|
|
72
|
+
{cat.badge}
|
|
73
|
+
</Badge>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="flex items-center justify-between mb-3">
|
|
78
|
+
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary group-hover:text-primary-foreground">
|
|
79
|
+
{cat.icon}
|
|
80
|
+
</div>
|
|
81
|
+
{cat.badge && (
|
|
82
|
+
<Badge variant="default" className="text-[10px] font-bold">
|
|
83
|
+
{cat.badge}
|
|
84
|
+
</Badge>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className="space-y-1">
|
|
90
|
+
<div className="flex items-center justify-between">
|
|
91
|
+
<h4 className="text-sm font-bold text-foreground group-hover:text-primary transition-colors">
|
|
92
|
+
{cat.title}
|
|
93
|
+
</h4>
|
|
94
|
+
<ArrowRightIcon className="size-3.5 text-muted-foreground transition-transform group-hover:translate-x-1 group-hover:text-primary" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{cat.subtitle && (
|
|
98
|
+
<p className="text-xs text-muted-foreground line-clamp-1">{cat.subtitle}</p>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{cat.itemCount !== undefined && (
|
|
102
|
+
<p className="text-[11px] font-medium text-muted-foreground/80">
|
|
103
|
+
{cat.itemCount.toLocaleString()} products
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{cat.tags && cat.tags.length > 0 && (
|
|
108
|
+
<div className="flex flex-wrap gap-1 pt-2">
|
|
109
|
+
{cat.tags.slice(0, 3).map((tag) => (
|
|
110
|
+
<span
|
|
111
|
+
key={tag}
|
|
112
|
+
className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground"
|
|
113
|
+
>
|
|
114
|
+
{tag}
|
|
115
|
+
</span>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type CheckoutAddress = {
|
|
2
|
+
city: string
|
|
3
|
+
country: string
|
|
4
|
+
fullName: string
|
|
5
|
+
phone: string
|
|
6
|
+
postalCode: string
|
|
7
|
+
state: string
|
|
8
|
+
streetAddress: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CheckoutShippingOption = {
|
|
12
|
+
deliveryEstimate: string
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
price: string
|
|
16
|
+
priceValue: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CheckoutPaymentSelection = {
|
|
20
|
+
cvv?: string
|
|
21
|
+
method: 'card' | 'upi' | 'wallet' | 'cod'
|
|
22
|
+
savedCardId?: string
|
|
23
|
+
upiId?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CheckoutItemSummary = {
|
|
27
|
+
id: string
|
|
28
|
+
image: string
|
|
29
|
+
price: string
|
|
30
|
+
quantity: number
|
|
31
|
+
title: string
|
|
32
|
+
variant?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CheckoutWizardProps = {
|
|
36
|
+
readonly className?: string
|
|
37
|
+
readonly currencySymbol?: string
|
|
38
|
+
readonly defaultAddress?: Partial<CheckoutAddress>
|
|
39
|
+
readonly initialStep?: 1 | 2 | 3
|
|
40
|
+
readonly items: readonly CheckoutItemSummary[]
|
|
41
|
+
readonly onPlaceOrder?: (data: {
|
|
42
|
+
address: CheckoutAddress
|
|
43
|
+
payment: CheckoutPaymentSelection
|
|
44
|
+
shipping: CheckoutShippingOption
|
|
45
|
+
}) => void
|
|
46
|
+
readonly shippingOptions?: readonly CheckoutShippingOption[]
|
|
47
|
+
readonly subtotal: number
|
|
48
|
+
}
|