@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
@@ -0,0 +1,311 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, forwardRef, type CSSProperties } from "react";
4
+ import { motion, type HTMLMotionProps } from "framer-motion";
5
+ import Image from "next/image";
6
+ import { ImageIcon } from "lucide-react";
7
+
8
+ // ─── Types ────────────────────────────────────────────────────────────────────
9
+
10
+ type ObjectFit = "cover" | "contain" | "fill" | "none" | "scale-down";
11
+
12
+ interface BaseSmartImageProps {
13
+ src: string | null | undefined;
14
+ alt: string;
15
+ className?: string;
16
+ imageClassName?: string;
17
+ priority?: boolean;
18
+ isRelative?: boolean;
19
+ sizes?: string;
20
+ objectFit?: ObjectFit;
21
+ /**
22
+ * Optional fallback image src rendered instead of the broken-image
23
+ * placeholder if the primary src fails to load.
24
+ */
25
+ fallbackSrc?: string;
26
+ /**
27
+ * Passed-through to the underlying Next.js <Image> to suppress
28
+ * hydration warnings on SSR-rendered images.
29
+ */
30
+ suppressHydrationWarning?: boolean;
31
+ }
32
+
33
+ /**
34
+ * SmartImageProps for the standard (non-motion) variant.
35
+ * Adds optional `style` for container sizing (e.g. explicit width/height).
36
+ */
37
+ export type SmartImageProps = BaseSmartImageProps & {
38
+ style?: CSSProperties;
39
+ draggable?: boolean;
40
+ };
41
+
42
+ /**
43
+ * SmartMotionImageProps for the Framer Motion variant.
44
+ * Extends SmartImageProps with all `motion.div` props (initial, animate, etc.)
45
+ */
46
+ export type SmartMotionImageProps = BaseSmartImageProps &
47
+ Omit<HTMLMotionProps<"div">, "draggable"> & {
48
+ style?: CSSProperties;
49
+ draggable?: boolean;
50
+ };
51
+
52
+ // ─── Source Validator ─────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Strictly validates that a source is a viable image path/URL.
56
+ * Rejects empty strings, the literal strings "null"/"undefined", and anything
57
+ * that doesn't start with a recognised URL scheme or path prefix.
58
+ */
59
+ const isValidSrc = (url: unknown): url is string => {
60
+ if (!url || typeof url !== "string") return false;
61
+ const s = url.trim();
62
+ if (s === "" || s === "null" || s === "undefined") return false;
63
+ return (
64
+ s.startsWith("/") ||
65
+ s.startsWith("http://") ||
66
+ s.startsWith("https://") ||
67
+ s.startsWith("data:image/") ||
68
+ s.startsWith("blob:")
69
+ );
70
+ };
71
+
72
+ // ─── Shared State Hook ────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * Tracks mount, load, and error state for a given `src`.
76
+ * Centralised here so both SmartImage and SmartMotionImage stay in sync.
77
+ */
78
+ function useImageState(src: string | null | undefined) {
79
+ const [isMounted, setIsMounted] = useState(false);
80
+ const [isLoading, setIsLoading] = useState(true);
81
+ const [hasError, setHasError] = useState(false);
82
+
83
+ useEffect(() => {
84
+ setIsMounted(true);
85
+ }, []);
86
+
87
+ // Reset when src changes so a freshly swapped-in URL doesn't inherit
88
+ // the previous URL's stale loaded/failed state.
89
+ useEffect(() => {
90
+ setIsLoading(true);
91
+ setHasError(false);
92
+ }, [src]);
93
+
94
+ const handleLoad = () => setIsLoading(false);
95
+ const handleError = () => {
96
+ setIsLoading(false);
97
+ setHasError(true);
98
+ };
99
+
100
+ return { isMounted, isLoading, hasError, handleLoad, handleError };
101
+ }
102
+
103
+ // ─── Placeholder ──────────────────────────────────────────────────────────────
104
+
105
+ interface ImagePlaceholderProps {
106
+ className: string;
107
+ /** When true, renders a visually distinct "failed to load" state. */
108
+ failed?: boolean;
109
+ }
110
+
111
+ /**
112
+ * "No image" / "Failed to load" placeholder.
113
+ * Uses lucide-react's ImageIcon (same dep already required by consumers).
114
+ */
115
+ function ImagePlaceholder({ className, failed }: ImagePlaceholderProps) {
116
+ return (
117
+ <div
118
+ className={`relative overflow-hidden flex flex-col items-center justify-center gap-1 bg-muted/40 ${className}`}
119
+ >
120
+ <ImageIcon className="w-4 h-4 text-muted-foreground/50 shrink-0" />
121
+ <span className="text-[10px] text-muted-foreground/70 text-center leading-tight px-1">
122
+ {failed ? "Failed to load" : "No image"}
123
+ </span>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ // ─── Shimmer Skeleton ─────────────────────────────────────────────────────────
129
+
130
+ /** Full-bleed sweep shimmer shown while the image is in-flight. */
131
+ function ImageShimmer() {
132
+ return (
133
+ <div className="absolute inset-0 z-10 bg-muted/60 overflow-hidden">
134
+ <div className="absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/10 to-transparent" />
135
+ </div>
136
+ );
137
+ }
138
+
139
+ // ─── SmartImage ───────────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Standard smart image component for Kasuvia storefronts and dashboards.
143
+ *
144
+ * Wraps Next.js `<Image fill>` with:
145
+ * - Strict src validation (rejects null / empty / "null" strings)
146
+ * - A shimmer skeleton shown while the image loads
147
+ * - A labelled placeholder shown when there is no valid src
148
+ * - A distinct failed-to-load placeholder (or optional `fallbackSrc`)
149
+ * - An `objectFit` prop (defaults to "cover") for flexibility
150
+ *
151
+ * Peer dependencies: next, framer-motion, lucide-react
152
+ */
153
+ export const SmartImage = forwardRef<HTMLImageElement, SmartImageProps>(function SmartImage(
154
+ {
155
+ src,
156
+ alt,
157
+ className = "",
158
+ imageClassName = "",
159
+ priority = false,
160
+ isRelative = true,
161
+ sizes = "100vw",
162
+ objectFit = "cover",
163
+ fallbackSrc,
164
+ suppressHydrationWarning = false,
165
+ style,
166
+ draggable,
167
+ },
168
+ ref
169
+ ) {
170
+ const { isMounted, isLoading, hasError, handleLoad, handleError } =
171
+ useImageState(src);
172
+
173
+ const resolvedSrc = isValidSrc(src) ? src.trim() : null;
174
+ const fallback = isValidSrc(fallbackSrc) ? fallbackSrc!.trim() : null;
175
+
176
+ if (!isMounted || (!resolvedSrc && !fallback)) {
177
+ return <ImagePlaceholder className={className} />;
178
+ }
179
+
180
+ if (hasError) {
181
+ if (fallback) {
182
+ return (
183
+ <div
184
+ className={`${isRelative ? "relative" : ""} overflow-hidden ${className}`}
185
+ style={style}
186
+ >
187
+ <Image
188
+ ref={ref}
189
+ src={fallback}
190
+ alt={alt}
191
+ fill
192
+ unoptimized
193
+ sizes={sizes}
194
+ className={`object-${objectFit} ${imageClassName}`}
195
+ draggable={draggable}
196
+ />
197
+ </div>
198
+ );
199
+ }
200
+ return <ImagePlaceholder className={className} failed />;
201
+ }
202
+
203
+ return (
204
+ <div
205
+ className={`${isRelative ? "relative" : ""} overflow-hidden ${className}`}
206
+ style={style}
207
+ >
208
+ {isLoading && <ImageShimmer />}
209
+ <Image
210
+ ref={ref}
211
+ src={resolvedSrc!}
212
+ alt={alt}
213
+ fill
214
+ priority={priority}
215
+ sizes={sizes}
216
+ unoptimized
217
+ suppressHydrationWarning={suppressHydrationWarning}
218
+ className={`object-${objectFit} ${imageClassName} transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`}
219
+ onLoad={handleLoad}
220
+ onError={handleError}
221
+ draggable={draggable}
222
+ />
223
+ </div>
224
+ );
225
+ });
226
+
227
+ // ─── SmartMotionImage ─────────────────────────────────────────────────────────
228
+
229
+ /**
230
+ * Framer Motion variant of SmartImage.
231
+ * Accepts all `motion.div` props (e.g. `initial`, `animate`, `whileHover`)
232
+ * alongside the standard SmartImage props.
233
+ *
234
+ * Peer dependencies: next, framer-motion, lucide-react
235
+ */
236
+ export const SmartMotionImage = forwardRef<HTMLDivElement, SmartMotionImageProps>(function SmartMotionImage(
237
+ {
238
+ src,
239
+ alt,
240
+ className = "",
241
+ imageClassName = "",
242
+ priority = false,
243
+ isRelative = true,
244
+ sizes = "100vw",
245
+ objectFit = "cover",
246
+ fallbackSrc,
247
+ style,
248
+ draggable,
249
+ ...motionProps
250
+ },
251
+ ref
252
+ ) {
253
+ const { isMounted, isLoading, hasError, handleLoad, handleError } =
254
+ useImageState(src);
255
+
256
+ const resolvedSrc = isValidSrc(src) ? src.trim() : null;
257
+ const fallback = isValidSrc(fallbackSrc) ? fallbackSrc!.trim() : null;
258
+
259
+ if (!isMounted || (!resolvedSrc && !fallback)) {
260
+ return <ImagePlaceholder className={className} />;
261
+ }
262
+
263
+ if (hasError) {
264
+ if (fallback) {
265
+ return (
266
+ <motion.div
267
+ ref={ref}
268
+ className={`${isRelative ? "relative" : ""} overflow-hidden ${className}`}
269
+ style={style}
270
+ {...motionProps}
271
+ >
272
+ <Image
273
+ src={fallback}
274
+ alt={alt}
275
+ fill
276
+ unoptimized
277
+ sizes={sizes}
278
+ className={`object-${objectFit} ${imageClassName}`}
279
+ draggable={draggable}
280
+ />
281
+ </motion.div>
282
+ );
283
+ }
284
+ return <ImagePlaceholder className={className} failed />;
285
+ }
286
+
287
+ return (
288
+ <motion.div
289
+ ref={ref}
290
+ className={`${isRelative ? "relative" : ""} overflow-hidden ${className}`}
291
+ style={style}
292
+ {...motionProps}
293
+ >
294
+ {isLoading && <ImageShimmer />}
295
+ <Image
296
+ src={resolvedSrc!}
297
+ alt={alt}
298
+ fill
299
+ priority={priority}
300
+ sizes={sizes}
301
+ unoptimized
302
+ className={`object-${objectFit} ${imageClassName} transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`}
303
+ onLoad={handleLoad}
304
+ onError={handleError}
305
+ draggable={draggable}
306
+ />
307
+ </motion.div>
308
+ );
309
+ });
310
+
311
+ export default SmartImage;
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface TruncatedValueProps {
4
+ /** The (possibly visually truncated, or compact/abbreviated) displayed value. */
5
+ value: ReactNode;
6
+ /** The full, untruncated/unabbreviated value shown on hover/title. */
7
+ fullValue?: ReactNode;
8
+ className?: string;
9
+ triggerClassName?: string;
10
+ }
11
+
12
+ /**
13
+ * TruncatedValue
14
+ * Wraps a string or numeric value so the full value is accessible on hover or title.
15
+ */
16
+ export function TruncatedValue({ value, fullValue, className = "", triggerClassName = "" }: TruncatedValueProps) {
17
+ const tooltipText = typeof fullValue === "string" ? fullValue : typeof value === "string" ? value : undefined;
18
+
19
+ return (
20
+ <span
21
+ title={tooltipText}
22
+ className={`truncate tabular-nums cursor-default inline-block max-w-full ${triggerClassName} ${className}`.trim()}
23
+ >
24
+ {value}
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from "react";
4
+ import { AlertTriangle } from "lucide-react";
5
+
6
+ export interface UseUnsavedChangesOptions {
7
+ browserMessage?: string;
8
+ dialogTitle?: string;
9
+ dialogBody?: string;
10
+ }
11
+
12
+ export interface UnsavedChangesDialogProps {
13
+ open: boolean;
14
+ onStay: () => void;
15
+ onLeave: () => void;
16
+ title?: string;
17
+ body?: string;
18
+ }
19
+
20
+ export interface UseUnsavedChangesReturn {
21
+ isDirty: boolean;
22
+ markDirty: () => void;
23
+ resetDirty: () => void;
24
+ guardedNavigate: (action: () => void) => void;
25
+ dialogProps: UnsavedChangesDialogProps;
26
+ }
27
+
28
+ export function useUnsavedChanges(
29
+ options: UseUnsavedChangesOptions = {}
30
+ ): UseUnsavedChangesReturn {
31
+ const {
32
+ browserMessage = "You have unsaved changes. If you leave, your work will be lost.",
33
+ dialogTitle = "Unsaved Changes",
34
+ dialogBody = "You have unsaved changes. If you leave now, all your progress will be lost.",
35
+ } = options;
36
+
37
+ const [isDirty, setIsDirty] = useState(false);
38
+ const [dialogOpen, setDialogOpen] = useState(false);
39
+ const pendingActionRef = useRef<(() => void) | null>(null);
40
+
41
+ useEffect(() => {
42
+ if (!isDirty) return;
43
+ const handler = (e: BeforeUnloadEvent) => {
44
+ e.preventDefault();
45
+ e.returnValue = browserMessage;
46
+ return browserMessage;
47
+ };
48
+ window.addEventListener("beforeunload", handler);
49
+ return () => window.removeEventListener("beforeunload", handler);
50
+ }, [isDirty, browserMessage]);
51
+
52
+ const markDirty = useCallback(() => setIsDirty(true), []);
53
+
54
+ const resetDirty = useCallback(() => {
55
+ setIsDirty(false);
56
+ setDialogOpen(false);
57
+ pendingActionRef.current = null;
58
+ }, []);
59
+
60
+ const guardedNavigate = useCallback(
61
+ (action: () => void) => {
62
+ if (!isDirty) {
63
+ action();
64
+ return;
65
+ }
66
+ pendingActionRef.current = action;
67
+ setDialogOpen(true);
68
+ },
69
+ [isDirty]
70
+ );
71
+
72
+ const handleStay = useCallback(() => {
73
+ setDialogOpen(false);
74
+ pendingActionRef.current = null;
75
+ }, []);
76
+
77
+ const handleLeave = useCallback(() => {
78
+ setDialogOpen(false);
79
+ setIsDirty(false);
80
+ const action = pendingActionRef.current;
81
+ pendingActionRef.current = null;
82
+ action?.();
83
+ }, []);
84
+
85
+ return {
86
+ isDirty,
87
+ markDirty,
88
+ resetDirty,
89
+ guardedNavigate,
90
+ dialogProps: {
91
+ open: dialogOpen,
92
+ onStay: handleStay,
93
+ onLeave: handleLeave,
94
+ title: dialogTitle,
95
+ body: dialogBody,
96
+ },
97
+ };
98
+ }
99
+
100
+ export function UnsavedChangesDialog({
101
+ open,
102
+ onStay,
103
+ onLeave,
104
+ title = "Unsaved Changes",
105
+ body = "You have unsaved changes. If you leave now, all your progress will be lost.",
106
+ }: UnsavedChangesDialogProps): React.JSX.Element | null {
107
+ if (!open) return null;
108
+
109
+ return (
110
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
111
+ <div className="w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-2xl space-y-4">
112
+ <div className="flex flex-col items-center text-center">
113
+ <div className="w-12 h-12 rounded-xl bg-amber-500/10 flex items-center justify-center mb-3 text-amber-500">
114
+ <AlertTriangle className="w-6 h-6" />
115
+ </div>
116
+ <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100">{title}</h3>
117
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1 leading-relaxed">{body}</p>
118
+ </div>
119
+ <div className="flex flex-col sm:flex-row gap-2 pt-2">
120
+ <button
121
+ type="button"
122
+ onClick={onStay}
123
+ className="flex-1 h-10 px-4 text-xs font-semibold rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-50"
124
+ >
125
+ Stay &amp; Keep Editing
126
+ </button>
127
+ <button
128
+ type="button"
129
+ onClick={onLeave}
130
+ className="flex-1 h-10 px-4 text-xs font-semibold rounded-xl bg-red-600 text-white hover:bg-red-700"
131
+ >
132
+ Leave Without Saving
133
+ </button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,15 @@
1
+ export * from "./TruncatedValue";
2
+ export * from "./AddressAutocomplete";
3
+ export * from "./OfflineBanner";
4
+ export * from "./MapPinDrop";
5
+ export * from "./SearchableInput";
6
+ export * from "./SearchablePopover";
7
+ export * from "./LucideReactIcon";
8
+ export * from "./UnsavedChangesGuard";
9
+ export * from "./MultiSelectPicker";
10
+ export * from "./SmartImage";
11
+ export * from "./ImageEditorModal";
12
+ export * from "./ImageViewerModal";
13
+ export * from "./AnalyticsScripts";
14
+ export * from "./CookieConsentBanner";
15
+
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ export interface CardSkeletonProps {
4
+ count?: number
5
+ containerClassName?: string
6
+ cardClassName?: string
7
+ }
8
+
9
+ export function CardSkeleton({
10
+ count = 4,
11
+ containerClassName = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
12
+ cardClassName = "rounded-xl border p-4 space-y-3 animate-pulse bg-card"
13
+ }: CardSkeletonProps) {
14
+ return (
15
+ <div className={containerClassName}>
16
+ {Array.from({ length: count }).map((_, i) => (
17
+ <div key={i} className={cardClassName}>
18
+ <div className="w-full aspect-square bg-muted rounded-lg" />
19
+ <div className="h-4 bg-muted rounded w-3/4" />
20
+ <div className="h-4 bg-muted rounded w-1/2" />
21
+ <div className="h-8 bg-muted rounded w-full mt-2" />
22
+ </div>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ export interface CartItemSkeletonProps {
4
+ count?: number
5
+ className?: string
6
+ }
7
+
8
+ export function CartItemSkeleton({
9
+ count = 3,
10
+ className = "space-y-4"
11
+ }: CartItemSkeletonProps) {
12
+ return (
13
+ <div className={className}>
14
+ {Array.from({ length: count }).map((_, i) => (
15
+ <div key={i} className="flex items-center gap-4 p-3 border-b animate-pulse">
16
+ <div className="w-16 h-16 bg-muted rounded-md shrink-0" />
17
+ <div className="flex-1 space-y-2">
18
+ <div className="h-4 bg-muted rounded w-2/3" />
19
+ <div className="h-3 bg-muted rounded w-1/3" />
20
+ </div>
21
+ <div className="h-6 w-16 bg-muted rounded shrink-0" />
22
+ </div>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ export interface OrderItemSkeletonProps {
4
+ count?: number
5
+ className?: string
6
+ }
7
+
8
+ export function OrderItemSkeleton({
9
+ count = 3,
10
+ className = "space-y-4"
11
+ }: OrderItemSkeletonProps) {
12
+ return (
13
+ <div className={className}>
14
+ {Array.from({ length: count }).map((_, i) => (
15
+ <div key={i} className="p-4 border rounded-lg animate-pulse space-y-3">
16
+ <div className="flex justify-between items-center">
17
+ <div className="h-4 bg-muted rounded w-1/3" />
18
+ <div className="h-5 w-20 bg-muted rounded-full" />
19
+ </div>
20
+ <div className="h-3 bg-muted rounded w-1/4" />
21
+ <div className="h-4 bg-muted rounded w-1/2" />
22
+ </div>
23
+ ))}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react'
2
+
3
+ export interface ProductCardSkeletonProps {
4
+ className?: string
5
+ imageClassName?: string
6
+ }
7
+
8
+ export function ProductCardSkeleton({
9
+ className = "flex flex-col h-full rounded-xl border bg-card overflow-hidden shadow-sm animate-pulse",
10
+ imageClassName = "w-full aspect-square bg-muted",
11
+ }: ProductCardSkeletonProps) {
12
+ return (
13
+ <div className={className}>
14
+ <div className={imageClassName} />
15
+ <div className="p-4 flex flex-col flex-1 space-y-3">
16
+ <div className="h-5 bg-muted rounded w-3/4" />
17
+ <div className="h-3 bg-muted rounded w-full" />
18
+ <div className="h-3 bg-muted rounded w-2/3" />
19
+ <div className="flex items-center justify-between pt-2 mt-auto">
20
+ <div className="h-6 bg-muted rounded w-20" />
21
+ </div>
22
+ <div className="w-full h-10 bg-muted rounded-lg mt-auto" />
23
+ </div>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,42 @@
1
+ import React from 'react'
2
+
3
+ export interface TableSkeletonProps {
4
+ rows?: number
5
+ columns?: number
6
+ className?: string
7
+ rowClassName?: string
8
+ }
9
+
10
+ export function TableSkeleton({
11
+ rows = 5,
12
+ columns = 4,
13
+ className = "w-full border-collapse",
14
+ rowClassName = "border-b"
15
+ }: TableSkeletonProps) {
16
+ return (
17
+ <div className="w-full overflow-x-auto">
18
+ <table className={className}>
19
+ <thead>
20
+ <tr className={rowClassName}>
21
+ {Array.from({ length: columns }).map((_, c) => (
22
+ <th key={c} className="p-3 text-left">
23
+ <div className="h-4 bg-muted rounded animate-pulse w-20" />
24
+ </th>
25
+ ))}
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ {Array.from({ length: rows }).map((_, r) => (
30
+ <tr key={r} className={rowClassName}>
31
+ {Array.from({ length: columns }).map((_, c) => (
32
+ <td key={c} className="p-3">
33
+ <div className="h-4 bg-muted rounded animate-pulse w-full max-w-[120px]" />
34
+ </td>
35
+ ))}
36
+ </tr>
37
+ ))}
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,5 @@
1
+ export * from './TableSkeleton'
2
+ export * from './CardSkeleton'
3
+ export * from './CartItemSkeleton'
4
+ export * from './OrderItemSkeleton'
5
+ export * from './ProductCardSkeleton'