@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,468 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
CheckCircle2Icon,
|
|
4
|
+
CreditCardIcon,
|
|
5
|
+
LockIcon,
|
|
6
|
+
MapPinIcon,
|
|
7
|
+
ShieldCheckIcon,
|
|
8
|
+
TruckIcon,
|
|
9
|
+
} from 'lucide-react'
|
|
10
|
+
import { cn } from '../../../lib/utils'
|
|
11
|
+
import { Button } from '../../../components/button'
|
|
12
|
+
import type {
|
|
13
|
+
CheckoutAddress,
|
|
14
|
+
CheckoutPaymentSelection,
|
|
15
|
+
CheckoutShippingOption,
|
|
16
|
+
CheckoutWizardProps,
|
|
17
|
+
} from './checkout-types'
|
|
18
|
+
|
|
19
|
+
const defaultOptions: readonly CheckoutShippingOption[] = [
|
|
20
|
+
{
|
|
21
|
+
deliveryEstimate: '3–5 business days',
|
|
22
|
+
id: 'standard',
|
|
23
|
+
label: 'Standard Delivery',
|
|
24
|
+
price: 'FREE',
|
|
25
|
+
priceValue: 0,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
deliveryEstimate: '1–2 business days',
|
|
29
|
+
id: 'express',
|
|
30
|
+
label: 'Express Courier',
|
|
31
|
+
price: '$12.00',
|
|
32
|
+
priceValue: 12,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
deliveryEstimate: 'Tomorrow by 12 PM',
|
|
36
|
+
id: 'overnight',
|
|
37
|
+
label: 'Priority Overnight',
|
|
38
|
+
price: '$24.00',
|
|
39
|
+
priceValue: 24,
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export function CheckoutWizard({
|
|
44
|
+
className,
|
|
45
|
+
currencySymbol = '$',
|
|
46
|
+
defaultAddress,
|
|
47
|
+
initialStep = 1,
|
|
48
|
+
items,
|
|
49
|
+
onPlaceOrder,
|
|
50
|
+
shippingOptions = defaultOptions,
|
|
51
|
+
subtotal,
|
|
52
|
+
}: CheckoutWizardProps) {
|
|
53
|
+
const [step, setStep] = useState<1 | 2 | 3>(initialStep)
|
|
54
|
+
const [address, setAddress] = useState<CheckoutAddress>({
|
|
55
|
+
city: defaultAddress?.city ?? 'San Francisco',
|
|
56
|
+
country: defaultAddress?.country ?? 'United States',
|
|
57
|
+
fullName: defaultAddress?.fullName ?? 'Alex Morgan',
|
|
58
|
+
phone: defaultAddress?.phone ?? '+1 (555) 349-2041',
|
|
59
|
+
postalCode: defaultAddress?.postalCode ?? '94107',
|
|
60
|
+
state: defaultAddress?.state ?? 'CA',
|
|
61
|
+
streetAddress: defaultAddress?.streetAddress ?? '450 Mission Street, Suite 800',
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const [selectedShipping, setSelectedShipping] = useState<CheckoutShippingOption>(
|
|
65
|
+
shippingOptions[0] ?? defaultOptions[0]!,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const [payment, setPayment] = useState<CheckoutPaymentSelection>({
|
|
69
|
+
cvv: '884',
|
|
70
|
+
method: 'card',
|
|
71
|
+
savedCardId: 'card-1',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const total = subtotal + selectedShipping.priceValue
|
|
75
|
+
|
|
76
|
+
const handlePlaceOrder = () => {
|
|
77
|
+
onPlaceOrder?.({
|
|
78
|
+
address,
|
|
79
|
+
payment,
|
|
80
|
+
shipping: selectedShipping,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className={cn('grid grid-cols-1 lg:grid-cols-12 gap-8', className)}>
|
|
86
|
+
{/* Left Column: Multi-Step Flow */}
|
|
87
|
+
<div className="lg:col-span-8 space-y-6">
|
|
88
|
+
{/* Step Indicator Header */}
|
|
89
|
+
<div className="flex items-center justify-between rounded-2xl border border-border/80 bg-card p-4 shadow-xs">
|
|
90
|
+
{[
|
|
91
|
+
{ num: 1, title: 'Shipping & Delivery', icon: MapPinIcon },
|
|
92
|
+
{ num: 2, title: 'Payment Method', icon: CreditCardIcon },
|
|
93
|
+
{ num: 3, title: 'Review & Place Order', icon: ShieldCheckIcon },
|
|
94
|
+
].map((s, idx) => {
|
|
95
|
+
const isDone = step > s.num
|
|
96
|
+
const isCurrent = step === s.num
|
|
97
|
+
const Icon = isDone ? CheckCircle2Icon : s.icon
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div key={s.num} className="flex items-center gap-2">
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={() => isDone && setStep(s.num as 1 | 2 | 3)}
|
|
104
|
+
disabled={!isDone}
|
|
105
|
+
className={cn(
|
|
106
|
+
'flex size-8 items-center justify-center rounded-full text-xs font-bold transition-all',
|
|
107
|
+
isDone && 'bg-emerald-600 text-white cursor-pointer',
|
|
108
|
+
isCurrent && 'bg-primary text-primary-foreground shadow-xs',
|
|
109
|
+
!isDone && !isCurrent && 'bg-muted text-muted-foreground',
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<Icon className="size-4" />
|
|
113
|
+
</button>
|
|
114
|
+
<div className="hidden sm:block text-left">
|
|
115
|
+
<div className="text-[10px] uppercase font-bold text-muted-foreground">
|
|
116
|
+
Step {s.num}
|
|
117
|
+
</div>
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
'text-xs font-semibold',
|
|
121
|
+
isCurrent ? 'text-foreground' : 'text-muted-foreground',
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{s.title}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{idx < 2 && <div className="h-px w-8 bg-border hidden md:block ml-2" />}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
})}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{/* Step 1: Shipping Address & Delivery Speed */}
|
|
134
|
+
{step === 1 && (
|
|
135
|
+
<div className="space-y-6 rounded-2xl border border-border/80 bg-card p-6 shadow-xs">
|
|
136
|
+
<div className="flex items-center justify-between border-b border-border/60 pb-4">
|
|
137
|
+
<div>
|
|
138
|
+
<h3 className="text-base font-bold text-foreground">Shipping Destination</h3>
|
|
139
|
+
<p className="text-xs text-muted-foreground">Where should we deliver your order?</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
|
|
144
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
145
|
+
<label className="font-semibold text-foreground">Full Name</label>
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
value={address.fullName}
|
|
149
|
+
onChange={(e) => setAddress({ ...address, fullName: e.target.value })}
|
|
150
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
155
|
+
<label className="font-semibold text-foreground">Street Address</label>
|
|
156
|
+
<input
|
|
157
|
+
type="text"
|
|
158
|
+
value={address.streetAddress}
|
|
159
|
+
onChange={(e) => setAddress({ ...address, streetAddress: e.target.value })}
|
|
160
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="space-y-1.5">
|
|
165
|
+
<label className="font-semibold text-foreground">City</label>
|
|
166
|
+
<input
|
|
167
|
+
type="text"
|
|
168
|
+
value={address.city}
|
|
169
|
+
onChange={(e) => setAddress({ ...address, city: e.target.value })}
|
|
170
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div className="space-y-1.5">
|
|
175
|
+
<label className="font-semibold text-foreground">State / Region</label>
|
|
176
|
+
<input
|
|
177
|
+
type="text"
|
|
178
|
+
value={address.state}
|
|
179
|
+
onChange={(e) => setAddress({ ...address, state: e.target.value })}
|
|
180
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="space-y-1.5">
|
|
185
|
+
<label className="font-semibold text-foreground">Postal / Zip Code</label>
|
|
186
|
+
<input
|
|
187
|
+
type="text"
|
|
188
|
+
value={address.postalCode}
|
|
189
|
+
onChange={(e) => setAddress({ ...address, postalCode: e.target.value })}
|
|
190
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="space-y-1.5">
|
|
195
|
+
<label className="font-semibold text-foreground">Phone Number</label>
|
|
196
|
+
<input
|
|
197
|
+
type="text"
|
|
198
|
+
value={address.phone}
|
|
199
|
+
onChange={(e) => setAddress({ ...address, phone: e.target.value })}
|
|
200
|
+
className="w-full rounded-lg border border-input bg-muted/20 px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Shipping Speed Options */}
|
|
206
|
+
<div className="pt-4 border-t border-border/60 space-y-3">
|
|
207
|
+
<label className="text-xs font-bold text-foreground">Select Delivery Speed</label>
|
|
208
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
209
|
+
{shippingOptions.map((opt) => (
|
|
210
|
+
<button
|
|
211
|
+
key={opt.id}
|
|
212
|
+
type="button"
|
|
213
|
+
onClick={() => setSelectedShipping(opt)}
|
|
214
|
+
className={cn(
|
|
215
|
+
'flex flex-col text-left p-3.5 rounded-xl border transition-all text-xs',
|
|
216
|
+
selectedShipping.id === opt.id
|
|
217
|
+
? 'border-primary bg-primary/5 ring-1 ring-primary'
|
|
218
|
+
: 'border-border/80 bg-muted/20 hover:border-border',
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
221
|
+
<div className="flex items-center justify-between font-bold text-foreground">
|
|
222
|
+
<span>{opt.label}</span>
|
|
223
|
+
<span className="text-primary">{opt.price}</span>
|
|
224
|
+
</div>
|
|
225
|
+
<span className="text-[11px] text-muted-foreground mt-1">
|
|
226
|
+
{opt.deliveryEstimate}
|
|
227
|
+
</span>
|
|
228
|
+
</button>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="flex justify-end pt-4">
|
|
234
|
+
<Button onClick={() => setStep(2)}>Continue to Payment</Button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Step 2: Payment Method */}
|
|
240
|
+
{step === 2 && (
|
|
241
|
+
<div className="space-y-6 rounded-2xl border border-border/80 bg-card p-6 shadow-xs">
|
|
242
|
+
<div className="flex items-center justify-between border-b border-border/60 pb-4">
|
|
243
|
+
<div>
|
|
244
|
+
<h3 className="text-base font-bold text-foreground">Payment Method</h3>
|
|
245
|
+
<p className="text-xs text-muted-foreground">
|
|
246
|
+
Transactions are 256-bit encrypted and secured.
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="flex items-center gap-1 text-emerald-600 text-xs font-semibold">
|
|
250
|
+
<LockIcon className="size-3.5" />
|
|
251
|
+
<span>SSL Encrypted</span>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Payment Method Tabs */}
|
|
256
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
257
|
+
{[
|
|
258
|
+
{ id: 'card', label: 'Credit Card' },
|
|
259
|
+
{ id: 'upi', label: 'UPI / QR' },
|
|
260
|
+
{ id: 'wallet', label: 'Wallets' },
|
|
261
|
+
{ id: 'cod', label: 'Cash on Delivery' },
|
|
262
|
+
].map((m) => (
|
|
263
|
+
<button
|
|
264
|
+
key={m.id}
|
|
265
|
+
type="button"
|
|
266
|
+
onClick={() =>
|
|
267
|
+
setPayment({ ...payment, method: m.id as CheckoutPaymentSelection['method'] })
|
|
268
|
+
}
|
|
269
|
+
className={cn(
|
|
270
|
+
'rounded-xl border p-3 text-xs font-bold transition-all text-center',
|
|
271
|
+
payment.method === m.id
|
|
272
|
+
? 'border-primary bg-primary/10 text-primary ring-1 ring-primary'
|
|
273
|
+
: 'border-border/80 bg-muted/20 text-muted-foreground hover:text-foreground',
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
{m.label}
|
|
277
|
+
</button>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Card Form */}
|
|
282
|
+
{payment.method === 'card' && (
|
|
283
|
+
<div className="rounded-xl border border-border/80 bg-muted/20 p-4 space-y-3 text-xs">
|
|
284
|
+
<div className="space-y-1">
|
|
285
|
+
<label className="font-semibold text-foreground">Card Number</label>
|
|
286
|
+
<input
|
|
287
|
+
type="text"
|
|
288
|
+
disabled
|
|
289
|
+
value="•••• •••• •••• 4242 (Visa Platinum)"
|
|
290
|
+
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-foreground opacity-80"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="grid grid-cols-2 gap-3">
|
|
294
|
+
<div className="space-y-1">
|
|
295
|
+
<label className="font-semibold text-foreground">Expiry Date</label>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
disabled
|
|
299
|
+
value="08 / 29"
|
|
300
|
+
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-foreground opacity-80"
|
|
301
|
+
/>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="space-y-1">
|
|
304
|
+
<label className="font-semibold text-foreground">CVV Security Code</label>
|
|
305
|
+
<input
|
|
306
|
+
type="password"
|
|
307
|
+
maxLength={4}
|
|
308
|
+
value={payment.cvv}
|
|
309
|
+
onChange={(e) => setPayment({ ...payment, cvv: e.target.value })}
|
|
310
|
+
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{payment.method === 'upi' && (
|
|
318
|
+
<div className="rounded-xl border border-border/80 bg-muted/20 p-4 text-xs space-y-2">
|
|
319
|
+
<label className="font-semibold text-foreground">
|
|
320
|
+
Enter UPI Virtual Payment Address (VPA)
|
|
321
|
+
</label>
|
|
322
|
+
<input
|
|
323
|
+
type="text"
|
|
324
|
+
placeholder="username@okaxis or mobile@upi"
|
|
325
|
+
value={payment.upiId ?? ''}
|
|
326
|
+
onChange={(e) => setPayment({ ...payment, upiId: e.target.value })}
|
|
327
|
+
className="w-full rounded-lg border border-input bg-background px-3 py-2 text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
<div className="flex justify-between pt-4">
|
|
333
|
+
<Button variant="outline" onClick={() => setStep(1)}>
|
|
334
|
+
Back to Shipping
|
|
335
|
+
</Button>
|
|
336
|
+
<Button onClick={() => setStep(3)}>Review Order</Button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{/* Step 3: Review & Place Order */}
|
|
342
|
+
{step === 3 && (
|
|
343
|
+
<div className="space-y-6 rounded-2xl border border-border/80 bg-card p-6 shadow-xs">
|
|
344
|
+
<div className="border-b border-border/60 pb-4">
|
|
345
|
+
<h3 className="text-base font-bold text-foreground">Final Order Review</h3>
|
|
346
|
+
<p className="text-xs text-muted-foreground">
|
|
347
|
+
Please double check your shipping and payment selection.
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
|
|
352
|
+
<div className="rounded-xl border border-border/80 bg-muted/20 p-3.5 space-y-1">
|
|
353
|
+
<div className="font-bold text-foreground flex items-center justify-between">
|
|
354
|
+
<span>Shipping Address</span>
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onClick={() => setStep(1)}
|
|
358
|
+
className="text-primary hover:underline"
|
|
359
|
+
>
|
|
360
|
+
Edit
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="text-muted-foreground">
|
|
364
|
+
<p className="font-semibold text-foreground">{address.fullName}</p>
|
|
365
|
+
<p>{address.streetAddress}</p>
|
|
366
|
+
<p>
|
|
367
|
+
{address.city}, {address.state} {address.postalCode}
|
|
368
|
+
</p>
|
|
369
|
+
<p>{address.phone}</p>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div className="rounded-xl border border-border/80 bg-muted/20 p-3.5 space-y-1">
|
|
374
|
+
<div className="font-bold text-foreground flex items-center justify-between">
|
|
375
|
+
<span>Payment & Delivery</span>
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
onClick={() => setStep(2)}
|
|
379
|
+
className="text-primary hover:underline"
|
|
380
|
+
>
|
|
381
|
+
Edit
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="text-muted-foreground">
|
|
385
|
+
<p className="font-semibold text-foreground capitalize">
|
|
386
|
+
Method: {payment.method}
|
|
387
|
+
</p>
|
|
388
|
+
<p>Delivery: {selectedShipping.label}</p>
|
|
389
|
+
<p>Speed: {selectedShipping.deliveryEstimate}</p>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div className="flex justify-between pt-4">
|
|
395
|
+
<Button variant="outline" onClick={() => setStep(2)}>
|
|
396
|
+
Back to Payment
|
|
397
|
+
</Button>
|
|
398
|
+
<Button size="lg" className="gap-2 px-6" onClick={handlePlaceOrder}>
|
|
399
|
+
<ShieldCheckIcon className="size-4" />
|
|
400
|
+
<span>
|
|
401
|
+
Place Order ({currencySymbol}
|
|
402
|
+
{total.toFixed(2)})
|
|
403
|
+
</span>
|
|
404
|
+
</Button>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{/* Right Column: Order Items Summary */}
|
|
411
|
+
<div className="lg:col-span-4 space-y-4">
|
|
412
|
+
<div className="rounded-2xl border border-border/80 bg-card p-5 shadow-xs space-y-4">
|
|
413
|
+
<div className="flex items-center justify-between border-b border-border/60 pb-3">
|
|
414
|
+
<h4 className="text-sm font-bold text-foreground">Order Summary</h4>
|
|
415
|
+
<span className="text-xs text-muted-foreground">{items.length} items</span>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div className="space-y-3 max-h-72 overflow-y-auto">
|
|
419
|
+
{items.map((it) => (
|
|
420
|
+
<div key={it.id} className="flex items-center gap-3 text-xs">
|
|
421
|
+
<img
|
|
422
|
+
src={it.image}
|
|
423
|
+
alt={it.title}
|
|
424
|
+
className="size-12 rounded-lg object-cover bg-muted border"
|
|
425
|
+
/>
|
|
426
|
+
<div className="flex-1 min-w-0">
|
|
427
|
+
<div className="font-semibold text-foreground truncate">{it.title}</div>
|
|
428
|
+
<div className="text-[11px] text-muted-foreground">Qty: {it.quantity}</div>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="font-bold text-foreground">{it.price}</div>
|
|
431
|
+
</div>
|
|
432
|
+
))}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div className="border-t border-border/60 pt-3 space-y-2 text-xs">
|
|
436
|
+
<div className="flex justify-between text-muted-foreground">
|
|
437
|
+
<span>Subtotal</span>
|
|
438
|
+
<span className="font-semibold text-foreground">
|
|
439
|
+
{currencySymbol}
|
|
440
|
+
{subtotal.toFixed(2)}
|
|
441
|
+
</span>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="flex justify-between text-muted-foreground">
|
|
444
|
+
<span>Shipping</span>
|
|
445
|
+
<span className="font-semibold text-foreground">{selectedShipping.price}</span>
|
|
446
|
+
</div>
|
|
447
|
+
<div className="border-t border-border/60 pt-2 flex justify-between text-base font-bold text-foreground">
|
|
448
|
+
<span>Total</span>
|
|
449
|
+
<span className="text-primary">
|
|
450
|
+
{currencySymbol}
|
|
451
|
+
{total.toFixed(2)}
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{/* Guarantee Banner */}
|
|
458
|
+
<div className="rounded-xl border border-border/60 bg-muted/30 p-3.5 text-xs flex items-center gap-3">
|
|
459
|
+
<TruckIcon className="size-5 text-primary shrink-0" />
|
|
460
|
+
<div className="text-muted-foreground">
|
|
461
|
+
<strong className="text-foreground">Free Returns within 30 days</strong>. If you are not
|
|
462
|
+
satisfied, return for a full refund.
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
)
|
|
468
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ComparisonProduct = {
|
|
2
|
+
readonly attributes: Record<string, string | boolean | number>
|
|
3
|
+
readonly badge?: string
|
|
4
|
+
readonly id: string
|
|
5
|
+
readonly image: string
|
|
6
|
+
readonly inStock: boolean
|
|
7
|
+
readonly price: string
|
|
8
|
+
readonly rating: number
|
|
9
|
+
readonly reviewsCount?: number
|
|
10
|
+
readonly title: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ComparisonFeatureGroup = {
|
|
14
|
+
readonly features: readonly {
|
|
15
|
+
readonly description?: string
|
|
16
|
+
readonly key: string
|
|
17
|
+
readonly label: string
|
|
18
|
+
}[]
|
|
19
|
+
readonly groupName: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ProductComparisonProps = {
|
|
23
|
+
readonly className?: string
|
|
24
|
+
readonly featureGroups: readonly ComparisonFeatureGroup[]
|
|
25
|
+
readonly onAddToCart?: (productId: string) => void
|
|
26
|
+
readonly onRemoveProduct?: (productId: string) => void
|
|
27
|
+
readonly products: readonly ComparisonProduct[]
|
|
28
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { CheckIcon, ShoppingBagIcon, StarIcon, XIcon } from 'lucide-react'
|
|
3
|
+
import { cn } from '../../../lib/utils'
|
|
4
|
+
import { Button } from '../../../components/button'
|
|
5
|
+
import { Badge } from '../../../components/badge'
|
|
6
|
+
import type { ProductComparisonProps } from './comparison-types'
|
|
7
|
+
|
|
8
|
+
export function ProductComparison({
|
|
9
|
+
className,
|
|
10
|
+
featureGroups,
|
|
11
|
+
onAddToCart,
|
|
12
|
+
onRemoveProduct,
|
|
13
|
+
products,
|
|
14
|
+
}: ProductComparisonProps) {
|
|
15
|
+
const [highlightDifferences, setHighlightDifferences] = useState(false)
|
|
16
|
+
|
|
17
|
+
if (products.length === 0) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex flex-col items-center justify-center p-12 text-center rounded-2xl border border-border/80 bg-card">
|
|
20
|
+
<p className="text-sm font-semibold text-foreground">No products selected for comparison</p>
|
|
21
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
22
|
+
Select 2 or more products from the catalog to compare features.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isDifference = (key: string) => {
|
|
29
|
+
if (products.length <= 1) return false
|
|
30
|
+
const firstVal = products[0]?.attributes[key]
|
|
31
|
+
return products.some((p) => p.attributes[key] !== firstVal)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={cn('space-y-4', className)}>
|
|
36
|
+
{/* Controls */}
|
|
37
|
+
<div className="flex items-center justify-between">
|
|
38
|
+
<div className="flex items-center gap-2">
|
|
39
|
+
<span className="text-xs font-semibold text-foreground">
|
|
40
|
+
Comparing {products.length} Products
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<label className="flex items-center gap-2 cursor-pointer text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
45
|
+
<input
|
|
46
|
+
type="checkbox"
|
|
47
|
+
checked={highlightDifferences}
|
|
48
|
+
onChange={(e) => setHighlightDifferences(e.target.checked)}
|
|
49
|
+
className="rounded border-border text-primary focus:ring-primary size-3.5"
|
|
50
|
+
/>
|
|
51
|
+
<span>Highlight Differences</span>
|
|
52
|
+
</label>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Comparison Table */}
|
|
56
|
+
<div className="overflow-x-auto rounded-2xl border border-border/80 bg-card shadow-xs">
|
|
57
|
+
<table className="w-full border-collapse text-left text-xs">
|
|
58
|
+
<thead>
|
|
59
|
+
<tr className="border-b border-border/80 bg-muted/20">
|
|
60
|
+
<th className="w-48 p-4 font-bold text-foreground">Overview</th>
|
|
61
|
+
{products.map((p) => (
|
|
62
|
+
<th key={p.id} className="min-w-56 p-4 align-top">
|
|
63
|
+
<div className="relative flex flex-col space-y-2">
|
|
64
|
+
{onRemoveProduct && (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onRemoveProduct(p.id)}
|
|
68
|
+
className="absolute -top-2 -right-2 rounded-full bg-muted p-1 text-muted-foreground hover:bg-destructive hover:text-destructive-foreground transition-colors"
|
|
69
|
+
aria-label={`Remove ${p.title}`}
|
|
70
|
+
>
|
|
71
|
+
<XIcon className="size-3" />
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<img
|
|
76
|
+
src={p.image}
|
|
77
|
+
alt={p.title}
|
|
78
|
+
className="aspect-square w-full rounded-xl object-cover bg-muted border border-border/40"
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
{p.badge && (
|
|
82
|
+
<Badge variant="secondary" className="w-fit text-[10px]">
|
|
83
|
+
{p.badge}
|
|
84
|
+
</Badge>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
<h4 className="font-bold text-foreground text-sm line-clamp-1">{p.title}</h4>
|
|
88
|
+
<div className="text-base font-bold text-primary">{p.price}</div>
|
|
89
|
+
|
|
90
|
+
{/* Rating */}
|
|
91
|
+
<div className="flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
92
|
+
<StarIcon className="size-3.5 fill-amber-400 text-amber-400" />
|
|
93
|
+
<span className="font-semibold text-foreground">{p.rating.toFixed(1)}</span>
|
|
94
|
+
{p.reviewsCount && <span>({p.reviewsCount})</span>}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<Button
|
|
98
|
+
size="sm"
|
|
99
|
+
className="w-full gap-1.5 mt-2 text-xs"
|
|
100
|
+
disabled={!p.inStock}
|
|
101
|
+
onClick={() => onAddToCart?.(p.id)}
|
|
102
|
+
>
|
|
103
|
+
<ShoppingBagIcon className="size-3.5" />
|
|
104
|
+
<span>{p.inStock ? 'Add to Cart' : 'Out of Stock'}</span>
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</th>
|
|
108
|
+
))}
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
|
|
112
|
+
<tbody>
|
|
113
|
+
{featureGroups.map((group) => (
|
|
114
|
+
<tr key={group.groupName} className="contents">
|
|
115
|
+
<td
|
|
116
|
+
colSpan={products.length + 1}
|
|
117
|
+
className="bg-muted/50 px-4 py-2 font-bold uppercase tracking-wider text-[11px] text-muted-foreground border-y border-border/80"
|
|
118
|
+
>
|
|
119
|
+
{group.groupName}
|
|
120
|
+
</td>
|
|
121
|
+
|
|
122
|
+
{group.features.map((feat) => {
|
|
123
|
+
const hasDiff = isDifference(feat.key)
|
|
124
|
+
const rowBg =
|
|
125
|
+
highlightDifferences && hasDiff ? 'bg-amber-500/10 dark:bg-amber-500/15' : ''
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<tr
|
|
129
|
+
key={feat.key}
|
|
130
|
+
className={cn('border-b border-border/40 transition-colors', rowBg)}
|
|
131
|
+
>
|
|
132
|
+
<td className="p-4 font-semibold text-foreground align-middle">
|
|
133
|
+
<div>{feat.label}</div>
|
|
134
|
+
{feat.description && (
|
|
135
|
+
<div className="text-[10px] text-muted-foreground font-normal">
|
|
136
|
+
{feat.description}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
</td>
|
|
140
|
+
|
|
141
|
+
{products.map((p) => {
|
|
142
|
+
const val = p.attributes[feat.key]
|
|
143
|
+
return (
|
|
144
|
+
<td key={p.id} className="p-4 text-muted-foreground align-middle">
|
|
145
|
+
{typeof val === 'boolean' ? (
|
|
146
|
+
val ? (
|
|
147
|
+
<CheckIcon className="size-4 text-emerald-600 font-bold" />
|
|
148
|
+
) : (
|
|
149
|
+
<XIcon className="size-4 text-muted-foreground/40" />
|
|
150
|
+
)
|
|
151
|
+
) : (
|
|
152
|
+
<span
|
|
153
|
+
className={cn(
|
|
154
|
+
'font-medium',
|
|
155
|
+
val ? 'text-foreground' : 'text-muted-foreground/50',
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{val ?? '—'}
|
|
159
|
+
</span>
|
|
160
|
+
)}
|
|
161
|
+
</td>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</tr>
|
|
165
|
+
)
|
|
166
|
+
})}
|
|
167
|
+
</tr>
|
|
168
|
+
))}
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|