@hanzo/ui 5.0.2 → 5.1.0
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/dist/accordion.js +1 -0
- package/dist/accordion.mjs +1 -0
- package/dist/alert-dialog.js +1 -0
- package/dist/alert-dialog.mjs +1 -0
- package/dist/alert.js +1 -0
- package/dist/alert.mjs +1 -0
- package/dist/avatar.js +1 -0
- package/dist/avatar.mjs +1 -0
- package/dist/badge.js +1 -0
- package/dist/badge.mjs +1 -0
- package/dist/breadcrumb.js +1 -0
- package/dist/breadcrumb.mjs +1 -0
- package/dist/calendar.js +1 -0
- package/dist/calendar.mjs +1 -0
- package/dist/carousel.js +1 -0
- package/dist/carousel.mjs +1 -0
- package/dist/checkbox.js +1 -0
- package/dist/checkbox.mjs +1 -0
- package/dist/chunk-3H5S2OQ3.mjs +1 -0
- package/dist/chunk-5GRJ7UQX.js +1 -0
- package/dist/chunk-63HNMH7C.js +1 -0
- package/dist/chunk-72TOQ4DM.mjs +1 -0
- package/dist/chunk-7AEFTV5R.mjs +1 -0
- package/dist/chunk-7M4AVV2R.js +1 -0
- package/dist/chunk-DKPVJSBC.js +1 -0
- package/dist/chunk-EI7MMDWY.js +1 -0
- package/dist/chunk-GANGDIZG.mjs +1 -0
- package/dist/chunk-GRGT2Z4K.js +1 -0
- package/dist/chunk-JCUUC6NY.mjs +1 -0
- package/dist/chunk-JUQMWLIN.js +1 -0
- package/dist/chunk-PRVEIITE.js +1 -0
- package/dist/chunk-SH52AKNZ.js +1 -0
- package/dist/chunk-TU67EJEW.mjs +1 -0
- package/dist/chunk-WN5KN73U.mjs +1 -0
- package/dist/chunk-YSXGDEY5.mjs +1 -0
- package/dist/chunk-Z76OOVUE.mjs +1 -0
- package/dist/collapsible.js +1 -0
- package/dist/collapsible.mjs +1 -0
- package/dist/command.js +1 -0
- package/dist/command.mjs +1 -0
- package/dist/context-menu.js +1 -0
- package/dist/context-menu.mjs +1 -0
- package/dist/dialog.js +1 -0
- package/dist/dialog.mjs +1 -0
- package/dist/drawer.js +1 -0
- package/dist/drawer.mjs +1 -0
- package/dist/dropdown-menu.js +1 -0
- package/dist/dropdown-menu.mjs +1 -0
- package/dist/form.js +1 -0
- package/dist/form.mjs +1 -0
- package/dist/hover-card.js +1 -0
- package/dist/hover-card.mjs +1 -0
- package/dist/index.js +1 -9079
- package/dist/index.mjs +1 -8700
- package/dist/input-otp.js +1 -0
- package/dist/input-otp.mjs +1 -0
- package/dist/lib/utils.js +1 -0
- package/dist/lib/utils.mjs +1 -0
- package/dist/navigation-menu.js +1 -0
- package/dist/navigation-menu.mjs +1 -0
- package/dist/popover.js +1 -0
- package/dist/popover.mjs +1 -0
- package/dist/progress.js +1 -0
- package/dist/progress.mjs +1 -0
- package/dist/radio-group.js +1 -0
- package/dist/radio-group.mjs +1 -0
- package/dist/resizable.js +1 -0
- package/dist/resizable.mjs +1 -0
- package/dist/scroll-area.js +1 -0
- package/dist/scroll-area.mjs +1 -0
- package/dist/select.js +1 -0
- package/dist/select.mjs +1 -0
- package/dist/separator.js +1 -0
- package/dist/separator.mjs +1 -0
- package/dist/sheet.js +1 -0
- package/dist/sheet.mjs +1 -0
- package/dist/skeleton.js +1 -0
- package/dist/skeleton.mjs +1 -0
- package/dist/slider.js +1 -0
- package/dist/slider.mjs +1 -0
- package/dist/sonner.js +1 -0
- package/dist/sonner.mjs +1 -0
- package/dist/src/utils.js +1 -0
- package/dist/src/utils.mjs +1 -0
- package/dist/switch.js +1 -0
- package/dist/switch.mjs +1 -0
- package/dist/table.js +1 -0
- package/dist/table.mjs +1 -0
- package/dist/tabs.js +1 -0
- package/dist/tabs.mjs +1 -0
- package/dist/tailwind/index.js +1 -0
- package/dist/tailwind/index.mjs +1 -0
- package/dist/textarea.js +1 -0
- package/dist/textarea.mjs +1 -0
- package/dist/toggle-group.js +1 -0
- package/dist/toggle-group.mjs +1 -0
- package/dist/toggle.js +1 -0
- package/dist/toggle.mjs +1 -0
- package/dist/tooltip.js +1 -0
- package/dist/tooltip.mjs +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.mjs +1 -0
- package/package.json +110 -81
- package/assets/ai-icons.tsx +0 -207
- package/assets/crypto.tsx +0 -33
- package/assets/file-type-icon.tsx +0 -66
- package/assets/file.tsx +0 -45
- package/assets/general.tsx +0 -2318
- package/assets/hanzo-logo.svg +0 -9
- package/assets/hanzo-logo.tsx +0 -17
- package/assets/index.ts +0 -122
- package/assets/index.tsx +0 -4
- package/assets/llm-provider.tsx +0 -1094
- package/blocks/auth/index.ts +0 -6
- package/blocks/auth/login-2fa.tsx +0 -165
- package/blocks/auth/login-basic.tsx +0 -94
- package/blocks/auth/login-social.tsx +0 -148
- package/blocks/auth/magic-link.tsx +0 -129
- package/blocks/auth/password-reset.tsx +0 -97
- package/blocks/auth/signup.tsx +0 -157
- package/blocks/components/accordian-block.tsx +0 -48
- package/blocks/components/block-component-props.ts +0 -11
- package/blocks/components/bullet-cards-block.tsx +0 -46
- package/blocks/components/card-block/index.tsx +0 -171
- package/blocks/components/card-block/link-out-button.tsx +0 -20
- package/blocks/components/card-block/util.ts +0 -28
- package/blocks/components/carte-blanche-block/index.tsx +0 -127
- package/blocks/components/carte-blanche-block/variant-content-left.tsx +0 -49
- package/blocks/components/content.tsx +0 -70
- package/blocks/components/cta-block.tsx +0 -115
- package/blocks/components/enh-heading-block.tsx +0 -204
- package/blocks/components/grid-block/grid-block-mutator.ts +0 -12
- package/blocks/components/grid-block/index.tsx +0 -83
- package/blocks/components/grid-block/mutator-registry.ts +0 -10
- package/blocks/components/grid-block/table-borders.mutator.ts +0 -47
- package/blocks/components/group-block.tsx +0 -83
- package/blocks/components/heading-block.tsx +0 -88
- package/blocks/components/image-block.tsx +0 -111
- package/blocks/components/index.ts +0 -30
- package/blocks/components/screenful-block/content.tsx +0 -123
- package/blocks/components/screenful-block/index.tsx +0 -107
- package/blocks/components/screenful-block/poster-background.tsx +0 -34
- package/blocks/components/screenful-block/video-background.tsx +0 -45
- package/blocks/components/space-block.tsx +0 -66
- package/blocks/components/video-block.tsx +0 -138
- package/blocks/data-display/activity-feed.tsx +0 -242
- package/blocks/data-display/data-table.tsx +0 -235
- package/blocks/data-display/stats-grid.tsx +0 -194
- package/blocks/def/accordian-block.ts +0 -14
- package/blocks/def/block.ts +0 -7
- package/blocks/def/bullet-cards-block.ts +0 -22
- package/blocks/def/card-block.ts +0 -22
- package/blocks/def/carte-blanche-block.ts +0 -21
- package/blocks/def/cta-block.ts +0 -19
- package/blocks/def/element-block.ts +0 -11
- package/blocks/def/enh-heading-block.ts +0 -44
- package/blocks/def/grid-block.ts +0 -16
- package/blocks/def/group-block.ts +0 -11
- package/blocks/def/heading-block.ts +0 -15
- package/blocks/def/image-block.ts +0 -31
- package/blocks/def/index.ts +0 -35
- package/blocks/def/screenful-block.ts +0 -54
- package/blocks/def/space-block.ts +0 -64
- package/blocks/def/video-block.ts +0 -9
- package/blocks/ecommerce/checkout.tsx +0 -242
- package/blocks/ecommerce/index.ts +0 -7
- package/blocks/ecommerce/product-detail.tsx +0 -257
- package/blocks/ecommerce/product-grid.tsx +0 -148
- package/blocks/ecommerce/shopping-cart.tsx +0 -181
- package/blocks/index.ts +0 -2
- package/blocks/marketing/cta-section.tsx +0 -207
- package/blocks/marketing/faq.tsx +0 -159
- package/blocks/marketing/features-grid.tsx +0 -156
- package/blocks/marketing/hero-section.tsx +0 -192
- package/blocks/marketing/index.ts +0 -6
- package/blocks/marketing/pricing-table.tsx +0 -121
- package/blocks/marketing/testimonials.tsx +0 -196
- package/components/index.ts +0 -9
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/tailwind.js +0 -2025
- package/dist/tailwind.js.map +0 -1
- package/dist/tailwind.mjs +0 -2013
- package/dist/tailwind.mjs.map +0 -1
- package/dist/types.js +0 -59
- package/dist/types.js.map +0 -1
- package/dist/types.mjs +0 -53
- package/dist/types.mjs.map +0 -1
- package/dist/utils.js +0 -30
- package/dist/utils.js.map +0 -1
- package/dist/utils.mjs +0 -26
- package/dist/utils.mjs.map +0 -1
- package/frameworks/core/index.ts +0 -6
- package/frameworks/core/utils/index.ts +0 -64
- package/frameworks/react/components/button.tsx +0 -26
- package/frameworks/react/components/index.ts +0 -5
- package/frameworks/react/hooks/index.ts +0 -5
- package/frameworks/react/index.ts +0 -9
- package/frameworks/react/package.json +0 -8
- package/frameworks/react/utils/index.ts +0 -2
- package/frameworks/react-native/index.ts +0 -9
- package/frameworks/react-native/package.json +0 -8
- package/frameworks/registry.json +0 -371
- package/frameworks/setup.sh +0 -69
- package/frameworks/svelte/index.ts +0 -9
- package/frameworks/svelte/package.json +0 -8
- package/frameworks/tracker.json +0 -1854
- package/frameworks/vue/index.ts +0 -9
- package/frameworks/vue/package.json +0 -8
- package/helpers/file.ts +0 -33
- package/helpers/memoization.ts +0 -40
- package/primitives/accordion.tsx +0 -74
- package/primitives/action-button.tsx +0 -42
- package/primitives/alert-dialog.tsx +0 -185
- package/primitives/alert.tsx +0 -74
- package/primitives/apply-typography.tsx +0 -55
- package/primitives/aspect-ratio.tsx +0 -5
- package/primitives/avatar.tsx +0 -57
- package/primitives/background-beams.tsx +0 -142
- package/primitives/badge.tsx +0 -45
- package/primitives/breadcrumb.tsx +0 -130
- package/primitives/breakpoint-indicator.tsx +0 -19
- package/primitives/button.tsx +0 -72
- package/primitives/calendar.tsx +0 -72
- package/primitives/card.tsx +0 -97
- package/primitives/carousel.tsx +0 -238
- package/primitives/chat/chat-input-area.tsx +0 -88
- package/primitives/chat/chat-input.tsx +0 -71
- package/primitives/chat/files-preview.tsx +0 -331
- package/primitives/chat/index.ts +0 -6
- package/primitives/chat/json-form.tsx +0 -8
- package/primitives/chat/message-list.tsx +0 -308
- package/primitives/chat/message.tsx +0 -569
- package/primitives/chat/sqlite-preview.tsx +0 -215
- package/primitives/checkbox.tsx +0 -32
- package/primitives/collapsible.tsx +0 -9
- package/primitives/combobox.tsx +0 -239
- package/primitives/command.tsx +0 -151
- package/primitives/context-menu.tsx +0 -206
- package/primitives/copy-to-clipboard-icon.tsx +0 -60
- package/primitives/dialog-video-controller.tsx +0 -38
- package/primitives/dialog.tsx +0 -128
- package/primitives/dot-pattern.tsx +0 -57
- package/primitives/dots-loader.tsx +0 -13
- package/primitives/drawer.tsx +0 -113
- package/primitives/dropdown-menu.tsx +0 -199
- package/primitives/error-message.tsx +0 -19
- package/primitives/file-uploader.tsx +0 -203
- package/primitives/form.tsx +0 -185
- package/primitives/hover-card.tsx +0 -28
- package/primitives/icons/github.tsx +0 -14
- package/primitives/icons/index.ts +0 -18
- package/primitives/icons/youtube-logo.tsx +0 -59
- package/primitives/index-client.ts +0 -4
- package/primitives/index-common.ts +0 -304
- package/primitives/index-next.ts +0 -4
- package/primitives/input-otp.tsx +0 -65
- package/primitives/input.tsx +0 -128
- package/primitives/label.tsx +0 -21
- package/primitives/list-adaptor.ts +0 -12
- package/primitives/list-box.tsx +0 -74
- package/primitives/loading-spinner.tsx +0 -33
- package/primitives/markdown-preview.tsx +0 -612
- package/primitives/mermaid.tsx +0 -196
- package/primitives/navigation-menu.tsx +0 -147
- package/primitives/next/image.tsx +0 -91
- package/primitives/next/index.ts +0 -7
- package/primitives/next/inline-icon.tsx +0 -36
- package/primitives/next/link-element.tsx +0 -109
- package/primitives/next/mdx-link.tsx +0 -22
- package/primitives/next/media-stack.tsx +0 -52
- package/primitives/next/nav-items.tsx +0 -45
- package/primitives/next/youtube-embed.tsx +0 -83
- package/primitives/pagination.tsx +0 -117
- package/primitives/popover.tsx +0 -34
- package/primitives/pretty-json-print.tsx +0 -28
- package/primitives/progress.tsx +0 -27
- package/primitives/prompt-textarea.tsx +0 -72
- package/primitives/qr-code.tsx +0 -112
- package/primitives/radio-group.tsx +0 -42
- package/primitives/resizable.tsx +0 -47
- package/primitives/scroll-area.tsx +0 -57
- package/primitives/search-input.tsx +0 -66
- package/primitives/select.tsx +0 -122
- package/primitives/separator.tsx +0 -26
- package/primitives/sheet.tsx +0 -139
- package/primitives/skeleton.tsx +0 -18
- package/primitives/slider.tsx +0 -63
- package/primitives/sonner.tsx +0 -35
- package/primitives/step-indicator.tsx +0 -69
- package/primitives/stepper.tsx +0 -272
- package/primitives/switch.tsx +0 -27
- package/primitives/table.tsx +0 -105
- package/primitives/tabs.tsx +0 -50
- package/primitives/text-area.tsx +0 -26
- package/primitives/text-link.tsx +0 -27
- package/primitives/textarea.tsx +0 -64
- package/primitives/textfield.tsx +0 -78
- package/primitives/toast.tsx +0 -30
- package/primitives/toggle-group.tsx +0 -63
- package/primitives/toggle.tsx +0 -44
- package/primitives/tooltip.tsx +0 -47
- package/primitives/video-player.tsx +0 -23
- package/src/button.ts +0 -1
- package/src/hooks/index.ts +0 -7
- package/src/hooks/use-click-away.ts +0 -31
- package/src/hooks/use-combined-refs.ts +0 -22
- package/src/hooks/use-copy-clipboard.ts +0 -30
- package/src/hooks/use-debounce.ts +0 -17
- package/src/hooks/use-fill-ids.ts +0 -25
- package/src/hooks/use-map.ts +0 -26
- package/src/hooks/use-measure.ts +0 -42
- package/src/hooks/use-reverse-video-playback.ts +0 -43
- package/src/hooks/use-scroll-restoration.ts +0 -50
- package/src/index-lean.ts +0 -87
- package/src/index.ts +0 -54
- package/src/mcp/README.md +0 -141
- package/src/mcp/enhanced-server.ts +0 -1208
- package/src/mcp/index.ts +0 -518
- package/src/mcp/package.json +0 -10
- package/src/registry/api.ts +0 -164
- package/src/registry/index.ts +0 -60
- package/src/registry/package.json +0 -10
- package/src/utils.ts +0 -19
- package/tailwind/colors.tailwind.js +0 -53
- package/tailwind/fontFamily.tailwind.ts +0 -7
- package/tailwind/fontSize.tailwind.ts +0 -13
- package/tailwind/index.ts +0 -7
- package/tailwind/safelist.tailwind.js +0 -26
- package/tailwind/screens.tailwind.js +0 -8
- package/tailwind/spacing.tailwind.js +0 -65
- package/tailwind/tailwind.config.hanzo-preset.d.ts +0 -5
- package/tailwind/tailwind.config.hanzo-preset.js +0 -915
- package/tailwind/tw-font-desc.ts +0 -15
- package/tailwind/typo-plugin/get-plugin-styles.js +0 -679
- package/tailwind/typo-plugin/index.d.ts +0 -9
- package/tailwind/typo-plugin/index.js +0 -141
- package/tailwind/typo-plugin/utils.js +0 -60
- package/tailwind/typography-test.mdx +0 -35
- package/tailwind/z-index.tailwind.js +0 -71
- package/types/animation-def.ts +0 -3
- package/types/breakpoints.ts +0 -11
- package/types/bullet-item.ts +0 -10
- package/types/button-def.ts +0 -39
- package/types/dimensions.ts +0 -8
- package/types/grid-def.ts +0 -56
- package/types/image-def.ts +0 -32
- package/types/index.ts +0 -30
- package/types/link-def.ts +0 -56
- package/types/media-stack-def.ts +0 -31
- package/types/t-shirt-size.ts +0 -5
- package/types/tshirt-dimensions.ts +0 -20
- package/types/video-def.ts +0 -25
- package/util/blob.ts +0 -33
- package/util/copy-to-clipboard.ts +0 -17
- package/util/create-shadow-root.ts +0 -22
- package/util/date.ts +0 -84
- package/util/debounce.ts +0 -11
- package/util/file.ts +0 -15
- package/util/format-and-abbreviate-as-currency.ts +0 -125
- package/util/format-text.ts +0 -34
- package/util/format-to-max-char.ts +0 -68
- package/util/index-client.ts +0 -3
- package/util/index.ts +0 -112
- package/util/number-abbreviate.ts +0 -49
- package/util/specifier.ts +0 -43
- package/util/spread-to-transform.ts +0 -25
- package/util/step-animation.ts +0 -90
- package/util/timing.ts +0 -3
- package/util/toasts.tsx +0 -17
- package/util/two-way-map.ts +0 -19
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { cn } from '@hanzo/ui/util'
|
|
4
|
-
import { Button } from '@hanzo/ui/primitives'
|
|
5
|
-
import { Input } from '@hanzo/ui/primitives'
|
|
6
|
-
import { Label } from '@hanzo/ui/primitives'
|
|
7
|
-
import { RadioGroup, RadioGroupItem } from '@hanzo/ui/primitives'
|
|
8
|
-
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@hanzo/ui/primitives'
|
|
9
|
-
import { Separator } from '@hanzo/ui/primitives'
|
|
10
|
-
import { Checkbox } from '@hanzo/ui/primitives'
|
|
11
|
-
|
|
12
|
-
interface CheckoutProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
13
|
-
orderSummary: {
|
|
14
|
-
items: Array<{
|
|
15
|
-
name: string
|
|
16
|
-
quantity: number
|
|
17
|
-
price: number
|
|
18
|
-
}>
|
|
19
|
-
subtotal: number
|
|
20
|
-
shipping: number
|
|
21
|
-
tax: number
|
|
22
|
-
total: number
|
|
23
|
-
currency?: string
|
|
24
|
-
}
|
|
25
|
-
onSubmit?: (data: any) => void
|
|
26
|
-
onBack?: () => void
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function Checkout({
|
|
30
|
-
className,
|
|
31
|
-
orderSummary,
|
|
32
|
-
onSubmit,
|
|
33
|
-
onBack,
|
|
34
|
-
...props
|
|
35
|
-
}: CheckoutProps) {
|
|
36
|
-
const formatPrice = (price: number, currency = '$') => {
|
|
37
|
-
return `${currency}${price.toFixed(2)}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
41
|
-
e.preventDefault()
|
|
42
|
-
const formData = new FormData(e.currentTarget)
|
|
43
|
-
const data = Object.fromEntries(formData.entries())
|
|
44
|
-
onSubmit?.(data)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className={cn('container py-8', className)} {...props}>
|
|
49
|
-
<form onSubmit={handleSubmit} className="grid gap-8 lg:grid-cols-3">
|
|
50
|
-
<div className="lg:col-span-2 space-y-6">
|
|
51
|
-
{/* Shipping Information */}
|
|
52
|
-
<Card>
|
|
53
|
-
<CardHeader>
|
|
54
|
-
<CardTitle>Shipping Information</CardTitle>
|
|
55
|
-
<CardDescription>
|
|
56
|
-
Enter your shipping address details
|
|
57
|
-
</CardDescription>
|
|
58
|
-
</CardHeader>
|
|
59
|
-
<CardContent className="grid gap-4">
|
|
60
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
61
|
-
<div className="grid gap-2">
|
|
62
|
-
<Label htmlFor="firstName">First Name</Label>
|
|
63
|
-
<Input id="firstName" name="firstName" required />
|
|
64
|
-
</div>
|
|
65
|
-
<div className="grid gap-2">
|
|
66
|
-
<Label htmlFor="lastName">Last Name</Label>
|
|
67
|
-
<Input id="lastName" name="lastName" required />
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
<div className="grid gap-2">
|
|
71
|
-
<Label htmlFor="email">Email</Label>
|
|
72
|
-
<Input id="email" name="email" type="email" required />
|
|
73
|
-
</div>
|
|
74
|
-
<div className="grid gap-2">
|
|
75
|
-
<Label htmlFor="address">Street Address</Label>
|
|
76
|
-
<Input id="address" name="address" required />
|
|
77
|
-
</div>
|
|
78
|
-
<div className="grid gap-2">
|
|
79
|
-
<Label htmlFor="address2">Apartment, suite, etc. (optional)</Label>
|
|
80
|
-
<Input id="address2" name="address2" />
|
|
81
|
-
</div>
|
|
82
|
-
<div className="grid gap-4 sm:grid-cols-3">
|
|
83
|
-
<div className="grid gap-2">
|
|
84
|
-
<Label htmlFor="city">City</Label>
|
|
85
|
-
<Input id="city" name="city" required />
|
|
86
|
-
</div>
|
|
87
|
-
<div className="grid gap-2">
|
|
88
|
-
<Label htmlFor="state">State / Province</Label>
|
|
89
|
-
<Input id="state" name="state" required />
|
|
90
|
-
</div>
|
|
91
|
-
<div className="grid gap-2">
|
|
92
|
-
<Label htmlFor="zip">ZIP / Postal Code</Label>
|
|
93
|
-
<Input id="zip" name="zip" required />
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<div className="grid gap-2">
|
|
97
|
-
<Label htmlFor="phone">Phone Number</Label>
|
|
98
|
-
<Input id="phone" name="phone" type="tel" />
|
|
99
|
-
</div>
|
|
100
|
-
</CardContent>
|
|
101
|
-
</Card>
|
|
102
|
-
|
|
103
|
-
{/* Payment Method */}
|
|
104
|
-
<Card>
|
|
105
|
-
<CardHeader>
|
|
106
|
-
<CardTitle>Payment Method</CardTitle>
|
|
107
|
-
<CardDescription>
|
|
108
|
-
Select your payment method
|
|
109
|
-
</CardDescription>
|
|
110
|
-
</CardHeader>
|
|
111
|
-
<CardContent className="grid gap-6">
|
|
112
|
-
<RadioGroup defaultValue="card" name="paymentMethod">
|
|
113
|
-
<div className="flex items-center space-x-2">
|
|
114
|
-
<RadioGroupItem value="card" id="card" />
|
|
115
|
-
<Label htmlFor="card">Credit Card</Label>
|
|
116
|
-
</div>
|
|
117
|
-
<div className="flex items-center space-x-2">
|
|
118
|
-
<RadioGroupItem value="paypal" id="paypal" />
|
|
119
|
-
<Label htmlFor="paypal">PayPal</Label>
|
|
120
|
-
</div>
|
|
121
|
-
<div className="flex items-center space-x-2">
|
|
122
|
-
<RadioGroupItem value="crypto" id="crypto" />
|
|
123
|
-
<Label htmlFor="crypto">Cryptocurrency</Label>
|
|
124
|
-
</div>
|
|
125
|
-
</RadioGroup>
|
|
126
|
-
|
|
127
|
-
<div className="grid gap-4">
|
|
128
|
-
<div className="grid gap-2">
|
|
129
|
-
<Label htmlFor="cardNumber">Card Number</Label>
|
|
130
|
-
<Input
|
|
131
|
-
id="cardNumber"
|
|
132
|
-
name="cardNumber"
|
|
133
|
-
placeholder="1234 5678 9012 3456"
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
<div className="grid gap-4 sm:grid-cols-3">
|
|
137
|
-
<div className="grid gap-2 sm:col-span-2">
|
|
138
|
-
<Label htmlFor="expiry">Expiry Date</Label>
|
|
139
|
-
<Input
|
|
140
|
-
id="expiry"
|
|
141
|
-
name="expiry"
|
|
142
|
-
placeholder="MM/YY"
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
<div className="grid gap-2">
|
|
146
|
-
<Label htmlFor="cvc">CVC</Label>
|
|
147
|
-
<Input
|
|
148
|
-
id="cvc"
|
|
149
|
-
name="cvc"
|
|
150
|
-
placeholder="123"
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
</CardContent>
|
|
156
|
-
</Card>
|
|
157
|
-
|
|
158
|
-
{/* Billing Address */}
|
|
159
|
-
<Card>
|
|
160
|
-
<CardHeader>
|
|
161
|
-
<CardTitle>Billing Address</CardTitle>
|
|
162
|
-
</CardHeader>
|
|
163
|
-
<CardContent>
|
|
164
|
-
<div className="flex items-center space-x-2">
|
|
165
|
-
<Checkbox id="sameAsShipping" name="sameAsShipping" defaultChecked />
|
|
166
|
-
<label
|
|
167
|
-
htmlFor="sameAsShipping"
|
|
168
|
-
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
169
|
-
>
|
|
170
|
-
Same as shipping address
|
|
171
|
-
</label>
|
|
172
|
-
</div>
|
|
173
|
-
</CardContent>
|
|
174
|
-
</Card>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{/* Order Summary */}
|
|
178
|
-
<div>
|
|
179
|
-
<Card>
|
|
180
|
-
<CardHeader>
|
|
181
|
-
<CardTitle>Order Summary</CardTitle>
|
|
182
|
-
</CardHeader>
|
|
183
|
-
<CardContent className="space-y-4">
|
|
184
|
-
<div className="space-y-2">
|
|
185
|
-
{orderSummary.items.map((item, i) => (
|
|
186
|
-
<div key={i} className="flex justify-between text-sm">
|
|
187
|
-
<span className="text-muted-foreground">
|
|
188
|
-
{item.name} x {item.quantity}
|
|
189
|
-
</span>
|
|
190
|
-
<span>
|
|
191
|
-
{formatPrice(item.price * item.quantity, orderSummary.currency)}
|
|
192
|
-
</span>
|
|
193
|
-
</div>
|
|
194
|
-
))}
|
|
195
|
-
</div>
|
|
196
|
-
<Separator />
|
|
197
|
-
<div className="space-y-2">
|
|
198
|
-
<div className="flex justify-between">
|
|
199
|
-
<span>Subtotal</span>
|
|
200
|
-
<span>{formatPrice(orderSummary.subtotal, orderSummary.currency)}</span>
|
|
201
|
-
</div>
|
|
202
|
-
<div className="flex justify-between">
|
|
203
|
-
<span>Shipping</span>
|
|
204
|
-
<span>{formatPrice(orderSummary.shipping, orderSummary.currency)}</span>
|
|
205
|
-
</div>
|
|
206
|
-
<div className="flex justify-between">
|
|
207
|
-
<span>Tax</span>
|
|
208
|
-
<span>{formatPrice(orderSummary.tax, orderSummary.currency)}</span>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
<Separator />
|
|
212
|
-
<div className="flex justify-between text-lg font-semibold">
|
|
213
|
-
<span>Total</span>
|
|
214
|
-
<span>{formatPrice(orderSummary.total, orderSummary.currency)}</span>
|
|
215
|
-
</div>
|
|
216
|
-
</CardContent>
|
|
217
|
-
<CardFooter className="flex-col gap-2">
|
|
218
|
-
<Button type="submit" className="w-full" size="lg">
|
|
219
|
-
Complete Order
|
|
220
|
-
</Button>
|
|
221
|
-
{onBack && (
|
|
222
|
-
<Button
|
|
223
|
-
type="button"
|
|
224
|
-
variant="outline"
|
|
225
|
-
className="w-full"
|
|
226
|
-
onClick={onBack}
|
|
227
|
-
>
|
|
228
|
-
Back to Cart
|
|
229
|
-
</Button>
|
|
230
|
-
)}
|
|
231
|
-
</CardFooter>
|
|
232
|
-
</Card>
|
|
233
|
-
|
|
234
|
-
<div className="mt-4 text-xs text-center text-muted-foreground">
|
|
235
|
-
Your payment information is encrypted and secure.
|
|
236
|
-
We never store your credit card details.
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
</form>
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { ProductGrid } from './product-grid'
|
|
2
|
-
export { ProductDetail } from './product-detail'
|
|
3
|
-
export { ShoppingCart } from './shopping-cart'
|
|
4
|
-
export { Checkout } from './checkout'
|
|
5
|
-
|
|
6
|
-
export type { Product } from './product-grid'
|
|
7
|
-
export type { CartItem } from './shopping-cart'
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { cn } from '@hanzo/ui/util'
|
|
5
|
-
import { Button } from '@hanzo/ui/primitives'
|
|
6
|
-
import { Badge } from '@hanzo/ui/primitives'
|
|
7
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@hanzo/ui/primitives'
|
|
8
|
-
import { Star, ShoppingCart, Heart, Share2 } from 'lucide-react'
|
|
9
|
-
|
|
10
|
-
interface ProductDetailProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
11
|
-
product: {
|
|
12
|
-
id: string | number
|
|
13
|
-
name: string
|
|
14
|
-
description: string
|
|
15
|
-
price: number
|
|
16
|
-
originalPrice?: number
|
|
17
|
-
currency?: string
|
|
18
|
-
images: string[]
|
|
19
|
-
rating?: number
|
|
20
|
-
reviewCount?: number
|
|
21
|
-
badge?: string
|
|
22
|
-
inStock?: boolean
|
|
23
|
-
variants?: Array<{
|
|
24
|
-
id: string
|
|
25
|
-
name: string
|
|
26
|
-
value: string
|
|
27
|
-
}>
|
|
28
|
-
features?: string[]
|
|
29
|
-
specifications?: Record<string, string>
|
|
30
|
-
}
|
|
31
|
-
onAddToCart?: (quantity: number, variant?: string) => void
|
|
32
|
-
onAddToWishlist?: () => void
|
|
33
|
-
onShare?: () => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function ProductDetail({
|
|
37
|
-
className,
|
|
38
|
-
product,
|
|
39
|
-
onAddToCart,
|
|
40
|
-
onAddToWishlist,
|
|
41
|
-
onShare,
|
|
42
|
-
...props
|
|
43
|
-
}: ProductDetailProps) {
|
|
44
|
-
const [selectedImage, setSelectedImage] = useState(0)
|
|
45
|
-
const [quantity, setQuantity] = useState(1)
|
|
46
|
-
const [selectedVariant, setSelectedVariant] = useState(
|
|
47
|
-
product.variants?.[0]?.id || ''
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const formatPrice = (price: number, currency = '$') => {
|
|
51
|
-
return `${currency}${price.toFixed(2)}`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const calculateDiscount = (price: number, originalPrice: number) => {
|
|
55
|
-
return Math.round(((originalPrice - price) / originalPrice) * 100)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className={cn('container py-8', className)} {...props}>
|
|
60
|
-
<div className="grid gap-8 lg:grid-cols-2">
|
|
61
|
-
{/* Images */}
|
|
62
|
-
<div className="space-y-4">
|
|
63
|
-
<div className="aspect-square overflow-hidden rounded-lg">
|
|
64
|
-
<img
|
|
65
|
-
src={product.images[selectedImage]}
|
|
66
|
-
alt={product.name}
|
|
67
|
-
className="h-full w-full object-cover"
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
{product.images.length > 1 && (
|
|
71
|
-
<div className="grid grid-cols-4 gap-4">
|
|
72
|
-
{product.images.map((image, i) => (
|
|
73
|
-
<button
|
|
74
|
-
key={i}
|
|
75
|
-
onClick={() => setSelectedImage(i)}
|
|
76
|
-
className={cn(
|
|
77
|
-
'aspect-square overflow-hidden rounded-lg border-2',
|
|
78
|
-
selectedImage === i ? 'border-primary' : 'border-transparent'
|
|
79
|
-
)}
|
|
80
|
-
>
|
|
81
|
-
<img
|
|
82
|
-
src={image}
|
|
83
|
-
alt=""
|
|
84
|
-
className="h-full w-full object-cover"
|
|
85
|
-
/>
|
|
86
|
-
</button>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
{/* Product Info */}
|
|
93
|
-
<div className="space-y-6">
|
|
94
|
-
<div>
|
|
95
|
-
{product.badge && (
|
|
96
|
-
<Badge className="mb-2">{product.badge}</Badge>
|
|
97
|
-
)}
|
|
98
|
-
<h1 className="text-3xl font-bold">{product.name}</h1>
|
|
99
|
-
{product.rating !== undefined && (
|
|
100
|
-
<div className="mt-4 flex items-center gap-2">
|
|
101
|
-
<div className="flex items-center gap-1">
|
|
102
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
103
|
-
<Star
|
|
104
|
-
key={i}
|
|
105
|
-
className={cn(
|
|
106
|
-
'h-5 w-5',
|
|
107
|
-
i < Math.floor(product.rating!)
|
|
108
|
-
? 'fill-primary text-primary'
|
|
109
|
-
: 'text-muted-foreground'
|
|
110
|
-
)}
|
|
111
|
-
/>
|
|
112
|
-
))}
|
|
113
|
-
</div>
|
|
114
|
-
<span className="font-medium">{product.rating.toFixed(1)}</span>
|
|
115
|
-
{product.reviewCount !== undefined && (
|
|
116
|
-
<span className="text-muted-foreground">
|
|
117
|
-
({product.reviewCount} reviews)
|
|
118
|
-
</span>
|
|
119
|
-
)}
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className="space-y-2">
|
|
125
|
-
<div className="flex items-center gap-2">
|
|
126
|
-
<span className="text-3xl font-bold">
|
|
127
|
-
{formatPrice(product.price, product.currency)}
|
|
128
|
-
</span>
|
|
129
|
-
{product.originalPrice && product.originalPrice > product.price && (
|
|
130
|
-
<>
|
|
131
|
-
<span className="text-xl text-muted-foreground line-through">
|
|
132
|
-
{formatPrice(product.originalPrice, product.currency)}
|
|
133
|
-
</span>
|
|
134
|
-
<Badge variant="destructive">
|
|
135
|
-
-{calculateDiscount(product.price, product.originalPrice)}%
|
|
136
|
-
</Badge>
|
|
137
|
-
</>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
{product.inStock === false && (
|
|
141
|
-
<p className="text-sm text-destructive">Out of stock</p>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<p className="text-muted-foreground">{product.description}</p>
|
|
146
|
-
|
|
147
|
-
{/* Variants */}
|
|
148
|
-
{product.variants && product.variants.length > 0 && (
|
|
149
|
-
<div className="space-y-2">
|
|
150
|
-
<label className="text-sm font-medium">
|
|
151
|
-
{product.variants[0].name}
|
|
152
|
-
</label>
|
|
153
|
-
<div className="flex flex-wrap gap-2">
|
|
154
|
-
{product.variants.map((variant) => (
|
|
155
|
-
<Button
|
|
156
|
-
key={variant.id}
|
|
157
|
-
variant={selectedVariant === variant.id ? 'default' : 'outline'}
|
|
158
|
-
size="sm"
|
|
159
|
-
onClick={() => setSelectedVariant(variant.id)}
|
|
160
|
-
>
|
|
161
|
-
{variant.value}
|
|
162
|
-
</Button>
|
|
163
|
-
))}
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{/* Quantity and Actions */}
|
|
169
|
-
<div className="space-y-4">
|
|
170
|
-
<div className="flex items-center gap-4">
|
|
171
|
-
<label className="text-sm font-medium">Quantity:</label>
|
|
172
|
-
<div className="flex items-center gap-2">
|
|
173
|
-
<Button
|
|
174
|
-
size="icon"
|
|
175
|
-
variant="outline"
|
|
176
|
-
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
177
|
-
disabled={quantity <= 1}
|
|
178
|
-
>
|
|
179
|
-
-
|
|
180
|
-
</Button>
|
|
181
|
-
<span className="w-12 text-center">{quantity}</span>
|
|
182
|
-
<Button
|
|
183
|
-
size="icon"
|
|
184
|
-
variant="outline"
|
|
185
|
-
onClick={() => setQuantity(quantity + 1)}
|
|
186
|
-
>
|
|
187
|
-
+
|
|
188
|
-
</Button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<div className="flex gap-4">
|
|
193
|
-
<Button
|
|
194
|
-
className="flex-1"
|
|
195
|
-
size="lg"
|
|
196
|
-
disabled={product.inStock === false}
|
|
197
|
-
onClick={() => onAddToCart?.(quantity, selectedVariant)}
|
|
198
|
-
>
|
|
199
|
-
<ShoppingCart className="mr-2 h-5 w-5" />
|
|
200
|
-
Add to Cart
|
|
201
|
-
</Button>
|
|
202
|
-
<Button
|
|
203
|
-
size="lg"
|
|
204
|
-
variant="outline"
|
|
205
|
-
onClick={onAddToWishlist}
|
|
206
|
-
>
|
|
207
|
-
<Heart className="h-5 w-5" />
|
|
208
|
-
</Button>
|
|
209
|
-
<Button
|
|
210
|
-
size="lg"
|
|
211
|
-
variant="outline"
|
|
212
|
-
onClick={onShare}
|
|
213
|
-
>
|
|
214
|
-
<Share2 className="h-5 w-5" />
|
|
215
|
-
</Button>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
|
|
219
|
-
{/* Tabs */}
|
|
220
|
-
{(product.features || product.specifications) && (
|
|
221
|
-
<Tabs defaultValue="features" className="w-full">
|
|
222
|
-
<TabsList className="grid w-full grid-cols-2">
|
|
223
|
-
{product.features && <TabsTrigger value="features">Features</TabsTrigger>}
|
|
224
|
-
{product.specifications && (
|
|
225
|
-
<TabsTrigger value="specifications">Specifications</TabsTrigger>
|
|
226
|
-
)}
|
|
227
|
-
</TabsList>
|
|
228
|
-
{product.features && (
|
|
229
|
-
<TabsContent value="features" className="space-y-2">
|
|
230
|
-
<ul className="list-inside list-disc space-y-1">
|
|
231
|
-
{product.features.map((feature, i) => (
|
|
232
|
-
<li key={i} className="text-sm">
|
|
233
|
-
{feature}
|
|
234
|
-
</li>
|
|
235
|
-
))}
|
|
236
|
-
</ul>
|
|
237
|
-
</TabsContent>
|
|
238
|
-
)}
|
|
239
|
-
{product.specifications && (
|
|
240
|
-
<TabsContent value="specifications">
|
|
241
|
-
<dl className="space-y-2">
|
|
242
|
-
{Object.entries(product.specifications).map(([key, value]) => (
|
|
243
|
-
<div key={key} className="flex gap-4 text-sm">
|
|
244
|
-
<dt className="min-w-[120px] font-medium">{key}:</dt>
|
|
245
|
-
<dd className="text-muted-foreground">{value}</dd>
|
|
246
|
-
</div>
|
|
247
|
-
))}
|
|
248
|
-
</dl>
|
|
249
|
-
</TabsContent>
|
|
250
|
-
)}
|
|
251
|
-
</Tabs>
|
|
252
|
-
)}
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { cn } from '@hanzo/ui/util'
|
|
4
|
-
import { Button } from '@hanzo/ui/primitives'
|
|
5
|
-
import { Card, CardContent, CardFooter } from '@hanzo/ui/primitives'
|
|
6
|
-
import { Badge } from '@hanzo/ui/primitives'
|
|
7
|
-
import { Star, ShoppingCart } from 'lucide-react'
|
|
8
|
-
|
|
9
|
-
export interface Product {
|
|
10
|
-
id: string | number
|
|
11
|
-
name: string
|
|
12
|
-
description?: string
|
|
13
|
-
price: number
|
|
14
|
-
originalPrice?: number
|
|
15
|
-
currency?: string
|
|
16
|
-
image: string
|
|
17
|
-
rating?: number
|
|
18
|
-
reviewCount?: number
|
|
19
|
-
badge?: string
|
|
20
|
-
inStock?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface ProductGridProps extends React.ComponentPropsWithoutRef<'section'> {
|
|
24
|
-
products: Product[]
|
|
25
|
-
columns?: 2 | 3 | 4
|
|
26
|
-
onAddToCart?: (product: Product) => void
|
|
27
|
-
onProductClick?: (product: Product) => void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function ProductGrid({
|
|
31
|
-
className,
|
|
32
|
-
products,
|
|
33
|
-
columns = 4,
|
|
34
|
-
onAddToCart,
|
|
35
|
-
onProductClick,
|
|
36
|
-
...props
|
|
37
|
-
}: ProductGridProps) {
|
|
38
|
-
const gridCols = {
|
|
39
|
-
2: 'sm:grid-cols-2',
|
|
40
|
-
3: 'sm:grid-cols-2 lg:grid-cols-3',
|
|
41
|
-
4: 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
42
|
-
}[columns]
|
|
43
|
-
|
|
44
|
-
const formatPrice = (price: number, currency = '$') => {
|
|
45
|
-
return `${currency}${price.toFixed(2)}`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const calculateDiscount = (price: number, originalPrice: number) => {
|
|
49
|
-
return Math.round(((originalPrice - price) / originalPrice) * 100)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<section className={cn('py-8', className)} {...props}>
|
|
54
|
-
<div className="container">
|
|
55
|
-
<div className={cn('grid gap-6', gridCols)}>
|
|
56
|
-
{products.map((product) => (
|
|
57
|
-
<Card
|
|
58
|
-
key={product.id}
|
|
59
|
-
className="group relative overflow-hidden"
|
|
60
|
-
>
|
|
61
|
-
{product.badge && (
|
|
62
|
-
<Badge className="absolute left-2 top-2 z-10">
|
|
63
|
-
{product.badge}
|
|
64
|
-
</Badge>
|
|
65
|
-
)}
|
|
66
|
-
{product.originalPrice && product.originalPrice > product.price && (
|
|
67
|
-
<Badge
|
|
68
|
-
variant="destructive"
|
|
69
|
-
className="absolute right-2 top-2 z-10"
|
|
70
|
-
>
|
|
71
|
-
-{calculateDiscount(product.price, product.originalPrice)}%
|
|
72
|
-
</Badge>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
<div
|
|
76
|
-
className="aspect-square cursor-pointer overflow-hidden"
|
|
77
|
-
onClick={() => onProductClick?.(product)}
|
|
78
|
-
>
|
|
79
|
-
<img
|
|
80
|
-
src={product.image}
|
|
81
|
-
alt={product.name}
|
|
82
|
-
className="h-full w-full object-cover transition-transform group-hover:scale-105"
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<CardContent className="p-4">
|
|
87
|
-
<h3
|
|
88
|
-
className="cursor-pointer font-semibold hover:underline"
|
|
89
|
-
onClick={() => onProductClick?.(product)}
|
|
90
|
-
>
|
|
91
|
-
{product.name}
|
|
92
|
-
</h3>
|
|
93
|
-
|
|
94
|
-
{product.description && (
|
|
95
|
-
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
|
96
|
-
{product.description}
|
|
97
|
-
</p>
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
{product.rating !== undefined && (
|
|
101
|
-
<div className="mt-2 flex items-center gap-2">
|
|
102
|
-
<div className="flex items-center gap-1">
|
|
103
|
-
<Star className="h-4 w-4 fill-primary text-primary" />
|
|
104
|
-
<span className="text-sm font-medium">
|
|
105
|
-
{product.rating.toFixed(1)}
|
|
106
|
-
</span>
|
|
107
|
-
</div>
|
|
108
|
-
{product.reviewCount !== undefined && (
|
|
109
|
-
<span className="text-sm text-muted-foreground">
|
|
110
|
-
({product.reviewCount})
|
|
111
|
-
</span>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
115
|
-
|
|
116
|
-
<div className="mt-3 flex items-center gap-2">
|
|
117
|
-
<span className="text-lg font-bold">
|
|
118
|
-
{formatPrice(product.price, product.currency)}
|
|
119
|
-
</span>
|
|
120
|
-
{product.originalPrice && product.originalPrice > product.price && (
|
|
121
|
-
<span className="text-sm text-muted-foreground line-through">
|
|
122
|
-
{formatPrice(product.originalPrice, product.currency)}
|
|
123
|
-
</span>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
{product.inStock === false && (
|
|
128
|
-
<p className="mt-2 text-sm text-destructive">Out of stock</p>
|
|
129
|
-
)}
|
|
130
|
-
</CardContent>
|
|
131
|
-
|
|
132
|
-
<CardFooter className="p-4 pt-0">
|
|
133
|
-
<Button
|
|
134
|
-
className="w-full"
|
|
135
|
-
disabled={product.inStock === false}
|
|
136
|
-
onClick={() => onAddToCart?.(product)}
|
|
137
|
-
>
|
|
138
|
-
<ShoppingCart className="mr-2 h-4 w-4" />
|
|
139
|
-
Add to cart
|
|
140
|
-
</Button>
|
|
141
|
-
</CardFooter>
|
|
142
|
-
</Card>
|
|
143
|
-
))}
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
</section>
|
|
147
|
-
)
|
|
148
|
-
}
|