@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
|
@@ -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 & 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
|
+
}
|