@huskel/sdk 0.1.0 → 0.2.0
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 +50 -68
- package/dist/index.d.mts +22 -43
- package/dist/index.d.ts +22 -43
- package/dist/index.js +153 -257
- package/dist/index.mjs +145 -253
- package/package.json +1 -1
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 });
|
|
@@ -41,170 +27,87 @@ __export(index_exports, {
|
|
|
41
27
|
getHuskelClient: () => getHuskelClient,
|
|
42
28
|
initHuskel: () => initHuskel,
|
|
43
29
|
useHuskel: () => useHuskel,
|
|
30
|
+
useIngest: () => useIngest,
|
|
44
31
|
useSearch: () => useSearch
|
|
45
32
|
});
|
|
46
33
|
module.exports = __toCommonJS(index_exports);
|
|
47
34
|
|
|
48
35
|
// src/api.ts
|
|
36
|
+
var MAX_RETRIES = 3;
|
|
37
|
+
var RETRY_DELAYS = [500, 1e3, 2e3];
|
|
38
|
+
function log(level, msg, data) {
|
|
39
|
+
const prefix = "[Huskel]";
|
|
40
|
+
if (level === "error") console.error(prefix, msg, data != null ? data : "");
|
|
41
|
+
else if (level === "warn") console.warn(prefix, msg, data != null ? data : "");
|
|
42
|
+
else console.log(prefix, msg, data != null ? data : "");
|
|
43
|
+
}
|
|
44
|
+
async function sleep(ms) {
|
|
45
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
}
|
|
49
47
|
var HuskelAPI = class {
|
|
50
|
-
constructor(apiUrl, siteId) {
|
|
48
|
+
constructor(apiUrl, siteId, apiToken) {
|
|
51
49
|
this.apiUrl = apiUrl;
|
|
52
50
|
this.siteId = siteId;
|
|
51
|
+
this.apiToken = apiToken;
|
|
53
52
|
}
|
|
54
|
-
async post(path, body) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
async post(path, body, attempt = 0) {
|
|
54
|
+
const url = `${this.apiUrl}${path}`;
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"X-Huskel-Token": this.apiToken,
|
|
61
|
+
"X-Huskel-Site": this.siteId
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(body)
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const err = { status: res.status, message: text };
|
|
68
|
+
if (res.status >= 400 && res.status < 500) {
|
|
69
|
+
log("error", `${path} failed [${res.status}]`, text);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
73
|
+
log("warn", `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
74
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
75
|
+
return this.post(path, body, attempt + 1);
|
|
76
|
+
}
|
|
77
|
+
log("error", `${path} failed after ${MAX_RETRIES} attempts`, err);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
return res.json();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e.status === void 0) {
|
|
83
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
84
|
+
log("warn", `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
85
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
86
|
+
return this.post(path, body, attempt + 1);
|
|
87
|
+
}
|
|
88
|
+
log("error", `${path} unreachable after ${MAX_RETRIES} attempts`);
|
|
89
|
+
}
|
|
90
|
+
throw e;
|
|
63
91
|
}
|
|
64
|
-
return res.json();
|
|
65
92
|
}
|
|
66
93
|
async ingest(product) {
|
|
94
|
+
log("info", "ingesting product", product.name);
|
|
67
95
|
return this.post("/ingest", { siteId: this.siteId, product });
|
|
68
96
|
}
|
|
69
97
|
async ingestBatch(products) {
|
|
98
|
+
log("info", `ingesting batch of ${products.length} products`);
|
|
70
99
|
return this.post("/ingest/batch", { siteId: this.siteId, products });
|
|
71
100
|
}
|
|
72
101
|
async search(query, limit = 10) {
|
|
102
|
+
log("info", "search query", query);
|
|
73
103
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
74
104
|
}
|
|
75
|
-
async pushConfig(config) {
|
|
76
|
-
return this.post("/sites/config", __spreadValues({ siteId: this.siteId }, config));
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/extractor.ts
|
|
81
|
-
function getText(el, selector) {
|
|
82
|
-
var _a, _b;
|
|
83
|
-
if (!selector) return "";
|
|
84
|
-
const found = el.querySelector(selector);
|
|
85
|
-
return (_b = (_a = found == null ? void 0 : found.textContent) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
86
|
-
}
|
|
87
|
-
function getAttr(el, selector, attr) {
|
|
88
|
-
var _a, _b;
|
|
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;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// src/observer.ts
|
|
137
|
-
var RouteObserver = class {
|
|
138
|
-
constructor() {
|
|
139
|
-
this.callbacks = [];
|
|
140
|
-
this.current = window.location.href;
|
|
141
|
-
this.patchHistory();
|
|
142
|
-
window.addEventListener("popstate", () => this.notify());
|
|
143
|
-
}
|
|
144
|
-
patchHistory() {
|
|
145
|
-
const notify = () => this.notify();
|
|
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);
|
|
152
|
-
}
|
|
153
|
-
notify() {
|
|
154
|
-
const next = window.location.href;
|
|
155
|
-
if (next !== this.current) {
|
|
156
|
-
this.current = next;
|
|
157
|
-
this.callbacks.forEach((cb) => cb(next));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
onChange(cb) {
|
|
161
|
-
this.callbacks.push(cb);
|
|
162
|
-
return () => {
|
|
163
|
-
this.callbacks = this.callbacks.filter((fn) => fn !== cb);
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
destroy() {
|
|
167
|
-
this.callbacks = [];
|
|
168
|
-
}
|
|
169
105
|
};
|
|
170
106
|
|
|
171
107
|
// src/client.ts
|
|
172
108
|
var HuskelClient = class {
|
|
173
109
|
constructor(config) {
|
|
174
|
-
this.
|
|
175
|
-
this.api = new HuskelAPI(config.apiUrl, config.siteId);
|
|
176
|
-
}
|
|
177
|
-
/** Push selectors config to backend once (idempotent) */
|
|
178
|
-
async configure() {
|
|
179
|
-
await this.api.pushConfig(this.config.selectors);
|
|
180
|
-
}
|
|
181
|
-
/** Scrape current DOM and ingest all products on the page */
|
|
182
|
-
async ingestCurrentPage() {
|
|
183
|
-
const products = extractProducts(__spreadValues({
|
|
184
|
-
siteId: this.config.siteId
|
|
185
|
-
}, this.config.selectors));
|
|
186
|
-
if (products.length === 0) return [];
|
|
187
|
-
await this.api.ingestBatch(products);
|
|
188
|
-
return products;
|
|
189
|
-
}
|
|
190
|
-
/** Start watching route changes and auto-ingesting */
|
|
191
|
-
start() {
|
|
192
|
-
if (!this.config.autoIngest) return;
|
|
193
|
-
this.observer = new RouteObserver();
|
|
194
|
-
this.unsubscribe = this.observer.onChange(() => {
|
|
195
|
-
var _a;
|
|
196
|
-
clearTimeout(this.debounceTimer);
|
|
197
|
-
this.debounceTimer = setTimeout(() => {
|
|
198
|
-
this.ingestCurrentPage().catch(console.error);
|
|
199
|
-
}, (_a = this.config.debounceMs) != null ? _a : 600);
|
|
200
|
-
});
|
|
201
|
-
this.ingestCurrentPage().catch(console.error);
|
|
202
|
-
}
|
|
203
|
-
stop() {
|
|
204
|
-
var _a, _b;
|
|
205
|
-
(_a = this.unsubscribe) == null ? void 0 : _a.call(this);
|
|
206
|
-
(_b = this.observer) == null ? void 0 : _b.destroy();
|
|
207
|
-
clearTimeout(this.debounceTimer);
|
|
110
|
+
this.api = new HuskelAPI(config.apiUrl, config.siteId, config.apiToken);
|
|
208
111
|
}
|
|
209
112
|
};
|
|
210
113
|
var instance = null;
|
|
@@ -217,15 +120,25 @@ function getHuskelClient() {
|
|
|
217
120
|
return instance;
|
|
218
121
|
}
|
|
219
122
|
|
|
220
|
-
// src/hooks/
|
|
123
|
+
// src/hooks/useHuskel.ts
|
|
221
124
|
var import_react = require("react");
|
|
125
|
+
function useHuskel(config) {
|
|
126
|
+
const clientRef = (0, import_react.useRef)(null);
|
|
127
|
+
if (!clientRef.current) {
|
|
128
|
+
clientRef.current = initHuskel(config);
|
|
129
|
+
}
|
|
130
|
+
return clientRef.current;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/hooks/useSearch.ts
|
|
134
|
+
var import_react2 = require("react");
|
|
222
135
|
function useSearch() {
|
|
223
|
-
const [results, setResults] = (0,
|
|
224
|
-
const [loading, setLoading] = (0,
|
|
225
|
-
const [error, setError] = (0,
|
|
226
|
-
const abortRef = (0,
|
|
227
|
-
const search = (0,
|
|
228
|
-
var _a, _b;
|
|
136
|
+
const [results, setResults] = (0, import_react2.useState)([]);
|
|
137
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
138
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
139
|
+
const abortRef = (0, import_react2.useRef)(null);
|
|
140
|
+
const search = (0, import_react2.useCallback)(async (query, limit = 10) => {
|
|
141
|
+
var _a, _b, _c;
|
|
229
142
|
if (!query.trim()) {
|
|
230
143
|
setResults([]);
|
|
231
144
|
return;
|
|
@@ -235,69 +148,68 @@ function useSearch() {
|
|
|
235
148
|
setLoading(true);
|
|
236
149
|
setError(null);
|
|
237
150
|
try {
|
|
238
|
-
const
|
|
239
|
-
const res = await client.api.search(query, limit);
|
|
151
|
+
const res = await getHuskelClient().api.search(query, limit);
|
|
240
152
|
setResults((_b = res.results) != null ? _b : []);
|
|
241
153
|
} catch (e) {
|
|
242
|
-
|
|
243
|
-
setError(e.message);
|
|
244
|
-
}
|
|
154
|
+
setError((_c = e.message) != null ? _c : "Search failed");
|
|
245
155
|
} finally {
|
|
246
156
|
setLoading(false);
|
|
247
157
|
}
|
|
248
158
|
}, []);
|
|
249
|
-
const clear = (0,
|
|
159
|
+
const clear = (0, import_react2.useCallback)(() => {
|
|
250
160
|
setResults([]);
|
|
251
161
|
setError(null);
|
|
252
162
|
}, []);
|
|
253
163
|
return { results, loading, error, search, clear };
|
|
254
164
|
}
|
|
255
165
|
|
|
256
|
-
// src/hooks/
|
|
257
|
-
var
|
|
258
|
-
function
|
|
259
|
-
const
|
|
260
|
-
(0,
|
|
166
|
+
// src/hooks/useIngest.ts
|
|
167
|
+
var import_react3 = require("react");
|
|
168
|
+
function useIngest() {
|
|
169
|
+
const [loading, setLoading] = (0, import_react3.useState)(false);
|
|
170
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
171
|
+
const ingest = (0, import_react3.useCallback)(async (product) => {
|
|
261
172
|
var _a;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
173
|
+
setLoading(true);
|
|
174
|
+
setError(null);
|
|
175
|
+
try {
|
|
176
|
+
await getHuskelClient().api.ingest(product);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
setError((_a = e.message) != null ? _a : "Ingest failed");
|
|
179
|
+
} finally {
|
|
180
|
+
setLoading(false);
|
|
181
|
+
}
|
|
268
182
|
}, []);
|
|
269
|
-
|
|
183
|
+
const ingestBatch = (0, import_react3.useCallback)(async (products) => {
|
|
184
|
+
var _a;
|
|
185
|
+
if (!products.length) return;
|
|
186
|
+
setLoading(true);
|
|
187
|
+
setError(null);
|
|
188
|
+
try {
|
|
189
|
+
await getHuskelClient().api.ingestBatch(products);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
setError((_a = e.message) != null ? _a : "Batch ingest failed");
|
|
192
|
+
} finally {
|
|
193
|
+
setLoading(false);
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
196
|
+
return { ingest, ingestBatch, loading, error };
|
|
270
197
|
}
|
|
271
198
|
|
|
272
199
|
// src/components/SearchBar.tsx
|
|
273
|
-
var
|
|
200
|
+
var import_react4 = require("react");
|
|
274
201
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
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; }
|
|
202
|
+
var S = `
|
|
203
|
+
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
204
|
+
.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}
|
|
205
|
+
.hsk-input:focus{border-color:#f47c3c}
|
|
206
|
+
.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}
|
|
207
|
+
.hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
|
|
208
|
+
.hsk-item:hover{background:#faf5f1}
|
|
209
|
+
.hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
|
|
210
|
+
.hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
211
|
+
.hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
|
|
212
|
+
.hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
|
|
301
213
|
`;
|
|
302
214
|
function SearchBar({
|
|
303
215
|
placeholder = "Search for what you want \u2014 how you want",
|
|
@@ -305,31 +217,31 @@ function SearchBar({
|
|
|
305
217
|
debounceMs = 300,
|
|
306
218
|
onSelect,
|
|
307
219
|
className,
|
|
220
|
+
inputClassName,
|
|
221
|
+
dropdownClassName,
|
|
308
222
|
renderResult
|
|
309
223
|
}) {
|
|
310
|
-
const [query, setQuery] = (0,
|
|
311
|
-
const [open, setOpen] = (0,
|
|
224
|
+
const [query, setQuery] = (0, import_react4.useState)("");
|
|
225
|
+
const [open, setOpen] = (0, import_react4.useState)(false);
|
|
312
226
|
const { results, loading, search, clear } = useSearch();
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
(0,
|
|
316
|
-
clearTimeout(
|
|
227
|
+
const timer = (0, import_react4.useRef)();
|
|
228
|
+
const wrap = (0, import_react4.useRef)(null);
|
|
229
|
+
(0, import_react4.useEffect)(() => {
|
|
230
|
+
clearTimeout(timer.current);
|
|
317
231
|
if (!query.trim()) {
|
|
318
232
|
clear();
|
|
319
233
|
setOpen(false);
|
|
320
234
|
return;
|
|
321
235
|
}
|
|
322
|
-
|
|
236
|
+
timer.current = setTimeout(() => {
|
|
323
237
|
search(query, limit);
|
|
324
238
|
setOpen(true);
|
|
325
239
|
}, debounceMs);
|
|
326
|
-
return () => clearTimeout(
|
|
240
|
+
return () => clearTimeout(timer.current);
|
|
327
241
|
}, [query]);
|
|
328
|
-
(0,
|
|
242
|
+
(0, import_react4.useEffect)(() => {
|
|
329
243
|
const handler = (e) => {
|
|
330
|
-
if (
|
|
331
|
-
setOpen(false);
|
|
332
|
-
}
|
|
244
|
+
if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
|
|
333
245
|
};
|
|
334
246
|
document.addEventListener("mousedown", handler);
|
|
335
247
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -340,12 +252,12 @@ function SearchBar({
|
|
|
340
252
|
onSelect == null ? void 0 : onSelect(r);
|
|
341
253
|
};
|
|
342
254
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
343
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children:
|
|
344
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `
|
|
255
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: S }),
|
|
256
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
|
|
345
257
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
346
258
|
"input",
|
|
347
259
|
{
|
|
348
|
-
className:
|
|
260
|
+
className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
|
|
349
261
|
type: "text",
|
|
350
262
|
value: query,
|
|
351
263
|
placeholder,
|
|
@@ -353,9 +265,9 @@ function SearchBar({
|
|
|
353
265
|
onFocus: () => results.length && setOpen(true)
|
|
354
266
|
}
|
|
355
267
|
),
|
|
356
|
-
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className:
|
|
357
|
-
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "
|
|
358
|
-
!loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
268
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
|
|
269
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hsk-msg", children: "Searching\u2026" }),
|
|
270
|
+
!loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-msg", children: [
|
|
359
271
|
'No results for "',
|
|
360
272
|
query,
|
|
361
273
|
'"'
|
|
@@ -363,11 +275,11 @@ function SearchBar({
|
|
|
363
275
|
results.map(
|
|
364
276
|
(r) => {
|
|
365
277
|
var _a, _b;
|
|
366
|
-
return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
278
|
+
return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
|
|
367
279
|
((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: r.product.images[0], alt: r.product.name }),
|
|
368
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
369
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "
|
|
370
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
280
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
281
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hsk-item-name", children: r.product.name }),
|
|
282
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hsk-item-price", children: [
|
|
371
283
|
(_b = r.product.currency) != null ? _b : "KES",
|
|
372
284
|
" ",
|
|
373
285
|
r.product.price
|
|
@@ -382,27 +294,19 @@ function SearchBar({
|
|
|
382
294
|
}
|
|
383
295
|
|
|
384
296
|
// src/components/Sparkle.tsx
|
|
385
|
-
var
|
|
297
|
+
var import_react5 = require("react");
|
|
386
298
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
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; }
|
|
299
|
+
var S2 = `
|
|
300
|
+
.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}
|
|
301
|
+
.hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
|
|
302
|
+
.hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
|
|
398
303
|
`;
|
|
399
|
-
function Sparkle({ productName,
|
|
400
|
-
const [loading, setLoading] = (0,
|
|
304
|
+
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
305
|
+
const [loading, setLoading] = (0, import_react5.useState)(false);
|
|
401
306
|
const handleClick = async () => {
|
|
402
307
|
setLoading(true);
|
|
403
308
|
try {
|
|
404
|
-
const
|
|
405
|
-
const res = await client.api.search(productName, 5);
|
|
309
|
+
const res = await getHuskelClient().api.search(productName, limit);
|
|
406
310
|
onResult == null ? void 0 : onResult(res.results);
|
|
407
311
|
} catch (e) {
|
|
408
312
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -411,20 +315,11 @@ function Sparkle({ productName, className, onResult }) {
|
|
|
411
315
|
}
|
|
412
316
|
};
|
|
413
317
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
414
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children:
|
|
415
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
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
|
-
)
|
|
318
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: S2 }),
|
|
319
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
|
|
320
|
+
"\u2726 ",
|
|
321
|
+
loading ? "Finding\u2026" : "Similar"
|
|
322
|
+
] })
|
|
428
323
|
] });
|
|
429
324
|
}
|
|
430
325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -436,5 +331,6 @@ function Sparkle({ productName, className, onResult }) {
|
|
|
436
331
|
getHuskelClient,
|
|
437
332
|
initHuskel,
|
|
438
333
|
useHuskel,
|
|
334
|
+
useIngest,
|
|
439
335
|
useSearch
|
|
440
336
|
});
|