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