@kasuvia/sdk 0.0.8 → 0.0.9
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 +42 -15
- package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
- package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
- package/dist/components/index.d.mts +497 -0
- package/dist/components/index.d.ts +497 -0
- package/dist/components/index.js +4217 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +4194 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/server.d.mts +26 -0
- package/dist/config/server.d.ts +26 -0
- package/dist/config/server.js +38 -0
- package/dist/config/server.js.map +1 -0
- package/dist/config/server.mjs +13 -0
- package/dist/config/server.mjs.map +1 -0
- package/dist/hooks/index.d.mts +393 -160
- package/dist/hooks/index.d.ts +393 -160
- package/dist/hooks/index.js +2792 -2137
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +2734 -2121
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index-CNW_LINy.d.mts +578 -0
- package/dist/index-CNW_LINy.d.ts +578 -0
- package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
- package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
- package/dist/index-NkI10J_E.d.mts +40 -0
- package/dist/index-NkI10J_E.d.ts +40 -0
- package/dist/index.d.mts +20437 -103
- package/dist/index.d.ts +20437 -103
- package/dist/index.js +2111 -1802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2006 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
- package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
- package/dist/services/index.d.mts +908 -2
- package/dist/services/index.d.ts +908 -2
- package/dist/services/index.js +1492 -1686
- package/dist/services/index.js.map +1 -1
- package/dist/services/index.mjs +1431 -1670
- package/dist/services/index.mjs.map +1 -1
- package/package.json +46 -6
- package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
- package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
- package/src/components/bespoke/index.ts +3 -0
- package/src/components/booking/SlotPicker.tsx +97 -0
- package/src/components/booking/index.ts +1 -0
- package/src/components/cart/CartItem.tsx +81 -0
- package/src/components/cart/CartSidebar.tsx +115 -0
- package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
- package/src/components/cart/index.ts +3 -0
- package/src/components/index.ts +8 -0
- package/src/components/orders/DeliverableReviewer.tsx +139 -0
- package/src/components/orders/index.ts +1 -0
- package/src/components/product/ProductCard.tsx +350 -0
- package/src/components/product/ProductGrid.tsx +84 -0
- package/src/components/product/index.ts +2 -0
- package/src/components/shared/AddressAutocomplete.tsx +212 -0
- package/src/components/shared/AnalyticsScripts.tsx +56 -0
- package/src/components/shared/CookieConsentBanner.tsx +98 -0
- package/src/components/shared/ImageEditorModal.tsx +1066 -0
- package/src/components/shared/ImageViewerModal.tsx +598 -0
- package/src/components/shared/LucideReactIcon.tsx +52 -0
- package/src/components/shared/MapPinDrop.tsx +101 -0
- package/src/components/shared/MultiSelectPicker.tsx +193 -0
- package/src/components/shared/OfflineBanner.tsx +46 -0
- package/src/components/shared/SearchableInput.tsx +153 -0
- package/src/components/shared/SearchablePopover.tsx +166 -0
- package/src/components/shared/SmartImage.tsx +311 -0
- package/src/components/shared/TruncatedValue.tsx +27 -0
- package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
- package/src/components/shared/index.ts +15 -0
- package/src/components/skeletons/CardSkeleton.tsx +26 -0
- package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
- package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
- package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
- package/src/components/skeletons/TableSkeleton.tsx +42 -0
- package/src/components/skeletons/index.ts +5 -0
- package/src/config/index.ts +22 -4
- package/src/config/server.ts +45 -0
- package/src/context/sdk-context.tsx +5 -2
- package/src/generated/auth-openapi.ts +2935 -0
- package/src/generated/cart-openapi.ts +652 -0
- package/src/generated/cms-openapi.ts +1668 -0
- package/src/generated/crm-openapi.ts +986 -0
- package/src/generated/graphql.ts +126 -0
- package/src/generated/index.ts +11 -0
- package/src/generated/media-openapi.ts +613 -0
- package/src/generated/notification-openapi.ts +862 -0
- package/src/generated/payment-openapi.ts +2713 -0
- package/src/generated/store-openapi.ts +9510 -0
- package/src/graphql/operations/collections.graphql +32 -0
- package/src/graphql/operations/products.graphql +197 -0
- package/src/graphql/operations/wishlist.graphql +25 -0
- package/src/hooks/index.ts +9 -2
- package/src/hooks/use-auth.ts +163 -69
- package/src/hooks/use-bespoke-availability.ts +39 -22
- package/src/hooks/use-business-config.ts +51 -0
- package/src/hooks/use-cart.ts +310 -238
- package/src/hooks/use-cms.ts +107 -127
- package/src/hooks/use-collections.ts +35 -55
- package/src/hooks/use-consultations.ts +9 -136
- package/src/hooks/use-countries.ts +19 -28
- package/src/hooks/use-currencies.ts +8 -20
- package/src/hooks/use-customer-address.ts +64 -0
- package/src/hooks/use-feedback.ts +34 -0
- package/src/hooks/use-languages.ts +26 -39
- package/src/hooks/use-media-upload.ts +23 -0
- package/src/hooks/use-notification-stream.ts +88 -0
- package/src/hooks/use-notifications.ts +163 -0
- package/src/hooks/use-order-stream.ts +29 -29
- package/src/hooks/use-orders.ts +91 -73
- package/src/hooks/use-payment.ts +116 -84
- package/src/hooks/use-products.ts +65 -269
- package/src/hooks/use-push-notifications.ts +122 -0
- package/src/hooks/use-reviews.ts +175 -0
- package/src/hooks/use-service-booking.ts +109 -132
- package/src/hooks/use-states.ts +16 -30
- package/src/hooks/use-wishlist.ts +44 -69
- package/src/index.ts +15 -11
- package/src/services/auth.service.ts +422 -132
- package/src/services/bespoke.service.ts +69 -10
- package/src/services/cart.service.ts +155 -186
- package/src/services/cms.service.ts +41 -50
- package/src/services/collection.service.ts +25 -63
- package/src/services/config.service.ts +10 -21
- package/src/services/consultation.service.ts +36 -93
- package/src/services/feedback.service.ts +76 -0
- package/src/services/index.ts +7 -1
- package/src/services/legal.service.ts +62 -0
- package/src/services/logistics.service.ts +235 -53
- package/src/services/media.service.ts +115 -0
- package/src/services/notifications.service.ts +204 -20
- package/src/services/order.service.ts +498 -127
- package/src/services/payment.service.ts +149 -100
- package/src/services/product.service.ts +157 -814
- package/src/services/review.service.ts +211 -0
- package/src/services/service-booking.service.ts +198 -99
- package/src/services/wishlist.service.ts +27 -74
- package/src/types/auth/index.ts +161 -18
- package/src/types/business/index.ts +9 -1
- package/src/types/cms/index.ts +91 -6
- package/src/types/cms/legal.ts +18 -0
- package/src/types/consultations/index.ts +60 -68
- package/src/types/index.ts +5 -3
- package/src/types/media/index.ts +35 -3
- package/src/types/payments/index.ts +80 -16
- package/src/types/reviews/index.ts +116 -0
- package/src/types/services/index.ts +113 -38
- package/src/types/store/checkout.ts +53 -0
- package/src/types/store/index.ts +273 -52
- package/src/types/store/invoice.ts +31 -0
- package/src/types/store/shipping.ts +84 -3
- package/src/types/ui/index.ts +1 -1
- package/src/utils/checkout.ts +19 -3
- package/src/utils/cookieConsent.ts +34 -0
- package/src/utils/format.ts +408 -0
- package/src/utils/geolocation.ts +134 -0
- package/src/utils/graphql-url.ts +47 -0
- package/src/utils/guest-session.ts +59 -0
- package/src/utils/http-client.ts +147 -0
- package/src/utils/index.ts +11 -4
- package/src/utils/status-colors.ts +79 -0
- package/src/utils/token-refresh.ts +115 -0
- package/src/utils/validators.ts +13 -0
- package/src/utils/youtube.ts +18 -0
- package/dist/index-C5YO9gTl.d.mts +0 -507
- package/dist/index-C5YO9gTl.d.ts +0 -507
- package/dist/index-CefG7_xq.d.mts +0 -793
- package/dist/index-DeHCf3zI.d.ts +0 -793
- package/src/graphql/collections.graphql.ts +0 -48
- package/src/graphql/index.ts +0 -7
- package/src/graphql/products.graphql.ts +0 -860
- package/src/graphql/wishlist.graphql.ts +0 -34
- package/src/hooks/use-consultation-stream.ts +0 -142
- package/src/types/bespoke/index.ts +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kasuvia/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Kasuvia SDK - Core data fetching, authentication, and API client layer",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"types": "./dist/services/index.d.ts",
|
|
21
21
|
"import": "./dist/services/index.mjs",
|
|
22
22
|
"require": "./dist/services/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./components": {
|
|
25
|
+
"types": "./dist/components/index.d.ts",
|
|
26
|
+
"import": "./dist/components/index.mjs",
|
|
27
|
+
"require": "./dist/components/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./config/server": {
|
|
30
|
+
"types": "./dist/config/server.d.ts",
|
|
31
|
+
"import": "./dist/config/server.mjs",
|
|
32
|
+
"require": "./dist/config/server.js"
|
|
23
33
|
}
|
|
24
34
|
},
|
|
25
35
|
"files": [
|
|
@@ -32,21 +42,49 @@
|
|
|
32
42
|
"dependencies": {
|
|
33
43
|
"@microsoft/fetch-event-source": "^2.0.1",
|
|
34
44
|
"@tanstack/react-query": "^5.100.13",
|
|
45
|
+
"client-only": "^0.0.1",
|
|
35
46
|
"graphql": "^16.14.0",
|
|
36
47
|
"graphql-request": "^7.4.0",
|
|
37
|
-
"
|
|
38
|
-
"uuid": "^14.0.0",
|
|
39
|
-
"zod": "^3.24.2"
|
|
48
|
+
"server-only": "^0.0.1"
|
|
40
49
|
},
|
|
41
50
|
"peerDependencies": {
|
|
51
|
+
"@use-gesture/react": ">=10.0.0",
|
|
52
|
+
"framer-motion": ">=10.0.0",
|
|
53
|
+
"lucide-react": ">=0.300.0",
|
|
54
|
+
"next": ">=13.0.0",
|
|
42
55
|
"react": ">=18.0.0",
|
|
43
|
-
"react-dom": ">=18.0.0"
|
|
56
|
+
"react-dom": ">=18.0.0",
|
|
57
|
+
"sonner": ">=1.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@use-gesture/react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"framer-motion": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"next": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"sonner": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
44
72
|
},
|
|
45
73
|
"devDependencies": {
|
|
74
|
+
"@graphql-codegen/cli": "^7.4.2",
|
|
75
|
+
"@graphql-codegen/typed-document-node": "^7.1.0",
|
|
76
|
+
"@graphql-codegen/typescript": "^6.1.0",
|
|
77
|
+
"@graphql-codegen/typescript-operations": "^6.1.7",
|
|
78
|
+
"@graphql-typed-document-node/core": "^3.2.0",
|
|
46
79
|
"@types/node": "^20",
|
|
47
80
|
"@types/react": "^19",
|
|
48
81
|
"@types/react-dom": "^19",
|
|
49
|
-
"@
|
|
82
|
+
"@use-gesture/react": "^10.3.1",
|
|
83
|
+
"framer-motion": "^12.38.0",
|
|
84
|
+
"lucide-react": "^0.475.0",
|
|
85
|
+
"next": "15.5.9",
|
|
86
|
+
"openapi-typescript": "^7.13.0",
|
|
87
|
+
"sonner": "^2.0.7",
|
|
50
88
|
"tsup": "^8.0.0",
|
|
51
89
|
"typescript": "^5.0.0"
|
|
52
90
|
},
|
|
@@ -63,6 +101,8 @@
|
|
|
63
101
|
"build": "tsup",
|
|
64
102
|
"dev": "tsup --watch",
|
|
65
103
|
"type-check": "tsc --noEmit",
|
|
104
|
+
"codegen": "graphql-codegen --config codegen.ts",
|
|
105
|
+
"codegen:rest": "openapi-typescript openapi/store-management.json -o src/generated/store-openapi.ts && openapi-typescript openapi/cart-management.json -o src/generated/cart-openapi.ts && openapi-typescript openapi/auth.json -o src/generated/auth-openapi.ts && openapi-typescript openapi/payment-system.json -o src/generated/payment-openapi.ts && openapi-typescript openapi/notification-system.json -o src/generated/notification-openapi.ts && openapi-typescript openapi/cms.json -o src/generated/cms-openapi.ts && openapi-typescript openapi/crm.json -o src/generated/crm-openapi.ts && openapi-typescript openapi/media-system.json -o src/generated/media-openapi.ts",
|
|
66
106
|
"clean": "rm -rf dist"
|
|
67
107
|
}
|
|
68
108
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useBespokeAvailability } from '../../hooks/use-bespoke-availability'
|
|
3
|
+
|
|
4
|
+
export interface BespokeAvailabilityBadgeProps {
|
|
5
|
+
productId: string
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BespokeAvailabilityBadge({ productId, className = "inline-flex items-center gap-2 px-3 py-1 text-xs rounded-full font-medium" }: BespokeAvailabilityBadgeProps) {
|
|
10
|
+
const { data: availability, isLoading } = useBespokeAvailability(productId)
|
|
11
|
+
|
|
12
|
+
if (isLoading) {
|
|
13
|
+
return <div className="h-5 w-28 bg-gray-200 dark:bg-gray-800 animate-pulse rounded-full" />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!availability) return null
|
|
17
|
+
|
|
18
|
+
if (availability.is_available) {
|
|
19
|
+
return (
|
|
20
|
+
<span className={`${className} bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800`}>
|
|
21
|
+
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-ping" />
|
|
22
|
+
{availability.available_slots} slots available this month
|
|
23
|
+
</span>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span className={`${className} bg-amber-100 dark:bg-amber-950 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800`}>
|
|
29
|
+
Fully booked — Next available: {availability.next_available_date ? new Date(availability.next_available_date).toLocaleDateString() : 'Next Month'}
|
|
30
|
+
</span>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export type BespokeStageKey = 'received' | 'development' | 'finished' | 'dispatched'
|
|
5
|
+
|
|
6
|
+
export interface BespokeProgressTrackerProps {
|
|
7
|
+
stage: BespokeStageKey
|
|
8
|
+
className?: string
|
|
9
|
+
activeStepClassName?: string
|
|
10
|
+
completedStepClassName?: string
|
|
11
|
+
inactiveStepClassName?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BespokeProgressTracker({
|
|
15
|
+
stage,
|
|
16
|
+
className = "flex items-center w-full my-4",
|
|
17
|
+
activeStepClassName = "bg-emerald-600 text-white font-bold",
|
|
18
|
+
completedStepClassName = "bg-emerald-500 text-white",
|
|
19
|
+
inactiveStepClassName = "border border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 text-gray-500",
|
|
20
|
+
}: BespokeProgressTrackerProps) {
|
|
21
|
+
const stages = [
|
|
22
|
+
{ key: 'received', label: 'Order Received' },
|
|
23
|
+
{ key: 'development', label: 'In Development' },
|
|
24
|
+
{ key: 'finished', label: 'Finished' },
|
|
25
|
+
{ key: 'dispatched', label: 'Ready / Dispatched' },
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
const currentIdx = stages.findIndex((s) => s.key === stage)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={className}>
|
|
32
|
+
{stages.map((s, i) => (
|
|
33
|
+
<div key={s.key} className="flex flex-1 flex-col items-center">
|
|
34
|
+
<div className="flex w-full items-center">
|
|
35
|
+
{i > 0 && (
|
|
36
|
+
<div className={`h-0.5 flex-1 ${i <= currentIdx ? 'bg-emerald-500' : 'bg-gray-200 dark:bg-gray-800'}`} />
|
|
37
|
+
)}
|
|
38
|
+
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold ${
|
|
39
|
+
i < currentIdx
|
|
40
|
+
? completedStepClassName
|
|
41
|
+
: i === currentIdx
|
|
42
|
+
? activeStepClassName
|
|
43
|
+
: inactiveStepClassName
|
|
44
|
+
}`}>
|
|
45
|
+
{i < currentIdx ? '✓' : i + 1}
|
|
46
|
+
</div>
|
|
47
|
+
{i < stages.length - 1 && (
|
|
48
|
+
<div className={`h-0.5 flex-1 ${i < currentIdx ? 'bg-emerald-500' : 'bg-gray-200 dark:bg-gray-800'}`} />
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
<p className={`mt-2 text-center text-xs ${i === currentIdx ? 'font-semibold text-gray-900 dark:text-gray-100' : 'text-gray-500'}`}>
|
|
52
|
+
{s.label}
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useState } from 'react'
|
|
3
|
+
import { useAvailableSlots } from '../../hooks/use-service-booking'
|
|
4
|
+
import { formatTimeString } from '../../utils/format'
|
|
5
|
+
|
|
6
|
+
export interface SlotPickerProps {
|
|
7
|
+
productId: string
|
|
8
|
+
/** A single date (YYYY-MM-DD) — the real backend checks one day at a time. */
|
|
9
|
+
date: string
|
|
10
|
+
staffId?: string
|
|
11
|
+
locationId?: string
|
|
12
|
+
onSelectSlot: (time: string) => void
|
|
13
|
+
className?: string
|
|
14
|
+
slotButtonClassName?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* REAL BUG FIXED (2026-09-20): rebuilt against the real backend response
|
|
19
|
+
* (GET /catalog/products/{id}/available-slots — a single `date`, not a
|
|
20
|
+
* dateFrom/dateTo range; `available_slots` is a flat array of ISO time
|
|
21
|
+
* strings, not slot objects with start_time/end_time/slots_remaining, none
|
|
22
|
+
* of which the real endpoint returns).
|
|
23
|
+
*/
|
|
24
|
+
export function SlotPicker({
|
|
25
|
+
productId,
|
|
26
|
+
date,
|
|
27
|
+
staffId,
|
|
28
|
+
locationId,
|
|
29
|
+
onSelectSlot,
|
|
30
|
+
className = "space-y-4",
|
|
31
|
+
slotButtonClassName = "p-3 border rounded-lg text-sm text-center hover:border-primary focus:outline-none transition-colors",
|
|
32
|
+
}: SlotPickerProps) {
|
|
33
|
+
const { data, isLoading, error } = useAvailableSlots(undefined, productId, date, { staffId, locationId })
|
|
34
|
+
const [selectedTime, setSelectedTime] = useState<string | null>(null)
|
|
35
|
+
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 animate-pulse">
|
|
39
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
40
|
+
<div key={i} className="h-12 bg-muted rounded-lg" />
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error || !data) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="text-xs text-destructive">
|
|
49
|
+
Failed to load available time slots. Please select a different date.
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!data.serviceable) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="text-xs text-muted-foreground py-4 text-center">
|
|
57
|
+
{data.reason || "This service isn't available for booking right now."}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (data.available_slots.length === 0) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="text-xs text-muted-foreground py-4 text-center">
|
|
65
|
+
No time slots available on this date.
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={className}>
|
|
72
|
+
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Available Time Slots</h4>
|
|
73
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
74
|
+
{data.available_slots.map((time) => {
|
|
75
|
+
const isSelected = selectedTime === time
|
|
76
|
+
return (
|
|
77
|
+
<button
|
|
78
|
+
key={time}
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => {
|
|
81
|
+
setSelectedTime(time)
|
|
82
|
+
onSelectSlot(time)
|
|
83
|
+
}}
|
|
84
|
+
className={`${slotButtonClassName} ${
|
|
85
|
+
isSelected
|
|
86
|
+
? 'border-primary bg-primary/10 text-primary font-semibold'
|
|
87
|
+
: 'border-border text-foreground'
|
|
88
|
+
}`}
|
|
89
|
+
>
|
|
90
|
+
{formatTimeString(time)}
|
|
91
|
+
</button>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SlotPicker'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { CartItem as CartItemType } from '../../types/store'
|
|
4
|
+
|
|
5
|
+
export interface CartItemProps {
|
|
6
|
+
item: CartItemType
|
|
7
|
+
onUpdateQty: (itemId: string, qty: number) => void
|
|
8
|
+
onRemove: (itemId: string) => void
|
|
9
|
+
formatPriceFn?: (price: number) => string
|
|
10
|
+
className?: string
|
|
11
|
+
imageClassName?: string
|
|
12
|
+
titleClassName?: string
|
|
13
|
+
priceClassName?: string
|
|
14
|
+
quantityGroupClassName?: string
|
|
15
|
+
buttonClassName?: string
|
|
16
|
+
removeButtonClassName?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CartItem({
|
|
20
|
+
item,
|
|
21
|
+
onUpdateQty,
|
|
22
|
+
onRemove,
|
|
23
|
+
formatPriceFn,
|
|
24
|
+
className = "flex items-center gap-4 py-3 border-b border-gray-100 dark:border-gray-800",
|
|
25
|
+
imageClassName = "w-16 h-16 object-cover rounded-md bg-gray-100 dark:bg-gray-800 shrink-0",
|
|
26
|
+
titleClassName = "text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1",
|
|
27
|
+
priceClassName = "text-sm font-semibold text-gray-900 dark:text-gray-100",
|
|
28
|
+
quantityGroupClassName = "flex items-center border rounded-md border-gray-200 dark:border-gray-700",
|
|
29
|
+
buttonClassName = "px-2 py-1 text-xs hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50",
|
|
30
|
+
removeButtonClassName = "text-xs text-red-600 hover:text-red-700 dark:text-red-400 transition-colors ml-auto",
|
|
31
|
+
}: CartItemProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={className}>
|
|
34
|
+
{item.image_url ? (
|
|
35
|
+
<img src={item.image_url} alt={item.product_name} className={imageClassName} />
|
|
36
|
+
) : (
|
|
37
|
+
<div className={imageClassName} />
|
|
38
|
+
)}
|
|
39
|
+
<div className="flex-1 min-w-0 space-y-1">
|
|
40
|
+
<h4 className={titleClassName}>{item.product_name}</h4>
|
|
41
|
+
{item.variant_attributes && (
|
|
42
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
43
|
+
{typeof item.variant_attributes === 'string'
|
|
44
|
+
? item.variant_attributes
|
|
45
|
+
: Object.entries(item.variant_attributes).map(([k, v]) => `${k}: ${v}`).join(', ')}
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
<div className={priceClassName}>
|
|
49
|
+
{formatPriceFn ? formatPriceFn(item.unit_price * item.quantity) : `${(item as { currency?: string }).currency || ''}${item.unit_price * item.quantity}`}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex flex-col items-end gap-2 shrink-0">
|
|
53
|
+
<div className={quantityGroupClassName}>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => onUpdateQty(item.id, Math.max(1, item.quantity - 1))}
|
|
57
|
+
disabled={item.quantity <= 1}
|
|
58
|
+
className={buttonClassName}
|
|
59
|
+
>
|
|
60
|
+
-
|
|
61
|
+
</button>
|
|
62
|
+
<span className="px-2 text-xs font-medium">{item.quantity}</span>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={() => onUpdateQty(item.id, item.quantity + 1)}
|
|
66
|
+
className={buttonClassName}
|
|
67
|
+
>
|
|
68
|
+
+
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => onRemove(item.id)}
|
|
74
|
+
className={removeButtonClassName}
|
|
75
|
+
>
|
|
76
|
+
Remove
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { useCart } from '../../hooks/use-cart'
|
|
4
|
+
import { CartItem } from './CartItem'
|
|
5
|
+
import { CartTypeMismatchBanner } from './CartTypeMismatchBanner'
|
|
6
|
+
import { CartItemSkeleton } from '../skeletons/CartItemSkeleton'
|
|
7
|
+
import { formatPrice } from '../../utils/format'
|
|
8
|
+
|
|
9
|
+
export interface CartSidebarProps {
|
|
10
|
+
onCheckout?: () => void
|
|
11
|
+
onClose?: () => void
|
|
12
|
+
formatPriceFn?: (price: number) => string
|
|
13
|
+
className?: string
|
|
14
|
+
headerClassName?: string
|
|
15
|
+
titleClassName?: string
|
|
16
|
+
bodyClassName?: string
|
|
17
|
+
footerClassName?: string
|
|
18
|
+
checkoutButtonClassName?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CartSidebar({
|
|
22
|
+
onCheckout,
|
|
23
|
+
onClose,
|
|
24
|
+
formatPriceFn,
|
|
25
|
+
className = "flex flex-col h-full w-full max-w-md bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800 shadow-xl",
|
|
26
|
+
headerClassName = "flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800",
|
|
27
|
+
titleClassName = "text-lg font-semibold text-gray-900 dark:text-gray-100",
|
|
28
|
+
bodyClassName = "flex-1 overflow-y-auto p-4 space-y-4",
|
|
29
|
+
footerClassName = "p-4 border-t border-gray-200 dark:border-gray-800 space-y-3 bg-gray-50/50 dark:bg-gray-900/50",
|
|
30
|
+
checkoutButtonClassName = "w-full py-3 px-4 bg-emerald-600 hover:bg-emerald-700 text-white font-medium rounded-lg transition-colors text-center block disabled:opacity-50",
|
|
31
|
+
}: CartSidebarProps) {
|
|
32
|
+
const {
|
|
33
|
+
items,
|
|
34
|
+
count,
|
|
35
|
+
subtotal,
|
|
36
|
+
isLoading,
|
|
37
|
+
updateQty,
|
|
38
|
+
removeItem,
|
|
39
|
+
productTypeMismatch,
|
|
40
|
+
handleClearCartAndAdd,
|
|
41
|
+
handleKeepCurrentCart,
|
|
42
|
+
setOpen,
|
|
43
|
+
} = useCart()
|
|
44
|
+
|
|
45
|
+
const handleClose = () => {
|
|
46
|
+
setOpen(false)
|
|
47
|
+
if (onClose) onClose()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={className}>
|
|
52
|
+
<div className={headerClassName}>
|
|
53
|
+
<h3 className={titleClassName}>Shopping Cart ({count})</h3>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={handleClose}
|
|
57
|
+
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
|
58
|
+
aria-label="Close cart"
|
|
59
|
+
>
|
|
60
|
+
✕
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className={bodyClassName}>
|
|
65
|
+
{productTypeMismatch && (
|
|
66
|
+
<CartTypeMismatchBanner
|
|
67
|
+
mismatch={productTypeMismatch}
|
|
68
|
+
onClearAndAdd={handleClearCartAndAdd}
|
|
69
|
+
onKeepCurrent={handleKeepCurrentCart}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{isLoading ? (
|
|
74
|
+
<CartItemSkeleton count={3} />
|
|
75
|
+
) : items.length === 0 ? (
|
|
76
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
77
|
+
Your cart is empty
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
items.map((item) => (
|
|
81
|
+
<CartItem
|
|
82
|
+
key={item.id}
|
|
83
|
+
item={item}
|
|
84
|
+
onUpdateQty={updateQty}
|
|
85
|
+
onRemove={removeItem}
|
|
86
|
+
formatPriceFn={formatPriceFn}
|
|
87
|
+
/>
|
|
88
|
+
))
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{items.length > 0 && (
|
|
93
|
+
<div className={footerClassName}>
|
|
94
|
+
<div className="flex justify-between items-center text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
95
|
+
<span>Subtotal</span>
|
|
96
|
+
{/* CartResponse carries no currency field — a consuming app
|
|
97
|
+
MUST supply formatPriceFn (its own business-currency-aware
|
|
98
|
+
formatter) for a correct display; the bare fallback is
|
|
99
|
+
honest about not knowing the real currency rather than
|
|
100
|
+
guessing one. */}
|
|
101
|
+
<span>{formatPriceFn ? formatPriceFn(subtotal) : formatPrice(subtotal, undefined)}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={onCheckout}
|
|
106
|
+
disabled={items.length === 0}
|
|
107
|
+
className={checkoutButtonClassName}
|
|
108
|
+
>
|
|
109
|
+
Proceed to Checkout
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export interface CartTypeMismatchBannerProps {
|
|
5
|
+
mismatch: {
|
|
6
|
+
message: string
|
|
7
|
+
existing_type: string
|
|
8
|
+
new_type: string
|
|
9
|
+
}
|
|
10
|
+
onClearAndAdd: () => void
|
|
11
|
+
onKeepCurrent: () => void
|
|
12
|
+
className?: string
|
|
13
|
+
titleClassName?: string
|
|
14
|
+
messageClassName?: string
|
|
15
|
+
buttonGroupClassName?: string
|
|
16
|
+
confirmButtonClassName?: string
|
|
17
|
+
cancelButtonClassName?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CartTypeMismatchBanner({
|
|
21
|
+
mismatch,
|
|
22
|
+
onClearAndAdd,
|
|
23
|
+
onKeepCurrent,
|
|
24
|
+
className = "p-4 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800 rounded-lg space-y-3",
|
|
25
|
+
titleClassName = "text-sm font-semibold text-amber-800 dark:text-amber-200",
|
|
26
|
+
messageClassName = "text-xs text-amber-700 dark:text-amber-300",
|
|
27
|
+
buttonGroupClassName = "flex items-center gap-2 pt-1",
|
|
28
|
+
confirmButtonClassName = "px-3 py-1.5 text-xs font-medium bg-amber-600 hover:bg-amber-700 text-white rounded transition-colors",
|
|
29
|
+
cancelButtonClassName = "px-3 py-1.5 text-xs font-medium border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 rounded hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors",
|
|
30
|
+
}: CartTypeMismatchBannerProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={className}>
|
|
33
|
+
<div className={titleClassName}>Cart Product Type Conflict</div>
|
|
34
|
+
<p className={messageClassName}>
|
|
35
|
+
{mismatch.message || `Your cart currently contains ${mismatch.existing_type} items. You cannot mix ${mismatch.existing_type} and ${mismatch.new_type} items in the same order.`}
|
|
36
|
+
</p>
|
|
37
|
+
<div className={buttonGroupClassName}>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={onClearAndAdd}
|
|
41
|
+
className={confirmButtonClassName}
|
|
42
|
+
>
|
|
43
|
+
Clear Cart & Add New Item
|
|
44
|
+
</button>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={onKeepCurrent}
|
|
48
|
+
className={cancelButtonClassName}
|
|
49
|
+
>
|
|
50
|
+
Keep Current Cart
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|