@huskel/sdk 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -114
- package/dist/index.d.mts +66 -43
- package/dist/index.d.ts +66 -43
- package/dist/index.js +354 -256
- package/dist/index.mjs +343 -250
- package/package.json +15 -9
package/dist/index.js
CHANGED
|
@@ -2,21 +2,7 @@
|
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
-
var __spreadValues = (a, b) => {
|
|
10
|
-
for (var prop in b || (b = {}))
|
|
11
|
-
if (__hasOwnProp.call(b, prop))
|
|
12
|
-
__defNormalProp(a, prop, b[prop]);
|
|
13
|
-
if (__getOwnPropSymbols)
|
|
14
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
-
if (__propIsEnum.call(b, prop))
|
|
16
|
-
__defNormalProp(a, prop, b[prop]);
|
|
17
|
-
}
|
|
18
|
-
return a;
|
|
19
|
-
};
|
|
20
6
|
var __export = (target, all) => {
|
|
21
7
|
for (var name in all)
|
|
22
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -36,175 +22,253 @@ var index_exports = {};
|
|
|
36
22
|
__export(index_exports, {
|
|
37
23
|
HuskelAPI: () => HuskelAPI,
|
|
38
24
|
HuskelClient: () => HuskelClient,
|
|
25
|
+
HuskelProvider: () => HuskelProvider,
|
|
39
26
|
SearchBar: () => SearchBar,
|
|
40
27
|
Sparkle: () => Sparkle,
|
|
41
28
|
getHuskelClient: () => getHuskelClient,
|
|
42
29
|
initHuskel: () => initHuskel,
|
|
43
30
|
useHuskel: () => useHuskel,
|
|
31
|
+
useIngest: () => useIngest,
|
|
44
32
|
useSearch: () => useSearch
|
|
45
33
|
});
|
|
46
34
|
module.exports = __toCommonJS(index_exports);
|
|
47
35
|
|
|
48
36
|
// src/api.ts
|
|
37
|
+
var MAX_RETRIES = 3;
|
|
38
|
+
var RETRY_DELAYS = [500, 1e3, 2e3];
|
|
39
|
+
function log(level, msg, data) {
|
|
40
|
+
const prefix = "[Huskel]";
|
|
41
|
+
if (level === "error") console.error(prefix, msg, data != null ? data : "");
|
|
42
|
+
else if (level === "warn") console.warn(prefix, msg, data != null ? data : "");
|
|
43
|
+
else console.log(prefix, msg, data != null ? data : "");
|
|
44
|
+
}
|
|
45
|
+
async function sleep(ms) {
|
|
46
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
47
|
+
}
|
|
49
48
|
var HuskelAPI = class {
|
|
50
|
-
constructor(apiUrl, siteId) {
|
|
49
|
+
constructor(apiUrl, siteId, apiToken) {
|
|
51
50
|
this.apiUrl = apiUrl;
|
|
52
51
|
this.siteId = siteId;
|
|
52
|
+
this.apiToken = apiToken;
|
|
53
53
|
}
|
|
54
|
-
async post(path, body) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
async post(path, body, attempt = 0) {
|
|
55
|
+
const url = `${this.apiUrl}${path}`;
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"X-Huskel-Token": this.apiToken,
|
|
62
|
+
"X-Huskel-Site": this.siteId
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body)
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
const err = { status: res.status, message: text };
|
|
69
|
+
if (res.status >= 400 && res.status < 500) {
|
|
70
|
+
log("error", `${path} failed [${res.status}]`, text);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
74
|
+
log("warn", `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
75
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
76
|
+
return this.post(path, body, attempt + 1);
|
|
77
|
+
}
|
|
78
|
+
log("error", `${path} failed after ${MAX_RETRIES} attempts`, err);
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
return res.json();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e.status === void 0) {
|
|
84
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
85
|
+
log("warn", `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
86
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
87
|
+
return this.post(path, body, attempt + 1);
|
|
88
|
+
}
|
|
89
|
+
log("error", `${path} unreachable after ${MAX_RETRIES} attempts`);
|
|
90
|
+
}
|
|
91
|
+
throw e;
|
|
63
92
|
}
|
|
64
|
-
return res.json();
|
|
65
93
|
}
|
|
66
94
|
async ingest(product) {
|
|
95
|
+
log("info", "ingesting product", product.name);
|
|
67
96
|
return this.post("/ingest", { siteId: this.siteId, product });
|
|
68
97
|
}
|
|
69
98
|
async ingestBatch(products) {
|
|
99
|
+
log("info", `ingesting batch of ${products.length} products`);
|
|
70
100
|
return this.post("/ingest/batch", { siteId: this.siteId, products });
|
|
71
101
|
}
|
|
72
102
|
async search(query, limit = 10) {
|
|
103
|
+
log("info", "search query", query);
|
|
73
104
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
74
105
|
}
|
|
75
|
-
async pushConfig(config) {
|
|
76
|
-
return this.post("/sites/config", __spreadValues({ siteId: this.siteId }, config));
|
|
77
|
-
}
|
|
78
106
|
};
|
|
79
107
|
|
|
80
|
-
// src/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!selector) return "";
|
|
90
|
-
const found = el.querySelector(selector);
|
|
91
|
-
return (_b = (_a = found == null ? void 0 : found.getAttribute(attr)) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
92
|
-
}
|
|
93
|
-
function getAll(el, selector) {
|
|
94
|
-
if (!selector) return [];
|
|
95
|
-
return Array.from(el.querySelectorAll(selector)).map((n) => {
|
|
96
|
-
var _a;
|
|
97
|
-
return n.getAttribute("src") || n.getAttribute("href") || ((_a = n.textContent) == null ? void 0 : _a.trim()) || "";
|
|
98
|
-
}).filter(Boolean);
|
|
99
|
-
}
|
|
100
|
-
function parsePrice(raw) {
|
|
101
|
-
const num = parseFloat(raw.replace(/[^0-9.]/g, ""));
|
|
102
|
-
return isNaN(num) ? void 0 : num;
|
|
103
|
-
}
|
|
104
|
-
function extractProducts(config) {
|
|
105
|
-
const containers = document.querySelectorAll(config.selectorContainer);
|
|
106
|
-
if (!containers.length) return [];
|
|
107
|
-
const products = [];
|
|
108
|
-
containers.forEach((el) => {
|
|
109
|
-
var _a;
|
|
110
|
-
const name = getText(el, config.selectorName);
|
|
111
|
-
const price = getText(el, config.selectorPrice);
|
|
112
|
-
const rawUrl = getAttr(el, config.selectorUrl, "href") || el.href || window.location.href;
|
|
113
|
-
const url = rawUrl.startsWith("http") ? rawUrl : `${window.location.origin}${rawUrl}`;
|
|
114
|
-
if (!name || !price || !url) return;
|
|
115
|
-
const product = {
|
|
116
|
-
name,
|
|
117
|
-
price,
|
|
118
|
-
url,
|
|
119
|
-
brand: getText(el, config.selectorBrand) || void 0,
|
|
120
|
-
description: getText(el, config.selectorDescription) || void 0,
|
|
121
|
-
originalPrice: getText(el, config.selectorOriginalPrice) || void 0,
|
|
122
|
-
discount: getText(el, config.selectorDiscount) || void 0,
|
|
123
|
-
currency: (_a = config.currency) != null ? _a : "KES",
|
|
124
|
-
availability: getText(el, config.selectorAvailability) || void 0,
|
|
125
|
-
rating: getText(el, config.selectorRating) || void 0,
|
|
126
|
-
category: getText(el, config.selectorCategory) || void 0,
|
|
127
|
-
images: config.selectorImage ? getAll(el, config.selectorImage) : void 0,
|
|
128
|
-
priceNumeric: parsePrice(price),
|
|
129
|
-
slug: url.split("/").filter(Boolean).pop()
|
|
130
|
-
};
|
|
131
|
-
products.push(product);
|
|
132
|
-
});
|
|
133
|
-
return products;
|
|
108
|
+
// src/client.ts
|
|
109
|
+
function getEnvVar(key) {
|
|
110
|
+
if (typeof globalThis !== "undefined") {
|
|
111
|
+
const g = globalThis;
|
|
112
|
+
if (g.process && g.process.env) {
|
|
113
|
+
return g.process.env[key];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return void 0;
|
|
134
117
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
118
|
+
function mapRawProduct(input) {
|
|
119
|
+
var _a;
|
|
120
|
+
const name = input.name || input.title || input.productName || "";
|
|
121
|
+
let price = "";
|
|
122
|
+
let priceNumeric = void 0;
|
|
123
|
+
if (input.price !== void 0) {
|
|
124
|
+
if (typeof input.price === "number") {
|
|
125
|
+
priceNumeric = input.price;
|
|
126
|
+
price = String(input.price);
|
|
127
|
+
} else {
|
|
128
|
+
price = input.price;
|
|
129
|
+
const num = parseFloat(input.price.replace(/[^0-9.]/g, ""));
|
|
130
|
+
priceNumeric = isNaN(num) ? void 0 : num;
|
|
131
|
+
}
|
|
143
132
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const wrap = (original) => function(...args) {
|
|
147
|
-
original.apply(this, args);
|
|
148
|
-
notify();
|
|
149
|
-
};
|
|
150
|
-
history.pushState = wrap(history.pushState);
|
|
151
|
-
history.replaceState = wrap(history.replaceState);
|
|
133
|
+
if (input.priceNumeric !== void 0) {
|
|
134
|
+
priceNumeric = input.priceNumeric;
|
|
152
135
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.current = next;
|
|
157
|
-
this.callbacks.forEach((cb) => cb(next));
|
|
158
|
-
}
|
|
136
|
+
let url = input.url || "";
|
|
137
|
+
if (!url && typeof window !== "undefined") {
|
|
138
|
+
url = window.location.href;
|
|
159
139
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.callbacks = this.callbacks.filter((fn) => fn !== cb);
|
|
164
|
-
};
|
|
140
|
+
let slug = input.slug || input.id || input.productId || "";
|
|
141
|
+
if (!slug && url) {
|
|
142
|
+
slug = url.split("/").filter(Boolean).pop() || "";
|
|
165
143
|
}
|
|
166
|
-
|
|
167
|
-
|
|
144
|
+
if (!slug && name) {
|
|
145
|
+
slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
168
146
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
let images = [];
|
|
148
|
+
if (input.images) {
|
|
149
|
+
images = input.images;
|
|
150
|
+
} else if (input.image) {
|
|
151
|
+
images = [input.image];
|
|
152
|
+
} else if (input.thumbnail) {
|
|
153
|
+
images = [input.thumbnail];
|
|
154
|
+
}
|
|
155
|
+
if (!name) {
|
|
156
|
+
console.warn("[Huskel] Validation warning: Product name/title is missing. Skipping:", input);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!price) {
|
|
160
|
+
console.warn("[Huskel] Validation warning: Product price is missing. Skipping:", input);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!url) {
|
|
164
|
+
console.warn("[Huskel] Validation warning: Product URL is missing. Skipping:", input);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
price,
|
|
170
|
+
url,
|
|
171
|
+
brand: input.brand,
|
|
172
|
+
description: input.description,
|
|
173
|
+
originalPrice: input.originalPrice,
|
|
174
|
+
discount: input.discount,
|
|
175
|
+
currency: (_a = input.currency) != null ? _a : "KES",
|
|
176
|
+
stock: input.stock,
|
|
177
|
+
availability: input.availability,
|
|
178
|
+
rating: input.rating,
|
|
179
|
+
reviewCount: input.reviewCount,
|
|
180
|
+
category: input.category,
|
|
181
|
+
subCategory: input.subCategory,
|
|
182
|
+
tags: input.tags,
|
|
183
|
+
images: images.length > 0 ? images : void 0,
|
|
184
|
+
specs: input.specs,
|
|
185
|
+
priceNumeric,
|
|
186
|
+
slug
|
|
187
|
+
};
|
|
188
|
+
}
|
|
172
189
|
var HuskelClient = class {
|
|
173
190
|
constructor(config) {
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
191
|
+
this.ingestQueue = [];
|
|
192
|
+
this.ingestTimer = null;
|
|
193
|
+
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
194
|
+
this.onlineHandler = null;
|
|
195
|
+
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
|
|
196
|
+
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
|
|
197
|
+
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
|
|
198
|
+
if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
|
|
199
|
+
if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
|
|
200
|
+
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
201
|
+
this.api = new HuskelAPI(apiUrl, siteId, apiToken);
|
|
202
|
+
instance = this;
|
|
203
|
+
if (typeof window !== "undefined") {
|
|
204
|
+
this.onlineHandler = () => {
|
|
205
|
+
console.log("[Huskel] Connectivity restored, flushing queued ingestions.");
|
|
206
|
+
this.flushQueue();
|
|
207
|
+
};
|
|
208
|
+
window.addEventListener("online", this.onlineHandler);
|
|
209
|
+
}
|
|
176
210
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
211
|
+
destroy() {
|
|
212
|
+
if (typeof window !== "undefined" && this.onlineHandler) {
|
|
213
|
+
window.removeEventListener("online", this.onlineHandler);
|
|
214
|
+
this.onlineHandler = null;
|
|
215
|
+
}
|
|
216
|
+
if (this.ingestTimer) {
|
|
217
|
+
clearTimeout(this.ingestTimer);
|
|
218
|
+
this.ingestTimer = null;
|
|
219
|
+
}
|
|
220
|
+
if (instance === this) instance = null;
|
|
180
221
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
async queueIngest(rawProduct) {
|
|
223
|
+
const product = mapRawProduct(rawProduct);
|
|
224
|
+
if (!product) return;
|
|
225
|
+
if (this.ingestedUrls.has(product.url)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.ingestedUrls.add(product.url);
|
|
229
|
+
this.ingestQueue.push(product);
|
|
230
|
+
this.scheduleFlush();
|
|
189
231
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.
|
|
198
|
-
|
|
199
|
-
}, (_a = this.config.debounceMs) != null ? _a : 600);
|
|
232
|
+
async queueIngestBatch(rawProducts) {
|
|
233
|
+
rawProducts.forEach((p) => {
|
|
234
|
+
const product = mapRawProduct(p);
|
|
235
|
+
if (!product) return;
|
|
236
|
+
if (this.ingestedUrls.has(product.url)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.ingestedUrls.add(product.url);
|
|
240
|
+
this.ingestQueue.push(product);
|
|
200
241
|
});
|
|
201
|
-
this.
|
|
242
|
+
if (this.ingestQueue.length > 0) {
|
|
243
|
+
this.scheduleFlush();
|
|
244
|
+
}
|
|
202
245
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
scheduleFlush() {
|
|
247
|
+
if (this.ingestTimer) return;
|
|
248
|
+
this.ingestTimer = setTimeout(() => {
|
|
249
|
+
this.flushQueue();
|
|
250
|
+
}, 300);
|
|
251
|
+
}
|
|
252
|
+
async flushQueue() {
|
|
253
|
+
this.ingestTimer = null;
|
|
254
|
+
if (this.ingestQueue.length === 0) return;
|
|
255
|
+
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
256
|
+
console.warn("[Huskel] Browser offline. Postponing ingestion.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const batch = [...this.ingestQueue];
|
|
260
|
+
this.ingestQueue = [];
|
|
261
|
+
try {
|
|
262
|
+
await this.api.ingestBatch(batch);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
if (e.status && e.status >= 400 && e.status < 500) {
|
|
265
|
+
console.error("[Huskel] Ingestion discarded due to client error:", e.message);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
console.warn("[Huskel] Ingestion failed. Re-queuing to retry.", e);
|
|
269
|
+
this.ingestQueue = [...batch, ...this.ingestQueue];
|
|
270
|
+
this.scheduleFlush();
|
|
271
|
+
}
|
|
208
272
|
}
|
|
209
273
|
};
|
|
210
274
|
var instance = null;
|
|
@@ -213,19 +277,67 @@ function initHuskel(config) {
|
|
|
213
277
|
return instance;
|
|
214
278
|
}
|
|
215
279
|
function getHuskelClient() {
|
|
216
|
-
if (!instance)
|
|
280
|
+
if (!instance) {
|
|
281
|
+
const siteId = getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID");
|
|
282
|
+
const apiUrl = getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL");
|
|
283
|
+
const apiToken = getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN");
|
|
284
|
+
if (siteId && apiUrl && apiToken) {
|
|
285
|
+
instance = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
286
|
+
} else {
|
|
287
|
+
throw new Error("[Huskel] Call initHuskel() or set NEXT_PUBLIC_HUSKEL_* environment variables before using the client.");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
217
290
|
return instance;
|
|
218
291
|
}
|
|
219
292
|
|
|
220
|
-
// src/hooks/
|
|
293
|
+
// src/hooks/useHuskel.ts
|
|
221
294
|
var import_react = require("react");
|
|
295
|
+
function useHuskel(config) {
|
|
296
|
+
const clientRef = (0, import_react.useRef)(null);
|
|
297
|
+
if (!clientRef.current) {
|
|
298
|
+
console.warn("[Huskel] useHuskel() is deprecated. Please wrap your application in <HuskelProvider> instead.");
|
|
299
|
+
clientRef.current = initHuskel(config);
|
|
300
|
+
}
|
|
301
|
+
return clientRef.current;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/hooks/useSearch.ts
|
|
305
|
+
var import_react3 = require("react");
|
|
306
|
+
|
|
307
|
+
// src/components/HuskelProvider.tsx
|
|
308
|
+
var import_react2 = require("react");
|
|
309
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
310
|
+
var HuskelContext = (0, import_react2.createContext)(null);
|
|
311
|
+
function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
|
|
312
|
+
const clientRef = (0, import_react2.useRef)(null);
|
|
313
|
+
if (!clientRef.current) {
|
|
314
|
+
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
315
|
+
}
|
|
316
|
+
(0, import_react2.useEffect)(() => {
|
|
317
|
+
return () => {
|
|
318
|
+
var _a;
|
|
319
|
+
(_a = clientRef.current) == null ? void 0 : _a.destroy();
|
|
320
|
+
};
|
|
321
|
+
}, []);
|
|
322
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HuskelContext.Provider, { value: clientRef.current, children });
|
|
323
|
+
}
|
|
324
|
+
function useHuskelContext() {
|
|
325
|
+
const context = (0, import_react2.useContext)(HuskelContext);
|
|
326
|
+
if (!context) {
|
|
327
|
+
return getHuskelClient();
|
|
328
|
+
}
|
|
329
|
+
return context;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/hooks/useSearch.ts
|
|
222
333
|
function useSearch() {
|
|
223
|
-
const
|
|
224
|
-
const [
|
|
225
|
-
const [
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
334
|
+
const client = useHuskelContext();
|
|
335
|
+
const [results, setResults] = (0, import_react3.useState)([]);
|
|
336
|
+
const [loading, setLoading] = (0, import_react3.useState)(false);
|
|
337
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
338
|
+
const abortRef = (0, import_react3.useRef)(null);
|
|
339
|
+
const search = (0, import_react3.useCallback)(async (query, limit = 10) => {
|
|
340
|
+
var _a, _b, _c;
|
|
229
341
|
if (!query.trim()) {
|
|
230
342
|
setResults([]);
|
|
231
343
|
return;
|
|
@@ -235,69 +347,69 @@ function useSearch() {
|
|
|
235
347
|
setLoading(true);
|
|
236
348
|
setError(null);
|
|
237
349
|
try {
|
|
238
|
-
const client = getHuskelClient();
|
|
239
350
|
const res = await client.api.search(query, limit);
|
|
240
351
|
setResults((_b = res.results) != null ? _b : []);
|
|
241
352
|
} catch (e) {
|
|
242
|
-
|
|
243
|
-
setError(e.message);
|
|
244
|
-
}
|
|
353
|
+
setError((_c = e.message) != null ? _c : "Search failed");
|
|
245
354
|
} finally {
|
|
246
355
|
setLoading(false);
|
|
247
356
|
}
|
|
248
|
-
}, []);
|
|
249
|
-
const clear = (0,
|
|
357
|
+
}, [client]);
|
|
358
|
+
const clear = (0, import_react3.useCallback)(() => {
|
|
250
359
|
setResults([]);
|
|
251
360
|
setError(null);
|
|
252
361
|
}, []);
|
|
253
362
|
return { results, loading, error, search, clear };
|
|
254
363
|
}
|
|
255
364
|
|
|
256
|
-
// src/hooks/
|
|
257
|
-
var
|
|
258
|
-
function
|
|
259
|
-
const
|
|
260
|
-
(0,
|
|
365
|
+
// src/hooks/useIngest.ts
|
|
366
|
+
var import_react4 = require("react");
|
|
367
|
+
function useIngest() {
|
|
368
|
+
const client = useHuskelContext();
|
|
369
|
+
const [loading, setLoading] = (0, import_react4.useState)(false);
|
|
370
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
371
|
+
const ingest = (0, import_react4.useCallback)(async (product) => {
|
|
261
372
|
var _a;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
client.
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
373
|
+
setLoading(true);
|
|
374
|
+
setError(null);
|
|
375
|
+
try {
|
|
376
|
+
await client.queueIngest(product);
|
|
377
|
+
} catch (e) {
|
|
378
|
+
setError((_a = e.message) != null ? _a : "Ingest failed");
|
|
379
|
+
} finally {
|
|
380
|
+
setLoading(false);
|
|
381
|
+
}
|
|
382
|
+
}, [client]);
|
|
383
|
+
const ingestBatch = (0, import_react4.useCallback)(async (products) => {
|
|
384
|
+
var _a;
|
|
385
|
+
if (!products.length) return;
|
|
386
|
+
setLoading(true);
|
|
387
|
+
setError(null);
|
|
388
|
+
try {
|
|
389
|
+
await client.queueIngestBatch(products);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
setError((_a = e.message) != null ? _a : "Batch ingest failed");
|
|
392
|
+
} finally {
|
|
393
|
+
setLoading(false);
|
|
394
|
+
}
|
|
395
|
+
}, [client]);
|
|
396
|
+
return { ingest, ingestBatch, loading, error };
|
|
270
397
|
}
|
|
271
398
|
|
|
272
399
|
// src/components/SearchBar.tsx
|
|
273
|
-
var
|
|
274
|
-
var
|
|
275
|
-
var
|
|
276
|
-
.
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
.
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
background: #fff; border: 1px solid #e2e2e2; border-radius: 8px;
|
|
287
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.10); z-index: 9999;
|
|
288
|
-
max-height: 360px; overflow-y: auto;
|
|
289
|
-
}
|
|
290
|
-
.huskel-search-item {
|
|
291
|
-
display: flex; align-items: center; gap: 12px;
|
|
292
|
-
padding: 10px 14px; cursor: pointer; transition: background 0.15s;
|
|
293
|
-
}
|
|
294
|
-
.huskel-search-item:hover { background: #faf5f1; }
|
|
295
|
-
.huskel-search-item img { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
|
|
296
|
-
.huskel-search-item-info { flex: 1; min-width: 0; }
|
|
297
|
-
.huskel-search-item-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
298
|
-
.huskel-search-item-price { font-size: 13px; color: #f47c3c; margin-top: 2px; }
|
|
299
|
-
.huskel-search-empty { padding: 16px; text-align: center; color: #888; font-size: 14px; }
|
|
300
|
-
.huskel-search-loading { padding: 16px; text-align: center; color: #aaa; font-size: 13px; }
|
|
400
|
+
var import_react5 = require("react");
|
|
401
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
402
|
+
var S = `
|
|
403
|
+
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
404
|
+
.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}
|
|
405
|
+
.hsk-input:focus{border-color:#f47c3c}
|
|
406
|
+
.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}
|
|
407
|
+
.hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
|
|
408
|
+
.hsk-item:hover{background:#faf5f1}
|
|
409
|
+
.hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
|
|
410
|
+
.hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
411
|
+
.hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
|
|
412
|
+
.hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
|
|
301
413
|
`;
|
|
302
414
|
function SearchBar({
|
|
303
415
|
placeholder = "Search for what you want \u2014 how you want",
|
|
@@ -305,31 +417,31 @@ function SearchBar({
|
|
|
305
417
|
debounceMs = 300,
|
|
306
418
|
onSelect,
|
|
307
419
|
className,
|
|
420
|
+
inputClassName,
|
|
421
|
+
dropdownClassName,
|
|
308
422
|
renderResult
|
|
309
423
|
}) {
|
|
310
|
-
const [query, setQuery] = (0,
|
|
311
|
-
const [open, setOpen] = (0,
|
|
424
|
+
const [query, setQuery] = (0, import_react5.useState)("");
|
|
425
|
+
const [open, setOpen] = (0, import_react5.useState)(false);
|
|
312
426
|
const { results, loading, search, clear } = useSearch();
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
(0,
|
|
316
|
-
clearTimeout(
|
|
427
|
+
const timer = (0, import_react5.useRef)();
|
|
428
|
+
const wrap = (0, import_react5.useRef)(null);
|
|
429
|
+
(0, import_react5.useEffect)(() => {
|
|
430
|
+
clearTimeout(timer.current);
|
|
317
431
|
if (!query.trim()) {
|
|
318
432
|
clear();
|
|
319
433
|
setOpen(false);
|
|
320
434
|
return;
|
|
321
435
|
}
|
|
322
|
-
|
|
436
|
+
timer.current = setTimeout(() => {
|
|
323
437
|
search(query, limit);
|
|
324
438
|
setOpen(true);
|
|
325
439
|
}, debounceMs);
|
|
326
|
-
return () => clearTimeout(
|
|
327
|
-
}, [query]);
|
|
328
|
-
(0,
|
|
440
|
+
return () => clearTimeout(timer.current);
|
|
441
|
+
}, [query, search, clear, limit, debounceMs]);
|
|
442
|
+
(0, import_react5.useEffect)(() => {
|
|
329
443
|
const handler = (e) => {
|
|
330
|
-
if (
|
|
331
|
-
setOpen(false);
|
|
332
|
-
}
|
|
444
|
+
if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
|
|
333
445
|
};
|
|
334
446
|
document.addEventListener("mousedown", handler);
|
|
335
447
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -339,13 +451,13 @@ function SearchBar({
|
|
|
339
451
|
setQuery(r.product.name);
|
|
340
452
|
onSelect == null ? void 0 : onSelect(r);
|
|
341
453
|
};
|
|
342
|
-
return /* @__PURE__ */ (0,
|
|
343
|
-
/* @__PURE__ */ (0,
|
|
344
|
-
/* @__PURE__ */ (0,
|
|
345
|
-
/* @__PURE__ */ (0,
|
|
454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: S }),
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
346
458
|
"input",
|
|
347
459
|
{
|
|
348
|
-
className:
|
|
460
|
+
className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
|
|
349
461
|
type: "text",
|
|
350
462
|
value: query,
|
|
351
463
|
placeholder,
|
|
@@ -353,9 +465,9 @@ function SearchBar({
|
|
|
353
465
|
onFocus: () => results.length && setOpen(true)
|
|
354
466
|
}
|
|
355
467
|
),
|
|
356
|
-
open && /* @__PURE__ */ (0,
|
|
357
|
-
loading && /* @__PURE__ */ (0,
|
|
358
|
-
!loading && results.length === 0 && /* @__PURE__ */ (0,
|
|
468
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
|
|
469
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-msg", children: "Searching\u2026" }),
|
|
470
|
+
!loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-msg", children: [
|
|
359
471
|
'No results for "',
|
|
360
472
|
query,
|
|
361
473
|
'"'
|
|
@@ -363,11 +475,11 @@ function SearchBar({
|
|
|
363
475
|
results.map(
|
|
364
476
|
(r) => {
|
|
365
477
|
var _a, _b;
|
|
366
|
-
return renderResult ? /* @__PURE__ */ (0,
|
|
367
|
-
((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0,
|
|
368
|
-
/* @__PURE__ */ (0,
|
|
369
|
-
/* @__PURE__ */ (0,
|
|
370
|
-
/* @__PURE__ */ (0,
|
|
478
|
+
return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
|
|
479
|
+
((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: r.product.images[0], alt: r.product.name }),
|
|
480
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-item-name", children: r.product.name }),
|
|
482
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item-price", children: [
|
|
371
483
|
(_b = r.product.currency) != null ? _b : "KES",
|
|
372
484
|
" ",
|
|
373
485
|
r.product.price
|
|
@@ -382,27 +494,20 @@ function SearchBar({
|
|
|
382
494
|
}
|
|
383
495
|
|
|
384
496
|
// src/components/Sparkle.tsx
|
|
385
|
-
var
|
|
386
|
-
var
|
|
387
|
-
var
|
|
388
|
-
.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
background: #f47c3c; color: #fff; border: none;
|
|
392
|
-
border-radius: 20px; cursor: pointer; letter-spacing: 0.02em;
|
|
393
|
-
transition: opacity 0.2s, transform 0.15s;
|
|
394
|
-
}
|
|
395
|
-
.huskel-sparkle-btn:hover { opacity: 0.88; transform: scale(1.04); }
|
|
396
|
-
.huskel-sparkle-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
397
|
-
.huskel-sparkle-icon { font-size: 13px; }
|
|
497
|
+
var import_react6 = require("react");
|
|
498
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
499
|
+
var S2 = `
|
|
500
|
+
.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}
|
|
501
|
+
.hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
|
|
502
|
+
.hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
|
|
398
503
|
`;
|
|
399
|
-
function Sparkle({ productName,
|
|
400
|
-
const
|
|
504
|
+
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
505
|
+
const client = useHuskelContext();
|
|
506
|
+
const [loading, setLoading] = (0, import_react6.useState)(false);
|
|
401
507
|
const handleClick = async () => {
|
|
402
508
|
setLoading(true);
|
|
403
509
|
try {
|
|
404
|
-
const
|
|
405
|
-
const res = await client.api.search(productName, 5);
|
|
510
|
+
const res = await client.api.search(productName, limit);
|
|
406
511
|
onResult == null ? void 0 : onResult(res.results);
|
|
407
512
|
} catch (e) {
|
|
408
513
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -410,31 +515,24 @@ function Sparkle({ productName, className, onResult }) {
|
|
|
410
515
|
setLoading(false);
|
|
411
516
|
}
|
|
412
517
|
};
|
|
413
|
-
return /* @__PURE__ */ (0,
|
|
414
|
-
/* @__PURE__ */ (0,
|
|
415
|
-
/* @__PURE__ */ (0,
|
|
416
|
-
"
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
onClick: handleClick,
|
|
420
|
-
disabled: loading,
|
|
421
|
-
title: "Find similar with AI",
|
|
422
|
-
children: [
|
|
423
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "huskel-sparkle-icon", children: "\u2726" }),
|
|
424
|
-
loading ? "Finding\u2026" : "Similar"
|
|
425
|
-
]
|
|
426
|
-
}
|
|
427
|
-
)
|
|
518
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
519
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: S2 }),
|
|
520
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
|
|
521
|
+
"\u2726 ",
|
|
522
|
+
loading ? "Finding\u2026" : "Similar"
|
|
523
|
+
] })
|
|
428
524
|
] });
|
|
429
525
|
}
|
|
430
526
|
// Annotate the CommonJS export names for ESM import in node:
|
|
431
527
|
0 && (module.exports = {
|
|
432
528
|
HuskelAPI,
|
|
433
529
|
HuskelClient,
|
|
530
|
+
HuskelProvider,
|
|
434
531
|
SearchBar,
|
|
435
532
|
Sparkle,
|
|
436
533
|
getHuskelClient,
|
|
437
534
|
initHuskel,
|
|
438
535
|
useHuskel,
|
|
536
|
+
useIngest,
|
|
439
537
|
useSearch
|
|
440
538
|
});
|