@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.
Files changed (176) hide show
  1. package/README.md +42 -15
  2. package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
  3. package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
  4. package/dist/components/index.d.mts +497 -0
  5. package/dist/components/index.d.ts +497 -0
  6. package/dist/components/index.js +4217 -0
  7. package/dist/components/index.js.map +1 -0
  8. package/dist/components/index.mjs +4194 -0
  9. package/dist/components/index.mjs.map +1 -0
  10. package/dist/config/server.d.mts +26 -0
  11. package/dist/config/server.d.ts +26 -0
  12. package/dist/config/server.js +38 -0
  13. package/dist/config/server.js.map +1 -0
  14. package/dist/config/server.mjs +13 -0
  15. package/dist/config/server.mjs.map +1 -0
  16. package/dist/hooks/index.d.mts +393 -160
  17. package/dist/hooks/index.d.ts +393 -160
  18. package/dist/hooks/index.js +2792 -2137
  19. package/dist/hooks/index.js.map +1 -1
  20. package/dist/hooks/index.mjs +2734 -2121
  21. package/dist/hooks/index.mjs.map +1 -1
  22. package/dist/index-CNW_LINy.d.mts +578 -0
  23. package/dist/index-CNW_LINy.d.ts +578 -0
  24. package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
  25. package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
  26. package/dist/index-NkI10J_E.d.mts +40 -0
  27. package/dist/index-NkI10J_E.d.ts +40 -0
  28. package/dist/index.d.mts +20437 -103
  29. package/dist/index.d.ts +20437 -103
  30. package/dist/index.js +2111 -1802
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +2006 -1786
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
  35. package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
  36. package/dist/services/index.d.mts +908 -2
  37. package/dist/services/index.d.ts +908 -2
  38. package/dist/services/index.js +1492 -1686
  39. package/dist/services/index.js.map +1 -1
  40. package/dist/services/index.mjs +1431 -1670
  41. package/dist/services/index.mjs.map +1 -1
  42. package/package.json +46 -6
  43. package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
  44. package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
  45. package/src/components/bespoke/index.ts +3 -0
  46. package/src/components/booking/SlotPicker.tsx +97 -0
  47. package/src/components/booking/index.ts +1 -0
  48. package/src/components/cart/CartItem.tsx +81 -0
  49. package/src/components/cart/CartSidebar.tsx +115 -0
  50. package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
  51. package/src/components/cart/index.ts +3 -0
  52. package/src/components/index.ts +8 -0
  53. package/src/components/orders/DeliverableReviewer.tsx +139 -0
  54. package/src/components/orders/index.ts +1 -0
  55. package/src/components/product/ProductCard.tsx +350 -0
  56. package/src/components/product/ProductGrid.tsx +84 -0
  57. package/src/components/product/index.ts +2 -0
  58. package/src/components/shared/AddressAutocomplete.tsx +212 -0
  59. package/src/components/shared/AnalyticsScripts.tsx +56 -0
  60. package/src/components/shared/CookieConsentBanner.tsx +98 -0
  61. package/src/components/shared/ImageEditorModal.tsx +1066 -0
  62. package/src/components/shared/ImageViewerModal.tsx +598 -0
  63. package/src/components/shared/LucideReactIcon.tsx +52 -0
  64. package/src/components/shared/MapPinDrop.tsx +101 -0
  65. package/src/components/shared/MultiSelectPicker.tsx +193 -0
  66. package/src/components/shared/OfflineBanner.tsx +46 -0
  67. package/src/components/shared/SearchableInput.tsx +153 -0
  68. package/src/components/shared/SearchablePopover.tsx +166 -0
  69. package/src/components/shared/SmartImage.tsx +311 -0
  70. package/src/components/shared/TruncatedValue.tsx +27 -0
  71. package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
  72. package/src/components/shared/index.ts +15 -0
  73. package/src/components/skeletons/CardSkeleton.tsx +26 -0
  74. package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
  75. package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
  76. package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
  77. package/src/components/skeletons/TableSkeleton.tsx +42 -0
  78. package/src/components/skeletons/index.ts +5 -0
  79. package/src/config/index.ts +22 -4
  80. package/src/config/server.ts +45 -0
  81. package/src/context/sdk-context.tsx +5 -2
  82. package/src/generated/auth-openapi.ts +2935 -0
  83. package/src/generated/cart-openapi.ts +652 -0
  84. package/src/generated/cms-openapi.ts +1668 -0
  85. package/src/generated/crm-openapi.ts +986 -0
  86. package/src/generated/graphql.ts +126 -0
  87. package/src/generated/index.ts +11 -0
  88. package/src/generated/media-openapi.ts +613 -0
  89. package/src/generated/notification-openapi.ts +862 -0
  90. package/src/generated/payment-openapi.ts +2713 -0
  91. package/src/generated/store-openapi.ts +9510 -0
  92. package/src/graphql/operations/collections.graphql +32 -0
  93. package/src/graphql/operations/products.graphql +197 -0
  94. package/src/graphql/operations/wishlist.graphql +25 -0
  95. package/src/hooks/index.ts +9 -2
  96. package/src/hooks/use-auth.ts +163 -69
  97. package/src/hooks/use-bespoke-availability.ts +39 -22
  98. package/src/hooks/use-business-config.ts +51 -0
  99. package/src/hooks/use-cart.ts +310 -238
  100. package/src/hooks/use-cms.ts +107 -127
  101. package/src/hooks/use-collections.ts +35 -55
  102. package/src/hooks/use-consultations.ts +9 -136
  103. package/src/hooks/use-countries.ts +19 -28
  104. package/src/hooks/use-currencies.ts +8 -20
  105. package/src/hooks/use-customer-address.ts +64 -0
  106. package/src/hooks/use-feedback.ts +34 -0
  107. package/src/hooks/use-languages.ts +26 -39
  108. package/src/hooks/use-media-upload.ts +23 -0
  109. package/src/hooks/use-notification-stream.ts +88 -0
  110. package/src/hooks/use-notifications.ts +163 -0
  111. package/src/hooks/use-order-stream.ts +29 -29
  112. package/src/hooks/use-orders.ts +91 -73
  113. package/src/hooks/use-payment.ts +116 -84
  114. package/src/hooks/use-products.ts +65 -269
  115. package/src/hooks/use-push-notifications.ts +122 -0
  116. package/src/hooks/use-reviews.ts +175 -0
  117. package/src/hooks/use-service-booking.ts +109 -132
  118. package/src/hooks/use-states.ts +16 -30
  119. package/src/hooks/use-wishlist.ts +44 -69
  120. package/src/index.ts +15 -11
  121. package/src/services/auth.service.ts +422 -132
  122. package/src/services/bespoke.service.ts +69 -10
  123. package/src/services/cart.service.ts +155 -186
  124. package/src/services/cms.service.ts +41 -50
  125. package/src/services/collection.service.ts +25 -63
  126. package/src/services/config.service.ts +10 -21
  127. package/src/services/consultation.service.ts +36 -93
  128. package/src/services/feedback.service.ts +76 -0
  129. package/src/services/index.ts +7 -1
  130. package/src/services/legal.service.ts +62 -0
  131. package/src/services/logistics.service.ts +235 -53
  132. package/src/services/media.service.ts +115 -0
  133. package/src/services/notifications.service.ts +204 -20
  134. package/src/services/order.service.ts +498 -127
  135. package/src/services/payment.service.ts +149 -100
  136. package/src/services/product.service.ts +157 -814
  137. package/src/services/review.service.ts +211 -0
  138. package/src/services/service-booking.service.ts +198 -99
  139. package/src/services/wishlist.service.ts +27 -74
  140. package/src/types/auth/index.ts +161 -18
  141. package/src/types/business/index.ts +9 -1
  142. package/src/types/cms/index.ts +91 -6
  143. package/src/types/cms/legal.ts +18 -0
  144. package/src/types/consultations/index.ts +60 -68
  145. package/src/types/index.ts +5 -3
  146. package/src/types/media/index.ts +35 -3
  147. package/src/types/payments/index.ts +80 -16
  148. package/src/types/reviews/index.ts +116 -0
  149. package/src/types/services/index.ts +113 -38
  150. package/src/types/store/checkout.ts +53 -0
  151. package/src/types/store/index.ts +273 -52
  152. package/src/types/store/invoice.ts +31 -0
  153. package/src/types/store/shipping.ts +84 -3
  154. package/src/types/ui/index.ts +1 -1
  155. package/src/utils/checkout.ts +19 -3
  156. package/src/utils/cookieConsent.ts +34 -0
  157. package/src/utils/format.ts +408 -0
  158. package/src/utils/geolocation.ts +134 -0
  159. package/src/utils/graphql-url.ts +47 -0
  160. package/src/utils/guest-session.ts +59 -0
  161. package/src/utils/http-client.ts +147 -0
  162. package/src/utils/index.ts +11 -4
  163. package/src/utils/status-colors.ts +79 -0
  164. package/src/utils/token-refresh.ts +115 -0
  165. package/src/utils/validators.ts +13 -0
  166. package/src/utils/youtube.ts +18 -0
  167. package/dist/index-C5YO9gTl.d.mts +0 -507
  168. package/dist/index-C5YO9gTl.d.ts +0 -507
  169. package/dist/index-CefG7_xq.d.mts +0 -793
  170. package/dist/index-DeHCf3zI.d.ts +0 -793
  171. package/src/graphql/collections.graphql.ts +0 -48
  172. package/src/graphql/index.ts +0 -7
  173. package/src/graphql/products.graphql.ts +0 -860
  174. package/src/graphql/wishlist.graphql.ts +0 -34
  175. package/src/hooks/use-consultation-stream.ts +0 -142
  176. 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.8",
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
- "react-use-websocket": "^4.13.0",
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
- "@types/uuid": "^10.0.0",
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,3 @@
1
+ export * from './BespokeAvailabilityBadge'
2
+ export * from './BespokeProgressTracker'
3
+
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './CartItem'
2
+ export * from './CartSidebar'
3
+ export * from './CartTypeMismatchBanner'
@@ -0,0 +1,8 @@
1
+ export * from './skeletons'
2
+ export * from './cart'
3
+ export * from './product'
4
+ export * from './booking'
5
+ export * from './bespoke'
6
+ export * from './orders'
7
+ export * from './shared'
8
+