@huskel/sdk 0.3.1 → 0.3.2
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/dist/index.d.mts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +68 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +68 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -53,6 +53,7 @@ interface HuskelConfig {
|
|
|
53
53
|
siteId?: string;
|
|
54
54
|
apiUrl?: string;
|
|
55
55
|
apiToken?: string;
|
|
56
|
+
shopperId?: string;
|
|
56
57
|
}
|
|
57
58
|
interface SearchRequest {
|
|
58
59
|
query: string;
|
|
@@ -82,7 +83,9 @@ declare class HuskelAPI {
|
|
|
82
83
|
private apiUrl;
|
|
83
84
|
private siteId;
|
|
84
85
|
private apiToken;
|
|
85
|
-
|
|
86
|
+
private getShopperId?;
|
|
87
|
+
private getSessionId?;
|
|
88
|
+
constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
|
|
86
89
|
private post;
|
|
87
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
88
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
@@ -95,7 +98,13 @@ declare class HuskelClient {
|
|
|
95
98
|
private ingestTimer;
|
|
96
99
|
private ingestedUrls;
|
|
97
100
|
private onlineHandler;
|
|
101
|
+
private shopperId?;
|
|
102
|
+
private sessionId;
|
|
98
103
|
constructor(config: HuskelConfig);
|
|
104
|
+
setShopperId(id: string | undefined): void;
|
|
105
|
+
getShopperId(): string | undefined;
|
|
106
|
+
getSessionId(): string;
|
|
107
|
+
private initSession;
|
|
99
108
|
destroy(): void;
|
|
100
109
|
queueIngest(rawProduct: RawProductInput): Promise<void>;
|
|
101
110
|
queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
|
|
@@ -172,6 +181,6 @@ declare function Sparkle({ productName, limit, onResult, className }: SparklePro
|
|
|
172
181
|
interface HuskelProviderProps extends HuskelConfig {
|
|
173
182
|
children: React.ReactNode;
|
|
174
183
|
}
|
|
175
|
-
declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
184
|
+
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
176
185
|
|
|
177
186
|
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ interface HuskelConfig {
|
|
|
53
53
|
siteId?: string;
|
|
54
54
|
apiUrl?: string;
|
|
55
55
|
apiToken?: string;
|
|
56
|
+
shopperId?: string;
|
|
56
57
|
}
|
|
57
58
|
interface SearchRequest {
|
|
58
59
|
query: string;
|
|
@@ -82,7 +83,9 @@ declare class HuskelAPI {
|
|
|
82
83
|
private apiUrl;
|
|
83
84
|
private siteId;
|
|
84
85
|
private apiToken;
|
|
85
|
-
|
|
86
|
+
private getShopperId?;
|
|
87
|
+
private getSessionId?;
|
|
88
|
+
constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
|
|
86
89
|
private post;
|
|
87
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
88
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
@@ -95,7 +98,13 @@ declare class HuskelClient {
|
|
|
95
98
|
private ingestTimer;
|
|
96
99
|
private ingestedUrls;
|
|
97
100
|
private onlineHandler;
|
|
101
|
+
private shopperId?;
|
|
102
|
+
private sessionId;
|
|
98
103
|
constructor(config: HuskelConfig);
|
|
104
|
+
setShopperId(id: string | undefined): void;
|
|
105
|
+
getShopperId(): string | undefined;
|
|
106
|
+
getSessionId(): string;
|
|
107
|
+
private initSession;
|
|
99
108
|
destroy(): void;
|
|
100
109
|
queueIngest(rawProduct: RawProductInput): Promise<void>;
|
|
101
110
|
queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
|
|
@@ -172,6 +181,6 @@ declare function Sparkle({ productName, limit, onResult, className }: SparklePro
|
|
|
172
181
|
interface HuskelProviderProps extends HuskelConfig {
|
|
173
182
|
children: React.ReactNode;
|
|
174
183
|
}
|
|
175
|
-
declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
184
|
+
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
176
185
|
|
|
177
186
|
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.js
CHANGED
|
@@ -65,21 +65,33 @@ async function sleep(ms) {
|
|
|
65
65
|
return new Promise((r) => setTimeout(r, ms));
|
|
66
66
|
}
|
|
67
67
|
var HuskelAPI = class {
|
|
68
|
-
constructor(apiUrl, siteId, apiToken) {
|
|
68
|
+
constructor(apiUrl, siteId, apiToken, getShopperId, getSessionId) {
|
|
69
69
|
this.apiUrl = apiUrl;
|
|
70
70
|
this.siteId = siteId;
|
|
71
71
|
this.apiToken = apiToken;
|
|
72
|
+
this.getShopperId = getShopperId;
|
|
73
|
+
this.getSessionId = getSessionId;
|
|
72
74
|
}
|
|
73
75
|
async post(path, body, attempt = 0) {
|
|
76
|
+
var _a, _b;
|
|
74
77
|
const url = `${this.apiUrl}${path}`;
|
|
75
78
|
try {
|
|
79
|
+
const headers = {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"X-Huskel-Token": this.apiToken,
|
|
82
|
+
"X-Huskel-Site": this.siteId
|
|
83
|
+
};
|
|
84
|
+
const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
|
|
85
|
+
if (shopperId) {
|
|
86
|
+
headers["X-Huskel-Shopper-Id"] = shopperId;
|
|
87
|
+
}
|
|
88
|
+
const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
|
|
89
|
+
if (sessionId) {
|
|
90
|
+
headers["X-Huskel-Session-Id"] = sessionId;
|
|
91
|
+
}
|
|
76
92
|
const res = await fetch(url, {
|
|
77
93
|
method: "POST",
|
|
78
|
-
headers
|
|
79
|
-
"Content-Type": "application/json",
|
|
80
|
-
"X-Huskel-Token": this.apiToken,
|
|
81
|
-
"X-Huskel-Site": this.siteId
|
|
82
|
-
},
|
|
94
|
+
headers,
|
|
83
95
|
body: JSON.stringify(body)
|
|
84
96
|
});
|
|
85
97
|
if (!res.ok) {
|
|
@@ -205,19 +217,38 @@ function mapRawProduct(input) {
|
|
|
205
217
|
slug
|
|
206
218
|
};
|
|
207
219
|
}
|
|
220
|
+
function generateUUID() {
|
|
221
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
222
|
+
return crypto.randomUUID();
|
|
223
|
+
}
|
|
224
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
225
|
+
const r = Math.random() * 16 | 0;
|
|
226
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
227
|
+
return v.toString(16);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
208
230
|
var HuskelClient = class {
|
|
209
231
|
constructor(config) {
|
|
210
232
|
this.ingestQueue = [];
|
|
211
233
|
this.ingestTimer = null;
|
|
212
234
|
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
213
235
|
this.onlineHandler = null;
|
|
236
|
+
this.sessionId = "";
|
|
214
237
|
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
|
|
215
238
|
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
|
|
216
239
|
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
|
|
217
240
|
if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
|
|
218
241
|
if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
|
|
219
242
|
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
220
|
-
this.
|
|
243
|
+
this.shopperId = config.shopperId;
|
|
244
|
+
this.initSession();
|
|
245
|
+
this.api = new HuskelAPI(
|
|
246
|
+
apiUrl,
|
|
247
|
+
siteId,
|
|
248
|
+
apiToken,
|
|
249
|
+
() => this.shopperId,
|
|
250
|
+
() => this.sessionId
|
|
251
|
+
);
|
|
221
252
|
instance = this;
|
|
222
253
|
if (typeof window !== "undefined") {
|
|
223
254
|
this.onlineHandler = () => {
|
|
@@ -227,6 +258,30 @@ var HuskelClient = class {
|
|
|
227
258
|
window.addEventListener("online", this.onlineHandler);
|
|
228
259
|
}
|
|
229
260
|
}
|
|
261
|
+
setShopperId(id) {
|
|
262
|
+
this.shopperId = id;
|
|
263
|
+
}
|
|
264
|
+
getShopperId() {
|
|
265
|
+
return this.shopperId;
|
|
266
|
+
}
|
|
267
|
+
getSessionId() {
|
|
268
|
+
return this.sessionId;
|
|
269
|
+
}
|
|
270
|
+
initSession() {
|
|
271
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
272
|
+
try {
|
|
273
|
+
let sid = window.sessionStorage.getItem("huskel_session_id");
|
|
274
|
+
if (!sid) {
|
|
275
|
+
sid = generateUUID();
|
|
276
|
+
window.sessionStorage.setItem("huskel_session_id", sid);
|
|
277
|
+
}
|
|
278
|
+
this.sessionId = sid;
|
|
279
|
+
return;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.sessionId = generateUUID();
|
|
284
|
+
}
|
|
230
285
|
destroy() {
|
|
231
286
|
if (typeof window !== "undefined" && this.onlineHandler) {
|
|
232
287
|
window.removeEventListener("online", this.onlineHandler);
|
|
@@ -327,11 +382,15 @@ var import_react3 = require("react");
|
|
|
327
382
|
var import_react2 = require("react");
|
|
328
383
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
329
384
|
var HuskelContext = (0, import_react2.createContext)(null);
|
|
330
|
-
function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
|
|
385
|
+
function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
|
|
331
386
|
const clientRef = (0, import_react2.useRef)(null);
|
|
332
387
|
if (!clientRef.current) {
|
|
333
|
-
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
388
|
+
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
|
|
334
389
|
}
|
|
390
|
+
(0, import_react2.useEffect)(() => {
|
|
391
|
+
var _a;
|
|
392
|
+
(_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
|
|
393
|
+
}, [shopperId]);
|
|
335
394
|
(0, import_react2.useEffect)(() => {
|
|
336
395
|
return () => {
|
|
337
396
|
var _a;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api.ts","../src/client.ts","../src/hooks/useHuskel.ts","../src/hooks/useSearch.ts","../src/components/HuskelProvider.tsx","../src/hooks/useIngest.ts","../src/hooks/usePageIngest.ts","../src/components/SearchBar.tsx","../src/components/Sparkle.tsx"],"sourcesContent":["export { initHuskel, getHuskelClient, HuskelClient } from './client';\r\nexport { HuskelAPI } from './api';\r\nexport { useHuskel } from './hooks/useHuskel';\r\nexport { useSearch } from './hooks/useSearch';\r\nexport { useIngest } from './hooks/useIngest';\r\nexport { usePageIngest } from './hooks/usePageIngest';\r\nexport { SearchBar } from './components/SearchBar';\r\nexport { Sparkle } from './components/Sparkle';\r\nexport { HuskelProvider } from './components/HuskelProvider';\r\nexport type {\r\n Product,\r\n RawProductInput,\r\n HuskelConfig,\r\n SearchRequest,\r\n SearchResult,\r\n SearchResponse,\r\n IngestResponse,\r\n HuskelError,\r\n} from './types';\r\n","import { Product, SearchResponse, IngestResponse, HuskelError } from './types';\r\n\r\nconst MAX_RETRIES = 3;\r\nconst RETRY_DELAYS = [500, 1000, 2000]; // ms\r\n\r\nfunction log(level: 'info' | 'warn' | 'error', msg: string, data?: unknown) {\r\n const prefix = '[Huskel]';\r\n if (level === 'error') console.error(prefix, msg, data ?? '');\r\n else if (level === 'warn') console.warn(prefix, msg, data ?? '');\r\n else console.log(prefix, msg, data ?? '');\r\n}\r\n\r\nasync function sleep(ms: number) {\r\n return new Promise(r => setTimeout(r, ms));\r\n}\r\n\r\nexport class HuskelAPI {\r\n constructor(\r\n private apiUrl: string,\r\n private siteId: string,\r\n private apiToken: string\r\n ) {}\r\n\r\n private async post<T>(path: string, body: unknown, attempt = 0): Promise<T> {\r\n const url = `${this.apiUrl}${path}`;\r\n\r\n try {\r\n const res = await fetch(url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'X-Huskel-Token': this.apiToken,\r\n 'X-Huskel-Site': this.siteId,\r\n },\r\n body: JSON.stringify(body),\r\n });\r\n\r\n if (!res.ok) {\r\n const text = await res.text();\r\n const err: HuskelError = { status: res.status, message: text };\r\n\r\n // Don't retry 4xx — developer errors\r\n if (res.status >= 400 && res.status < 500) {\r\n log('error', `${path} failed [${res.status}]`, text);\r\n throw err;\r\n }\r\n\r\n // Retry 5xx\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n\r\n log('error', `${path} failed after ${MAX_RETRIES} attempts`, err);\r\n throw err;\r\n }\r\n\r\n return res.json();\r\n } catch (e) {\r\n // Network error (offline, DNS, etc.)\r\n if ((e as HuskelError).status === undefined) {\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n log('error', `${path} unreachable after ${MAX_RETRIES} attempts`);\r\n }\r\n throw e;\r\n }\r\n }\r\n\r\n async ingest(product: Product): Promise<IngestResponse> {\r\n log('info', 'ingesting product', product.name);\r\n return this.post('/ingest', { siteId: this.siteId, product });\r\n }\r\n\r\n async ingestBatch(products: Product[]): Promise<IngestResponse> {\r\n log('info', `ingesting batch of ${products.length} products`);\r\n return this.post('/ingest/batch', { siteId: this.siteId, products });\r\n }\r\n\r\n async search(query: string, limit = 10): Promise<SearchResponse> {\r\n log('info', 'search query', query);\r\n return this.post('/search', { query, siteId: this.siteId, limit });\r\n }\r\n}\r\n","import { HuskelConfig, Product, RawProductInput } from './types';\r\nimport { HuskelAPI } from './api';\r\n\r\nfunction getEnvVar(key: string): string | undefined {\r\n if (typeof globalThis !== 'undefined') {\r\n const g = globalThis as any;\r\n if (g.process && g.process.env) {\r\n return g.process.env[key];\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction mapRawProduct(input: RawProductInput): Product | null {\r\n const name = input.name || input.title || input.productName || '';\r\n \r\n let price = '';\r\n let priceNumeric: number | undefined = undefined;\r\n\r\n if (input.price !== undefined) {\r\n if (typeof input.price === 'number') {\r\n priceNumeric = input.price;\r\n price = String(input.price);\r\n } else {\r\n price = input.price;\r\n const num = parseFloat(input.price.replace(/[^0-9.]/g, ''));\r\n priceNumeric = isNaN(num) ? undefined : num;\r\n }\r\n }\r\n if (input.priceNumeric !== undefined) {\r\n priceNumeric = input.priceNumeric;\r\n }\r\n\r\n let url = input.url || '';\r\n if (!url && typeof window !== 'undefined') {\r\n url = window.location.href;\r\n }\r\n\r\n let slug = input.slug || input.id || input.productId || '';\r\n if (!slug && url) {\r\n slug = url.split('/').filter(Boolean).pop() || '';\r\n }\r\n if (!slug && name) {\r\n slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');\r\n }\r\n\r\n let images: string[] = [];\r\n if (input.images) {\r\n images = input.images;\r\n } else if (input.image) {\r\n images = [input.image];\r\n } else if (input.thumbnail) {\r\n images = [input.thumbnail];\r\n }\r\n\r\n if (!name) {\r\n console.warn('[Huskel] Validation warning: Product name/title is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!price) {\r\n console.warn('[Huskel] Validation warning: Product price is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!url) {\r\n console.warn('[Huskel] Validation warning: Product URL is missing. Skipping:', input);\r\n return null;\r\n }\r\n\r\n return {\r\n name,\r\n price,\r\n url,\r\n brand: input.brand,\r\n description: input.description,\r\n originalPrice: input.originalPrice,\r\n discount: input.discount,\r\n currency: input.currency ?? 'KES',\r\n stock: input.stock,\r\n availability: input.availability,\r\n rating: input.rating,\r\n reviewCount: input.reviewCount,\r\n category: input.category,\r\n subCategory: input.subCategory,\r\n tags: input.tags,\r\n images: images.length > 0 ? images : undefined,\r\n specs: input.specs,\r\n priceNumeric,\r\n slug,\r\n };\r\n}\r\n\r\nexport class HuskelClient {\r\n readonly api: HuskelAPI;\r\n private ingestQueue: Product[] = [];\r\n private ingestTimer: ReturnType<typeof setTimeout> | null = null;\r\n private ingestedUrls = new Set<string>();\r\n private onlineHandler: (() => void) | null = null;\r\n\r\n constructor(config: HuskelConfig) {\r\n const siteId = config.siteId || getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID') || '';\r\n const apiUrl = config.apiUrl || getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL') || '';\r\n const apiToken = config.apiToken || getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN') || '';\r\n\r\n // Runtime validation — fail loudly so misconfiguration is never silent\r\n if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId=\"...\"> or NEXT_PUBLIC_HUSKEL_SITE_ID.');\r\n if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl=\"...\"> or NEXT_PUBLIC_HUSKEL_API_URL.');\r\n if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken=\"...\"> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');\r\n\r\n this.api = new HuskelAPI(apiUrl, siteId, apiToken);\r\n instance = this;\r\n\r\n if (typeof window !== 'undefined') {\r\n this.onlineHandler = () => {\r\n console.log('[Huskel] Connectivity restored, flushing queued ingestions.');\r\n this.flushQueue();\r\n };\r\n window.addEventListener('online', this.onlineHandler);\r\n }\r\n }\r\n\r\n destroy() {\r\n if (typeof window !== 'undefined' && this.onlineHandler) {\r\n window.removeEventListener('online', this.onlineHandler);\r\n this.onlineHandler = null;\r\n }\r\n if (this.ingestTimer) {\r\n clearTimeout(this.ingestTimer);\r\n this.ingestTimer = null;\r\n }\r\n if (instance === this) instance = null;\r\n }\r\n\r\n async queueIngest(rawProduct: RawProductInput): Promise<void> {\r\n const product = mapRawProduct(rawProduct);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n\r\n this.ingestQueue.push(product);\r\n this.scheduleFlush();\r\n }\r\n\r\n async queueIngestBatch(rawProducts: RawProductInput[]): Promise<void> {\r\n rawProducts.forEach(p => {\r\n const product = mapRawProduct(p);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n this.ingestQueue.push(product);\r\n });\r\n\r\n if (this.ingestQueue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n private scheduleFlush() {\r\n if (this.ingestTimer) return;\r\n this.ingestTimer = setTimeout(() => {\r\n this.flushQueue();\r\n }, 300);\r\n }\r\n\r\n private async flushQueue() {\r\n this.ingestTimer = null;\r\n if (this.ingestQueue.length === 0) return;\r\n\r\n if (typeof navigator !== 'undefined' && !navigator.onLine) {\r\n console.warn('[Huskel] Browser offline. Postponing ingestion.');\r\n return;\r\n }\r\n\r\n const batch = [...this.ingestQueue];\r\n this.ingestQueue = [];\r\n\r\n try {\r\n await this.api.ingestBatch(batch);\r\n } catch (e: any) {\r\n if (e.status && e.status >= 400 && e.status < 500) {\r\n console.error('[Huskel] Ingestion discarded due to client error:', e.message);\r\n return;\r\n }\r\n\r\n // Re-queue and schedule another flush so items are not stuck forever\r\n console.warn('[Huskel] Ingestion failed. Re-queuing to retry.', e);\r\n this.ingestQueue = [...batch, ...this.ingestQueue];\r\n this.scheduleFlush();\r\n }\r\n }\r\n}\r\n\r\nlet instance: HuskelClient | null = null;\r\n\r\nexport function initHuskel(config: HuskelConfig): HuskelClient {\r\n instance = new HuskelClient(config);\r\n return instance;\r\n}\r\n\r\nexport function getHuskelClient(): HuskelClient {\r\n if (!instance) {\r\n const siteId = getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID');\r\n const apiUrl = getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL');\r\n const apiToken = getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN');\r\n\r\n if (siteId && apiUrl && apiToken) {\r\n instance = new HuskelClient({ siteId, apiUrl, apiToken });\r\n } else {\r\n throw new Error('[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.');\r\n }\r\n }\r\n return instance;\r\n}\r\n","import { useRef } from 'react';\r\nimport { HuskelConfig } from '../types';\r\nimport { HuskelClient, initHuskel } from '../client';\r\n\r\n/**\r\n * @deprecated Use <HuskelProvider> instead to avoid SSR issues.\r\n */\r\nexport function useHuskel(config: HuskelConfig): HuskelClient {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n console.warn('[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.');\r\n clientRef.current = initHuskel(config);\r\n }\r\n\r\n return clientRef.current;\r\n}\r\n","import { useState, useCallback, useRef } from 'react';\r\nimport { SearchResult } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseSearchReturn {\r\n results: SearchResult[];\r\n loading: boolean;\r\n error: string | null;\r\n search: (query: string, limit?: number) => Promise<void>;\r\n clear: () => void;\r\n}\r\n\r\nexport function useSearch(): UseSearchReturn {\r\n const client = useHuskelContext();\r\n const [results, setResults] = useState<SearchResult[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const abortRef = useRef<AbortController | null>(null);\r\n\r\n const search = useCallback(async (query: string, limit = 10) => {\r\n if (!query.trim()) { setResults([]); return; }\r\n abortRef.current?.abort();\r\n abortRef.current = new AbortController();\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n const res = await client.api.search(query, limit);\r\n setResults(res.results ?? []);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Search failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const clear = useCallback(() => { setResults([]); setError(null); }, []);\r\n\r\n return { results, loading, error, search, clear };\r\n}\r\n","'use client';\r\n\r\nimport React, { createContext, useContext, useEffect, useRef } from 'react';\r\nimport { HuskelClient, getHuskelClient } from '../client';\r\nimport { HuskelConfig } from '../types';\r\n\r\nexport const HuskelContext = createContext<HuskelClient | null>(null);\r\n\r\ninterface HuskelProviderProps extends HuskelConfig {\r\n children: React.ReactNode;\r\n}\r\n\r\nexport function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps) {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });\r\n }\r\n\r\n // Clean up the online listener and timers when the provider unmounts\r\n // (prevents leaks during hot module reload and React StrictMode double-mount)\r\n useEffect(() => {\r\n return () => {\r\n clientRef.current?.destroy();\r\n };\r\n }, []);\r\n\r\n return (\r\n <HuskelContext.Provider value={clientRef.current}>\r\n {children}\r\n </HuskelContext.Provider>\r\n );\r\n}\r\n\r\nexport function useHuskelContext(): HuskelClient {\r\n const context = useContext(HuskelContext);\r\n if (!context) {\r\n return getHuskelClient();\r\n }\r\n return context;\r\n}\r\n\r\n","import { useCallback, useState } from 'react';\r\nimport { RawProductInput } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseIngestReturn {\r\n ingest: (product: RawProductInput) => Promise<void>;\r\n ingestBatch: (products: RawProductInput[]) => Promise<void>;\r\n loading: boolean;\r\n error: string | null;\r\n}\r\n\r\nexport function useIngest(): UseIngestReturn {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const ingest = useCallback(async (product: RawProductInput) => {\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngest(product);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const ingestBatch = useCallback(async (products: RawProductInput[]) => {\r\n if (!products.length) return;\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngestBatch(products);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Batch ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n return { ingest, ingestBatch, loading, error };\r\n}\r\n","import { useEffect, useRef } from 'react';\nimport { RawProductInput } from '../types';\nimport { getHuskelClient } from '../client';\n\n/**\n * usePageIngest — drop this into any product page component.\n * The moment a customer's browser renders the page, the product is\n * automatically captured and queued for ingestion into the vector index.\n *\n * No configuration needed beyond <HuskelProvider> in your layout.\n *\n * @example\n * // Product detail page — Next.js or React\n * export function ProductPage({ product }) {\n * usePageIngest({\n * name: product.title,\n * price: product.price,\n * url: window.location.href,\n * images: [product.thumbnail],\n * category: product.category,\n * });\n * return <div>...</div>;\n * }\n */\nexport function usePageIngest(product: RawProductInput | null | undefined): void {\n // Use url as the stable key — avoids re-ingesting on unrelated re-renders\n const ingestedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!product) return;\n\n // Resolve URL — falls back to window.location if not provided\n const url =\n product.url ||\n (typeof window !== 'undefined' ? window.location.href : '');\n\n // Guard: only ingest once per URL per component lifecycle\n if (ingestedRef.current === url) return;\n ingestedRef.current = url;\n\n try {\n getHuskelClient().queueIngest({ ...product, url });\n } catch {\n // Client not initialised — silently skip\n }\n }, [product?.url ?? product?.name]);\n}\n","import React, { useState, useEffect, useRef } from 'react';\r\nimport { useSearch } from '../hooks/useSearch';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SearchBarProps {\r\n placeholder?: string;\r\n limit?: number;\r\n debounceMs?: number;\r\n onSelect?: (result: SearchResult) => void;\r\n className?: string;\r\n inputClassName?: string;\r\n dropdownClassName?: string;\r\n renderResult?: (result: SearchResult) => React.ReactNode;\r\n}\r\n\r\nconst S = `\r\n .hsk-wrap{position:relative;width:100%;font-family:inherit}\r\n .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}\r\n .hsk-input:focus{border-color:#f47c3c}\r\n .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}\r\n .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}\r\n .hsk-item:hover{background:#faf5f1}\r\n .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}\r\n .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\r\n .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}\r\n .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}\r\n`;\r\n\r\nexport function SearchBar({\r\n placeholder = 'Search for what you want — how you want',\r\n limit = 10,\r\n debounceMs = 300,\r\n onSelect,\r\n className,\r\n inputClassName,\r\n dropdownClassName,\r\n renderResult,\r\n}: SearchBarProps) {\r\n const [query, setQuery] = useState('');\r\n const [open, setOpen] = useState(false);\r\n const { results, loading, search, clear } = useSearch();\r\n const timer = useRef<ReturnType<typeof setTimeout>>();\r\n const wrap = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n clearTimeout(timer.current);\r\n if (!query.trim()) { clear(); setOpen(false); return; }\r\n timer.current = setTimeout(() => { search(query, limit); setOpen(true); }, debounceMs);\r\n return () => clearTimeout(timer.current);\r\n }, [query, search, clear, limit, debounceMs]);\r\n\r\n useEffect(() => {\r\n const handler = (e: MouseEvent) => {\r\n if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);\r\n };\r\n document.addEventListener('mousedown', handler);\r\n return () => document.removeEventListener('mousedown', handler);\r\n }, []);\r\n\r\n const handleSelect = (r: SearchResult) => {\r\n setOpen(false);\r\n setQuery(r.product.name);\r\n onSelect?.(r);\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <div className={`hsk-wrap ${className ?? ''}`} ref={wrap}>\r\n <input\r\n className={`hsk-input ${inputClassName ?? ''}`}\r\n type=\"text\"\r\n value={query}\r\n placeholder={placeholder}\r\n onChange={e => setQuery(e.target.value)}\r\n onFocus={() => results.length && setOpen(true)}\r\n />\r\n {open && (\r\n <div className={`hsk-drop ${dropdownClassName ?? ''}`}>\r\n {loading && <div className=\"hsk-msg\">Searching…</div>}\r\n {!loading && results.length === 0 && <div className=\"hsk-msg\">No results for \"{query}\"</div>}\r\n {results.map(r =>\r\n renderResult ? (\r\n <div key={r.id} onClick={() => handleSelect(r)}>{renderResult(r)}</div>\r\n ) : (\r\n <div key={r.id} className=\"hsk-item\" onClick={() => handleSelect(r)}>\r\n {r.product.images?.[0] && <img src={r.product.images[0]} alt={r.product.name} />}\r\n <div>\r\n <div className=\"hsk-item-name\">{r.product.name}</div>\r\n <div className=\"hsk-item-price\">{r.product.currency ?? 'KES'} {r.product.price}</div>\r\n </div>\r\n </div>\r\n )\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </>\r\n );\r\n}\r\n","import React, { useState } from 'react';\r\nimport { useHuskelContext } from './HuskelProvider';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SparkleProps {\r\n productName: string;\r\n limit?: number;\r\n onResult?: (results: SearchResult[]) => void;\r\n className?: string;\r\n}\r\n\r\nconst S = `\r\n .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}\r\n .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}\r\n .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}\r\n`;\r\n\r\nexport function Sparkle({ productName, limit = 5, onResult, className }: SparkleProps) {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n\r\n const handleClick = async () => {\r\n setLoading(true);\r\n try {\r\n const res = await client.api.search(productName, limit);\r\n onResult?.(res.results);\r\n } catch (e) {\r\n console.error('[Huskel Sparkle]', e);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <button className={`hsk-sparkle ${className ?? ''}`} onClick={handleClick} disabled={loading}>\r\n ✦ {loading ? 'Finding…' : 'Similar'}\r\n </button>\r\n </>\r\n );\r\n}\r\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_react","import_jsx_runtime","import_react","import_jsx_runtime","S"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api.ts","../src/client.ts","../src/hooks/useHuskel.ts","../src/hooks/useSearch.ts","../src/components/HuskelProvider.tsx","../src/hooks/useIngest.ts","../src/hooks/usePageIngest.ts","../src/components/SearchBar.tsx","../src/components/Sparkle.tsx"],"sourcesContent":["export { initHuskel, getHuskelClient, HuskelClient } from './client';\r\nexport { HuskelAPI } from './api';\r\nexport { useHuskel } from './hooks/useHuskel';\r\nexport { useSearch } from './hooks/useSearch';\r\nexport { useIngest } from './hooks/useIngest';\r\nexport { usePageIngest } from './hooks/usePageIngest';\r\nexport { SearchBar } from './components/SearchBar';\r\nexport { Sparkle } from './components/Sparkle';\r\nexport { HuskelProvider } from './components/HuskelProvider';\r\nexport type {\r\n Product,\r\n RawProductInput,\r\n HuskelConfig,\r\n SearchRequest,\r\n SearchResult,\r\n SearchResponse,\r\n IngestResponse,\r\n HuskelError,\r\n} from './types';\r\n","import { Product, SearchResponse, IngestResponse, HuskelError } from './types';\r\n\r\nconst MAX_RETRIES = 3;\r\nconst RETRY_DELAYS = [500, 1000, 2000]; // ms\r\n\r\nfunction log(level: 'info' | 'warn' | 'error', msg: string, data?: unknown) {\r\n const prefix = '[Huskel]';\r\n if (level === 'error') console.error(prefix, msg, data ?? '');\r\n else if (level === 'warn') console.warn(prefix, msg, data ?? '');\r\n else console.log(prefix, msg, data ?? '');\r\n}\r\n\r\nasync function sleep(ms: number) {\r\n return new Promise(r => setTimeout(r, ms));\r\n}\r\n\r\nexport class HuskelAPI {\r\n constructor(\r\n private apiUrl: string,\r\n private siteId: string,\r\n private apiToken: string,\r\n private getShopperId?: () => string | undefined,\r\n private getSessionId?: () => string | undefined\r\n ) {}\r\n\r\n private async post<T>(path: string, body: unknown, attempt = 0): Promise<T> {\r\n const url = `${this.apiUrl}${path}`;\r\n\r\n try {\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n 'X-Huskel-Token': this.apiToken,\r\n 'X-Huskel-Site': this.siteId,\r\n };\r\n\r\n const shopperId = this.getShopperId?.();\r\n if (shopperId) {\r\n headers['X-Huskel-Shopper-Id'] = shopperId;\r\n }\r\n\r\n const sessionId = this.getSessionId?.();\r\n if (sessionId) {\r\n headers['X-Huskel-Session-Id'] = sessionId;\r\n }\r\n\r\n const res = await fetch(url, {\r\n method: 'POST',\r\n headers,\r\n body: JSON.stringify(body),\r\n });\r\n\r\n if (!res.ok) {\r\n const text = await res.text();\r\n const err: HuskelError = { status: res.status, message: text };\r\n\r\n // Don't retry 4xx — developer errors\r\n if (res.status >= 400 && res.status < 500) {\r\n log('error', `${path} failed [${res.status}]`, text);\r\n throw err;\r\n }\r\n\r\n // Retry 5xx\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n\r\n log('error', `${path} failed after ${MAX_RETRIES} attempts`, err);\r\n throw err;\r\n }\r\n\r\n return res.json();\r\n } catch (e) {\r\n // Network error (offline, DNS, etc.)\r\n if ((e as HuskelError).status === undefined) {\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n log('error', `${path} unreachable after ${MAX_RETRIES} attempts`);\r\n }\r\n throw e;\r\n }\r\n }\r\n\r\n async ingest(product: Product): Promise<IngestResponse> {\r\n log('info', 'ingesting product', product.name);\r\n return this.post('/ingest', { siteId: this.siteId, product });\r\n }\r\n\r\n async ingestBatch(products: Product[]): Promise<IngestResponse> {\r\n log('info', `ingesting batch of ${products.length} products`);\r\n return this.post('/ingest/batch', { siteId: this.siteId, products });\r\n }\r\n\r\n async search(query: string, limit = 10): Promise<SearchResponse> {\r\n log('info', 'search query', query);\r\n return this.post('/search', { query, siteId: this.siteId, limit });\r\n }\r\n}\r\n","import { HuskelConfig, Product, RawProductInput } from './types';\r\nimport { HuskelAPI } from './api';\r\n\r\nfunction getEnvVar(key: string): string | undefined {\r\n if (typeof globalThis !== 'undefined') {\r\n const g = globalThis as any;\r\n if (g.process && g.process.env) {\r\n return g.process.env[key];\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction mapRawProduct(input: RawProductInput): Product | null {\r\n const name = input.name || input.title || input.productName || '';\r\n \r\n let price = '';\r\n let priceNumeric: number | undefined = undefined;\r\n\r\n if (input.price !== undefined) {\r\n if (typeof input.price === 'number') {\r\n priceNumeric = input.price;\r\n price = String(input.price);\r\n } else {\r\n price = input.price;\r\n const num = parseFloat(input.price.replace(/[^0-9.]/g, ''));\r\n priceNumeric = isNaN(num) ? undefined : num;\r\n }\r\n }\r\n if (input.priceNumeric !== undefined) {\r\n priceNumeric = input.priceNumeric;\r\n }\r\n\r\n let url = input.url || '';\r\n if (!url && typeof window !== 'undefined') {\r\n url = window.location.href;\r\n }\r\n\r\n let slug = input.slug || input.id || input.productId || '';\r\n if (!slug && url) {\r\n slug = url.split('/').filter(Boolean).pop() || '';\r\n }\r\n if (!slug && name) {\r\n slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');\r\n }\r\n\r\n let images: string[] = [];\r\n if (input.images) {\r\n images = input.images;\r\n } else if (input.image) {\r\n images = [input.image];\r\n } else if (input.thumbnail) {\r\n images = [input.thumbnail];\r\n }\r\n\r\n if (!name) {\r\n console.warn('[Huskel] Validation warning: Product name/title is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!price) {\r\n console.warn('[Huskel] Validation warning: Product price is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!url) {\r\n console.warn('[Huskel] Validation warning: Product URL is missing. Skipping:', input);\r\n return null;\r\n }\r\n\r\n return {\r\n name,\r\n price,\r\n url,\r\n brand: input.brand,\r\n description: input.description,\r\n originalPrice: input.originalPrice,\r\n discount: input.discount,\r\n currency: input.currency ?? 'KES',\r\n stock: input.stock,\r\n availability: input.availability,\r\n rating: input.rating,\r\n reviewCount: input.reviewCount,\r\n category: input.category,\r\n subCategory: input.subCategory,\r\n tags: input.tags,\r\n images: images.length > 0 ? images : undefined,\r\n specs: input.specs,\r\n priceNumeric,\r\n slug,\r\n };\r\n}\r\n\r\nfunction generateUUID(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n}\r\n\r\nexport class HuskelClient {\r\n readonly api: HuskelAPI;\r\n private ingestQueue: Product[] = [];\r\n private ingestTimer: ReturnType<typeof setTimeout> | null = null;\r\n private ingestedUrls = new Set<string>();\r\n private onlineHandler: (() => void) | null = null;\r\n private shopperId?: string;\r\n private sessionId: string = '';\r\n\r\n constructor(config: HuskelConfig) {\r\n const siteId = config.siteId || getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID') || '';\r\n const apiUrl = config.apiUrl || getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL') || '';\r\n const apiToken = config.apiToken || getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN') || '';\r\n\r\n // Runtime validation — fail loudly so misconfiguration is never silent\r\n if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId=\"...\"> or NEXT_PUBLIC_HUSKEL_SITE_ID.');\r\n if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl=\"...\"> or NEXT_PUBLIC_HUSKEL_API_URL.');\r\n if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken=\"...\"> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');\r\n\r\n this.shopperId = config.shopperId;\r\n this.initSession();\r\n\r\n this.api = new HuskelAPI(\r\n apiUrl,\r\n siteId,\r\n apiToken,\r\n () => this.shopperId,\r\n () => this.sessionId\r\n );\r\n instance = this;\r\n\r\n if (typeof window !== 'undefined') {\r\n this.onlineHandler = () => {\r\n console.log('[Huskel] Connectivity restored, flushing queued ingestions.');\r\n this.flushQueue();\r\n };\r\n window.addEventListener('online', this.onlineHandler);\r\n }\r\n }\r\n\r\n setShopperId(id: string | undefined) {\r\n this.shopperId = id;\r\n }\r\n\r\n getShopperId(): string | undefined {\r\n return this.shopperId;\r\n }\r\n\r\n getSessionId(): string {\r\n return this.sessionId;\r\n }\r\n\r\n private initSession() {\r\n if (typeof window !== 'undefined' && window.sessionStorage) {\r\n try {\r\n let sid = window.sessionStorage.getItem('huskel_session_id');\r\n if (!sid) {\r\n sid = generateUUID();\r\n window.sessionStorage.setItem('huskel_session_id', sid);\r\n }\r\n this.sessionId = sid;\r\n return;\r\n } catch (e) {\r\n // Fallback if sessionStorage is disabled or private mode\r\n }\r\n }\r\n this.sessionId = generateUUID();\r\n }\r\n\r\n destroy() {\r\n if (typeof window !== 'undefined' && this.onlineHandler) {\r\n window.removeEventListener('online', this.onlineHandler);\r\n this.onlineHandler = null;\r\n }\r\n if (this.ingestTimer) {\r\n clearTimeout(this.ingestTimer);\r\n this.ingestTimer = null;\r\n }\r\n if (instance === this) instance = null;\r\n }\r\n\r\n async queueIngest(rawProduct: RawProductInput): Promise<void> {\r\n const product = mapRawProduct(rawProduct);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n\r\n this.ingestQueue.push(product);\r\n this.scheduleFlush();\r\n }\r\n\r\n async queueIngestBatch(rawProducts: RawProductInput[]): Promise<void> {\r\n rawProducts.forEach(p => {\r\n const product = mapRawProduct(p);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n this.ingestQueue.push(product);\r\n });\r\n\r\n if (this.ingestQueue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n private scheduleFlush() {\r\n if (this.ingestTimer) return;\r\n this.ingestTimer = setTimeout(() => {\r\n this.flushQueue();\r\n }, 300);\r\n }\r\n\r\n private async flushQueue() {\r\n this.ingestTimer = null;\r\n if (this.ingestQueue.length === 0) return;\r\n\r\n if (typeof navigator !== 'undefined' && !navigator.onLine) {\r\n console.warn('[Huskel] Browser offline. Postponing ingestion.');\r\n return;\r\n }\r\n\r\n const batch = [...this.ingestQueue];\r\n this.ingestQueue = [];\r\n\r\n try {\r\n await this.api.ingestBatch(batch);\r\n } catch (e: any) {\r\n if (e.status && e.status >= 400 && e.status < 500) {\r\n console.error('[Huskel] Ingestion discarded due to client error:', e.message);\r\n return;\r\n }\r\n\r\n // Re-queue and schedule another flush so items are not stuck forever\r\n console.warn('[Huskel] Ingestion failed. Re-queuing to retry.', e);\r\n this.ingestQueue = [...batch, ...this.ingestQueue];\r\n this.scheduleFlush();\r\n }\r\n }\r\n}\r\n\r\nlet instance: HuskelClient | null = null;\r\n\r\nexport function initHuskel(config: HuskelConfig): HuskelClient {\r\n instance = new HuskelClient(config);\r\n return instance;\r\n}\r\n\r\nexport function getHuskelClient(): HuskelClient {\r\n if (!instance) {\r\n const siteId = getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID');\r\n const apiUrl = getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL');\r\n const apiToken = getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN');\r\n\r\n if (siteId && apiUrl && apiToken) {\r\n instance = new HuskelClient({ siteId, apiUrl, apiToken });\r\n } else {\r\n throw new Error('[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.');\r\n }\r\n }\r\n return instance;\r\n}\r\n","import { useRef } from 'react';\r\nimport { HuskelConfig } from '../types';\r\nimport { HuskelClient, initHuskel } from '../client';\r\n\r\n/**\r\n * @deprecated Use <HuskelProvider> instead to avoid SSR issues.\r\n */\r\nexport function useHuskel(config: HuskelConfig): HuskelClient {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n console.warn('[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.');\r\n clientRef.current = initHuskel(config);\r\n }\r\n\r\n return clientRef.current;\r\n}\r\n","import { useState, useCallback, useRef } from 'react';\r\nimport { SearchResult } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseSearchReturn {\r\n results: SearchResult[];\r\n loading: boolean;\r\n error: string | null;\r\n search: (query: string, limit?: number) => Promise<void>;\r\n clear: () => void;\r\n}\r\n\r\nexport function useSearch(): UseSearchReturn {\r\n const client = useHuskelContext();\r\n const [results, setResults] = useState<SearchResult[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const abortRef = useRef<AbortController | null>(null);\r\n\r\n const search = useCallback(async (query: string, limit = 10) => {\r\n if (!query.trim()) { setResults([]); return; }\r\n abortRef.current?.abort();\r\n abortRef.current = new AbortController();\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n const res = await client.api.search(query, limit);\r\n setResults(res.results ?? []);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Search failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const clear = useCallback(() => { setResults([]); setError(null); }, []);\r\n\r\n return { results, loading, error, search, clear };\r\n}\r\n","'use client';\r\n\r\nimport React, { createContext, useContext, useEffect, useRef } from 'react';\r\nimport { HuskelClient, getHuskelClient } from '../client';\r\nimport { HuskelConfig } from '../types';\r\n\r\nexport const HuskelContext = createContext<HuskelClient | null>(null);\r\n\r\ninterface HuskelProviderProps extends HuskelConfig {\r\n children: React.ReactNode;\r\n}\r\n\r\nexport function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps) {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });\r\n }\r\n\r\n // Update shopperId dynamically when it changes (e.g., shopper logs in/out)\r\n useEffect(() => {\r\n clientRef.current?.setShopperId(shopperId);\r\n }, [shopperId]);\r\n\r\n // Clean up the online listener and timers when the provider unmounts\r\n // (prevents leaks during hot module reload and React StrictMode double-mount)\r\n useEffect(() => {\r\n return () => {\r\n clientRef.current?.destroy();\r\n };\r\n }, []);\r\n\r\n return (\r\n <HuskelContext.Provider value={clientRef.current}>\r\n {children}\r\n </HuskelContext.Provider>\r\n );\r\n}\r\n\r\nexport function useHuskelContext(): HuskelClient {\r\n const context = useContext(HuskelContext);\r\n if (!context) {\r\n return getHuskelClient();\r\n }\r\n return context;\r\n}\r\n\r\n","import { useCallback, useState } from 'react';\r\nimport { RawProductInput } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseIngestReturn {\r\n ingest: (product: RawProductInput) => Promise<void>;\r\n ingestBatch: (products: RawProductInput[]) => Promise<void>;\r\n loading: boolean;\r\n error: string | null;\r\n}\r\n\r\nexport function useIngest(): UseIngestReturn {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const ingest = useCallback(async (product: RawProductInput) => {\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngest(product);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const ingestBatch = useCallback(async (products: RawProductInput[]) => {\r\n if (!products.length) return;\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngestBatch(products);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Batch ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n return { ingest, ingestBatch, loading, error };\r\n}\r\n","import { useEffect, useRef } from 'react';\nimport { RawProductInput } from '../types';\nimport { getHuskelClient } from '../client';\n\n/**\n * usePageIngest — drop this into any product page component.\n * The moment a customer's browser renders the page, the product is\n * automatically captured and queued for ingestion into the vector index.\n *\n * No configuration needed beyond <HuskelProvider> in your layout.\n *\n * @example\n * // Product detail page — Next.js or React\n * export function ProductPage({ product }) {\n * usePageIngest({\n * name: product.title,\n * price: product.price,\n * url: window.location.href,\n * images: [product.thumbnail],\n * category: product.category,\n * });\n * return <div>...</div>;\n * }\n */\nexport function usePageIngest(product: RawProductInput | null | undefined): void {\n // Use url as the stable key — avoids re-ingesting on unrelated re-renders\n const ingestedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!product) return;\n\n // Resolve URL — falls back to window.location if not provided\n const url =\n product.url ||\n (typeof window !== 'undefined' ? window.location.href : '');\n\n // Guard: only ingest once per URL per component lifecycle\n if (ingestedRef.current === url) return;\n ingestedRef.current = url;\n\n try {\n getHuskelClient().queueIngest({ ...product, url });\n } catch {\n // Client not initialised — silently skip\n }\n }, [product?.url ?? product?.name]);\n}\n","import React, { useState, useEffect, useRef } from 'react';\r\nimport { useSearch } from '../hooks/useSearch';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SearchBarProps {\r\n placeholder?: string;\r\n limit?: number;\r\n debounceMs?: number;\r\n onSelect?: (result: SearchResult) => void;\r\n className?: string;\r\n inputClassName?: string;\r\n dropdownClassName?: string;\r\n renderResult?: (result: SearchResult) => React.ReactNode;\r\n}\r\n\r\nconst S = `\r\n .hsk-wrap{position:relative;width:100%;font-family:inherit}\r\n .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}\r\n .hsk-input:focus{border-color:#f47c3c}\r\n .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}\r\n .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}\r\n .hsk-item:hover{background:#faf5f1}\r\n .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}\r\n .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\r\n .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}\r\n .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}\r\n`;\r\n\r\nexport function SearchBar({\r\n placeholder = 'Search for what you want — how you want',\r\n limit = 10,\r\n debounceMs = 300,\r\n onSelect,\r\n className,\r\n inputClassName,\r\n dropdownClassName,\r\n renderResult,\r\n}: SearchBarProps) {\r\n const [query, setQuery] = useState('');\r\n const [open, setOpen] = useState(false);\r\n const { results, loading, search, clear } = useSearch();\r\n const timer = useRef<ReturnType<typeof setTimeout>>();\r\n const wrap = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n clearTimeout(timer.current);\r\n if (!query.trim()) { clear(); setOpen(false); return; }\r\n timer.current = setTimeout(() => { search(query, limit); setOpen(true); }, debounceMs);\r\n return () => clearTimeout(timer.current);\r\n }, [query, search, clear, limit, debounceMs]);\r\n\r\n useEffect(() => {\r\n const handler = (e: MouseEvent) => {\r\n if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);\r\n };\r\n document.addEventListener('mousedown', handler);\r\n return () => document.removeEventListener('mousedown', handler);\r\n }, []);\r\n\r\n const handleSelect = (r: SearchResult) => {\r\n setOpen(false);\r\n setQuery(r.product.name);\r\n onSelect?.(r);\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <div className={`hsk-wrap ${className ?? ''}`} ref={wrap}>\r\n <input\r\n className={`hsk-input ${inputClassName ?? ''}`}\r\n type=\"text\"\r\n value={query}\r\n placeholder={placeholder}\r\n onChange={e => setQuery(e.target.value)}\r\n onFocus={() => results.length && setOpen(true)}\r\n />\r\n {open && (\r\n <div className={`hsk-drop ${dropdownClassName ?? ''}`}>\r\n {loading && <div className=\"hsk-msg\">Searching…</div>}\r\n {!loading && results.length === 0 && <div className=\"hsk-msg\">No results for \"{query}\"</div>}\r\n {results.map(r =>\r\n renderResult ? (\r\n <div key={r.id} onClick={() => handleSelect(r)}>{renderResult(r)}</div>\r\n ) : (\r\n <div key={r.id} className=\"hsk-item\" onClick={() => handleSelect(r)}>\r\n {r.product.images?.[0] && <img src={r.product.images[0]} alt={r.product.name} />}\r\n <div>\r\n <div className=\"hsk-item-name\">{r.product.name}</div>\r\n <div className=\"hsk-item-price\">{r.product.currency ?? 'KES'} {r.product.price}</div>\r\n </div>\r\n </div>\r\n )\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </>\r\n );\r\n}\r\n","import React, { useState } from 'react';\r\nimport { useHuskelContext } from './HuskelProvider';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SparkleProps {\r\n productName: string;\r\n limit?: number;\r\n onResult?: (results: SearchResult[]) => void;\r\n className?: string;\r\n}\r\n\r\nconst S = `\r\n .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}\r\n .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}\r\n .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}\r\n`;\r\n\r\nexport function Sparkle({ productName, limit = 5, onResult, className }: SparkleProps) {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n\r\n const handleClick = async () => {\r\n setLoading(true);\r\n try {\r\n const res = await client.api.search(productName, limit);\r\n onResult?.(res.results);\r\n } catch (e) {\r\n console.error('[Huskel Sparkle]', e);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <button className={`hsk-sparkle ${className ?? ''}`} onClick={handleClick} disabled={loading}>\r\n ✦ {loading ? 'Finding…' : 'Similar'}\r\n </button>\r\n </>\r\n );\r\n}\r\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_react","import_jsx_runtime","import_react","import_jsx_runtime","S"]}
|
package/dist/index.mjs
CHANGED
|
@@ -32,21 +32,33 @@ async function sleep(ms) {
|
|
|
32
32
|
return new Promise((r) => setTimeout(r, ms));
|
|
33
33
|
}
|
|
34
34
|
var HuskelAPI = class {
|
|
35
|
-
constructor(apiUrl, siteId, apiToken) {
|
|
35
|
+
constructor(apiUrl, siteId, apiToken, getShopperId, getSessionId) {
|
|
36
36
|
this.apiUrl = apiUrl;
|
|
37
37
|
this.siteId = siteId;
|
|
38
38
|
this.apiToken = apiToken;
|
|
39
|
+
this.getShopperId = getShopperId;
|
|
40
|
+
this.getSessionId = getSessionId;
|
|
39
41
|
}
|
|
40
42
|
async post(path, body, attempt = 0) {
|
|
43
|
+
var _a, _b;
|
|
41
44
|
const url = `${this.apiUrl}${path}`;
|
|
42
45
|
try {
|
|
46
|
+
const headers = {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"X-Huskel-Token": this.apiToken,
|
|
49
|
+
"X-Huskel-Site": this.siteId
|
|
50
|
+
};
|
|
51
|
+
const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
|
|
52
|
+
if (shopperId) {
|
|
53
|
+
headers["X-Huskel-Shopper-Id"] = shopperId;
|
|
54
|
+
}
|
|
55
|
+
const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
|
|
56
|
+
if (sessionId) {
|
|
57
|
+
headers["X-Huskel-Session-Id"] = sessionId;
|
|
58
|
+
}
|
|
43
59
|
const res = await fetch(url, {
|
|
44
60
|
method: "POST",
|
|
45
|
-
headers
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
"X-Huskel-Token": this.apiToken,
|
|
48
|
-
"X-Huskel-Site": this.siteId
|
|
49
|
-
},
|
|
61
|
+
headers,
|
|
50
62
|
body: JSON.stringify(body)
|
|
51
63
|
});
|
|
52
64
|
if (!res.ok) {
|
|
@@ -172,19 +184,38 @@ function mapRawProduct(input) {
|
|
|
172
184
|
slug
|
|
173
185
|
};
|
|
174
186
|
}
|
|
187
|
+
function generateUUID() {
|
|
188
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
189
|
+
return crypto.randomUUID();
|
|
190
|
+
}
|
|
191
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
192
|
+
const r = Math.random() * 16 | 0;
|
|
193
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
194
|
+
return v.toString(16);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
175
197
|
var HuskelClient = class {
|
|
176
198
|
constructor(config) {
|
|
177
199
|
this.ingestQueue = [];
|
|
178
200
|
this.ingestTimer = null;
|
|
179
201
|
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
180
202
|
this.onlineHandler = null;
|
|
203
|
+
this.sessionId = "";
|
|
181
204
|
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
|
|
182
205
|
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
|
|
183
206
|
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
|
|
184
207
|
if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
|
|
185
208
|
if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
|
|
186
209
|
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
187
|
-
this.
|
|
210
|
+
this.shopperId = config.shopperId;
|
|
211
|
+
this.initSession();
|
|
212
|
+
this.api = new HuskelAPI(
|
|
213
|
+
apiUrl,
|
|
214
|
+
siteId,
|
|
215
|
+
apiToken,
|
|
216
|
+
() => this.shopperId,
|
|
217
|
+
() => this.sessionId
|
|
218
|
+
);
|
|
188
219
|
instance = this;
|
|
189
220
|
if (typeof window !== "undefined") {
|
|
190
221
|
this.onlineHandler = () => {
|
|
@@ -194,6 +225,30 @@ var HuskelClient = class {
|
|
|
194
225
|
window.addEventListener("online", this.onlineHandler);
|
|
195
226
|
}
|
|
196
227
|
}
|
|
228
|
+
setShopperId(id) {
|
|
229
|
+
this.shopperId = id;
|
|
230
|
+
}
|
|
231
|
+
getShopperId() {
|
|
232
|
+
return this.shopperId;
|
|
233
|
+
}
|
|
234
|
+
getSessionId() {
|
|
235
|
+
return this.sessionId;
|
|
236
|
+
}
|
|
237
|
+
initSession() {
|
|
238
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
239
|
+
try {
|
|
240
|
+
let sid = window.sessionStorage.getItem("huskel_session_id");
|
|
241
|
+
if (!sid) {
|
|
242
|
+
sid = generateUUID();
|
|
243
|
+
window.sessionStorage.setItem("huskel_session_id", sid);
|
|
244
|
+
}
|
|
245
|
+
this.sessionId = sid;
|
|
246
|
+
return;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
this.sessionId = generateUUID();
|
|
251
|
+
}
|
|
197
252
|
destroy() {
|
|
198
253
|
if (typeof window !== "undefined" && this.onlineHandler) {
|
|
199
254
|
window.removeEventListener("online", this.onlineHandler);
|
|
@@ -294,11 +349,15 @@ import { useState, useCallback, useRef as useRef3 } from "react";
|
|
|
294
349
|
import { createContext, useContext, useEffect, useRef as useRef2 } from "react";
|
|
295
350
|
import { jsx } from "react/jsx-runtime";
|
|
296
351
|
var HuskelContext = createContext(null);
|
|
297
|
-
function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
|
|
352
|
+
function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
|
|
298
353
|
const clientRef = useRef2(null);
|
|
299
354
|
if (!clientRef.current) {
|
|
300
|
-
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
355
|
+
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
|
|
301
356
|
}
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
var _a;
|
|
359
|
+
(_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
|
|
360
|
+
}, [shopperId]);
|
|
302
361
|
useEffect(() => {
|
|
303
362
|
return () => {
|
|
304
363
|
var _a;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api.ts","../src/client.ts","../src/hooks/useHuskel.ts","../src/hooks/useSearch.ts","../src/components/HuskelProvider.tsx","../src/hooks/useIngest.ts","../src/hooks/usePageIngest.ts","../src/components/SearchBar.tsx","../src/components/Sparkle.tsx"],"sourcesContent":["import { Product, SearchResponse, IngestResponse, HuskelError } from './types';\r\n\r\nconst MAX_RETRIES = 3;\r\nconst RETRY_DELAYS = [500, 1000, 2000]; // ms\r\n\r\nfunction log(level: 'info' | 'warn' | 'error', msg: string, data?: unknown) {\r\n const prefix = '[Huskel]';\r\n if (level === 'error') console.error(prefix, msg, data ?? '');\r\n else if (level === 'warn') console.warn(prefix, msg, data ?? '');\r\n else console.log(prefix, msg, data ?? '');\r\n}\r\n\r\nasync function sleep(ms: number) {\r\n return new Promise(r => setTimeout(r, ms));\r\n}\r\n\r\nexport class HuskelAPI {\r\n constructor(\r\n private apiUrl: string,\r\n private siteId: string,\r\n private apiToken: string\r\n ) {}\r\n\r\n private async post<T>(path: string, body: unknown, attempt = 0): Promise<T> {\r\n const url = `${this.apiUrl}${path}`;\r\n\r\n try {\r\n const res = await fetch(url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'X-Huskel-Token': this.apiToken,\r\n 'X-Huskel-Site': this.siteId,\r\n },\r\n body: JSON.stringify(body),\r\n });\r\n\r\n if (!res.ok) {\r\n const text = await res.text();\r\n const err: HuskelError = { status: res.status, message: text };\r\n\r\n // Don't retry 4xx — developer errors\r\n if (res.status >= 400 && res.status < 500) {\r\n log('error', `${path} failed [${res.status}]`, text);\r\n throw err;\r\n }\r\n\r\n // Retry 5xx\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n\r\n log('error', `${path} failed after ${MAX_RETRIES} attempts`, err);\r\n throw err;\r\n }\r\n\r\n return res.json();\r\n } catch (e) {\r\n // Network error (offline, DNS, etc.)\r\n if ((e as HuskelError).status === undefined) {\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n log('error', `${path} unreachable after ${MAX_RETRIES} attempts`);\r\n }\r\n throw e;\r\n }\r\n }\r\n\r\n async ingest(product: Product): Promise<IngestResponse> {\r\n log('info', 'ingesting product', product.name);\r\n return this.post('/ingest', { siteId: this.siteId, product });\r\n }\r\n\r\n async ingestBatch(products: Product[]): Promise<IngestResponse> {\r\n log('info', `ingesting batch of ${products.length} products`);\r\n return this.post('/ingest/batch', { siteId: this.siteId, products });\r\n }\r\n\r\n async search(query: string, limit = 10): Promise<SearchResponse> {\r\n log('info', 'search query', query);\r\n return this.post('/search', { query, siteId: this.siteId, limit });\r\n }\r\n}\r\n","import { HuskelConfig, Product, RawProductInput } from './types';\r\nimport { HuskelAPI } from './api';\r\n\r\nfunction getEnvVar(key: string): string | undefined {\r\n if (typeof globalThis !== 'undefined') {\r\n const g = globalThis as any;\r\n if (g.process && g.process.env) {\r\n return g.process.env[key];\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction mapRawProduct(input: RawProductInput): Product | null {\r\n const name = input.name || input.title || input.productName || '';\r\n \r\n let price = '';\r\n let priceNumeric: number | undefined = undefined;\r\n\r\n if (input.price !== undefined) {\r\n if (typeof input.price === 'number') {\r\n priceNumeric = input.price;\r\n price = String(input.price);\r\n } else {\r\n price = input.price;\r\n const num = parseFloat(input.price.replace(/[^0-9.]/g, ''));\r\n priceNumeric = isNaN(num) ? undefined : num;\r\n }\r\n }\r\n if (input.priceNumeric !== undefined) {\r\n priceNumeric = input.priceNumeric;\r\n }\r\n\r\n let url = input.url || '';\r\n if (!url && typeof window !== 'undefined') {\r\n url = window.location.href;\r\n }\r\n\r\n let slug = input.slug || input.id || input.productId || '';\r\n if (!slug && url) {\r\n slug = url.split('/').filter(Boolean).pop() || '';\r\n }\r\n if (!slug && name) {\r\n slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');\r\n }\r\n\r\n let images: string[] = [];\r\n if (input.images) {\r\n images = input.images;\r\n } else if (input.image) {\r\n images = [input.image];\r\n } else if (input.thumbnail) {\r\n images = [input.thumbnail];\r\n }\r\n\r\n if (!name) {\r\n console.warn('[Huskel] Validation warning: Product name/title is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!price) {\r\n console.warn('[Huskel] Validation warning: Product price is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!url) {\r\n console.warn('[Huskel] Validation warning: Product URL is missing. Skipping:', input);\r\n return null;\r\n }\r\n\r\n return {\r\n name,\r\n price,\r\n url,\r\n brand: input.brand,\r\n description: input.description,\r\n originalPrice: input.originalPrice,\r\n discount: input.discount,\r\n currency: input.currency ?? 'KES',\r\n stock: input.stock,\r\n availability: input.availability,\r\n rating: input.rating,\r\n reviewCount: input.reviewCount,\r\n category: input.category,\r\n subCategory: input.subCategory,\r\n tags: input.tags,\r\n images: images.length > 0 ? images : undefined,\r\n specs: input.specs,\r\n priceNumeric,\r\n slug,\r\n };\r\n}\r\n\r\nexport class HuskelClient {\r\n readonly api: HuskelAPI;\r\n private ingestQueue: Product[] = [];\r\n private ingestTimer: ReturnType<typeof setTimeout> | null = null;\r\n private ingestedUrls = new Set<string>();\r\n private onlineHandler: (() => void) | null = null;\r\n\r\n constructor(config: HuskelConfig) {\r\n const siteId = config.siteId || getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID') || '';\r\n const apiUrl = config.apiUrl || getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL') || '';\r\n const apiToken = config.apiToken || getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN') || '';\r\n\r\n // Runtime validation — fail loudly so misconfiguration is never silent\r\n if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId=\"...\"> or NEXT_PUBLIC_HUSKEL_SITE_ID.');\r\n if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl=\"...\"> or NEXT_PUBLIC_HUSKEL_API_URL.');\r\n if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken=\"...\"> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');\r\n\r\n this.api = new HuskelAPI(apiUrl, siteId, apiToken);\r\n instance = this;\r\n\r\n if (typeof window !== 'undefined') {\r\n this.onlineHandler = () => {\r\n console.log('[Huskel] Connectivity restored, flushing queued ingestions.');\r\n this.flushQueue();\r\n };\r\n window.addEventListener('online', this.onlineHandler);\r\n }\r\n }\r\n\r\n destroy() {\r\n if (typeof window !== 'undefined' && this.onlineHandler) {\r\n window.removeEventListener('online', this.onlineHandler);\r\n this.onlineHandler = null;\r\n }\r\n if (this.ingestTimer) {\r\n clearTimeout(this.ingestTimer);\r\n this.ingestTimer = null;\r\n }\r\n if (instance === this) instance = null;\r\n }\r\n\r\n async queueIngest(rawProduct: RawProductInput): Promise<void> {\r\n const product = mapRawProduct(rawProduct);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n\r\n this.ingestQueue.push(product);\r\n this.scheduleFlush();\r\n }\r\n\r\n async queueIngestBatch(rawProducts: RawProductInput[]): Promise<void> {\r\n rawProducts.forEach(p => {\r\n const product = mapRawProduct(p);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n this.ingestQueue.push(product);\r\n });\r\n\r\n if (this.ingestQueue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n private scheduleFlush() {\r\n if (this.ingestTimer) return;\r\n this.ingestTimer = setTimeout(() => {\r\n this.flushQueue();\r\n }, 300);\r\n }\r\n\r\n private async flushQueue() {\r\n this.ingestTimer = null;\r\n if (this.ingestQueue.length === 0) return;\r\n\r\n if (typeof navigator !== 'undefined' && !navigator.onLine) {\r\n console.warn('[Huskel] Browser offline. Postponing ingestion.');\r\n return;\r\n }\r\n\r\n const batch = [...this.ingestQueue];\r\n this.ingestQueue = [];\r\n\r\n try {\r\n await this.api.ingestBatch(batch);\r\n } catch (e: any) {\r\n if (e.status && e.status >= 400 && e.status < 500) {\r\n console.error('[Huskel] Ingestion discarded due to client error:', e.message);\r\n return;\r\n }\r\n\r\n // Re-queue and schedule another flush so items are not stuck forever\r\n console.warn('[Huskel] Ingestion failed. Re-queuing to retry.', e);\r\n this.ingestQueue = [...batch, ...this.ingestQueue];\r\n this.scheduleFlush();\r\n }\r\n }\r\n}\r\n\r\nlet instance: HuskelClient | null = null;\r\n\r\nexport function initHuskel(config: HuskelConfig): HuskelClient {\r\n instance = new HuskelClient(config);\r\n return instance;\r\n}\r\n\r\nexport function getHuskelClient(): HuskelClient {\r\n if (!instance) {\r\n const siteId = getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID');\r\n const apiUrl = getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL');\r\n const apiToken = getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN');\r\n\r\n if (siteId && apiUrl && apiToken) {\r\n instance = new HuskelClient({ siteId, apiUrl, apiToken });\r\n } else {\r\n throw new Error('[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.');\r\n }\r\n }\r\n return instance;\r\n}\r\n","import { useRef } from 'react';\r\nimport { HuskelConfig } from '../types';\r\nimport { HuskelClient, initHuskel } from '../client';\r\n\r\n/**\r\n * @deprecated Use <HuskelProvider> instead to avoid SSR issues.\r\n */\r\nexport function useHuskel(config: HuskelConfig): HuskelClient {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n console.warn('[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.');\r\n clientRef.current = initHuskel(config);\r\n }\r\n\r\n return clientRef.current;\r\n}\r\n","import { useState, useCallback, useRef } from 'react';\r\nimport { SearchResult } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseSearchReturn {\r\n results: SearchResult[];\r\n loading: boolean;\r\n error: string | null;\r\n search: (query: string, limit?: number) => Promise<void>;\r\n clear: () => void;\r\n}\r\n\r\nexport function useSearch(): UseSearchReturn {\r\n const client = useHuskelContext();\r\n const [results, setResults] = useState<SearchResult[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const abortRef = useRef<AbortController | null>(null);\r\n\r\n const search = useCallback(async (query: string, limit = 10) => {\r\n if (!query.trim()) { setResults([]); return; }\r\n abortRef.current?.abort();\r\n abortRef.current = new AbortController();\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n const res = await client.api.search(query, limit);\r\n setResults(res.results ?? []);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Search failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const clear = useCallback(() => { setResults([]); setError(null); }, []);\r\n\r\n return { results, loading, error, search, clear };\r\n}\r\n","'use client';\r\n\r\nimport React, { createContext, useContext, useEffect, useRef } from 'react';\r\nimport { HuskelClient, getHuskelClient } from '../client';\r\nimport { HuskelConfig } from '../types';\r\n\r\nexport const HuskelContext = createContext<HuskelClient | null>(null);\r\n\r\ninterface HuskelProviderProps extends HuskelConfig {\r\n children: React.ReactNode;\r\n}\r\n\r\nexport function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps) {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });\r\n }\r\n\r\n // Clean up the online listener and timers when the provider unmounts\r\n // (prevents leaks during hot module reload and React StrictMode double-mount)\r\n useEffect(() => {\r\n return () => {\r\n clientRef.current?.destroy();\r\n };\r\n }, []);\r\n\r\n return (\r\n <HuskelContext.Provider value={clientRef.current}>\r\n {children}\r\n </HuskelContext.Provider>\r\n );\r\n}\r\n\r\nexport function useHuskelContext(): HuskelClient {\r\n const context = useContext(HuskelContext);\r\n if (!context) {\r\n return getHuskelClient();\r\n }\r\n return context;\r\n}\r\n\r\n","import { useCallback, useState } from 'react';\r\nimport { RawProductInput } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseIngestReturn {\r\n ingest: (product: RawProductInput) => Promise<void>;\r\n ingestBatch: (products: RawProductInput[]) => Promise<void>;\r\n loading: boolean;\r\n error: string | null;\r\n}\r\n\r\nexport function useIngest(): UseIngestReturn {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const ingest = useCallback(async (product: RawProductInput) => {\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngest(product);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const ingestBatch = useCallback(async (products: RawProductInput[]) => {\r\n if (!products.length) return;\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngestBatch(products);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Batch ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n return { ingest, ingestBatch, loading, error };\r\n}\r\n","import { useEffect, useRef } from 'react';\nimport { RawProductInput } from '../types';\nimport { getHuskelClient } from '../client';\n\n/**\n * usePageIngest — drop this into any product page component.\n * The moment a customer's browser renders the page, the product is\n * automatically captured and queued for ingestion into the vector index.\n *\n * No configuration needed beyond <HuskelProvider> in your layout.\n *\n * @example\n * // Product detail page — Next.js or React\n * export function ProductPage({ product }) {\n * usePageIngest({\n * name: product.title,\n * price: product.price,\n * url: window.location.href,\n * images: [product.thumbnail],\n * category: product.category,\n * });\n * return <div>...</div>;\n * }\n */\nexport function usePageIngest(product: RawProductInput | null | undefined): void {\n // Use url as the stable key — avoids re-ingesting on unrelated re-renders\n const ingestedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!product) return;\n\n // Resolve URL — falls back to window.location if not provided\n const url =\n product.url ||\n (typeof window !== 'undefined' ? window.location.href : '');\n\n // Guard: only ingest once per URL per component lifecycle\n if (ingestedRef.current === url) return;\n ingestedRef.current = url;\n\n try {\n getHuskelClient().queueIngest({ ...product, url });\n } catch {\n // Client not initialised — silently skip\n }\n }, [product?.url ?? product?.name]);\n}\n","import React, { useState, useEffect, useRef } from 'react';\r\nimport { useSearch } from '../hooks/useSearch';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SearchBarProps {\r\n placeholder?: string;\r\n limit?: number;\r\n debounceMs?: number;\r\n onSelect?: (result: SearchResult) => void;\r\n className?: string;\r\n inputClassName?: string;\r\n dropdownClassName?: string;\r\n renderResult?: (result: SearchResult) => React.ReactNode;\r\n}\r\n\r\nconst S = `\r\n .hsk-wrap{position:relative;width:100%;font-family:inherit}\r\n .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}\r\n .hsk-input:focus{border-color:#f47c3c}\r\n .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}\r\n .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}\r\n .hsk-item:hover{background:#faf5f1}\r\n .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}\r\n .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\r\n .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}\r\n .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}\r\n`;\r\n\r\nexport function SearchBar({\r\n placeholder = 'Search for what you want — how you want',\r\n limit = 10,\r\n debounceMs = 300,\r\n onSelect,\r\n className,\r\n inputClassName,\r\n dropdownClassName,\r\n renderResult,\r\n}: SearchBarProps) {\r\n const [query, setQuery] = useState('');\r\n const [open, setOpen] = useState(false);\r\n const { results, loading, search, clear } = useSearch();\r\n const timer = useRef<ReturnType<typeof setTimeout>>();\r\n const wrap = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n clearTimeout(timer.current);\r\n if (!query.trim()) { clear(); setOpen(false); return; }\r\n timer.current = setTimeout(() => { search(query, limit); setOpen(true); }, debounceMs);\r\n return () => clearTimeout(timer.current);\r\n }, [query, search, clear, limit, debounceMs]);\r\n\r\n useEffect(() => {\r\n const handler = (e: MouseEvent) => {\r\n if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);\r\n };\r\n document.addEventListener('mousedown', handler);\r\n return () => document.removeEventListener('mousedown', handler);\r\n }, []);\r\n\r\n const handleSelect = (r: SearchResult) => {\r\n setOpen(false);\r\n setQuery(r.product.name);\r\n onSelect?.(r);\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <div className={`hsk-wrap ${className ?? ''}`} ref={wrap}>\r\n <input\r\n className={`hsk-input ${inputClassName ?? ''}`}\r\n type=\"text\"\r\n value={query}\r\n placeholder={placeholder}\r\n onChange={e => setQuery(e.target.value)}\r\n onFocus={() => results.length && setOpen(true)}\r\n />\r\n {open && (\r\n <div className={`hsk-drop ${dropdownClassName ?? ''}`}>\r\n {loading && <div className=\"hsk-msg\">Searching…</div>}\r\n {!loading && results.length === 0 && <div className=\"hsk-msg\">No results for \"{query}\"</div>}\r\n {results.map(r =>\r\n renderResult ? (\r\n <div key={r.id} onClick={() => handleSelect(r)}>{renderResult(r)}</div>\r\n ) : (\r\n <div key={r.id} className=\"hsk-item\" onClick={() => handleSelect(r)}>\r\n {r.product.images?.[0] && <img src={r.product.images[0]} alt={r.product.name} />}\r\n <div>\r\n <div className=\"hsk-item-name\">{r.product.name}</div>\r\n <div className=\"hsk-item-price\">{r.product.currency ?? 'KES'} {r.product.price}</div>\r\n </div>\r\n </div>\r\n )\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </>\r\n );\r\n}\r\n","import React, { useState } from 'react';\r\nimport { useHuskelContext } from './HuskelProvider';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SparkleProps {\r\n productName: string;\r\n limit?: number;\r\n onResult?: (results: SearchResult[]) => void;\r\n className?: string;\r\n}\r\n\r\nconst S = `\r\n .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}\r\n .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}\r\n .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}\r\n`;\r\n\r\nexport function Sparkle({ productName, limit = 5, onResult, className }: SparkleProps) {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n\r\n const handleClick = async () => {\r\n setLoading(true);\r\n try {\r\n const res = await client.api.search(productName, limit);\r\n onResult?.(res.results);\r\n } catch (e) {\r\n console.error('[Huskel Sparkle]', e);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <button className={`hsk-sparkle ${className ?? ''}`} onClick={handleClick} disabled={loading}>\r\n ✦ {loading ? 'Finding…' : 'Similar'}\r\n </button>\r\n </>\r\n );\r\n}\r\n"],"mappings":"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","names":["useRef","useRef","useRef","useRef","useCallback","useState","useState","useCallback","useEffect","useRef","useRef","useEffect","useState","useEffect","useRef","jsx","useState","useRef","useEffect","useState","Fragment","jsx","jsxs","S","useState"]}
|
|
1
|
+
{"version":3,"sources":["../src/api.ts","../src/client.ts","../src/hooks/useHuskel.ts","../src/hooks/useSearch.ts","../src/components/HuskelProvider.tsx","../src/hooks/useIngest.ts","../src/hooks/usePageIngest.ts","../src/components/SearchBar.tsx","../src/components/Sparkle.tsx"],"sourcesContent":["import { Product, SearchResponse, IngestResponse, HuskelError } from './types';\r\n\r\nconst MAX_RETRIES = 3;\r\nconst RETRY_DELAYS = [500, 1000, 2000]; // ms\r\n\r\nfunction log(level: 'info' | 'warn' | 'error', msg: string, data?: unknown) {\r\n const prefix = '[Huskel]';\r\n if (level === 'error') console.error(prefix, msg, data ?? '');\r\n else if (level === 'warn') console.warn(prefix, msg, data ?? '');\r\n else console.log(prefix, msg, data ?? '');\r\n}\r\n\r\nasync function sleep(ms: number) {\r\n return new Promise(r => setTimeout(r, ms));\r\n}\r\n\r\nexport class HuskelAPI {\r\n constructor(\r\n private apiUrl: string,\r\n private siteId: string,\r\n private apiToken: string,\r\n private getShopperId?: () => string | undefined,\r\n private getSessionId?: () => string | undefined\r\n ) {}\r\n\r\n private async post<T>(path: string, body: unknown, attempt = 0): Promise<T> {\r\n const url = `${this.apiUrl}${path}`;\r\n\r\n try {\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n 'X-Huskel-Token': this.apiToken,\r\n 'X-Huskel-Site': this.siteId,\r\n };\r\n\r\n const shopperId = this.getShopperId?.();\r\n if (shopperId) {\r\n headers['X-Huskel-Shopper-Id'] = shopperId;\r\n }\r\n\r\n const sessionId = this.getSessionId?.();\r\n if (sessionId) {\r\n headers['X-Huskel-Session-Id'] = sessionId;\r\n }\r\n\r\n const res = await fetch(url, {\r\n method: 'POST',\r\n headers,\r\n body: JSON.stringify(body),\r\n });\r\n\r\n if (!res.ok) {\r\n const text = await res.text();\r\n const err: HuskelError = { status: res.status, message: text };\r\n\r\n // Don't retry 4xx — developer errors\r\n if (res.status >= 400 && res.status < 500) {\r\n log('error', `${path} failed [${res.status}]`, text);\r\n throw err;\r\n }\r\n\r\n // Retry 5xx\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n\r\n log('error', `${path} failed after ${MAX_RETRIES} attempts`, err);\r\n throw err;\r\n }\r\n\r\n return res.json();\r\n } catch (e) {\r\n // Network error (offline, DNS, etc.)\r\n if ((e as HuskelError).status === undefined) {\r\n if (attempt < MAX_RETRIES - 1) {\r\n log('warn', `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);\r\n await sleep(RETRY_DELAYS[attempt]);\r\n return this.post(path, body, attempt + 1);\r\n }\r\n log('error', `${path} unreachable after ${MAX_RETRIES} attempts`);\r\n }\r\n throw e;\r\n }\r\n }\r\n\r\n async ingest(product: Product): Promise<IngestResponse> {\r\n log('info', 'ingesting product', product.name);\r\n return this.post('/ingest', { siteId: this.siteId, product });\r\n }\r\n\r\n async ingestBatch(products: Product[]): Promise<IngestResponse> {\r\n log('info', `ingesting batch of ${products.length} products`);\r\n return this.post('/ingest/batch', { siteId: this.siteId, products });\r\n }\r\n\r\n async search(query: string, limit = 10): Promise<SearchResponse> {\r\n log('info', 'search query', query);\r\n return this.post('/search', { query, siteId: this.siteId, limit });\r\n }\r\n}\r\n","import { HuskelConfig, Product, RawProductInput } from './types';\r\nimport { HuskelAPI } from './api';\r\n\r\nfunction getEnvVar(key: string): string | undefined {\r\n if (typeof globalThis !== 'undefined') {\r\n const g = globalThis as any;\r\n if (g.process && g.process.env) {\r\n return g.process.env[key];\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\nfunction mapRawProduct(input: RawProductInput): Product | null {\r\n const name = input.name || input.title || input.productName || '';\r\n \r\n let price = '';\r\n let priceNumeric: number | undefined = undefined;\r\n\r\n if (input.price !== undefined) {\r\n if (typeof input.price === 'number') {\r\n priceNumeric = input.price;\r\n price = String(input.price);\r\n } else {\r\n price = input.price;\r\n const num = parseFloat(input.price.replace(/[^0-9.]/g, ''));\r\n priceNumeric = isNaN(num) ? undefined : num;\r\n }\r\n }\r\n if (input.priceNumeric !== undefined) {\r\n priceNumeric = input.priceNumeric;\r\n }\r\n\r\n let url = input.url || '';\r\n if (!url && typeof window !== 'undefined') {\r\n url = window.location.href;\r\n }\r\n\r\n let slug = input.slug || input.id || input.productId || '';\r\n if (!slug && url) {\r\n slug = url.split('/').filter(Boolean).pop() || '';\r\n }\r\n if (!slug && name) {\r\n slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');\r\n }\r\n\r\n let images: string[] = [];\r\n if (input.images) {\r\n images = input.images;\r\n } else if (input.image) {\r\n images = [input.image];\r\n } else if (input.thumbnail) {\r\n images = [input.thumbnail];\r\n }\r\n\r\n if (!name) {\r\n console.warn('[Huskel] Validation warning: Product name/title is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!price) {\r\n console.warn('[Huskel] Validation warning: Product price is missing. Skipping:', input);\r\n return null;\r\n }\r\n if (!url) {\r\n console.warn('[Huskel] Validation warning: Product URL is missing. Skipping:', input);\r\n return null;\r\n }\r\n\r\n return {\r\n name,\r\n price,\r\n url,\r\n brand: input.brand,\r\n description: input.description,\r\n originalPrice: input.originalPrice,\r\n discount: input.discount,\r\n currency: input.currency ?? 'KES',\r\n stock: input.stock,\r\n availability: input.availability,\r\n rating: input.rating,\r\n reviewCount: input.reviewCount,\r\n category: input.category,\r\n subCategory: input.subCategory,\r\n tags: input.tags,\r\n images: images.length > 0 ? images : undefined,\r\n specs: input.specs,\r\n priceNumeric,\r\n slug,\r\n };\r\n}\r\n\r\nfunction generateUUID(): string {\r\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n}\r\n\r\nexport class HuskelClient {\r\n readonly api: HuskelAPI;\r\n private ingestQueue: Product[] = [];\r\n private ingestTimer: ReturnType<typeof setTimeout> | null = null;\r\n private ingestedUrls = new Set<string>();\r\n private onlineHandler: (() => void) | null = null;\r\n private shopperId?: string;\r\n private sessionId: string = '';\r\n\r\n constructor(config: HuskelConfig) {\r\n const siteId = config.siteId || getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID') || '';\r\n const apiUrl = config.apiUrl || getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL') || '';\r\n const apiToken = config.apiToken || getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN') || '';\r\n\r\n // Runtime validation — fail loudly so misconfiguration is never silent\r\n if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId=\"...\"> or NEXT_PUBLIC_HUSKEL_SITE_ID.');\r\n if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl=\"...\"> or NEXT_PUBLIC_HUSKEL_API_URL.');\r\n if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken=\"...\"> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');\r\n\r\n this.shopperId = config.shopperId;\r\n this.initSession();\r\n\r\n this.api = new HuskelAPI(\r\n apiUrl,\r\n siteId,\r\n apiToken,\r\n () => this.shopperId,\r\n () => this.sessionId\r\n );\r\n instance = this;\r\n\r\n if (typeof window !== 'undefined') {\r\n this.onlineHandler = () => {\r\n console.log('[Huskel] Connectivity restored, flushing queued ingestions.');\r\n this.flushQueue();\r\n };\r\n window.addEventListener('online', this.onlineHandler);\r\n }\r\n }\r\n\r\n setShopperId(id: string | undefined) {\r\n this.shopperId = id;\r\n }\r\n\r\n getShopperId(): string | undefined {\r\n return this.shopperId;\r\n }\r\n\r\n getSessionId(): string {\r\n return this.sessionId;\r\n }\r\n\r\n private initSession() {\r\n if (typeof window !== 'undefined' && window.sessionStorage) {\r\n try {\r\n let sid = window.sessionStorage.getItem('huskel_session_id');\r\n if (!sid) {\r\n sid = generateUUID();\r\n window.sessionStorage.setItem('huskel_session_id', sid);\r\n }\r\n this.sessionId = sid;\r\n return;\r\n } catch (e) {\r\n // Fallback if sessionStorage is disabled or private mode\r\n }\r\n }\r\n this.sessionId = generateUUID();\r\n }\r\n\r\n destroy() {\r\n if (typeof window !== 'undefined' && this.onlineHandler) {\r\n window.removeEventListener('online', this.onlineHandler);\r\n this.onlineHandler = null;\r\n }\r\n if (this.ingestTimer) {\r\n clearTimeout(this.ingestTimer);\r\n this.ingestTimer = null;\r\n }\r\n if (instance === this) instance = null;\r\n }\r\n\r\n async queueIngest(rawProduct: RawProductInput): Promise<void> {\r\n const product = mapRawProduct(rawProduct);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n\r\n this.ingestQueue.push(product);\r\n this.scheduleFlush();\r\n }\r\n\r\n async queueIngestBatch(rawProducts: RawProductInput[]): Promise<void> {\r\n rawProducts.forEach(p => {\r\n const product = mapRawProduct(p);\r\n if (!product) return;\r\n\r\n if (this.ingestedUrls.has(product.url)) {\r\n return;\r\n }\r\n this.ingestedUrls.add(product.url);\r\n this.ingestQueue.push(product);\r\n });\r\n\r\n if (this.ingestQueue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n private scheduleFlush() {\r\n if (this.ingestTimer) return;\r\n this.ingestTimer = setTimeout(() => {\r\n this.flushQueue();\r\n }, 300);\r\n }\r\n\r\n private async flushQueue() {\r\n this.ingestTimer = null;\r\n if (this.ingestQueue.length === 0) return;\r\n\r\n if (typeof navigator !== 'undefined' && !navigator.onLine) {\r\n console.warn('[Huskel] Browser offline. Postponing ingestion.');\r\n return;\r\n }\r\n\r\n const batch = [...this.ingestQueue];\r\n this.ingestQueue = [];\r\n\r\n try {\r\n await this.api.ingestBatch(batch);\r\n } catch (e: any) {\r\n if (e.status && e.status >= 400 && e.status < 500) {\r\n console.error('[Huskel] Ingestion discarded due to client error:', e.message);\r\n return;\r\n }\r\n\r\n // Re-queue and schedule another flush so items are not stuck forever\r\n console.warn('[Huskel] Ingestion failed. Re-queuing to retry.', e);\r\n this.ingestQueue = [...batch, ...this.ingestQueue];\r\n this.scheduleFlush();\r\n }\r\n }\r\n}\r\n\r\nlet instance: HuskelClient | null = null;\r\n\r\nexport function initHuskel(config: HuskelConfig): HuskelClient {\r\n instance = new HuskelClient(config);\r\n return instance;\r\n}\r\n\r\nexport function getHuskelClient(): HuskelClient {\r\n if (!instance) {\r\n const siteId = getEnvVar('NEXT_PUBLIC_HUSKEL_SITE_ID');\r\n const apiUrl = getEnvVar('NEXT_PUBLIC_HUSKEL_API_URL');\r\n const apiToken = getEnvVar('NEXT_PUBLIC_HUSKEL_API_TOKEN');\r\n\r\n if (siteId && apiUrl && apiToken) {\r\n instance = new HuskelClient({ siteId, apiUrl, apiToken });\r\n } else {\r\n throw new Error('[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.');\r\n }\r\n }\r\n return instance;\r\n}\r\n","import { useRef } from 'react';\r\nimport { HuskelConfig } from '../types';\r\nimport { HuskelClient, initHuskel } from '../client';\r\n\r\n/**\r\n * @deprecated Use <HuskelProvider> instead to avoid SSR issues.\r\n */\r\nexport function useHuskel(config: HuskelConfig): HuskelClient {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n console.warn('[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.');\r\n clientRef.current = initHuskel(config);\r\n }\r\n\r\n return clientRef.current;\r\n}\r\n","import { useState, useCallback, useRef } from 'react';\r\nimport { SearchResult } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseSearchReturn {\r\n results: SearchResult[];\r\n loading: boolean;\r\n error: string | null;\r\n search: (query: string, limit?: number) => Promise<void>;\r\n clear: () => void;\r\n}\r\n\r\nexport function useSearch(): UseSearchReturn {\r\n const client = useHuskelContext();\r\n const [results, setResults] = useState<SearchResult[]>([]);\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const abortRef = useRef<AbortController | null>(null);\r\n\r\n const search = useCallback(async (query: string, limit = 10) => {\r\n if (!query.trim()) { setResults([]); return; }\r\n abortRef.current?.abort();\r\n abortRef.current = new AbortController();\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n const res = await client.api.search(query, limit);\r\n setResults(res.results ?? []);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Search failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const clear = useCallback(() => { setResults([]); setError(null); }, []);\r\n\r\n return { results, loading, error, search, clear };\r\n}\r\n","'use client';\r\n\r\nimport React, { createContext, useContext, useEffect, useRef } from 'react';\r\nimport { HuskelClient, getHuskelClient } from '../client';\r\nimport { HuskelConfig } from '../types';\r\n\r\nexport const HuskelContext = createContext<HuskelClient | null>(null);\r\n\r\ninterface HuskelProviderProps extends HuskelConfig {\r\n children: React.ReactNode;\r\n}\r\n\r\nexport function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps) {\r\n const clientRef = useRef<HuskelClient | null>(null);\r\n\r\n if (!clientRef.current) {\r\n clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });\r\n }\r\n\r\n // Update shopperId dynamically when it changes (e.g., shopper logs in/out)\r\n useEffect(() => {\r\n clientRef.current?.setShopperId(shopperId);\r\n }, [shopperId]);\r\n\r\n // Clean up the online listener and timers when the provider unmounts\r\n // (prevents leaks during hot module reload and React StrictMode double-mount)\r\n useEffect(() => {\r\n return () => {\r\n clientRef.current?.destroy();\r\n };\r\n }, []);\r\n\r\n return (\r\n <HuskelContext.Provider value={clientRef.current}>\r\n {children}\r\n </HuskelContext.Provider>\r\n );\r\n}\r\n\r\nexport function useHuskelContext(): HuskelClient {\r\n const context = useContext(HuskelContext);\r\n if (!context) {\r\n return getHuskelClient();\r\n }\r\n return context;\r\n}\r\n\r\n","import { useCallback, useState } from 'react';\r\nimport { RawProductInput } from '../types';\r\nimport { useHuskelContext } from '../components/HuskelProvider';\r\n\r\ninterface UseIngestReturn {\r\n ingest: (product: RawProductInput) => Promise<void>;\r\n ingestBatch: (products: RawProductInput[]) => Promise<void>;\r\n loading: boolean;\r\n error: string | null;\r\n}\r\n\r\nexport function useIngest(): UseIngestReturn {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const ingest = useCallback(async (product: RawProductInput) => {\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngest(product);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n const ingestBatch = useCallback(async (products: RawProductInput[]) => {\r\n if (!products.length) return;\r\n setLoading(true);\r\n setError(null);\r\n try {\r\n await client.queueIngestBatch(products);\r\n } catch (e: unknown) {\r\n setError((e as Error).message ?? 'Batch ingest failed');\r\n } finally {\r\n setLoading(false);\r\n }\r\n }, [client]);\r\n\r\n return { ingest, ingestBatch, loading, error };\r\n}\r\n","import { useEffect, useRef } from 'react';\nimport { RawProductInput } from '../types';\nimport { getHuskelClient } from '../client';\n\n/**\n * usePageIngest — drop this into any product page component.\n * The moment a customer's browser renders the page, the product is\n * automatically captured and queued for ingestion into the vector index.\n *\n * No configuration needed beyond <HuskelProvider> in your layout.\n *\n * @example\n * // Product detail page — Next.js or React\n * export function ProductPage({ product }) {\n * usePageIngest({\n * name: product.title,\n * price: product.price,\n * url: window.location.href,\n * images: [product.thumbnail],\n * category: product.category,\n * });\n * return <div>...</div>;\n * }\n */\nexport function usePageIngest(product: RawProductInput | null | undefined): void {\n // Use url as the stable key — avoids re-ingesting on unrelated re-renders\n const ingestedRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (!product) return;\n\n // Resolve URL — falls back to window.location if not provided\n const url =\n product.url ||\n (typeof window !== 'undefined' ? window.location.href : '');\n\n // Guard: only ingest once per URL per component lifecycle\n if (ingestedRef.current === url) return;\n ingestedRef.current = url;\n\n try {\n getHuskelClient().queueIngest({ ...product, url });\n } catch {\n // Client not initialised — silently skip\n }\n }, [product?.url ?? product?.name]);\n}\n","import React, { useState, useEffect, useRef } from 'react';\r\nimport { useSearch } from '../hooks/useSearch';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SearchBarProps {\r\n placeholder?: string;\r\n limit?: number;\r\n debounceMs?: number;\r\n onSelect?: (result: SearchResult) => void;\r\n className?: string;\r\n inputClassName?: string;\r\n dropdownClassName?: string;\r\n renderResult?: (result: SearchResult) => React.ReactNode;\r\n}\r\n\r\nconst S = `\r\n .hsk-wrap{position:relative;width:100%;font-family:inherit}\r\n .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}\r\n .hsk-input:focus{border-color:#f47c3c}\r\n .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}\r\n .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}\r\n .hsk-item:hover{background:#faf5f1}\r\n .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}\r\n .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\r\n .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}\r\n .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}\r\n`;\r\n\r\nexport function SearchBar({\r\n placeholder = 'Search for what you want — how you want',\r\n limit = 10,\r\n debounceMs = 300,\r\n onSelect,\r\n className,\r\n inputClassName,\r\n dropdownClassName,\r\n renderResult,\r\n}: SearchBarProps) {\r\n const [query, setQuery] = useState('');\r\n const [open, setOpen] = useState(false);\r\n const { results, loading, search, clear } = useSearch();\r\n const timer = useRef<ReturnType<typeof setTimeout>>();\r\n const wrap = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n clearTimeout(timer.current);\r\n if (!query.trim()) { clear(); setOpen(false); return; }\r\n timer.current = setTimeout(() => { search(query, limit); setOpen(true); }, debounceMs);\r\n return () => clearTimeout(timer.current);\r\n }, [query, search, clear, limit, debounceMs]);\r\n\r\n useEffect(() => {\r\n const handler = (e: MouseEvent) => {\r\n if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);\r\n };\r\n document.addEventListener('mousedown', handler);\r\n return () => document.removeEventListener('mousedown', handler);\r\n }, []);\r\n\r\n const handleSelect = (r: SearchResult) => {\r\n setOpen(false);\r\n setQuery(r.product.name);\r\n onSelect?.(r);\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <div className={`hsk-wrap ${className ?? ''}`} ref={wrap}>\r\n <input\r\n className={`hsk-input ${inputClassName ?? ''}`}\r\n type=\"text\"\r\n value={query}\r\n placeholder={placeholder}\r\n onChange={e => setQuery(e.target.value)}\r\n onFocus={() => results.length && setOpen(true)}\r\n />\r\n {open && (\r\n <div className={`hsk-drop ${dropdownClassName ?? ''}`}>\r\n {loading && <div className=\"hsk-msg\">Searching…</div>}\r\n {!loading && results.length === 0 && <div className=\"hsk-msg\">No results for \"{query}\"</div>}\r\n {results.map(r =>\r\n renderResult ? (\r\n <div key={r.id} onClick={() => handleSelect(r)}>{renderResult(r)}</div>\r\n ) : (\r\n <div key={r.id} className=\"hsk-item\" onClick={() => handleSelect(r)}>\r\n {r.product.images?.[0] && <img src={r.product.images[0]} alt={r.product.name} />}\r\n <div>\r\n <div className=\"hsk-item-name\">{r.product.name}</div>\r\n <div className=\"hsk-item-price\">{r.product.currency ?? 'KES'} {r.product.price}</div>\r\n </div>\r\n </div>\r\n )\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </>\r\n );\r\n}\r\n","import React, { useState } from 'react';\r\nimport { useHuskelContext } from './HuskelProvider';\r\nimport { SearchResult } from '../types';\r\n\r\ninterface SparkleProps {\r\n productName: string;\r\n limit?: number;\r\n onResult?: (results: SearchResult[]) => void;\r\n className?: string;\r\n}\r\n\r\nconst S = `\r\n .hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}\r\n .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}\r\n .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}\r\n`;\r\n\r\nexport function Sparkle({ productName, limit = 5, onResult, className }: SparkleProps) {\r\n const client = useHuskelContext();\r\n const [loading, setLoading] = useState(false);\r\n\r\n const handleClick = async () => {\r\n setLoading(true);\r\n try {\r\n const res = await client.api.search(productName, limit);\r\n onResult?.(res.results);\r\n } catch (e) {\r\n console.error('[Huskel Sparkle]', e);\r\n } finally {\r\n setLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <>\r\n <style>{S}</style>\r\n <button className={`hsk-sparkle ${className ?? ''}`} onClick={handleClick} disabled={loading}>\r\n ✦ {loading ? 'Finding…' : 'Similar'}\r\n </button>\r\n </>\r\n );\r\n}\r\n"],"mappings":"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","names":["useRef","useRef","useRef","useRef","useCallback","useState","useState","useCallback","useEffect","useRef","useRef","useEffect","useState","useEffect","useRef","jsx","useState","useRef","useEffect","useState","Fragment","jsx","jsxs","S","useState"]}
|