@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.mjs
CHANGED
|
@@ -1,180 +1,79 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
-
var __spreadValues = (a, b) => {
|
|
7
|
-
for (var prop in b || (b = {}))
|
|
8
|
-
if (__hasOwnProp.call(b, prop))
|
|
9
|
-
__defNormalProp(a, prop, b[prop]);
|
|
10
|
-
if (__getOwnPropSymbols)
|
|
11
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
-
if (__propIsEnum.call(b, prop))
|
|
13
|
-
__defNormalProp(a, prop, b[prop]);
|
|
14
|
-
}
|
|
15
|
-
return a;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
1
|
// src/api.ts
|
|
2
|
+
var MAX_RETRIES = 3;
|
|
3
|
+
var RETRY_DELAYS = [500, 1e3, 2e3];
|
|
4
|
+
function log(level, msg, data) {
|
|
5
|
+
const prefix = "[Huskel]";
|
|
6
|
+
if (level === "error") console.error(prefix, msg, data != null ? data : "");
|
|
7
|
+
else if (level === "warn") console.warn(prefix, msg, data != null ? data : "");
|
|
8
|
+
else console.log(prefix, msg, data != null ? data : "");
|
|
9
|
+
}
|
|
10
|
+
async function sleep(ms) {
|
|
11
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
}
|
|
19
13
|
var HuskelAPI = class {
|
|
20
|
-
constructor(apiUrl, siteId) {
|
|
14
|
+
constructor(apiUrl, siteId, apiToken) {
|
|
21
15
|
this.apiUrl = apiUrl;
|
|
22
16
|
this.siteId = siteId;
|
|
17
|
+
this.apiToken = apiToken;
|
|
23
18
|
}
|
|
24
|
-
async post(path, body) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
async post(path, body, attempt = 0) {
|
|
20
|
+
const url = `${this.apiUrl}${path}`;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"X-Huskel-Token": this.apiToken,
|
|
27
|
+
"X-Huskel-Site": this.siteId
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(body)
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
const text = await res.text();
|
|
33
|
+
const err = { status: res.status, message: text };
|
|
34
|
+
if (res.status >= 400 && res.status < 500) {
|
|
35
|
+
log("error", `${path} failed [${res.status}]`, text);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
39
|
+
log("warn", `${path} [${res.status}] retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
40
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
41
|
+
return this.post(path, body, attempt + 1);
|
|
42
|
+
}
|
|
43
|
+
log("error", `${path} failed after ${MAX_RETRIES} attempts`, err);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
return res.json();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (e.status === void 0) {
|
|
49
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
50
|
+
log("warn", `${path} network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
|
|
51
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
52
|
+
return this.post(path, body, attempt + 1);
|
|
53
|
+
}
|
|
54
|
+
log("error", `${path} unreachable after ${MAX_RETRIES} attempts`);
|
|
55
|
+
}
|
|
56
|
+
throw e;
|
|
33
57
|
}
|
|
34
|
-
return res.json();
|
|
35
58
|
}
|
|
36
59
|
async ingest(product) {
|
|
60
|
+
log("info", "ingesting product", product.name);
|
|
37
61
|
return this.post("/ingest", { siteId: this.siteId, product });
|
|
38
62
|
}
|
|
39
63
|
async ingestBatch(products) {
|
|
64
|
+
log("info", `ingesting batch of ${products.length} products`);
|
|
40
65
|
return this.post("/ingest/batch", { siteId: this.siteId, products });
|
|
41
66
|
}
|
|
42
67
|
async search(query, limit = 10) {
|
|
68
|
+
log("info", "search query", query);
|
|
43
69
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
44
70
|
}
|
|
45
|
-
async pushConfig(config) {
|
|
46
|
-
return this.post("/sites/config", __spreadValues({ siteId: this.siteId }, config));
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// src/extractor.ts
|
|
51
|
-
function getText(el, selector) {
|
|
52
|
-
var _a, _b;
|
|
53
|
-
if (!selector) return "";
|
|
54
|
-
const found = el.querySelector(selector);
|
|
55
|
-
return (_b = (_a = found == null ? void 0 : found.textContent) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
56
|
-
}
|
|
57
|
-
function getAttr(el, selector, attr) {
|
|
58
|
-
var _a, _b;
|
|
59
|
-
if (!selector) return "";
|
|
60
|
-
const found = el.querySelector(selector);
|
|
61
|
-
return (_b = (_a = found == null ? void 0 : found.getAttribute(attr)) == null ? void 0 : _a.trim()) != null ? _b : "";
|
|
62
|
-
}
|
|
63
|
-
function getAll(el, selector) {
|
|
64
|
-
if (!selector) return [];
|
|
65
|
-
return Array.from(el.querySelectorAll(selector)).map((n) => {
|
|
66
|
-
var _a;
|
|
67
|
-
return n.getAttribute("src") || n.getAttribute("href") || ((_a = n.textContent) == null ? void 0 : _a.trim()) || "";
|
|
68
|
-
}).filter(Boolean);
|
|
69
|
-
}
|
|
70
|
-
function parsePrice(raw) {
|
|
71
|
-
const num = parseFloat(raw.replace(/[^0-9.]/g, ""));
|
|
72
|
-
return isNaN(num) ? void 0 : num;
|
|
73
|
-
}
|
|
74
|
-
function extractProducts(config) {
|
|
75
|
-
const containers = document.querySelectorAll(config.selectorContainer);
|
|
76
|
-
if (!containers.length) return [];
|
|
77
|
-
const products = [];
|
|
78
|
-
containers.forEach((el) => {
|
|
79
|
-
var _a;
|
|
80
|
-
const name = getText(el, config.selectorName);
|
|
81
|
-
const price = getText(el, config.selectorPrice);
|
|
82
|
-
const rawUrl = getAttr(el, config.selectorUrl, "href") || el.href || window.location.href;
|
|
83
|
-
const url = rawUrl.startsWith("http") ? rawUrl : `${window.location.origin}${rawUrl}`;
|
|
84
|
-
if (!name || !price || !url) return;
|
|
85
|
-
const product = {
|
|
86
|
-
name,
|
|
87
|
-
price,
|
|
88
|
-
url,
|
|
89
|
-
brand: getText(el, config.selectorBrand) || void 0,
|
|
90
|
-
description: getText(el, config.selectorDescription) || void 0,
|
|
91
|
-
originalPrice: getText(el, config.selectorOriginalPrice) || void 0,
|
|
92
|
-
discount: getText(el, config.selectorDiscount) || void 0,
|
|
93
|
-
currency: (_a = config.currency) != null ? _a : "KES",
|
|
94
|
-
availability: getText(el, config.selectorAvailability) || void 0,
|
|
95
|
-
rating: getText(el, config.selectorRating) || void 0,
|
|
96
|
-
category: getText(el, config.selectorCategory) || void 0,
|
|
97
|
-
images: config.selectorImage ? getAll(el, config.selectorImage) : void 0,
|
|
98
|
-
priceNumeric: parsePrice(price),
|
|
99
|
-
slug: url.split("/").filter(Boolean).pop()
|
|
100
|
-
};
|
|
101
|
-
products.push(product);
|
|
102
|
-
});
|
|
103
|
-
return products;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// src/observer.ts
|
|
107
|
-
var RouteObserver = class {
|
|
108
|
-
constructor() {
|
|
109
|
-
this.callbacks = [];
|
|
110
|
-
this.current = window.location.href;
|
|
111
|
-
this.patchHistory();
|
|
112
|
-
window.addEventListener("popstate", () => this.notify());
|
|
113
|
-
}
|
|
114
|
-
patchHistory() {
|
|
115
|
-
const notify = () => this.notify();
|
|
116
|
-
const wrap = (original) => function(...args) {
|
|
117
|
-
original.apply(this, args);
|
|
118
|
-
notify();
|
|
119
|
-
};
|
|
120
|
-
history.pushState = wrap(history.pushState);
|
|
121
|
-
history.replaceState = wrap(history.replaceState);
|
|
122
|
-
}
|
|
123
|
-
notify() {
|
|
124
|
-
const next = window.location.href;
|
|
125
|
-
if (next !== this.current) {
|
|
126
|
-
this.current = next;
|
|
127
|
-
this.callbacks.forEach((cb) => cb(next));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
onChange(cb) {
|
|
131
|
-
this.callbacks.push(cb);
|
|
132
|
-
return () => {
|
|
133
|
-
this.callbacks = this.callbacks.filter((fn) => fn !== cb);
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
destroy() {
|
|
137
|
-
this.callbacks = [];
|
|
138
|
-
}
|
|
139
71
|
};
|
|
140
72
|
|
|
141
73
|
// src/client.ts
|
|
142
74
|
var HuskelClient = class {
|
|
143
75
|
constructor(config) {
|
|
144
|
-
this.
|
|
145
|
-
this.api = new HuskelAPI(config.apiUrl, config.siteId);
|
|
146
|
-
}
|
|
147
|
-
/** Push selectors config to backend once (idempotent) */
|
|
148
|
-
async configure() {
|
|
149
|
-
await this.api.pushConfig(this.config.selectors);
|
|
150
|
-
}
|
|
151
|
-
/** Scrape current DOM and ingest all products on the page */
|
|
152
|
-
async ingestCurrentPage() {
|
|
153
|
-
const products = extractProducts(__spreadValues({
|
|
154
|
-
siteId: this.config.siteId
|
|
155
|
-
}, this.config.selectors));
|
|
156
|
-
if (products.length === 0) return [];
|
|
157
|
-
await this.api.ingestBatch(products);
|
|
158
|
-
return products;
|
|
159
|
-
}
|
|
160
|
-
/** Start watching route changes and auto-ingesting */
|
|
161
|
-
start() {
|
|
162
|
-
if (!this.config.autoIngest) return;
|
|
163
|
-
this.observer = new RouteObserver();
|
|
164
|
-
this.unsubscribe = this.observer.onChange(() => {
|
|
165
|
-
var _a;
|
|
166
|
-
clearTimeout(this.debounceTimer);
|
|
167
|
-
this.debounceTimer = setTimeout(() => {
|
|
168
|
-
this.ingestCurrentPage().catch(console.error);
|
|
169
|
-
}, (_a = this.config.debounceMs) != null ? _a : 600);
|
|
170
|
-
});
|
|
171
|
-
this.ingestCurrentPage().catch(console.error);
|
|
172
|
-
}
|
|
173
|
-
stop() {
|
|
174
|
-
var _a, _b;
|
|
175
|
-
(_a = this.unsubscribe) == null ? void 0 : _a.call(this);
|
|
176
|
-
(_b = this.observer) == null ? void 0 : _b.destroy();
|
|
177
|
-
clearTimeout(this.debounceTimer);
|
|
76
|
+
this.api = new HuskelAPI(config.apiUrl, config.siteId, config.apiToken);
|
|
178
77
|
}
|
|
179
78
|
};
|
|
180
79
|
var instance = null;
|
|
@@ -187,15 +86,25 @@ function getHuskelClient() {
|
|
|
187
86
|
return instance;
|
|
188
87
|
}
|
|
189
88
|
|
|
89
|
+
// src/hooks/useHuskel.ts
|
|
90
|
+
import { useRef } from "react";
|
|
91
|
+
function useHuskel(config) {
|
|
92
|
+
const clientRef = useRef(null);
|
|
93
|
+
if (!clientRef.current) {
|
|
94
|
+
clientRef.current = initHuskel(config);
|
|
95
|
+
}
|
|
96
|
+
return clientRef.current;
|
|
97
|
+
}
|
|
98
|
+
|
|
190
99
|
// src/hooks/useSearch.ts
|
|
191
|
-
import { useState, useCallback, useRef } from "react";
|
|
100
|
+
import { useState, useCallback, useRef as useRef2 } from "react";
|
|
192
101
|
function useSearch() {
|
|
193
102
|
const [results, setResults] = useState([]);
|
|
194
103
|
const [loading, setLoading] = useState(false);
|
|
195
104
|
const [error, setError] = useState(null);
|
|
196
|
-
const abortRef =
|
|
105
|
+
const abortRef = useRef2(null);
|
|
197
106
|
const search = useCallback(async (query, limit = 10) => {
|
|
198
|
-
var _a, _b;
|
|
107
|
+
var _a, _b, _c;
|
|
199
108
|
if (!query.trim()) {
|
|
200
109
|
setResults([]);
|
|
201
110
|
return;
|
|
@@ -205,13 +114,10 @@ function useSearch() {
|
|
|
205
114
|
setLoading(true);
|
|
206
115
|
setError(null);
|
|
207
116
|
try {
|
|
208
|
-
const
|
|
209
|
-
const res = await client.api.search(query, limit);
|
|
117
|
+
const res = await getHuskelClient().api.search(query, limit);
|
|
210
118
|
setResults((_b = res.results) != null ? _b : []);
|
|
211
119
|
} catch (e) {
|
|
212
|
-
|
|
213
|
-
setError(e.message);
|
|
214
|
-
}
|
|
120
|
+
setError((_c = e.message) != null ? _c : "Search failed");
|
|
215
121
|
} finally {
|
|
216
122
|
setLoading(false);
|
|
217
123
|
}
|
|
@@ -223,51 +129,53 @@ function useSearch() {
|
|
|
223
129
|
return { results, loading, error, search, clear };
|
|
224
130
|
}
|
|
225
131
|
|
|
226
|
-
// src/hooks/
|
|
227
|
-
import {
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
|
|
132
|
+
// src/hooks/useIngest.ts
|
|
133
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
134
|
+
function useIngest() {
|
|
135
|
+
const [loading, setLoading] = useState2(false);
|
|
136
|
+
const [error, setError] = useState2(null);
|
|
137
|
+
const ingest = useCallback2(async (product) => {
|
|
231
138
|
var _a;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
139
|
+
setLoading(true);
|
|
140
|
+
setError(null);
|
|
141
|
+
try {
|
|
142
|
+
await getHuskelClient().api.ingest(product);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
setError((_a = e.message) != null ? _a : "Ingest failed");
|
|
145
|
+
} finally {
|
|
146
|
+
setLoading(false);
|
|
147
|
+
}
|
|
238
148
|
}, []);
|
|
239
|
-
|
|
149
|
+
const ingestBatch = useCallback2(async (products) => {
|
|
150
|
+
var _a;
|
|
151
|
+
if (!products.length) return;
|
|
152
|
+
setLoading(true);
|
|
153
|
+
setError(null);
|
|
154
|
+
try {
|
|
155
|
+
await getHuskelClient().api.ingestBatch(products);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
setError((_a = e.message) != null ? _a : "Batch ingest failed");
|
|
158
|
+
} finally {
|
|
159
|
+
setLoading(false);
|
|
160
|
+
}
|
|
161
|
+
}, []);
|
|
162
|
+
return { ingest, ingestBatch, loading, error };
|
|
240
163
|
}
|
|
241
164
|
|
|
242
165
|
// src/components/SearchBar.tsx
|
|
243
|
-
import { useState as
|
|
166
|
+
import { useState as useState3, useEffect as useEffect2, useRef as useRef3 } from "react";
|
|
244
167
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
245
|
-
var
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
.
|
|
254
|
-
.
|
|
255
|
-
|
|
256
|
-
background: #fff; border: 1px solid #e2e2e2; border-radius: 8px;
|
|
257
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.10); z-index: 9999;
|
|
258
|
-
max-height: 360px; overflow-y: auto;
|
|
259
|
-
}
|
|
260
|
-
.huskel-search-item {
|
|
261
|
-
display: flex; align-items: center; gap: 12px;
|
|
262
|
-
padding: 10px 14px; cursor: pointer; transition: background 0.15s;
|
|
263
|
-
}
|
|
264
|
-
.huskel-search-item:hover { background: #faf5f1; }
|
|
265
|
-
.huskel-search-item img { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
|
|
266
|
-
.huskel-search-item-info { flex: 1; min-width: 0; }
|
|
267
|
-
.huskel-search-item-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
268
|
-
.huskel-search-item-price { font-size: 13px; color: #f47c3c; margin-top: 2px; }
|
|
269
|
-
.huskel-search-empty { padding: 16px; text-align: center; color: #888; font-size: 14px; }
|
|
270
|
-
.huskel-search-loading { padding: 16px; text-align: center; color: #aaa; font-size: 13px; }
|
|
168
|
+
var S = `
|
|
169
|
+
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
170
|
+
.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}
|
|
171
|
+
.hsk-input:focus{border-color:#f47c3c}
|
|
172
|
+
.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}
|
|
173
|
+
.hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
|
|
174
|
+
.hsk-item:hover{background:#faf5f1}
|
|
175
|
+
.hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
|
|
176
|
+
.hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
177
|
+
.hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
|
|
178
|
+
.hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
|
|
271
179
|
`;
|
|
272
180
|
function SearchBar({
|
|
273
181
|
placeholder = "Search for what you want \u2014 how you want",
|
|
@@ -275,31 +183,31 @@ function SearchBar({
|
|
|
275
183
|
debounceMs = 300,
|
|
276
184
|
onSelect,
|
|
277
185
|
className,
|
|
186
|
+
inputClassName,
|
|
187
|
+
dropdownClassName,
|
|
278
188
|
renderResult
|
|
279
189
|
}) {
|
|
280
|
-
const [query, setQuery] =
|
|
281
|
-
const [open, setOpen] =
|
|
190
|
+
const [query, setQuery] = useState3("");
|
|
191
|
+
const [open, setOpen] = useState3(false);
|
|
282
192
|
const { results, loading, search, clear } = useSearch();
|
|
283
|
-
const
|
|
284
|
-
const
|
|
193
|
+
const timer = useRef3();
|
|
194
|
+
const wrap = useRef3(null);
|
|
285
195
|
useEffect2(() => {
|
|
286
|
-
clearTimeout(
|
|
196
|
+
clearTimeout(timer.current);
|
|
287
197
|
if (!query.trim()) {
|
|
288
198
|
clear();
|
|
289
199
|
setOpen(false);
|
|
290
200
|
return;
|
|
291
201
|
}
|
|
292
|
-
|
|
202
|
+
timer.current = setTimeout(() => {
|
|
293
203
|
search(query, limit);
|
|
294
204
|
setOpen(true);
|
|
295
205
|
}, debounceMs);
|
|
296
|
-
return () => clearTimeout(
|
|
206
|
+
return () => clearTimeout(timer.current);
|
|
297
207
|
}, [query]);
|
|
298
208
|
useEffect2(() => {
|
|
299
209
|
const handler = (e) => {
|
|
300
|
-
if (
|
|
301
|
-
setOpen(false);
|
|
302
|
-
}
|
|
210
|
+
if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
|
|
303
211
|
};
|
|
304
212
|
document.addEventListener("mousedown", handler);
|
|
305
213
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -310,12 +218,12 @@ function SearchBar({
|
|
|
310
218
|
onSelect == null ? void 0 : onSelect(r);
|
|
311
219
|
};
|
|
312
220
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
313
|
-
/* @__PURE__ */ jsx("style", { children:
|
|
314
|
-
/* @__PURE__ */ jsxs("div", { className: `
|
|
221
|
+
/* @__PURE__ */ jsx("style", { children: S }),
|
|
222
|
+
/* @__PURE__ */ jsxs("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
|
|
315
223
|
/* @__PURE__ */ jsx(
|
|
316
224
|
"input",
|
|
317
225
|
{
|
|
318
|
-
className:
|
|
226
|
+
className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
|
|
319
227
|
type: "text",
|
|
320
228
|
value: query,
|
|
321
229
|
placeholder,
|
|
@@ -323,9 +231,9 @@ function SearchBar({
|
|
|
323
231
|
onFocus: () => results.length && setOpen(true)
|
|
324
232
|
}
|
|
325
233
|
),
|
|
326
|
-
open && /* @__PURE__ */ jsxs("div", { className:
|
|
327
|
-
loading && /* @__PURE__ */ jsx("div", { className: "
|
|
328
|
-
!loading && results.length === 0 && /* @__PURE__ */ jsxs("div", { className: "
|
|
234
|
+
open && /* @__PURE__ */ jsxs("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
|
|
235
|
+
loading && /* @__PURE__ */ jsx("div", { className: "hsk-msg", children: "Searching\u2026" }),
|
|
236
|
+
!loading && results.length === 0 && /* @__PURE__ */ jsxs("div", { className: "hsk-msg", children: [
|
|
329
237
|
'No results for "',
|
|
330
238
|
query,
|
|
331
239
|
'"'
|
|
@@ -333,11 +241,11 @@ function SearchBar({
|
|
|
333
241
|
results.map(
|
|
334
242
|
(r) => {
|
|
335
243
|
var _a, _b;
|
|
336
|
-
return renderResult ? /* @__PURE__ */ jsx("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ jsxs("div", { className: "
|
|
244
|
+
return renderResult ? /* @__PURE__ */ jsx("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ jsxs("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
|
|
337
245
|
((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ jsx("img", { src: r.product.images[0], alt: r.product.name }),
|
|
338
|
-
/* @__PURE__ */ jsxs("div", {
|
|
339
|
-
/* @__PURE__ */ jsx("div", { className: "
|
|
340
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
246
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
247
|
+
/* @__PURE__ */ jsx("div", { className: "hsk-item-name", children: r.product.name }),
|
|
248
|
+
/* @__PURE__ */ jsxs("div", { className: "hsk-item-price", children: [
|
|
341
249
|
(_b = r.product.currency) != null ? _b : "KES",
|
|
342
250
|
" ",
|
|
343
251
|
r.product.price
|
|
@@ -352,27 +260,19 @@ function SearchBar({
|
|
|
352
260
|
}
|
|
353
261
|
|
|
354
262
|
// src/components/Sparkle.tsx
|
|
355
|
-
import { useState as
|
|
263
|
+
import { useState as useState4 } from "react";
|
|
356
264
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
357
|
-
var
|
|
358
|
-
.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
background: #f47c3c; color: #fff; border: none;
|
|
362
|
-
border-radius: 20px; cursor: pointer; letter-spacing: 0.02em;
|
|
363
|
-
transition: opacity 0.2s, transform 0.15s;
|
|
364
|
-
}
|
|
365
|
-
.huskel-sparkle-btn:hover { opacity: 0.88; transform: scale(1.04); }
|
|
366
|
-
.huskel-sparkle-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
367
|
-
.huskel-sparkle-icon { font-size: 13px; }
|
|
265
|
+
var S2 = `
|
|
266
|
+
.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}
|
|
267
|
+
.hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
|
|
268
|
+
.hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
|
|
368
269
|
`;
|
|
369
|
-
function Sparkle({ productName,
|
|
370
|
-
const [loading, setLoading] =
|
|
270
|
+
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
271
|
+
const [loading, setLoading] = useState4(false);
|
|
371
272
|
const handleClick = async () => {
|
|
372
273
|
setLoading(true);
|
|
373
274
|
try {
|
|
374
|
-
const
|
|
375
|
-
const res = await client.api.search(productName, 5);
|
|
275
|
+
const res = await getHuskelClient().api.search(productName, limit);
|
|
376
276
|
onResult == null ? void 0 : onResult(res.results);
|
|
377
277
|
} catch (e) {
|
|
378
278
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -381,20 +281,11 @@ function Sparkle({ productName, className, onResult }) {
|
|
|
381
281
|
}
|
|
382
282
|
};
|
|
383
283
|
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
384
|
-
/* @__PURE__ */ jsx2("style", { children:
|
|
385
|
-
/* @__PURE__ */ jsxs2(
|
|
386
|
-
"
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
onClick: handleClick,
|
|
390
|
-
disabled: loading,
|
|
391
|
-
title: "Find similar with AI",
|
|
392
|
-
children: [
|
|
393
|
-
/* @__PURE__ */ jsx2("span", { className: "huskel-sparkle-icon", children: "\u2726" }),
|
|
394
|
-
loading ? "Finding\u2026" : "Similar"
|
|
395
|
-
]
|
|
396
|
-
}
|
|
397
|
-
)
|
|
284
|
+
/* @__PURE__ */ jsx2("style", { children: S2 }),
|
|
285
|
+
/* @__PURE__ */ jsxs2("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
|
|
286
|
+
"\u2726 ",
|
|
287
|
+
loading ? "Finding\u2026" : "Similar"
|
|
288
|
+
] })
|
|
398
289
|
] });
|
|
399
290
|
}
|
|
400
291
|
export {
|
|
@@ -405,5 +296,6 @@ export {
|
|
|
405
296
|
getHuskelClient,
|
|
406
297
|
initHuskel,
|
|
407
298
|
useHuskel,
|
|
299
|
+
useIngest,
|
|
408
300
|
useSearch
|
|
409
301
|
};
|