@jamwidgets/core 0.1.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 +277 -0
- package/dist/index.d.ts +491 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1021 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jamwidgets/core
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic API client, types, and controllers for Jamwidgets.
|
|
5
|
+
* Use this package directly or through framework-specific wrappers like @jamwidgets/astro.
|
|
6
|
+
*/
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Constants
|
|
9
|
+
// =============================================================================
|
|
10
|
+
export const DEFAULT_ENDPOINT = "https://jamwidgets.com";
|
|
11
|
+
export const API_PATH = "/api/v1";
|
|
12
|
+
export const VISITOR_STORAGE_KEY = "jamwidgets_visitor_id";
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Helpers
|
|
15
|
+
// =============================================================================
|
|
16
|
+
/** Build full API URL from endpoint and path */
|
|
17
|
+
export function buildUrl(endpoint, path) {
|
|
18
|
+
const base = (endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
19
|
+
return `${base}${API_PATH}${path}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read Jamwidgets config from meta tags in the document head.
|
|
23
|
+
* Looks for:
|
|
24
|
+
* - <meta name="jamwidgets-site-key" content="sk-xxx" />
|
|
25
|
+
* - <meta name="jamwidgets-endpoint" content="https://..." /> (optional)
|
|
26
|
+
*
|
|
27
|
+
* Also supports legacy meta tag names (seriph-site-key, seriph-endpoint) for backward compatibility.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // In your HTML head:
|
|
31
|
+
* <meta name="jamwidgets-site-key" content="sk-xxx" />
|
|
32
|
+
*
|
|
33
|
+
* // In your JS:
|
|
34
|
+
* const config = getConfigFromMeta();
|
|
35
|
+
* if (config) {
|
|
36
|
+
* const poll = createPoll({ ...config, slug: "my-poll" });
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
export function getConfigFromMeta() {
|
|
40
|
+
if (typeof document === "undefined") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// Try new meta tag names first, fall back to legacy names
|
|
44
|
+
const siteKeyMeta = document.querySelector('meta[name="jamwidgets-site-key"]') ||
|
|
45
|
+
document.querySelector('meta[name="seriph-site-key"]');
|
|
46
|
+
const siteKey = siteKeyMeta?.getAttribute("content");
|
|
47
|
+
if (!siteKey) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const endpointMeta = document.querySelector('meta[name="jamwidgets-endpoint"]') ||
|
|
51
|
+
document.querySelector('meta[name="seriph-endpoint"]');
|
|
52
|
+
const endpoint = endpointMeta?.getAttribute("content") || undefined;
|
|
53
|
+
return { siteKey, endpoint };
|
|
54
|
+
}
|
|
55
|
+
/** Get site key from config, with fallback to meta tag */
|
|
56
|
+
export function getSiteKey(config) {
|
|
57
|
+
if (config.siteKey) {
|
|
58
|
+
return config.siteKey;
|
|
59
|
+
}
|
|
60
|
+
// Try reading from meta tag
|
|
61
|
+
const metaConfig = getConfigFromMeta();
|
|
62
|
+
if (metaConfig?.siteKey) {
|
|
63
|
+
return metaConfig.siteKey;
|
|
64
|
+
}
|
|
65
|
+
throw new Error("siteKey is required. Either pass it as a prop or add <meta name=\"jamwidgets-site-key\" content=\"your-key\" /> to your document head.");
|
|
66
|
+
}
|
|
67
|
+
/** Resolve full config, merging props with meta tag fallbacks */
|
|
68
|
+
export function resolveConfig(config) {
|
|
69
|
+
const metaConfig = getConfigFromMeta();
|
|
70
|
+
const siteKey = config.siteKey || metaConfig?.siteKey;
|
|
71
|
+
if (!siteKey) {
|
|
72
|
+
throw new Error("siteKey is required. Either pass it as a prop or add <meta name=\"jamwidgets-site-key\" content=\"your-key\" /> to your document head.");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
siteKey,
|
|
76
|
+
endpoint: config.endpoint || metaConfig?.endpoint,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Visitor Token Management
|
|
81
|
+
// =============================================================================
|
|
82
|
+
/** Custom visitor ID set by the site (e.g., authenticated user ID) */
|
|
83
|
+
let customVisitorId = null;
|
|
84
|
+
/** Generate a random UUID v4 */
|
|
85
|
+
function generateUUID() {
|
|
86
|
+
// Use crypto.randomUUID if available, otherwise fallback
|
|
87
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
88
|
+
return crypto.randomUUID();
|
|
89
|
+
}
|
|
90
|
+
// Fallback for older environments
|
|
91
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
92
|
+
const r = (Math.random() * 16) | 0;
|
|
93
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
94
|
+
return v.toString(16);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Set a custom visitor ID (e.g., authenticated user ID).
|
|
99
|
+
* Useful for non-static sites where you have user sessions.
|
|
100
|
+
* Set to null to revert to localStorage-based ID.
|
|
101
|
+
*/
|
|
102
|
+
export function setVisitorId(id) {
|
|
103
|
+
customVisitorId = id;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get the current visitor ID.
|
|
107
|
+
* Priority: custom ID > localStorage > generated UUID (SSR fallback)
|
|
108
|
+
*/
|
|
109
|
+
export function getVisitorId() {
|
|
110
|
+
// Use custom ID if set (for authenticated users)
|
|
111
|
+
if (customVisitorId) {
|
|
112
|
+
return customVisitorId;
|
|
113
|
+
}
|
|
114
|
+
// SSR check - localStorage only exists in browser
|
|
115
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") {
|
|
116
|
+
// Return a temporary UUID for SSR - will be replaced client-side
|
|
117
|
+
return generateUUID();
|
|
118
|
+
}
|
|
119
|
+
// Use localStorage for anonymous visitors
|
|
120
|
+
let visitorId = localStorage.getItem(VISITOR_STORAGE_KEY);
|
|
121
|
+
if (!visitorId) {
|
|
122
|
+
visitorId = generateUUID();
|
|
123
|
+
localStorage.setItem(VISITOR_STORAGE_KEY, visitorId);
|
|
124
|
+
}
|
|
125
|
+
return visitorId;
|
|
126
|
+
}
|
|
127
|
+
/** Get common headers for API requests */
|
|
128
|
+
function getHeaders(siteKey) {
|
|
129
|
+
let visitorId;
|
|
130
|
+
try {
|
|
131
|
+
visitorId = getVisitorId();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Fallback if localStorage access fails (e.g., private browsing, SSR)
|
|
135
|
+
visitorId = generateUUID();
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
"X-Jamwidgets-Key": siteKey,
|
|
139
|
+
"X-Jamwidgets-Visitor": visitorId,
|
|
140
|
+
// Legacy headers for backward compatibility with older server versions
|
|
141
|
+
"X-Seriph-Key": siteKey,
|
|
142
|
+
"X-Seriph-Visitor": visitorId,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export async function submitForm(options) {
|
|
146
|
+
const { endpoint, formSlug, data, formLoadTime } = options;
|
|
147
|
+
const siteKey = getSiteKey(options);
|
|
148
|
+
const url = buildUrl(endpoint, `/forms/${formSlug}/submit`);
|
|
149
|
+
const payload = {
|
|
150
|
+
...data,
|
|
151
|
+
_seriph_ts: formLoadTime || Math.floor(Date.now() / 1000),
|
|
152
|
+
};
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
...getHeaders(siteKey),
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(payload),
|
|
160
|
+
});
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`Form submission failed: ${response.statusText}`);
|
|
163
|
+
}
|
|
164
|
+
return response.json();
|
|
165
|
+
}
|
|
166
|
+
export async function fetchComments(options) {
|
|
167
|
+
const { endpoint, pageId } = options;
|
|
168
|
+
const siteKey = getSiteKey(options);
|
|
169
|
+
const url = buildUrl(endpoint, `/comments/${encodeURIComponent(pageId)}`);
|
|
170
|
+
const response = await fetch(url, {
|
|
171
|
+
headers: getHeaders(siteKey),
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new Error(`Failed to fetch comments: ${response.statusText}`);
|
|
175
|
+
}
|
|
176
|
+
const data = await response.json();
|
|
177
|
+
return data.comment_threads || [];
|
|
178
|
+
}
|
|
179
|
+
export async function postComment(options) {
|
|
180
|
+
const { endpoint, pageId, authorName, authorEmail, content, parentId } = options;
|
|
181
|
+
const siteKey = getSiteKey(options);
|
|
182
|
+
const url = buildUrl(endpoint, `/comments/${encodeURIComponent(pageId)}`);
|
|
183
|
+
const response = await fetch(url, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
...getHeaders(siteKey),
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
authorName,
|
|
191
|
+
authorEmail,
|
|
192
|
+
content,
|
|
193
|
+
parentId,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
throw new Error(`Failed to post comment: ${response.statusText}`);
|
|
198
|
+
}
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
return data.comment;
|
|
201
|
+
}
|
|
202
|
+
export async function fetchReactions(options) {
|
|
203
|
+
const { endpoint, pageId } = options;
|
|
204
|
+
const siteKey = getSiteKey(options);
|
|
205
|
+
const url = buildUrl(endpoint, `/reactions/${encodeURIComponent(pageId)}`);
|
|
206
|
+
const response = await fetch(url, {
|
|
207
|
+
headers: getHeaders(siteKey),
|
|
208
|
+
});
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
throw new Error(`Failed to fetch reactions: ${response.statusText}`);
|
|
211
|
+
}
|
|
212
|
+
const data = await response.json();
|
|
213
|
+
const result = data.reaction_counts_with_user;
|
|
214
|
+
return {
|
|
215
|
+
...result,
|
|
216
|
+
userReactions: result.userReactions || [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
export async function addReaction(options) {
|
|
220
|
+
const { endpoint, pageId, reactionType = "like" } = options;
|
|
221
|
+
const siteKey = getSiteKey(options);
|
|
222
|
+
const url = buildUrl(endpoint, `/reactions/${encodeURIComponent(pageId)}`);
|
|
223
|
+
const response = await fetch(url, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
...getHeaders(siteKey),
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({ reactionType }),
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`Failed to add reaction: ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
const data = await response.json();
|
|
235
|
+
return data.reaction;
|
|
236
|
+
}
|
|
237
|
+
export async function removeReaction(options) {
|
|
238
|
+
const { endpoint, pageId, reactionType = "like" } = options;
|
|
239
|
+
const siteKey = getSiteKey(options);
|
|
240
|
+
const url = buildUrl(endpoint, `/reactions/${encodeURIComponent(pageId)}`);
|
|
241
|
+
const response = await fetch(url, {
|
|
242
|
+
method: "DELETE",
|
|
243
|
+
headers: {
|
|
244
|
+
...getHeaders(siteKey),
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({ reactionType }),
|
|
248
|
+
});
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(`Failed to remove reaction: ${response.statusText}`);
|
|
251
|
+
}
|
|
252
|
+
const data = await response.json();
|
|
253
|
+
return data.reaction;
|
|
254
|
+
}
|
|
255
|
+
export async function subscribe(options) {
|
|
256
|
+
const { endpoint, email } = options;
|
|
257
|
+
const siteKey = getSiteKey(options);
|
|
258
|
+
const url = buildUrl(endpoint, "/subscribe");
|
|
259
|
+
const response = await fetch(url, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: {
|
|
262
|
+
...getHeaders(siteKey),
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
},
|
|
265
|
+
body: JSON.stringify({ email }),
|
|
266
|
+
});
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new Error(`Subscription failed: ${response.statusText}`);
|
|
269
|
+
}
|
|
270
|
+
return response.json();
|
|
271
|
+
}
|
|
272
|
+
export async function fetchPosts(options) {
|
|
273
|
+
const { endpoint, tag, limit = 500 } = options;
|
|
274
|
+
const siteKey = getSiteKey(options);
|
|
275
|
+
const baseUrl = (endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "") + API_PATH;
|
|
276
|
+
const url = new URL(`${baseUrl}/posts`);
|
|
277
|
+
url.searchParams.set("limit", String(limit));
|
|
278
|
+
if (tag) {
|
|
279
|
+
url.searchParams.set("tag", tag);
|
|
280
|
+
}
|
|
281
|
+
const response = await fetch(url.toString(), {
|
|
282
|
+
headers: getHeaders(siteKey),
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
throw new Error(`Failed to fetch posts: ${response.status} ${response.statusText}`);
|
|
286
|
+
}
|
|
287
|
+
const data = await response.json();
|
|
288
|
+
return data.posts;
|
|
289
|
+
}
|
|
290
|
+
export async function fetchPost(options) {
|
|
291
|
+
const { endpoint, slug } = options;
|
|
292
|
+
const siteKey = getSiteKey(options);
|
|
293
|
+
const baseUrl = (endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "") + API_PATH;
|
|
294
|
+
const response = await fetch(`${baseUrl}/posts/${encodeURIComponent(slug)}`, {
|
|
295
|
+
headers: getHeaders(siteKey),
|
|
296
|
+
});
|
|
297
|
+
if (response.status === 404) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
throw new Error(`Failed to fetch post: ${response.status} ${response.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
const data = await response.json();
|
|
304
|
+
return data.public_post || data;
|
|
305
|
+
}
|
|
306
|
+
export async function joinWaitlist(options) {
|
|
307
|
+
const { endpoint, email, name, source } = options;
|
|
308
|
+
const siteKey = getSiteKey(options);
|
|
309
|
+
const url = buildUrl(endpoint, "/waitlist");
|
|
310
|
+
const response = await fetch(url, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
...getHeaders(siteKey),
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({ email, name, source }),
|
|
317
|
+
});
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
throw new Error(`Failed to join waitlist: ${response.statusText}`);
|
|
320
|
+
}
|
|
321
|
+
return response.json();
|
|
322
|
+
}
|
|
323
|
+
export async function getViewCounts(options) {
|
|
324
|
+
const { endpoint, pageId } = options;
|
|
325
|
+
const siteKey = getSiteKey(options);
|
|
326
|
+
const url = buildUrl(endpoint, `/views/${encodeURIComponent(pageId)}`);
|
|
327
|
+
const response = await fetch(url, {
|
|
328
|
+
headers: getHeaders(siteKey),
|
|
329
|
+
});
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`Failed to get view counts: ${response.statusText}`);
|
|
332
|
+
}
|
|
333
|
+
const data = await response.json();
|
|
334
|
+
return data.page_view_counts;
|
|
335
|
+
}
|
|
336
|
+
export async function recordView(options) {
|
|
337
|
+
const { endpoint, pageId } = options;
|
|
338
|
+
const siteKey = getSiteKey(options);
|
|
339
|
+
const url = buildUrl(endpoint, `/views/${encodeURIComponent(pageId)}`);
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: getHeaders(siteKey),
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
throw new Error(`Failed to record view: ${response.statusText}`);
|
|
346
|
+
}
|
|
347
|
+
const data = await response.json();
|
|
348
|
+
return data.record_view_response;
|
|
349
|
+
}
|
|
350
|
+
export async function submitFeedback(options) {
|
|
351
|
+
const { endpoint, type, content, email, pageUrl } = options;
|
|
352
|
+
const siteKey = getSiteKey(options);
|
|
353
|
+
const url = buildUrl(endpoint, "/feedback");
|
|
354
|
+
const response = await fetch(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: {
|
|
357
|
+
...getHeaders(siteKey),
|
|
358
|
+
"Content-Type": "application/json",
|
|
359
|
+
},
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
feedback_type: type,
|
|
362
|
+
content,
|
|
363
|
+
email,
|
|
364
|
+
page_url: pageUrl,
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`Failed to submit feedback: ${response.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
return response.json();
|
|
371
|
+
}
|
|
372
|
+
export async function fetchPoll(options) {
|
|
373
|
+
const { endpoint, slug } = options;
|
|
374
|
+
const siteKey = getSiteKey(options);
|
|
375
|
+
const url = buildUrl(endpoint, `/polls/${slug}`);
|
|
376
|
+
const response = await fetch(url, {
|
|
377
|
+
headers: getHeaders(siteKey),
|
|
378
|
+
});
|
|
379
|
+
if (!response.ok) {
|
|
380
|
+
throw new Error(`Failed to fetch poll: ${response.statusText}`);
|
|
381
|
+
}
|
|
382
|
+
const data = await response.json();
|
|
383
|
+
return data.poll_with_results;
|
|
384
|
+
}
|
|
385
|
+
export async function votePoll(options) {
|
|
386
|
+
const { endpoint, slug, selectedOptions } = options;
|
|
387
|
+
const siteKey = getSiteKey(options);
|
|
388
|
+
const url = buildUrl(endpoint, `/polls/${slug}/vote`);
|
|
389
|
+
const response = await fetch(url, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: {
|
|
392
|
+
...getHeaders(siteKey),
|
|
393
|
+
"Content-Type": "application/json",
|
|
394
|
+
},
|
|
395
|
+
body: JSON.stringify({ selected_options: selectedOptions }),
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new Error(`Failed to vote: ${response.statusText}`);
|
|
399
|
+
}
|
|
400
|
+
const data = await response.json();
|
|
401
|
+
return data.vote_response;
|
|
402
|
+
}
|
|
403
|
+
export async function fetchAnnouncements(options) {
|
|
404
|
+
const { endpoint } = options;
|
|
405
|
+
const siteKey = getSiteKey(options);
|
|
406
|
+
const url = buildUrl(endpoint, "/announcements");
|
|
407
|
+
const response = await fetch(url, {
|
|
408
|
+
headers: getHeaders(siteKey),
|
|
409
|
+
});
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
throw new Error(`Failed to fetch announcements: ${response.statusText}`);
|
|
412
|
+
}
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
return data.announcements || [];
|
|
415
|
+
}
|
|
416
|
+
export async function dismissAnnouncement(options) {
|
|
417
|
+
const { endpoint, announcementId } = options;
|
|
418
|
+
const siteKey = getSiteKey(options);
|
|
419
|
+
const url = buildUrl(endpoint, `/announcements/${announcementId}/dismiss`);
|
|
420
|
+
const response = await fetch(url, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: getHeaders(siteKey),
|
|
423
|
+
});
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
throw new Error(`Failed to dismiss announcement: ${response.statusText}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Headless controller for subscribe forms.
|
|
430
|
+
* Manages state without any DOM/framework dependencies.
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* const controller = new SubscribeController({ siteKey: 'xxx' });
|
|
434
|
+
* controller.subscribe((state) => {
|
|
435
|
+
* console.log(state.status, state.message, state.error);
|
|
436
|
+
* });
|
|
437
|
+
* await controller.submit('user@example.com');
|
|
438
|
+
*/
|
|
439
|
+
export class SubscribeController {
|
|
440
|
+
config;
|
|
441
|
+
listeners = new Set();
|
|
442
|
+
_state = { status: "idle", message: null, error: null };
|
|
443
|
+
constructor(config) {
|
|
444
|
+
this.config = config;
|
|
445
|
+
}
|
|
446
|
+
/** Get current state */
|
|
447
|
+
getState() {
|
|
448
|
+
return { ...this._state };
|
|
449
|
+
}
|
|
450
|
+
/** Subscribe to state changes */
|
|
451
|
+
subscribe(listener) {
|
|
452
|
+
this.listeners.add(listener);
|
|
453
|
+
// Immediately call with current state
|
|
454
|
+
listener(this.getState());
|
|
455
|
+
return () => this.listeners.delete(listener);
|
|
456
|
+
}
|
|
457
|
+
notify() {
|
|
458
|
+
const state = this.getState();
|
|
459
|
+
for (const listener of this.listeners) {
|
|
460
|
+
listener(state);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Submit email for subscription */
|
|
464
|
+
async submit(email) {
|
|
465
|
+
this._state = { status: "loading", message: null, error: null };
|
|
466
|
+
this.notify();
|
|
467
|
+
try {
|
|
468
|
+
const result = await subscribe({ ...this.config, email });
|
|
469
|
+
this._state = { status: "success", message: result.message, error: null };
|
|
470
|
+
this.notify();
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
475
|
+
this._state = { status: "error", message: error.message, error };
|
|
476
|
+
this.notify();
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/** Reset to idle state */
|
|
481
|
+
reset() {
|
|
482
|
+
this._state = { status: "idle", message: null, error: null };
|
|
483
|
+
this.notify();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Headless controller for waitlist forms.
|
|
488
|
+
* Manages state without any DOM/framework dependencies.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* const controller = new WaitlistController({ siteKey: 'xxx' });
|
|
492
|
+
* controller.subscribe((state) => {
|
|
493
|
+
* console.log(state.status, state.message, state.position);
|
|
494
|
+
* });
|
|
495
|
+
* await controller.join('user@example.com', { name: 'John', source: 'homepage' });
|
|
496
|
+
*/
|
|
497
|
+
export class WaitlistController {
|
|
498
|
+
config;
|
|
499
|
+
listeners = new Set();
|
|
500
|
+
_state = { status: "idle", message: null, position: null, error: null };
|
|
501
|
+
constructor(config) {
|
|
502
|
+
this.config = config;
|
|
503
|
+
}
|
|
504
|
+
getState() {
|
|
505
|
+
return { ...this._state };
|
|
506
|
+
}
|
|
507
|
+
subscribe(listener) {
|
|
508
|
+
this.listeners.add(listener);
|
|
509
|
+
listener(this.getState());
|
|
510
|
+
return () => this.listeners.delete(listener);
|
|
511
|
+
}
|
|
512
|
+
notify() {
|
|
513
|
+
const state = this.getState();
|
|
514
|
+
for (const listener of this.listeners) {
|
|
515
|
+
listener(state);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/** Join the waitlist */
|
|
519
|
+
async join(email, options) {
|
|
520
|
+
this._state = { status: "loading", message: null, position: null, error: null };
|
|
521
|
+
this.notify();
|
|
522
|
+
try {
|
|
523
|
+
const result = await joinWaitlist({
|
|
524
|
+
...this.config,
|
|
525
|
+
email,
|
|
526
|
+
name: options?.name,
|
|
527
|
+
source: options?.source,
|
|
528
|
+
});
|
|
529
|
+
this._state = {
|
|
530
|
+
status: "success",
|
|
531
|
+
message: result.message,
|
|
532
|
+
position: result.position ?? null,
|
|
533
|
+
error: null,
|
|
534
|
+
};
|
|
535
|
+
this.notify();
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
540
|
+
this._state = { status: "error", message: error.message, position: null, error };
|
|
541
|
+
this.notify();
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
reset() {
|
|
546
|
+
this._state = { status: "idle", message: null, position: null, error: null };
|
|
547
|
+
this.notify();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Headless controller for forms.
|
|
552
|
+
* Manages state without any DOM/framework dependencies.
|
|
553
|
+
*/
|
|
554
|
+
export class FormController {
|
|
555
|
+
config;
|
|
556
|
+
formSlug;
|
|
557
|
+
listeners = new Set();
|
|
558
|
+
loadTime;
|
|
559
|
+
_state = { status: "idle", message: null, error: null };
|
|
560
|
+
constructor(config, formSlug) {
|
|
561
|
+
this.config = config;
|
|
562
|
+
this.formSlug = formSlug;
|
|
563
|
+
this.loadTime = Math.floor(Date.now() / 1000);
|
|
564
|
+
}
|
|
565
|
+
getState() {
|
|
566
|
+
return { ...this._state };
|
|
567
|
+
}
|
|
568
|
+
subscribe(listener) {
|
|
569
|
+
this.listeners.add(listener);
|
|
570
|
+
listener(this.getState());
|
|
571
|
+
return () => this.listeners.delete(listener);
|
|
572
|
+
}
|
|
573
|
+
notify() {
|
|
574
|
+
const state = this.getState();
|
|
575
|
+
for (const listener of this.listeners) {
|
|
576
|
+
listener(state);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async submit(data) {
|
|
580
|
+
this._state = { status: "loading", message: null, error: null };
|
|
581
|
+
this.notify();
|
|
582
|
+
try {
|
|
583
|
+
const result = await submitForm({
|
|
584
|
+
...this.config,
|
|
585
|
+
formSlug: this.formSlug,
|
|
586
|
+
data,
|
|
587
|
+
formLoadTime: this.loadTime,
|
|
588
|
+
});
|
|
589
|
+
this._state = { status: "success", message: result.message, error: null };
|
|
590
|
+
this.notify();
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
595
|
+
this._state = { status: "error", message: error.message, error };
|
|
596
|
+
this.notify();
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
reset() {
|
|
601
|
+
this._state = { status: "idle", message: null, error: null };
|
|
602
|
+
this.loadTime = Math.floor(Date.now() / 1000);
|
|
603
|
+
this.notify();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Headless controller for reactions.
|
|
608
|
+
* Manages state and counts without any DOM/framework dependencies.
|
|
609
|
+
*/
|
|
610
|
+
export class ReactionsController {
|
|
611
|
+
config;
|
|
612
|
+
pageId;
|
|
613
|
+
listeners = new Set();
|
|
614
|
+
_state = { counts: {}, userReactions: [], status: "idle", error: null };
|
|
615
|
+
constructor(config, pageId) {
|
|
616
|
+
this.config = config;
|
|
617
|
+
this.pageId = pageId;
|
|
618
|
+
}
|
|
619
|
+
getState() {
|
|
620
|
+
return { ...this._state, counts: { ...this._state.counts }, userReactions: [...this._state.userReactions] };
|
|
621
|
+
}
|
|
622
|
+
subscribe(listener) {
|
|
623
|
+
this.listeners.add(listener);
|
|
624
|
+
listener(this.getState());
|
|
625
|
+
return () => this.listeners.delete(listener);
|
|
626
|
+
}
|
|
627
|
+
notify() {
|
|
628
|
+
const state = this.getState();
|
|
629
|
+
for (const listener of this.listeners) {
|
|
630
|
+
listener(state);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/** Fetch reactions from the server */
|
|
634
|
+
async fetch() {
|
|
635
|
+
this._state = { ...this._state, status: "loading", error: null };
|
|
636
|
+
this.notify();
|
|
637
|
+
try {
|
|
638
|
+
const result = await fetchReactions({ ...this.config, pageId: this.pageId });
|
|
639
|
+
this._state = {
|
|
640
|
+
...this._state,
|
|
641
|
+
counts: result.counts,
|
|
642
|
+
userReactions: result.userReactions,
|
|
643
|
+
status: "success",
|
|
644
|
+
error: null,
|
|
645
|
+
};
|
|
646
|
+
this.notify();
|
|
647
|
+
return this.getState();
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
651
|
+
this._state = { ...this._state, status: "error", error };
|
|
652
|
+
this.notify();
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async add(reactionType = "like") {
|
|
657
|
+
try {
|
|
658
|
+
const result = await addReaction({ ...this.config, pageId: this.pageId, reactionType });
|
|
659
|
+
this._state.counts[reactionType] = result.count;
|
|
660
|
+
if (!this._state.userReactions.includes(reactionType)) {
|
|
661
|
+
this._state.userReactions = [...this._state.userReactions, reactionType];
|
|
662
|
+
}
|
|
663
|
+
this.notify();
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
667
|
+
this._state = { ...this._state, error };
|
|
668
|
+
this.notify();
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async remove(reactionType = "like") {
|
|
673
|
+
try {
|
|
674
|
+
const result = await removeReaction({ ...this.config, pageId: this.pageId, reactionType });
|
|
675
|
+
this._state.counts[reactionType] = result.count;
|
|
676
|
+
this._state.userReactions = this._state.userReactions.filter(r => r !== reactionType);
|
|
677
|
+
this.notify();
|
|
678
|
+
}
|
|
679
|
+
catch (e) {
|
|
680
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
681
|
+
this._state = { ...this._state, error };
|
|
682
|
+
this.notify();
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Headless controller for comments.
|
|
689
|
+
* Manages state and comment list without any DOM/framework dependencies.
|
|
690
|
+
*/
|
|
691
|
+
export class CommentsController {
|
|
692
|
+
config;
|
|
693
|
+
pageId;
|
|
694
|
+
listeners = new Set();
|
|
695
|
+
_state = { comments: [], status: "idle", error: null };
|
|
696
|
+
constructor(config, pageId) {
|
|
697
|
+
this.config = config;
|
|
698
|
+
this.pageId = pageId;
|
|
699
|
+
}
|
|
700
|
+
getState() {
|
|
701
|
+
return { ...this._state, comments: [...this._state.comments] };
|
|
702
|
+
}
|
|
703
|
+
subscribe(listener) {
|
|
704
|
+
this.listeners.add(listener);
|
|
705
|
+
listener(this.getState());
|
|
706
|
+
return () => this.listeners.delete(listener);
|
|
707
|
+
}
|
|
708
|
+
notify() {
|
|
709
|
+
const state = this.getState();
|
|
710
|
+
for (const listener of this.listeners) {
|
|
711
|
+
listener(state);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** Fetch comments from the server */
|
|
715
|
+
async fetch() {
|
|
716
|
+
this._state = { ...this._state, status: "loading", error: null };
|
|
717
|
+
this.notify();
|
|
718
|
+
try {
|
|
719
|
+
const comments = await fetchComments({ ...this.config, pageId: this.pageId });
|
|
720
|
+
this._state = { comments, status: "success", error: null };
|
|
721
|
+
this.notify();
|
|
722
|
+
return comments;
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
726
|
+
this._state = { ...this._state, status: "error", error };
|
|
727
|
+
this.notify();
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
async post(authorName, content, options) {
|
|
732
|
+
try {
|
|
733
|
+
const comment = await postComment({
|
|
734
|
+
...this.config,
|
|
735
|
+
pageId: this.pageId,
|
|
736
|
+
authorName,
|
|
737
|
+
content,
|
|
738
|
+
authorEmail: options?.authorEmail,
|
|
739
|
+
parentId: options?.parentId,
|
|
740
|
+
});
|
|
741
|
+
// Reload to get updated tree structure
|
|
742
|
+
await this.fetch();
|
|
743
|
+
return comment;
|
|
744
|
+
}
|
|
745
|
+
catch (e) {
|
|
746
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
747
|
+
this._state = { ...this._state, error };
|
|
748
|
+
this.notify();
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Headless controller for feedback forms.
|
|
755
|
+
* Manages state without any DOM/framework dependencies.
|
|
756
|
+
*/
|
|
757
|
+
export class FeedbackController {
|
|
758
|
+
config;
|
|
759
|
+
listeners = new Set();
|
|
760
|
+
_state = { status: "idle", message: null, error: null };
|
|
761
|
+
constructor(config) {
|
|
762
|
+
this.config = config;
|
|
763
|
+
}
|
|
764
|
+
getState() {
|
|
765
|
+
return { ...this._state };
|
|
766
|
+
}
|
|
767
|
+
subscribe(listener) {
|
|
768
|
+
this.listeners.add(listener);
|
|
769
|
+
listener(this.getState());
|
|
770
|
+
return () => this.listeners.delete(listener);
|
|
771
|
+
}
|
|
772
|
+
notify() {
|
|
773
|
+
const state = this.getState();
|
|
774
|
+
for (const listener of this.listeners) {
|
|
775
|
+
listener(state);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async submit(type, content, options) {
|
|
779
|
+
this._state = { status: "loading", message: null, error: null };
|
|
780
|
+
this.notify();
|
|
781
|
+
try {
|
|
782
|
+
const result = await submitFeedback({
|
|
783
|
+
...this.config,
|
|
784
|
+
type,
|
|
785
|
+
content,
|
|
786
|
+
email: options?.email,
|
|
787
|
+
pageUrl: options?.pageUrl,
|
|
788
|
+
});
|
|
789
|
+
this._state = { status: "success", message: result.message, error: null };
|
|
790
|
+
this.notify();
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
795
|
+
this._state = { status: "error", message: error.message, error };
|
|
796
|
+
this.notify();
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
reset() {
|
|
801
|
+
this._state = { status: "idle", message: null, error: null };
|
|
802
|
+
this.notify();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Headless controller for polls.
|
|
807
|
+
* Manages poll state, voting, and results.
|
|
808
|
+
*/
|
|
809
|
+
export class PollController {
|
|
810
|
+
config;
|
|
811
|
+
slug;
|
|
812
|
+
listeners = new Set();
|
|
813
|
+
_state = { poll: null, status: "idle", error: null };
|
|
814
|
+
constructor(config, slug) {
|
|
815
|
+
this.config = config;
|
|
816
|
+
this.slug = slug;
|
|
817
|
+
}
|
|
818
|
+
getState() {
|
|
819
|
+
return { ...this._state };
|
|
820
|
+
}
|
|
821
|
+
subscribe(listener) {
|
|
822
|
+
this.listeners.add(listener);
|
|
823
|
+
listener(this.getState());
|
|
824
|
+
return () => this.listeners.delete(listener);
|
|
825
|
+
}
|
|
826
|
+
notify() {
|
|
827
|
+
const state = this.getState();
|
|
828
|
+
for (const listener of this.listeners) {
|
|
829
|
+
listener(state);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/** Fetch poll data from the server */
|
|
833
|
+
async fetch() {
|
|
834
|
+
this._state = { ...this._state, status: "loading", error: null };
|
|
835
|
+
this.notify();
|
|
836
|
+
try {
|
|
837
|
+
const poll = await fetchPoll({ ...this.config, slug: this.slug });
|
|
838
|
+
this._state = { poll, status: "success", error: null };
|
|
839
|
+
this.notify();
|
|
840
|
+
return poll;
|
|
841
|
+
}
|
|
842
|
+
catch (e) {
|
|
843
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
844
|
+
this._state = { ...this._state, status: "error", error };
|
|
845
|
+
this.notify();
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/** Vote on the poll */
|
|
850
|
+
async vote(selectedOptions) {
|
|
851
|
+
try {
|
|
852
|
+
const result = await votePoll({
|
|
853
|
+
...this.config,
|
|
854
|
+
slug: this.slug,
|
|
855
|
+
selectedOptions,
|
|
856
|
+
});
|
|
857
|
+
// Update state with new results
|
|
858
|
+
if (this._state.poll) {
|
|
859
|
+
this._state.poll = {
|
|
860
|
+
...this._state.poll,
|
|
861
|
+
results: result.results,
|
|
862
|
+
totalVotes: result.totalVotes,
|
|
863
|
+
userVotes: selectedOptions,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
this.notify();
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
catch (e) {
|
|
870
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
871
|
+
this._state = { ...this._state, error };
|
|
872
|
+
this.notify();
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/** Check if user has voted */
|
|
877
|
+
hasVoted() {
|
|
878
|
+
return !!this._state.poll?.userVotes && this._state.poll.userVotes.length > 0;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Headless controller for announcements.
|
|
883
|
+
* Manages announcement list and dismissals.
|
|
884
|
+
*/
|
|
885
|
+
export class AnnouncementsController {
|
|
886
|
+
config;
|
|
887
|
+
listeners = new Set();
|
|
888
|
+
_state = {
|
|
889
|
+
announcements: [],
|
|
890
|
+
dismissed: new Set(),
|
|
891
|
+
status: "idle",
|
|
892
|
+
error: null,
|
|
893
|
+
};
|
|
894
|
+
constructor(config) {
|
|
895
|
+
this.config = config;
|
|
896
|
+
}
|
|
897
|
+
getState() {
|
|
898
|
+
return {
|
|
899
|
+
...this._state,
|
|
900
|
+
announcements: [...this._state.announcements],
|
|
901
|
+
dismissed: new Set(this._state.dismissed),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/** Get visible (non-dismissed) announcements */
|
|
905
|
+
getVisibleAnnouncements() {
|
|
906
|
+
return this._state.announcements.filter((a) => !this._state.dismissed.has(a.id));
|
|
907
|
+
}
|
|
908
|
+
subscribe(listener) {
|
|
909
|
+
this.listeners.add(listener);
|
|
910
|
+
listener(this.getState());
|
|
911
|
+
return () => this.listeners.delete(listener);
|
|
912
|
+
}
|
|
913
|
+
notify() {
|
|
914
|
+
const state = this.getState();
|
|
915
|
+
for (const listener of this.listeners) {
|
|
916
|
+
listener(state);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/** Fetch announcements from the server */
|
|
920
|
+
async fetch() {
|
|
921
|
+
this._state = { ...this._state, status: "loading", error: null };
|
|
922
|
+
this.notify();
|
|
923
|
+
try {
|
|
924
|
+
const announcements = await fetchAnnouncements(this.config);
|
|
925
|
+
this._state = { ...this._state, announcements, status: "success", error: null };
|
|
926
|
+
this.notify();
|
|
927
|
+
return announcements;
|
|
928
|
+
}
|
|
929
|
+
catch (e) {
|
|
930
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
931
|
+
this._state = { ...this._state, status: "error", error };
|
|
932
|
+
this.notify();
|
|
933
|
+
throw error;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/** Dismiss an announcement */
|
|
937
|
+
async dismiss(announcementId) {
|
|
938
|
+
try {
|
|
939
|
+
await dismissAnnouncement({ ...this.config, announcementId });
|
|
940
|
+
this._state.dismissed.add(announcementId);
|
|
941
|
+
this.notify();
|
|
942
|
+
}
|
|
943
|
+
catch (e) {
|
|
944
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
945
|
+
this._state = { ...this._state, error };
|
|
946
|
+
this.notify();
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Headless controller for view counts.
|
|
953
|
+
* Tracks page views and unique visitors.
|
|
954
|
+
*/
|
|
955
|
+
export class ViewCountsController {
|
|
956
|
+
config;
|
|
957
|
+
pageId;
|
|
958
|
+
listeners = new Set();
|
|
959
|
+
_state;
|
|
960
|
+
constructor(config, pageId) {
|
|
961
|
+
this.config = config;
|
|
962
|
+
this.pageId = pageId;
|
|
963
|
+
this._state = { pageId, views: 0, uniqueVisitors: 0, status: "idle", error: null };
|
|
964
|
+
}
|
|
965
|
+
getState() {
|
|
966
|
+
return { ...this._state };
|
|
967
|
+
}
|
|
968
|
+
subscribe(listener) {
|
|
969
|
+
this.listeners.add(listener);
|
|
970
|
+
listener(this.getState());
|
|
971
|
+
return () => this.listeners.delete(listener);
|
|
972
|
+
}
|
|
973
|
+
notify() {
|
|
974
|
+
const state = this.getState();
|
|
975
|
+
for (const listener of this.listeners) {
|
|
976
|
+
listener(state);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/** Fetch view counts from the server */
|
|
980
|
+
async fetch() {
|
|
981
|
+
this._state = { ...this._state, status: "loading", error: null };
|
|
982
|
+
this.notify();
|
|
983
|
+
try {
|
|
984
|
+
const counts = await getViewCounts({ ...this.config, pageId: this.pageId });
|
|
985
|
+
this._state = {
|
|
986
|
+
...this._state,
|
|
987
|
+
views: counts.views,
|
|
988
|
+
uniqueVisitors: counts.uniqueVisitors,
|
|
989
|
+
status: "success",
|
|
990
|
+
error: null,
|
|
991
|
+
};
|
|
992
|
+
this.notify();
|
|
993
|
+
return counts;
|
|
994
|
+
}
|
|
995
|
+
catch (e) {
|
|
996
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
997
|
+
this._state = { ...this._state, status: "error", error };
|
|
998
|
+
this.notify();
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/** Record a view (automatically tracks unique visitors via visitor token) */
|
|
1003
|
+
async record() {
|
|
1004
|
+
try {
|
|
1005
|
+
const result = await recordView({ ...this.config, pageId: this.pageId });
|
|
1006
|
+
this._state = {
|
|
1007
|
+
...this._state,
|
|
1008
|
+
views: result.views,
|
|
1009
|
+
uniqueVisitors: result.uniqueVisitors,
|
|
1010
|
+
};
|
|
1011
|
+
this.notify();
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
catch (e) {
|
|
1015
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
1016
|
+
this._state = { ...this._state, error };
|
|
1017
|
+
this.notify();
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|