@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,212 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { MapPin, Loader2, Check } from "lucide-react";
|
|
5
|
+
import { autocompleteAddress } from "../../services/logistics.service";
|
|
6
|
+
import { useSdkContext, resolveHookArgs } from "../../context/sdk-context";
|
|
7
|
+
import type { SdkConfig } from "../../config";
|
|
8
|
+
|
|
9
|
+
export interface AddressSuggestion {
|
|
10
|
+
place_id?: string | number;
|
|
11
|
+
display_name: string;
|
|
12
|
+
line1: string;
|
|
13
|
+
city?: string;
|
|
14
|
+
state?: string;
|
|
15
|
+
postal_code?: string;
|
|
16
|
+
country?: string;
|
|
17
|
+
latitude?: number;
|
|
18
|
+
longitude?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AddressAutocompleteProps {
|
|
22
|
+
/** The street-address line 1 text. */
|
|
23
|
+
value: string;
|
|
24
|
+
/** Fires on every keystroke - the field stays fully editable. */
|
|
25
|
+
onChange: (line1: string) => void;
|
|
26
|
+
/** Fires once when a suggestion is picked, with structured components. */
|
|
27
|
+
onResolve?: (suggestion: AddressSuggestion) => void;
|
|
28
|
+
/** ISO-2 country code to bias results toward (e.g. "NG"). */
|
|
29
|
+
country?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
"aria-label"?: string;
|
|
35
|
+
config?: SdkConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* REAL BUG FIXED (2026-09-22): this called a hardcoded third-party endpoint
|
|
40
|
+
* (`nominatim.openstreetmap.org`) directly from the browser — a completely
|
|
41
|
+
* different data source than the rest of this platform's address/logistics
|
|
42
|
+
* system, which already has a real, working backend endpoint for exactly
|
|
43
|
+
* this (`GET /logistics/address/autocomplete`, `autocompleteAddress` in
|
|
44
|
+
* logistics.service.ts, live-verified this session). Calling Nominatim
|
|
45
|
+
* directly is also a real operational risk: OpenStreetMap's usage policy
|
|
46
|
+
* rate-limits and blocks unauthenticated browser traffic at production
|
|
47
|
+
* volume, and it has no idea this business is in Nigeria the way the real
|
|
48
|
+
* backend's geocoding provider is tuned to be. Rebuilt to call the real
|
|
49
|
+
* backend through the SDK's own service function instead.
|
|
50
|
+
*/
|
|
51
|
+
export function AddressAutocomplete({
|
|
52
|
+
value,
|
|
53
|
+
onChange,
|
|
54
|
+
onResolve,
|
|
55
|
+
country,
|
|
56
|
+
disabled,
|
|
57
|
+
placeholder = "Start typing an address…",
|
|
58
|
+
className = "",
|
|
59
|
+
id,
|
|
60
|
+
config,
|
|
61
|
+
...rest
|
|
62
|
+
}: AddressAutocompleteProps) {
|
|
63
|
+
const ctx = useSdkContext();
|
|
64
|
+
const generatedId = useId();
|
|
65
|
+
const fieldId = id ?? generatedId;
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
const [highlight, setHighlight] = useState(-1);
|
|
68
|
+
const [justPicked, setJustPicked] = useState(false);
|
|
69
|
+
const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);
|
|
70
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
71
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!open || justPicked || disabled || !value || value.trim().length < 3) {
|
|
75
|
+
setSuggestions([]);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let isMounted = true;
|
|
80
|
+
const timer = setTimeout(async () => {
|
|
81
|
+
setIsLoading(true);
|
|
82
|
+
try {
|
|
83
|
+
const { config: cfg } = resolveHookArgs(config, undefined, ctx);
|
|
84
|
+
const { results } = await autocompleteAddress(cfg, value, country);
|
|
85
|
+
if (isMounted) {
|
|
86
|
+
setSuggestions(
|
|
87
|
+
results.map((r) => ({
|
|
88
|
+
place_id: r.place_id,
|
|
89
|
+
display_name: r.display_name,
|
|
90
|
+
line1: r.line1,
|
|
91
|
+
city: r.city ?? undefined,
|
|
92
|
+
state: r.state ?? undefined,
|
|
93
|
+
postal_code: r.postal_code ?? undefined,
|
|
94
|
+
country: r.country_code ?? r.country ?? undefined,
|
|
95
|
+
latitude: r.latitude ?? undefined,
|
|
96
|
+
longitude: r.longitude ?? undefined,
|
|
97
|
+
}))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// A real backend failure (network, 5xx) degrades to "no suggestions"
|
|
102
|
+
// here — the field stays fully editable either way (onChange above
|
|
103
|
+
// already applies on every keystroke), so this never blocks the
|
|
104
|
+
// customer from typing their address manually.
|
|
105
|
+
if (isMounted) setSuggestions([]);
|
|
106
|
+
} finally {
|
|
107
|
+
if (isMounted) setIsLoading(false);
|
|
108
|
+
}
|
|
109
|
+
}, 350);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
isMounted = false;
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
};
|
|
115
|
+
}, [value, country, open, justPicked, disabled, config, ctx]);
|
|
116
|
+
|
|
117
|
+
// Close on outside click.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const onDocClick = (e: MouseEvent) => {
|
|
120
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
|
|
121
|
+
};
|
|
122
|
+
document.addEventListener("mousedown", onDocClick);
|
|
123
|
+
return () => document.removeEventListener("mousedown", onDocClick);
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const pick = (s: AddressSuggestion) => {
|
|
127
|
+
onChange(s.line1 || value);
|
|
128
|
+
onResolve?.(s);
|
|
129
|
+
setJustPicked(true);
|
|
130
|
+
setOpen(false);
|
|
131
|
+
setHighlight(-1);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const showDropdown = open && !justPicked && (isLoading || suggestions.length > 0);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div ref={wrapRef} className="relative w-full">
|
|
138
|
+
<div className="relative flex items-center">
|
|
139
|
+
<MapPin className="absolute left-3 w-4 h-4 text-muted-foreground pointer-events-none" />
|
|
140
|
+
<input
|
|
141
|
+
id={fieldId}
|
|
142
|
+
value={value}
|
|
143
|
+
disabled={disabled}
|
|
144
|
+
placeholder={placeholder}
|
|
145
|
+
autoComplete="off"
|
|
146
|
+
className={`w-full h-10 pl-9 pr-9 py-2 text-sm bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50 ${className}`.trim()}
|
|
147
|
+
onChange={(e) => {
|
|
148
|
+
onChange(e.target.value);
|
|
149
|
+
setJustPicked(false);
|
|
150
|
+
setOpen(true);
|
|
151
|
+
}}
|
|
152
|
+
onFocus={() => { if (!justPicked) setOpen(true); }}
|
|
153
|
+
onKeyDown={(e) => {
|
|
154
|
+
if (!showDropdown) return;
|
|
155
|
+
if (e.key === "ArrowDown") {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
setHighlight((h) => Math.min(h + 1, suggestions.length - 1));
|
|
158
|
+
} else if (e.key === "ArrowUp") {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
setHighlight((h) => Math.max(h - 1, 0));
|
|
161
|
+
} else if (e.key === "Enter" && highlight >= 0 && suggestions[highlight]) {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
pick(suggestions[highlight]);
|
|
164
|
+
} else if (e.key === "Escape") {
|
|
165
|
+
setOpen(false);
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
{...rest}
|
|
169
|
+
/>
|
|
170
|
+
{isLoading ? (
|
|
171
|
+
<Loader2 className="absolute right-3 w-4 h-4 text-muted-foreground animate-spin" />
|
|
172
|
+
) : justPicked ? (
|
|
173
|
+
<Check className="absolute right-3 w-4 h-4 text-primary" />
|
|
174
|
+
) : null}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{showDropdown && (
|
|
178
|
+
<div className="absolute z-50 w-full mt-1 bg-card border rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
|
179
|
+
{isLoading && suggestions.length === 0 ? (
|
|
180
|
+
<div className="flex items-center gap-2 px-3 py-3 text-sm text-muted-foreground">
|
|
181
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" /> Searching addresses…
|
|
182
|
+
</div>
|
|
183
|
+
) : suggestions.length === 0 ? (
|
|
184
|
+
<div className="px-3 py-3 text-sm text-muted-foreground">
|
|
185
|
+
No matches found. Enter address manually.
|
|
186
|
+
</div>
|
|
187
|
+
) : (
|
|
188
|
+
suggestions.map((s, i) => (
|
|
189
|
+
<button
|
|
190
|
+
key={s.place_id || `${s.display_name}-${i}`}
|
|
191
|
+
type="button"
|
|
192
|
+
onMouseEnter={() => setHighlight(i)}
|
|
193
|
+
onClick={() => pick(s)}
|
|
194
|
+
className={`w-full text-left flex items-start gap-2 px-3 py-2 text-sm transition-colors ${
|
|
195
|
+
i === highlight ? "bg-muted" : "hover:bg-muted/50"
|
|
196
|
+
}`}
|
|
197
|
+
>
|
|
198
|
+
<MapPin className="w-3.5 h-3.5 mt-0.5 text-muted-foreground shrink-0" />
|
|
199
|
+
<span className="min-w-0">
|
|
200
|
+
<span className="block font-medium text-foreground truncate">
|
|
201
|
+
{s.line1 || s.display_name}
|
|
202
|
+
</span>
|
|
203
|
+
<span className="block text-xs text-muted-foreground truncate">{s.display_name}</span>
|
|
204
|
+
</span>
|
|
205
|
+
</button>
|
|
206
|
+
))
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Script from "next/script";
|
|
5
|
+
import { getCookieConsent, COOKIE_CONSENT_CHANGED_EVENT } from "../../utils/cookieConsent";
|
|
6
|
+
|
|
7
|
+
export interface AnalyticsScriptsProps {
|
|
8
|
+
websiteId?: string;
|
|
9
|
+
scriptUrl?: string;
|
|
10
|
+
recorderUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* AnalyticsScripts — Consent-gated Umami tracker + session-replay loader.
|
|
15
|
+
*
|
|
16
|
+
* REAL GAP FIXED (owner-requested legal-compliance review): Umami's tracker
|
|
17
|
+
* AND its session-replay/heatmap recorder used to render unconditionally the
|
|
18
|
+
* moment a project had analytics provisioned — no consent check anywhere.
|
|
19
|
+
* Session replay specifically is non-essential tracking under GDPR/NDPR
|
|
20
|
+
* consent rules regardless of PII-masking/sample-rate mitigations already
|
|
21
|
+
* built into the recorder's own config — masking reduces risk, it doesn't
|
|
22
|
+
* remove the consent requirement.
|
|
23
|
+
*
|
|
24
|
+
* This component is the actual technical gate: it renders nothing until the
|
|
25
|
+
* visitor has affirmatively accepted via CookieConsentBanner, and re-checks
|
|
26
|
+
* live if they change their choice without needing a page reload.
|
|
27
|
+
*
|
|
28
|
+
* Peer dependencies: next (next/script)
|
|
29
|
+
*/
|
|
30
|
+
export function AnalyticsScripts({ websiteId, scriptUrl, recorderUrl }: AnalyticsScriptsProps) {
|
|
31
|
+
const [accepted, setAccepted] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const sync = () => setAccepted(getCookieConsent() === "accepted");
|
|
35
|
+
sync();
|
|
36
|
+
window.addEventListener(COOKIE_CONSENT_CHANGED_EVENT, sync);
|
|
37
|
+
window.addEventListener("storage", sync);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener(COOKIE_CONSENT_CHANGED_EVENT, sync);
|
|
40
|
+
window.removeEventListener("storage", sync);
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
if (!accepted || !websiteId) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{scriptUrl && (
|
|
49
|
+
<Script async defer data-website-id={websiteId} src={scriptUrl} strategy="afterInteractive" />
|
|
50
|
+
)}
|
|
51
|
+
{recorderUrl && (
|
|
52
|
+
<Script defer data-website-id={websiteId} src={recorderUrl} strategy="afterInteractive" />
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { getCookieConsent, setCookieConsent } from "../../utils/cookieConsent";
|
|
5
|
+
|
|
6
|
+
export interface CookieConsentBannerProps {
|
|
7
|
+
/**
|
|
8
|
+
* URL for the privacy policy page. Defaults to "/privacy".
|
|
9
|
+
*/
|
|
10
|
+
privacyUrl?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Custom className for the outer banner container.
|
|
13
|
+
*/
|
|
14
|
+
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom className for the "Accept" button.
|
|
17
|
+
*/
|
|
18
|
+
acceptButtonClassName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Custom className for the "Reject non-essential" button.
|
|
21
|
+
*/
|
|
22
|
+
rejectButtonClassName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CookieConsentBanner — GDPR/NDPR compliant consent gate.
|
|
27
|
+
*
|
|
28
|
+
* Owner-requested (legal-compliance review): analytics/session-replay tracking
|
|
29
|
+
* must not run before the visitor has actually chosen to allow it — see
|
|
30
|
+
* AnalyticsScripts, which gates the real script tags on this same stored choice.
|
|
31
|
+
*
|
|
32
|
+
* Shown once, bottom banner (not a blocking modal — the standard, least-intrusive
|
|
33
|
+
* pattern), with Accept and Reject weighted equally, matching GDPR/NDPR guidance
|
|
34
|
+
* against making "reject" harder to find than "accept".
|
|
35
|
+
*
|
|
36
|
+
* Styled with utility classes by default. Pass custom classNames to restyle with
|
|
37
|
+
* your storefront's theme (e.g. shadcn/Tailwind classes) without touching the SDK.
|
|
38
|
+
*/
|
|
39
|
+
export function CookieConsentBanner({
|
|
40
|
+
privacyUrl = "/privacy",
|
|
41
|
+
className,
|
|
42
|
+
acceptButtonClassName,
|
|
43
|
+
rejectButtonClassName,
|
|
44
|
+
}: CookieConsentBannerProps) {
|
|
45
|
+
const [visible, setVisible] = useState(false);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setVisible(getCookieConsent() === null);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
if (!visible) return null;
|
|
52
|
+
|
|
53
|
+
const choose = (value: "accepted" | "rejected") => {
|
|
54
|
+
setCookieConsent(value);
|
|
55
|
+
setVisible(false);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const bannerClass = className ??
|
|
59
|
+
"fixed inset-x-0 bottom-0 z-50 border-t border-border bg-background/95 backdrop-blur-sm p-4 shadow-[0_-4px_16px_rgba(0,0,0,0.08)]";
|
|
60
|
+
|
|
61
|
+
const rejectClass = rejectButtonClassName ??
|
|
62
|
+
"inline-flex items-center justify-center rounded-md border border-input bg-background px-3 h-9 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors";
|
|
63
|
+
|
|
64
|
+
const acceptClass = acceptButtonClassName ??
|
|
65
|
+
"inline-flex items-center justify-center rounded-md bg-primary px-3 h-9 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors";
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={bannerClass}>
|
|
69
|
+
<div className="mx-auto flex max-w-4xl flex-col items-center gap-3 sm:flex-row sm:justify-between">
|
|
70
|
+
<p className="text-center text-sm text-muted-foreground sm:text-left">
|
|
71
|
+
We use cookies to understand how you use this site and improve your experience.{" "}
|
|
72
|
+
<a
|
|
73
|
+
href={privacyUrl}
|
|
74
|
+
className="underline underline-offset-2 hover:text-foreground"
|
|
75
|
+
>
|
|
76
|
+
Learn more
|
|
77
|
+
</a>
|
|
78
|
+
</p>
|
|
79
|
+
<div className="flex shrink-0 gap-2">
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className={rejectClass}
|
|
83
|
+
onClick={() => choose("rejected")}
|
|
84
|
+
>
|
|
85
|
+
Reject non-essential
|
|
86
|
+
</button>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={acceptClass}
|
|
90
|
+
onClick={() => choose("accepted")}
|
|
91
|
+
>
|
|
92
|
+
Accept
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|