@contentcredits/sdk 2.0.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/dist/api/client.d.ts +13 -0
- package/dist/api/comments.d.ts +27 -0
- package/dist/api/credits.d.ts +16 -0
- package/dist/auth/popup.d.ts +18 -0
- package/dist/auth/storage.d.ts +6 -0
- package/dist/auth/token.d.ts +17 -0
- package/dist/comments/index.d.ts +9 -0
- package/dist/comments/panel.d.ts +8 -0
- package/dist/comments/widget.d.ts +7 -0
- package/dist/content-credits.cjs.js +2574 -0
- package/dist/content-credits.cjs.js.map +1 -0
- package/dist/content-credits.d.ts +175 -0
- package/dist/content-credits.esm.js +2572 -0
- package/dist/content-credits.esm.js.map +1 -0
- package/dist/content-credits.umd.min.js +2 -0
- package/dist/content-credits.umd.min.js.map +1 -0
- package/dist/core/config.d.ts +2 -0
- package/dist/core/events.d.ts +8 -0
- package/dist/core/state.d.ts +10 -0
- package/dist/extension/bridge.d.ts +19 -0
- package/dist/extension/detector.d.ts +13 -0
- package/dist/index.d.ts +58 -0
- package/dist/paywall/gate.d.ts +20 -0
- package/dist/paywall/index.d.ts +9 -0
- package/dist/paywall/renderer.d.ts +16 -0
- package/dist/types/index.d.ts +174 -0
- package/dist/ui/sanitize.d.ts +24 -0
- package/dist/ui/shadow.d.ts +12 -0
- package/dist/ui/styles.d.ts +2 -0
- package/package.json +52 -0
|
@@ -0,0 +1,2574 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function resolveConfig(raw) {
|
|
4
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
5
|
+
if (!raw.apiKey || typeof raw.apiKey !== 'string' || raw.apiKey.trim() === '') {
|
|
6
|
+
throw new Error('[ContentCredits] apiKey is required. Get yours from the Content Credits admin panel.');
|
|
7
|
+
}
|
|
8
|
+
const articleUrl = (_a = raw.articleUrl) !== null && _a !== void 0 ? _a : window.location.href;
|
|
9
|
+
let hostName;
|
|
10
|
+
try {
|
|
11
|
+
hostName = new URL(articleUrl).hostname;
|
|
12
|
+
}
|
|
13
|
+
catch (_l) {
|
|
14
|
+
throw new Error(`[ContentCredits] Invalid articleUrl: "${articleUrl}"`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
apiKey: raw.apiKey.trim(),
|
|
18
|
+
articleUrl,
|
|
19
|
+
hostName,
|
|
20
|
+
pageTitle: document.title,
|
|
21
|
+
contentSelector: (_b = raw.contentSelector) !== null && _b !== void 0 ? _b : '.cc-premium-content',
|
|
22
|
+
teaserParagraphs: (_c = raw.teaserParagraphs) !== null && _c !== void 0 ? _c : 2,
|
|
23
|
+
enableComments: (_d = raw.enableComments) !== null && _d !== void 0 ? _d : true,
|
|
24
|
+
extensionId: (_e = raw.extensionId) !== null && _e !== void 0 ? _e : "ljehdpabbhgccmanhjdfacjnaigpgcml",
|
|
25
|
+
debug: (_f = raw.debug) !== null && _f !== void 0 ? _f : false,
|
|
26
|
+
apiBaseUrl: "https://api.contentcredits.com",
|
|
27
|
+
accountsUrl: "https://accounts.contentcredits.com",
|
|
28
|
+
paywallTemplate: raw.paywallTemplate,
|
|
29
|
+
onAccessGranted: raw.onAccessGranted,
|
|
30
|
+
theme: {
|
|
31
|
+
primaryColor: (_h = (_g = raw.theme) === null || _g === void 0 ? void 0 : _g.primaryColor) !== null && _h !== void 0 ? _h : '#44C678',
|
|
32
|
+
fontFamily: (_k = (_j = raw.theme) === null || _j === void 0 ? void 0 : _j.fontFamily) !== null && _k !== void 0 ? _k : "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createInitialState() {
|
|
38
|
+
return {
|
|
39
|
+
isLoading: false,
|
|
40
|
+
isExtensionAvailable: false,
|
|
41
|
+
isLoggedIn: false,
|
|
42
|
+
hasAccess: false,
|
|
43
|
+
isLoaded: false,
|
|
44
|
+
user: null,
|
|
45
|
+
creditBalance: null,
|
|
46
|
+
requiredCredits: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createState() {
|
|
50
|
+
let current = createInitialState();
|
|
51
|
+
const subscribers = [];
|
|
52
|
+
function get() {
|
|
53
|
+
return Object.assign({}, current);
|
|
54
|
+
}
|
|
55
|
+
function set(patch) {
|
|
56
|
+
current = Object.assign(Object.assign({}, current), patch);
|
|
57
|
+
subscribers.forEach(fn => fn(get()));
|
|
58
|
+
}
|
|
59
|
+
function subscribe(fn) {
|
|
60
|
+
subscribers.push(fn);
|
|
61
|
+
return () => {
|
|
62
|
+
const i = subscribers.indexOf(fn);
|
|
63
|
+
if (i >= 0)
|
|
64
|
+
subscribers.splice(i, 1);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function reset() {
|
|
68
|
+
current = createInitialState();
|
|
69
|
+
subscribers.forEach(fn => fn(get()));
|
|
70
|
+
}
|
|
71
|
+
function setUser(user) {
|
|
72
|
+
var _a;
|
|
73
|
+
set({
|
|
74
|
+
user,
|
|
75
|
+
isLoggedIn: user !== null,
|
|
76
|
+
creditBalance: (_a = user === null || user === void 0 ? void 0 : user.credits) !== null && _a !== void 0 ? _a : null,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { get, set, subscribe, reset, setUser };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createEventEmitter() {
|
|
83
|
+
const listeners = {};
|
|
84
|
+
function on(event, handler) {
|
|
85
|
+
if (!listeners[event]) {
|
|
86
|
+
listeners[event] = [];
|
|
87
|
+
}
|
|
88
|
+
listeners[event].push(handler);
|
|
89
|
+
// Return unsubscribe function
|
|
90
|
+
return () => off(event, handler);
|
|
91
|
+
}
|
|
92
|
+
function off(event, handler) {
|
|
93
|
+
const arr = listeners[event];
|
|
94
|
+
if (!arr)
|
|
95
|
+
return;
|
|
96
|
+
const idx = arr.indexOf(handler);
|
|
97
|
+
if (idx >= 0)
|
|
98
|
+
arr.splice(idx, 1);
|
|
99
|
+
}
|
|
100
|
+
function emit(event, payload) {
|
|
101
|
+
const arr = listeners[event];
|
|
102
|
+
if (arr) {
|
|
103
|
+
arr.forEach(handler => {
|
|
104
|
+
try {
|
|
105
|
+
handler(payload);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.warn(`[ContentCredits] Error in "${event}" handler:`, e);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Also dispatch as a native CustomEvent on document so vanilla listeners work
|
|
113
|
+
try {
|
|
114
|
+
document.dispatchEvent(new CustomEvent(`contentcredits:${event}`, { detail: payload, bubbles: false }));
|
|
115
|
+
}
|
|
116
|
+
catch (_a) {
|
|
117
|
+
// ignore environments without CustomEvent
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function removeAll() {
|
|
121
|
+
Object.keys(listeners).forEach(k => {
|
|
122
|
+
delete listeners[k];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { on, off, emit, removeAll };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decode a JWT payload without verifying the signature.
|
|
130
|
+
* Signature verification happens server-side on every API call.
|
|
131
|
+
*/
|
|
132
|
+
function decodeJwt(token) {
|
|
133
|
+
try {
|
|
134
|
+
const parts = token.split('.');
|
|
135
|
+
if (parts.length !== 3)
|
|
136
|
+
return null;
|
|
137
|
+
const base64Url = parts[1];
|
|
138
|
+
// Normalise Base64URL → Base64
|
|
139
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
140
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
141
|
+
const json = decodeURIComponent(atob(padded)
|
|
142
|
+
.split('')
|
|
143
|
+
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
|
144
|
+
.join(''));
|
|
145
|
+
return JSON.parse(json);
|
|
146
|
+
}
|
|
147
|
+
catch (_a) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Returns true if the JWT is expired (or unparseable). */
|
|
152
|
+
function isTokenExpired(token) {
|
|
153
|
+
const payload = decodeJwt(token);
|
|
154
|
+
if (!payload || typeof payload.exp !== 'number')
|
|
155
|
+
return true;
|
|
156
|
+
// exp is in seconds; compare against current time in seconds
|
|
157
|
+
return Date.now() / 1000 > payload.exp;
|
|
158
|
+
}
|
|
159
|
+
/** Extract the user ID from a JWT. Returns null if token is invalid. */
|
|
160
|
+
function getUserIdFromToken(token) {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
const payload = decodeJwt(token);
|
|
163
|
+
return payload ? ((_b = (_a = payload.id) !== null && _a !== void 0 ? _a : payload._id) !== null && _b !== void 0 ? _b : null) : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const SESSION_KEY = 'cc_sdk_token';
|
|
167
|
+
/**
|
|
168
|
+
* Two-layer token storage:
|
|
169
|
+
* 1. In-memory (primary) — invisible to other scripts, survives page navigations
|
|
170
|
+
* within the same JS context but gone on hard reload.
|
|
171
|
+
* 2. sessionStorage (secondary) — survives soft reloads, cleared when the tab
|
|
172
|
+
* closes, never shared across tabs.
|
|
173
|
+
*
|
|
174
|
+
* We intentionally never use document.cookie (no HttpOnly = XSS risk) or
|
|
175
|
+
* localStorage (persists indefinitely across sessions).
|
|
176
|
+
*/
|
|
177
|
+
let memoryToken = null;
|
|
178
|
+
const tokenStorage = {
|
|
179
|
+
set(token) {
|
|
180
|
+
memoryToken = token;
|
|
181
|
+
try {
|
|
182
|
+
sessionStorage.setItem(SESSION_KEY, token);
|
|
183
|
+
}
|
|
184
|
+
catch (_a) {
|
|
185
|
+
// sessionStorage unavailable (e.g. private mode with strict settings) — ok
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
get() {
|
|
189
|
+
// Memory hit
|
|
190
|
+
if (memoryToken) {
|
|
191
|
+
if (isTokenExpired(memoryToken)) {
|
|
192
|
+
this.clear();
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return memoryToken;
|
|
196
|
+
}
|
|
197
|
+
// sessionStorage fallback (page reloaded but tab still open)
|
|
198
|
+
try {
|
|
199
|
+
const stored = sessionStorage.getItem(SESSION_KEY);
|
|
200
|
+
if (stored) {
|
|
201
|
+
if (isTokenExpired(stored)) {
|
|
202
|
+
this.clear();
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
memoryToken = stored; // warm up memory layer
|
|
206
|
+
return stored;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (_a) {
|
|
210
|
+
// ignore
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
},
|
|
214
|
+
clear() {
|
|
215
|
+
memoryToken = null;
|
|
216
|
+
try {
|
|
217
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
218
|
+
}
|
|
219
|
+
catch (_a) {
|
|
220
|
+
// ignore
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
has() {
|
|
224
|
+
return this.get() !== null;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const REQUEST_TIMEOUT_MS = 12000;
|
|
229
|
+
const MAX_RETRIES = 3;
|
|
230
|
+
const RETRY_DELAY_MS = 400;
|
|
231
|
+
/** Simple hash for deduplication key */
|
|
232
|
+
function requestKey(method, url, body) {
|
|
233
|
+
return `${method}:${url}:${body !== null && body !== void 0 ? body : ''}`;
|
|
234
|
+
}
|
|
235
|
+
const inFlight = new Map();
|
|
236
|
+
async function sleep(ms) {
|
|
237
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
238
|
+
}
|
|
239
|
+
function shouldRetry(status) {
|
|
240
|
+
return status >= 500 || status === 429;
|
|
241
|
+
}
|
|
242
|
+
function createApiClient(baseUrl, emitter) {
|
|
243
|
+
async function request(method, path, body, attempt = 0) {
|
|
244
|
+
const url = `${baseUrl}${path}`;
|
|
245
|
+
const serialisedBody = body ? JSON.stringify(body) : undefined;
|
|
246
|
+
const key = requestKey(method, url, serialisedBody);
|
|
247
|
+
// Deduplicate concurrent identical requests
|
|
248
|
+
const existing = inFlight.get(key);
|
|
249
|
+
if (existing)
|
|
250
|
+
return existing;
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
253
|
+
const headers = {
|
|
254
|
+
'Content-Type': 'application/json',
|
|
255
|
+
};
|
|
256
|
+
const token = tokenStorage.get();
|
|
257
|
+
if (token) {
|
|
258
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
259
|
+
}
|
|
260
|
+
const promise = fetch(url, {
|
|
261
|
+
method,
|
|
262
|
+
headers,
|
|
263
|
+
body: serialisedBody,
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
credentials: 'omit', // SDK uses explicit Bearer header, not cookies
|
|
266
|
+
})
|
|
267
|
+
.then(async (response) => {
|
|
268
|
+
var _a;
|
|
269
|
+
clearTimeout(timeoutId);
|
|
270
|
+
if (response.status === 401) {
|
|
271
|
+
tokenStorage.clear();
|
|
272
|
+
emitter.emit('auth:logout', {});
|
|
273
|
+
throw new ApiError(401, 'Unauthorized — session expired');
|
|
274
|
+
}
|
|
275
|
+
let data;
|
|
276
|
+
try {
|
|
277
|
+
data = await response.json();
|
|
278
|
+
}
|
|
279
|
+
catch (_b) {
|
|
280
|
+
throw new ApiError(response.status, 'Invalid JSON response from server');
|
|
281
|
+
}
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
const msg = (_a = data === null || data === void 0 ? void 0 : data.message) !== null && _a !== void 0 ? _a : `HTTP ${response.status}`;
|
|
284
|
+
throw new ApiError(response.status, msg, data);
|
|
285
|
+
}
|
|
286
|
+
return data;
|
|
287
|
+
})
|
|
288
|
+
.catch(async (err) => {
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
// Retry on network error or server error (not client error)
|
|
291
|
+
const isNetworkError = err instanceof TypeError && err.message.includes('fetch');
|
|
292
|
+
const isServerError = err instanceof ApiError && shouldRetry(err.status);
|
|
293
|
+
if ((isNetworkError || isServerError) && attempt < MAX_RETRIES) {
|
|
294
|
+
inFlight.delete(key);
|
|
295
|
+
await sleep(RETRY_DELAY_MS * Math.pow(2, attempt));
|
|
296
|
+
return request(method, path, body, attempt + 1);
|
|
297
|
+
}
|
|
298
|
+
throw err;
|
|
299
|
+
})
|
|
300
|
+
.finally(() => {
|
|
301
|
+
inFlight.delete(key);
|
|
302
|
+
});
|
|
303
|
+
inFlight.set(key, promise);
|
|
304
|
+
return promise;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
get: (path) => request('GET', path),
|
|
308
|
+
post: (path, body) => request('POST', path, body),
|
|
309
|
+
put: (path, body) => request('PUT', path, body),
|
|
310
|
+
delete: (path) => request('DELETE', path),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
class ApiError extends Error {
|
|
314
|
+
constructor(status, message, data) {
|
|
315
|
+
super(message);
|
|
316
|
+
this.status = status;
|
|
317
|
+
this.data = data;
|
|
318
|
+
this.name = 'ApiError';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function createCreditsApi(client) {
|
|
323
|
+
return {
|
|
324
|
+
checkAccess(params) {
|
|
325
|
+
return client.post('/credits/check-article-access', {
|
|
326
|
+
apiKey: params.apiKey,
|
|
327
|
+
postUrl: params.postUrl,
|
|
328
|
+
postName: params.postName,
|
|
329
|
+
hostName: params.hostName,
|
|
330
|
+
});
|
|
331
|
+
},
|
|
332
|
+
purchaseArticle(params) {
|
|
333
|
+
return client.post('/credits/purchase-article', {
|
|
334
|
+
apiKey: params.apiKey,
|
|
335
|
+
postUrl: params.postUrl,
|
|
336
|
+
postName: params.postName,
|
|
337
|
+
hostName: params.hostName,
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createCommentsApi(client) {
|
|
344
|
+
return {
|
|
345
|
+
// Backend returns the thread object directly (no success wrapper)
|
|
346
|
+
ensureThread(params) {
|
|
347
|
+
return client.post('/comments/threads/ensure', {
|
|
348
|
+
pageUrl: params.pageUrl,
|
|
349
|
+
hostname: params.hostname,
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
// Backend returns { thread, comments } — no success wrapper
|
|
353
|
+
getComments(params) {
|
|
354
|
+
const encoded = encodeURIComponent(params.pageUrl);
|
|
355
|
+
return client.get(`/comments/comments/by-url?url=${encoded}&sortBy=${params.sortBy}`);
|
|
356
|
+
},
|
|
357
|
+
// Backend returns the created comment object directly
|
|
358
|
+
postComment(params) {
|
|
359
|
+
return client.post('/comments/comments', Object.assign({ threadId: params.threadId, content: params.content }, (params.parentCommentId ? { parentCommentId: params.parentCommentId } : {})));
|
|
360
|
+
},
|
|
361
|
+
// Backend returns the updated comment object directly
|
|
362
|
+
editComment(commentId, content) {
|
|
363
|
+
return client.put(`/comments/comments/${commentId}`, { content });
|
|
364
|
+
},
|
|
365
|
+
// Backend returns the deleted comment object directly
|
|
366
|
+
deleteComment(commentId) {
|
|
367
|
+
return client.delete(`/comments/comments/${commentId}`);
|
|
368
|
+
},
|
|
369
|
+
// Backend returns { success: true, data: { _id, likeCount, hasLiked } }
|
|
370
|
+
toggleLike(commentId) {
|
|
371
|
+
return client.post(`/comments/comments/${commentId}/toggle-like`, {});
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const GATE_ATTR = 'data-cc-gated';
|
|
377
|
+
function createGate(options) {
|
|
378
|
+
let gated = false;
|
|
379
|
+
let contentEl = null;
|
|
380
|
+
let hiddenNodes = [];
|
|
381
|
+
function findContent() {
|
|
382
|
+
return document.querySelector(options.selector);
|
|
383
|
+
}
|
|
384
|
+
function hide() {
|
|
385
|
+
contentEl = findContent();
|
|
386
|
+
if (!contentEl)
|
|
387
|
+
return false;
|
|
388
|
+
if (contentEl.hasAttribute(GATE_ATTR))
|
|
389
|
+
return true; // already gated
|
|
390
|
+
const paragraphs = Array.from(contentEl.querySelectorAll('p, h2, h3, h4, blockquote, ul, ol'));
|
|
391
|
+
// Collect nodes to hide (everything after the teaser threshold)
|
|
392
|
+
if (paragraphs.length > options.teaserParagraphs) {
|
|
393
|
+
const hideFrom = paragraphs[options.teaserParagraphs];
|
|
394
|
+
const childNodes = Array.from(contentEl.childNodes);
|
|
395
|
+
const pivotIndex = childNodes.findIndex(n => n === hideFrom || contentEl.contains(n) && n.compareDocumentPosition(hideFrom) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
396
|
+
hiddenNodes = childNodes.slice(pivotIndex < 0 ? options.teaserParagraphs : pivotIndex);
|
|
397
|
+
hiddenNodes.forEach(n => {
|
|
398
|
+
var _a, _b, _c, _d;
|
|
399
|
+
if (n instanceof HTMLElement || n instanceof Text) {
|
|
400
|
+
(_b = (_a = n.style) === null || _a === void 0 ? void 0 : _a.setProperty) === null || _b === void 0 ? void 0 : _b.call(_a, 'display', 'none');
|
|
401
|
+
(_d = (_c = n).setAttribute) === null || _d === void 0 ? void 0 : _d.call(_c, 'data-cc-hidden', 'true');
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// Not enough paragraphs to split — hide the entire content
|
|
407
|
+
hiddenNodes = Array.from(contentEl.childNodes);
|
|
408
|
+
hiddenNodes.forEach(n => {
|
|
409
|
+
if (n instanceof HTMLElement)
|
|
410
|
+
n.style.display = 'none';
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
contentEl.setAttribute(GATE_ATTR, 'true');
|
|
414
|
+
gated = true;
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
function reveal() {
|
|
418
|
+
if (!gated)
|
|
419
|
+
return;
|
|
420
|
+
hiddenNodes.forEach(n => {
|
|
421
|
+
if (n instanceof HTMLElement) {
|
|
422
|
+
n.style.removeProperty('display');
|
|
423
|
+
n.removeAttribute('data-cc-hidden');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
hiddenNodes = [];
|
|
427
|
+
contentEl === null || contentEl === void 0 ? void 0 : contentEl.removeAttribute(GATE_ATTR);
|
|
428
|
+
gated = false;
|
|
429
|
+
}
|
|
430
|
+
function isGated() {
|
|
431
|
+
return gated;
|
|
432
|
+
}
|
|
433
|
+
return { hide, reveal, isGated };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Creates an isolated Shadow DOM host attached to document.body.
|
|
438
|
+
* All SDK UI lives inside this shadow root so partner CSS cannot
|
|
439
|
+
* bleed in and SDK CSS cannot bleed out.
|
|
440
|
+
*/
|
|
441
|
+
function createShadowHost(id) {
|
|
442
|
+
// Reuse if already in the DOM (e.g. hot-reload)
|
|
443
|
+
let host = document.getElementById(id);
|
|
444
|
+
if (!host) {
|
|
445
|
+
host = document.createElement('div');
|
|
446
|
+
host.id = id;
|
|
447
|
+
// The host element itself is invisible; only its shadow children show
|
|
448
|
+
host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;z-index:2147483647;';
|
|
449
|
+
document.body.appendChild(host);
|
|
450
|
+
}
|
|
451
|
+
const existing = host._ccShadow;
|
|
452
|
+
if (existing)
|
|
453
|
+
return { host, root: existing };
|
|
454
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
455
|
+
host._ccShadow = root;
|
|
456
|
+
return { host, root };
|
|
457
|
+
}
|
|
458
|
+
function removeShadowHost(id) {
|
|
459
|
+
const host = document.getElementById(id);
|
|
460
|
+
if (host)
|
|
461
|
+
host.remove();
|
|
462
|
+
}
|
|
463
|
+
/** Inject a <style> tag into a shadow root */
|
|
464
|
+
function injectStyles(root, css) {
|
|
465
|
+
const existing = root.querySelector('style[data-cc-styles]');
|
|
466
|
+
if (existing) {
|
|
467
|
+
existing.textContent = css;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const style = document.createElement('style');
|
|
471
|
+
style.dataset.ccStyles = 'true';
|
|
472
|
+
style.textContent = css;
|
|
473
|
+
root.appendChild(style);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function getPaywallStyles(primaryColor, fontFamily) {
|
|
477
|
+
return `
|
|
478
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
479
|
+
|
|
480
|
+
.cc-paywall-gate {
|
|
481
|
+
position: relative;
|
|
482
|
+
width: 100%;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.cc-teaser-fade {
|
|
486
|
+
position: relative;
|
|
487
|
+
}
|
|
488
|
+
.cc-teaser-fade::after {
|
|
489
|
+
content: '';
|
|
490
|
+
position: absolute;
|
|
491
|
+
bottom: 0; left: 0; width: 100%;
|
|
492
|
+
height: 120px;
|
|
493
|
+
background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
|
|
494
|
+
pointer-events: none;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.cc-paywall-overlay {
|
|
498
|
+
background: #fff;
|
|
499
|
+
border: 1px solid #e5e7eb;
|
|
500
|
+
border-radius: 12px;
|
|
501
|
+
padding: 32px 24px;
|
|
502
|
+
max-width: 480px;
|
|
503
|
+
margin: 24px auto;
|
|
504
|
+
text-align: center;
|
|
505
|
+
font-family: ${fontFamily};
|
|
506
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.cc-paywall-overlay h2 {
|
|
510
|
+
font-size: 20px;
|
|
511
|
+
font-weight: 700;
|
|
512
|
+
color: #111827;
|
|
513
|
+
margin-bottom: 8px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.cc-paywall-overlay p {
|
|
517
|
+
font-size: 14px;
|
|
518
|
+
color: #6b7280;
|
|
519
|
+
margin-bottom: 24px;
|
|
520
|
+
line-height: 1.6;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.cc-btn {
|
|
524
|
+
display: inline-flex;
|
|
525
|
+
align-items: center;
|
|
526
|
+
justify-content: center;
|
|
527
|
+
gap: 8px;
|
|
528
|
+
height: 44px;
|
|
529
|
+
padding: 0 20px;
|
|
530
|
+
border: none;
|
|
531
|
+
border-radius: 8px;
|
|
532
|
+
font-family: ${fontFamily};
|
|
533
|
+
font-size: 15px;
|
|
534
|
+
font-weight: 600;
|
|
535
|
+
cursor: pointer;
|
|
536
|
+
transition: opacity 0.15s ease, transform 0.1s ease;
|
|
537
|
+
width: 100%;
|
|
538
|
+
max-width: 320px;
|
|
539
|
+
}
|
|
540
|
+
.cc-btn:hover:not(:disabled) { opacity: 0.88; }
|
|
541
|
+
.cc-btn:active:not(:disabled) { transform: scale(0.98); }
|
|
542
|
+
.cc-btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
543
|
+
|
|
544
|
+
.cc-btn-primary {
|
|
545
|
+
background: ${primaryColor};
|
|
546
|
+
color: #fff;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.cc-btn-secondary {
|
|
550
|
+
background: #111827;
|
|
551
|
+
color: #fff;
|
|
552
|
+
margin-top: 10px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.cc-btn-outline {
|
|
556
|
+
background: transparent;
|
|
557
|
+
color: #111827;
|
|
558
|
+
border: 2px solid #111827;
|
|
559
|
+
margin-top: 10px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.cc-credit-badge {
|
|
563
|
+
display: inline-block;
|
|
564
|
+
background: #fef3c7;
|
|
565
|
+
color: #92400e;
|
|
566
|
+
border-radius: 20px;
|
|
567
|
+
padding: 2px 10px;
|
|
568
|
+
font-size: 13px;
|
|
569
|
+
font-weight: 600;
|
|
570
|
+
margin-bottom: 16px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.cc-spinner {
|
|
574
|
+
width: 18px; height: 18px;
|
|
575
|
+
border: 2px solid rgba(255,255,255,0.4);
|
|
576
|
+
border-top-color: #fff;
|
|
577
|
+
border-radius: 50%;
|
|
578
|
+
animation: cc-spin 0.7s linear infinite;
|
|
579
|
+
flex-shrink: 0;
|
|
580
|
+
}
|
|
581
|
+
@keyframes cc-spin { to { transform: rotate(360deg); } }
|
|
582
|
+
|
|
583
|
+
.cc-powered-by {
|
|
584
|
+
margin-top: 20px;
|
|
585
|
+
font-size: 12px;
|
|
586
|
+
color: #9ca3af;
|
|
587
|
+
}
|
|
588
|
+
.cc-powered-by a {
|
|
589
|
+
color: ${primaryColor};
|
|
590
|
+
text-decoration: none;
|
|
591
|
+
font-weight: 600;
|
|
592
|
+
}
|
|
593
|
+
.cc-powered-by a:hover { text-decoration: underline; }
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
function getCommentStyles(primaryColor, fontFamily) {
|
|
597
|
+
return `
|
|
598
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
599
|
+
|
|
600
|
+
/* ── Widget Button ───────────────────────────────────── */
|
|
601
|
+
.cc-widget-btn {
|
|
602
|
+
position: fixed;
|
|
603
|
+
top: 50%;
|
|
604
|
+
right: 0;
|
|
605
|
+
transform: translateY(-50%);
|
|
606
|
+
width: auto;
|
|
607
|
+
height: 60px;
|
|
608
|
+
background: ${primaryColor};
|
|
609
|
+
border-radius: 10px 0 0 10px;
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
gap: 8px;
|
|
613
|
+
padding-left: 12px;
|
|
614
|
+
padding-right: 6px;
|
|
615
|
+
z-index: 2147483646;
|
|
616
|
+
box-shadow: -2px 0 16px rgba(0,0,0,0.12);
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
user-select: none;
|
|
619
|
+
font-family: ${fontFamily};
|
|
620
|
+
transition: background 0.2s;
|
|
621
|
+
pointer-events: all;
|
|
622
|
+
}
|
|
623
|
+
.cc-widget-btn:hover { filter: brightness(1.08); }
|
|
624
|
+
|
|
625
|
+
.cc-widget-icon { color: #fff; display: flex; align-items: center; }
|
|
626
|
+
.cc-widget-badge {
|
|
627
|
+
background: #fff;
|
|
628
|
+
color: ${primaryColor};
|
|
629
|
+
border-radius: 12px;
|
|
630
|
+
padding: 2px 7px;
|
|
631
|
+
font-size: 12px;
|
|
632
|
+
font-weight: 700;
|
|
633
|
+
min-width: 20px;
|
|
634
|
+
text-align: center;
|
|
635
|
+
}
|
|
636
|
+
.cc-widget-drag-handle {
|
|
637
|
+
color: rgba(255,255,255,0.7);
|
|
638
|
+
cursor: grab;
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
padding: 0 4px;
|
|
642
|
+
}
|
|
643
|
+
.cc-widget-drag-handle:active { cursor: grabbing; }
|
|
644
|
+
|
|
645
|
+
/* ── Panel Overlay ───────────────────────────────────── */
|
|
646
|
+
.cc-panel-backdrop {
|
|
647
|
+
position: fixed;
|
|
648
|
+
inset: 0;
|
|
649
|
+
background: rgba(0,0,0,0.35);
|
|
650
|
+
z-index: 2147483645;
|
|
651
|
+
opacity: 0;
|
|
652
|
+
transition: opacity 0.25s;
|
|
653
|
+
pointer-events: all;
|
|
654
|
+
}
|
|
655
|
+
.cc-panel-backdrop.cc-visible { opacity: 1; }
|
|
656
|
+
|
|
657
|
+
.cc-panel {
|
|
658
|
+
position: fixed;
|
|
659
|
+
top: 0; right: -500px;
|
|
660
|
+
width: 460px;
|
|
661
|
+
max-width: 95vw;
|
|
662
|
+
height: 100%;
|
|
663
|
+
background: #f9fafb;
|
|
664
|
+
z-index: 2147483646;
|
|
665
|
+
display: flex;
|
|
666
|
+
flex-direction: column;
|
|
667
|
+
box-shadow: -4px 0 32px rgba(0,0,0,0.12);
|
|
668
|
+
transition: right 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
|
669
|
+
font-family: ${fontFamily};
|
|
670
|
+
pointer-events: all;
|
|
671
|
+
}
|
|
672
|
+
.cc-panel.cc-open { right: 0; }
|
|
673
|
+
|
|
674
|
+
/* ── Panel Header ────────────────────────────────────── */
|
|
675
|
+
.cc-panel-header {
|
|
676
|
+
display: flex;
|
|
677
|
+
align-items: center;
|
|
678
|
+
gap: 10px;
|
|
679
|
+
padding: 16px 20px;
|
|
680
|
+
border-bottom: 1px solid #e5e7eb;
|
|
681
|
+
background: #fff;
|
|
682
|
+
flex-shrink: 0;
|
|
683
|
+
}
|
|
684
|
+
.cc-panel-title {
|
|
685
|
+
font-size: 16px;
|
|
686
|
+
font-weight: 700;
|
|
687
|
+
color: #111827;
|
|
688
|
+
flex: 1;
|
|
689
|
+
}
|
|
690
|
+
.cc-panel-count {
|
|
691
|
+
font-size: 13px;
|
|
692
|
+
color: #6b7280;
|
|
693
|
+
font-weight: 400;
|
|
694
|
+
}
|
|
695
|
+
.cc-panel-close-btn {
|
|
696
|
+
background: transparent;
|
|
697
|
+
border: none;
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
color: #6b7280;
|
|
700
|
+
padding: 4px;
|
|
701
|
+
display: flex;
|
|
702
|
+
align-items: center;
|
|
703
|
+
border-radius: 6px;
|
|
704
|
+
transition: background 0.15s;
|
|
705
|
+
}
|
|
706
|
+
.cc-panel-close-btn:hover { background: #f3f4f6; }
|
|
707
|
+
|
|
708
|
+
.cc-back-btn {
|
|
709
|
+
background: transparent;
|
|
710
|
+
border: none;
|
|
711
|
+
cursor: pointer;
|
|
712
|
+
color: #6b7280;
|
|
713
|
+
padding: 4px;
|
|
714
|
+
display: none;
|
|
715
|
+
align-items: center;
|
|
716
|
+
border-radius: 6px;
|
|
717
|
+
transition: background 0.15s;
|
|
718
|
+
}
|
|
719
|
+
.cc-back-btn.cc-visible { display: flex; }
|
|
720
|
+
.cc-back-btn:hover { background: #f3f4f6; }
|
|
721
|
+
|
|
722
|
+
/* ── Sort Bar ────────────────────────────────────────── */
|
|
723
|
+
.cc-sort-bar {
|
|
724
|
+
display: flex;
|
|
725
|
+
align-items: center;
|
|
726
|
+
gap: 6px;
|
|
727
|
+
padding: 10px 20px;
|
|
728
|
+
border-bottom: 1px solid #f3f4f6;
|
|
729
|
+
background: #fff;
|
|
730
|
+
flex-shrink: 0;
|
|
731
|
+
}
|
|
732
|
+
.cc-sort-label { font-size: 12px; color: #9ca3af; font-weight: 500; }
|
|
733
|
+
.cc-sort-btn {
|
|
734
|
+
background: transparent;
|
|
735
|
+
border: none;
|
|
736
|
+
cursor: pointer;
|
|
737
|
+
font-size: 12px;
|
|
738
|
+
font-weight: 600;
|
|
739
|
+
color: #6b7280;
|
|
740
|
+
padding: 4px 8px;
|
|
741
|
+
border-radius: 6px;
|
|
742
|
+
font-family: ${fontFamily};
|
|
743
|
+
transition: background 0.15s, color 0.15s;
|
|
744
|
+
}
|
|
745
|
+
.cc-sort-btn:hover { background: #f3f4f6; }
|
|
746
|
+
.cc-sort-btn.cc-active { background: #111827; color: #fff; }
|
|
747
|
+
|
|
748
|
+
/* ── Comments List ───────────────────────────────────── */
|
|
749
|
+
.cc-comments-list {
|
|
750
|
+
flex: 1;
|
|
751
|
+
overflow-y: auto;
|
|
752
|
+
overscroll-behavior: contain;
|
|
753
|
+
}
|
|
754
|
+
.cc-comments-list::-webkit-scrollbar { width: 4px; }
|
|
755
|
+
.cc-comments-list::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 4px; }
|
|
756
|
+
|
|
757
|
+
/* ── Comment Card ────────────────────────────────────── */
|
|
758
|
+
.cc-comment-card {
|
|
759
|
+
padding: 16px 20px;
|
|
760
|
+
background: #fff;
|
|
761
|
+
border-bottom: 1px solid #f3f4f6;
|
|
762
|
+
}
|
|
763
|
+
.cc-comment-card.cc-reply {
|
|
764
|
+
padding-left: 36px;
|
|
765
|
+
background: #fafafa;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.cc-comment-header {
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: flex-start;
|
|
771
|
+
justify-content: space-between;
|
|
772
|
+
margin-bottom: 8px;
|
|
773
|
+
}
|
|
774
|
+
.cc-comment-author-row { display: flex; align-items: center; gap: 10px; }
|
|
775
|
+
|
|
776
|
+
.cc-avatar {
|
|
777
|
+
width: 32px; height: 32px;
|
|
778
|
+
border-radius: 50%;
|
|
779
|
+
flex-shrink: 0;
|
|
780
|
+
display: flex;
|
|
781
|
+
align-items: center;
|
|
782
|
+
justify-content: center;
|
|
783
|
+
color: #fff;
|
|
784
|
+
font-weight: 700;
|
|
785
|
+
font-size: 12px;
|
|
786
|
+
overflow: hidden;
|
|
787
|
+
}
|
|
788
|
+
.cc-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
789
|
+
|
|
790
|
+
.cc-author-name {
|
|
791
|
+
font-size: 14px;
|
|
792
|
+
font-weight: 700;
|
|
793
|
+
color: #111827;
|
|
794
|
+
}
|
|
795
|
+
.cc-comment-time {
|
|
796
|
+
font-size: 11px;
|
|
797
|
+
color: #9ca3af;
|
|
798
|
+
margin-top: 2px;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.cc-comment-body {
|
|
802
|
+
font-size: 14px;
|
|
803
|
+
color: #374151;
|
|
804
|
+
line-height: 1.6;
|
|
805
|
+
margin-bottom: 12px;
|
|
806
|
+
word-break: break-word;
|
|
807
|
+
white-space: pre-wrap;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.cc-comment-actions {
|
|
811
|
+
display: flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
gap: 12px;
|
|
814
|
+
}
|
|
815
|
+
.cc-action-btn {
|
|
816
|
+
display: inline-flex;
|
|
817
|
+
align-items: center;
|
|
818
|
+
gap: 5px;
|
|
819
|
+
background: transparent;
|
|
820
|
+
border: none;
|
|
821
|
+
cursor: pointer;
|
|
822
|
+
font-size: 12px;
|
|
823
|
+
font-weight: 500;
|
|
824
|
+
color: #6b7280;
|
|
825
|
+
padding: 3px 6px;
|
|
826
|
+
border-radius: 6px;
|
|
827
|
+
font-family: ${fontFamily};
|
|
828
|
+
transition: background 0.15s, color 0.15s;
|
|
829
|
+
}
|
|
830
|
+
.cc-action-btn:hover { background: #f3f4f6; }
|
|
831
|
+
.cc-action-btn.cc-liked { color: #ef4444; }
|
|
832
|
+
.cc-action-btn.cc-danger:hover { color: #ef4444; background: #fef2f2; }
|
|
833
|
+
.cc-action-btn.cc-owner-actions { margin-left: auto; }
|
|
834
|
+
|
|
835
|
+
/* ── Reply Subthread ─────────────────────────────────── */
|
|
836
|
+
.cc-subthread-label {
|
|
837
|
+
font-size: 11px;
|
|
838
|
+
font-weight: 600;
|
|
839
|
+
color: #94a3b8;
|
|
840
|
+
text-transform: uppercase;
|
|
841
|
+
letter-spacing: 0.05em;
|
|
842
|
+
padding: 12px 20px 8px;
|
|
843
|
+
background: #f9fafb;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* ── Empty / Loading / Error ─────────────────────────── */
|
|
847
|
+
.cc-empty-state, .cc-loading-state, .cc-error-state {
|
|
848
|
+
text-align: center;
|
|
849
|
+
padding: 60px 20px;
|
|
850
|
+
color: #9ca3af;
|
|
851
|
+
}
|
|
852
|
+
.cc-empty-state p, .cc-loading-state p, .cc-error-state p {
|
|
853
|
+
font-size: 14px;
|
|
854
|
+
font-weight: 500;
|
|
855
|
+
color: #9ca3af;
|
|
856
|
+
margin-bottom: 6px;
|
|
857
|
+
}
|
|
858
|
+
.cc-empty-state span, .cc-loading-state span, .cc-error-state span {
|
|
859
|
+
font-size: 13px;
|
|
860
|
+
color: #d1d5db;
|
|
861
|
+
}
|
|
862
|
+
.cc-error-state .cc-retry-btn {
|
|
863
|
+
margin-top: 16px;
|
|
864
|
+
padding: 8px 16px;
|
|
865
|
+
background: #111827;
|
|
866
|
+
color: #fff;
|
|
867
|
+
border: none;
|
|
868
|
+
border-radius: 6px;
|
|
869
|
+
font-size: 13px;
|
|
870
|
+
font-weight: 500;
|
|
871
|
+
cursor: pointer;
|
|
872
|
+
font-family: ${fontFamily};
|
|
873
|
+
}
|
|
874
|
+
.cc-error-icon { color: #ef4444; margin-bottom: 12px; }
|
|
875
|
+
|
|
876
|
+
.cc-spinner-lg {
|
|
877
|
+
width: 28px; height: 28px;
|
|
878
|
+
border: 2px solid #e5e7eb;
|
|
879
|
+
border-top-color: ${primaryColor};
|
|
880
|
+
border-radius: 50%;
|
|
881
|
+
animation: cc-spin 0.7s linear infinite;
|
|
882
|
+
margin: 0 auto 12px;
|
|
883
|
+
}
|
|
884
|
+
@keyframes cc-spin { to { transform: rotate(360deg); } }
|
|
885
|
+
|
|
886
|
+
/* ── Compose Box ─────────────────────────────────────── */
|
|
887
|
+
.cc-compose {
|
|
888
|
+
border-top: 1px solid #e5e7eb;
|
|
889
|
+
padding: 12px 20px;
|
|
890
|
+
background: #fff;
|
|
891
|
+
flex-shrink: 0;
|
|
892
|
+
position: relative;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.cc-compose-textarea {
|
|
896
|
+
width: 100%;
|
|
897
|
+
min-height: 72px;
|
|
898
|
+
max-height: 180px;
|
|
899
|
+
border: 1.5px solid #d1d5db;
|
|
900
|
+
border-radius: 8px;
|
|
901
|
+
padding: 10px 12px;
|
|
902
|
+
font-size: 14px;
|
|
903
|
+
font-family: ${fontFamily};
|
|
904
|
+
color: #111827;
|
|
905
|
+
resize: vertical;
|
|
906
|
+
outline: none;
|
|
907
|
+
background: #fff;
|
|
908
|
+
transition: border-color 0.15s;
|
|
909
|
+
line-height: 1.5;
|
|
910
|
+
}
|
|
911
|
+
.cc-compose-textarea:focus { border-color: ${primaryColor}; }
|
|
912
|
+
.cc-compose-textarea::placeholder { color: #9ca3af; }
|
|
913
|
+
|
|
914
|
+
.cc-compose-actions {
|
|
915
|
+
display: flex;
|
|
916
|
+
justify-content: space-between;
|
|
917
|
+
align-items: center;
|
|
918
|
+
margin-top: 8px;
|
|
919
|
+
gap: 8px;
|
|
920
|
+
}
|
|
921
|
+
.cc-compose-cancel {
|
|
922
|
+
background: transparent;
|
|
923
|
+
border: none;
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
font-size: 13px;
|
|
926
|
+
color: #6b7280;
|
|
927
|
+
font-family: ${fontFamily};
|
|
928
|
+
padding: 6px;
|
|
929
|
+
display: none;
|
|
930
|
+
}
|
|
931
|
+
.cc-compose-cancel.cc-visible { display: block; }
|
|
932
|
+
.cc-compose-submit {
|
|
933
|
+
display: inline-flex;
|
|
934
|
+
align-items: center;
|
|
935
|
+
gap: 6px;
|
|
936
|
+
height: 36px;
|
|
937
|
+
padding: 0 16px;
|
|
938
|
+
background: ${primaryColor};
|
|
939
|
+
color: #fff;
|
|
940
|
+
border: none;
|
|
941
|
+
border-radius: 7px;
|
|
942
|
+
font-size: 13px;
|
|
943
|
+
font-weight: 600;
|
|
944
|
+
cursor: pointer;
|
|
945
|
+
font-family: ${fontFamily};
|
|
946
|
+
margin-left: auto;
|
|
947
|
+
transition: opacity 0.15s;
|
|
948
|
+
}
|
|
949
|
+
.cc-compose-submit:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
950
|
+
|
|
951
|
+
.cc-spinner-sm {
|
|
952
|
+
width: 14px; height: 14px;
|
|
953
|
+
border: 2px solid rgba(255,255,255,0.4);
|
|
954
|
+
border-top-color: #fff;
|
|
955
|
+
border-radius: 50%;
|
|
956
|
+
animation: cc-spin 0.7s linear infinite;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/* ── Login Overlay inside compose ────────────────────── */
|
|
960
|
+
.cc-login-overlay {
|
|
961
|
+
position: absolute;
|
|
962
|
+
inset: 0;
|
|
963
|
+
background: rgba(255,255,255,0.95);
|
|
964
|
+
display: flex;
|
|
965
|
+
flex-direction: column;
|
|
966
|
+
align-items: center;
|
|
967
|
+
justify-content: center;
|
|
968
|
+
gap: 10px;
|
|
969
|
+
padding: 16px;
|
|
970
|
+
border-top: 1px solid #e5e7eb;
|
|
971
|
+
}
|
|
972
|
+
.cc-login-overlay p {
|
|
973
|
+
font-size: 14px;
|
|
974
|
+
color: #6b7280;
|
|
975
|
+
text-align: center;
|
|
976
|
+
}
|
|
977
|
+
.cc-login-overlay-btn {
|
|
978
|
+
height: 40px;
|
|
979
|
+
padding: 0 20px;
|
|
980
|
+
background: ${primaryColor};
|
|
981
|
+
color: #fff;
|
|
982
|
+
border: none;
|
|
983
|
+
border-radius: 8px;
|
|
984
|
+
font-size: 14px;
|
|
985
|
+
font-weight: 600;
|
|
986
|
+
cursor: pointer;
|
|
987
|
+
font-family: ${fontFamily};
|
|
988
|
+
}
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Safe DOM text node — never use innerHTML with user-generated content.
|
|
994
|
+
* Returns a text node that can be appended to any element.
|
|
995
|
+
*/
|
|
996
|
+
/**
|
|
997
|
+
* Set an element's text content safely (no HTML injection).
|
|
998
|
+
*/
|
|
999
|
+
function setTextContent(el, str) {
|
|
1000
|
+
el.textContent = str;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Create an element with safe text content.
|
|
1004
|
+
*/
|
|
1005
|
+
function el(tag, text, attrs) {
|
|
1006
|
+
const element = document.createElement(tag);
|
|
1007
|
+
if (text !== undefined)
|
|
1008
|
+
element.textContent = text;
|
|
1009
|
+
return element;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Render comment content as safe DOM nodes.
|
|
1013
|
+
* Supports newlines → <br> but no raw HTML from user input.
|
|
1014
|
+
* Returns a DocumentFragment.
|
|
1015
|
+
*/
|
|
1016
|
+
function renderCommentContent(raw) {
|
|
1017
|
+
const fragment = document.createDocumentFragment();
|
|
1018
|
+
// Replace \n with a delimiter we can split on
|
|
1019
|
+
const lines = raw.split('\n');
|
|
1020
|
+
lines.forEach((line, i) => {
|
|
1021
|
+
fragment.appendChild(document.createTextNode(line));
|
|
1022
|
+
if (i < lines.length - 1) {
|
|
1023
|
+
fragment.appendChild(document.createElement('br'));
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
return fragment;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Validate that a URL is safe (http/https only).
|
|
1030
|
+
* Returns null if unsafe.
|
|
1031
|
+
*/
|
|
1032
|
+
function sanitizeUrl(url) {
|
|
1033
|
+
try {
|
|
1034
|
+
const parsed = new URL(url);
|
|
1035
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
1036
|
+
return null;
|
|
1037
|
+
return parsed.toString();
|
|
1038
|
+
}
|
|
1039
|
+
catch (_a) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const HOST_ID = 'cc-paywall-host';
|
|
1045
|
+
function createPaywallRenderer(config) {
|
|
1046
|
+
let root = null;
|
|
1047
|
+
let overlay = null;
|
|
1048
|
+
function init() {
|
|
1049
|
+
const { root: shadowRoot } = createShadowHost(HOST_ID);
|
|
1050
|
+
root = shadowRoot;
|
|
1051
|
+
injectStyles(root, getPaywallStyles(config.theme.primaryColor, config.theme.fontFamily));
|
|
1052
|
+
overlay = el('div');
|
|
1053
|
+
overlay.className = 'cc-paywall-overlay';
|
|
1054
|
+
root.appendChild(overlay);
|
|
1055
|
+
}
|
|
1056
|
+
function render(state, callbacks, meta) {
|
|
1057
|
+
var _a, _b, _c;
|
|
1058
|
+
if (!overlay)
|
|
1059
|
+
init();
|
|
1060
|
+
if (!overlay)
|
|
1061
|
+
return;
|
|
1062
|
+
// Clear previous content
|
|
1063
|
+
while (overlay.firstChild)
|
|
1064
|
+
overlay.removeChild(overlay.firstChild);
|
|
1065
|
+
switch (state) {
|
|
1066
|
+
case 'checking':
|
|
1067
|
+
renderChecking(overlay);
|
|
1068
|
+
break;
|
|
1069
|
+
case 'login':
|
|
1070
|
+
renderLogin(overlay, callbacks);
|
|
1071
|
+
break;
|
|
1072
|
+
case 'purchase':
|
|
1073
|
+
renderPurchase(overlay, callbacks, (_a = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _a !== void 0 ? _a : null);
|
|
1074
|
+
break;
|
|
1075
|
+
case 'insufficient':
|
|
1076
|
+
renderInsufficient(overlay, callbacks, (_b = meta === null || meta === void 0 ? void 0 : meta.requiredCredits) !== null && _b !== void 0 ? _b : null, (_c = meta === null || meta === void 0 ? void 0 : meta.creditBalance) !== null && _c !== void 0 ? _c : null);
|
|
1077
|
+
break;
|
|
1078
|
+
case 'loading':
|
|
1079
|
+
renderLoading(overlay);
|
|
1080
|
+
break;
|
|
1081
|
+
case 'granted':
|
|
1082
|
+
destroy();
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function renderChecking(parent) {
|
|
1087
|
+
const spinner = el('div');
|
|
1088
|
+
spinner.className = 'cc-spinner';
|
|
1089
|
+
spinner.style.margin = '0 auto 12px';
|
|
1090
|
+
spinner.style.width = '24px';
|
|
1091
|
+
spinner.style.height = '24px';
|
|
1092
|
+
spinner.style.borderWidth = '2px';
|
|
1093
|
+
spinner.style.borderColor = '#e5e7eb';
|
|
1094
|
+
spinner.style.borderTopColor = config.theme.primaryColor;
|
|
1095
|
+
const text = el('p', 'Checking access...');
|
|
1096
|
+
text.style.cssText = 'font-size:14px;color:#6b7280;text-align:center;font-family:' + config.theme.fontFamily;
|
|
1097
|
+
parent.appendChild(spinner);
|
|
1098
|
+
parent.appendChild(text);
|
|
1099
|
+
}
|
|
1100
|
+
function renderLogin(parent, cb) {
|
|
1101
|
+
parent.appendChild(el('h2', 'This article requires a subscription'));
|
|
1102
|
+
parent.appendChild(el('p', 'Log in with your Content Credits account to unlock this article.'));
|
|
1103
|
+
const btn = el('button', 'Login & Buy with Content Credits');
|
|
1104
|
+
btn.className = 'cc-btn cc-btn-primary';
|
|
1105
|
+
btn.addEventListener('click', cb.onLogin);
|
|
1106
|
+
parent.appendChild(btn);
|
|
1107
|
+
parent.appendChild(poweredBy());
|
|
1108
|
+
}
|
|
1109
|
+
function renderPurchase(parent, cb, credits) {
|
|
1110
|
+
parent.appendChild(el('h2', 'Unlock this article'));
|
|
1111
|
+
if (credits !== null) {
|
|
1112
|
+
const badge = el('span', `${credits} credit${credits !== 1 ? 's' : ''}`);
|
|
1113
|
+
badge.className = 'cc-credit-badge';
|
|
1114
|
+
parent.appendChild(badge);
|
|
1115
|
+
}
|
|
1116
|
+
parent.appendChild(el('p', 'Use your Content Credits balance to instantly access this premium article.'));
|
|
1117
|
+
const btn = el('button', credits !== null ? `Buy for ${credits} Credit${credits !== 1 ? 's' : ''}` : 'Buy with Content Credits');
|
|
1118
|
+
btn.className = 'cc-btn cc-btn-primary';
|
|
1119
|
+
btn.addEventListener('click', cb.onPurchase);
|
|
1120
|
+
parent.appendChild(btn);
|
|
1121
|
+
parent.appendChild(poweredBy());
|
|
1122
|
+
}
|
|
1123
|
+
function renderInsufficient(parent, cb, required, available) {
|
|
1124
|
+
parent.appendChild(el('h2', 'Not enough credits'));
|
|
1125
|
+
const detail = required !== null && available !== null
|
|
1126
|
+
? `You need ${required} credit${required !== 1 ? 's' : ''} but have ${available}. Top up to unlock this article.`
|
|
1127
|
+
: 'You don\'t have enough credits to unlock this article. Purchase more to continue.';
|
|
1128
|
+
parent.appendChild(el('p', detail));
|
|
1129
|
+
const btn = el('button', 'Buy More Credits');
|
|
1130
|
+
btn.className = 'cc-btn cc-btn-primary';
|
|
1131
|
+
btn.addEventListener('click', cb.onBuyMoreCredits);
|
|
1132
|
+
parent.appendChild(btn);
|
|
1133
|
+
parent.appendChild(poweredBy());
|
|
1134
|
+
}
|
|
1135
|
+
function renderLoading(parent) {
|
|
1136
|
+
const btn = el('button');
|
|
1137
|
+
btn.className = 'cc-btn cc-btn-primary';
|
|
1138
|
+
btn.disabled = true;
|
|
1139
|
+
const spinner = el('span');
|
|
1140
|
+
spinner.className = 'cc-spinner';
|
|
1141
|
+
btn.appendChild(spinner);
|
|
1142
|
+
btn.appendChild(document.createTextNode(' Processing…'));
|
|
1143
|
+
parent.appendChild(btn);
|
|
1144
|
+
}
|
|
1145
|
+
function poweredBy() {
|
|
1146
|
+
const div = el('div');
|
|
1147
|
+
div.className = 'cc-powered-by';
|
|
1148
|
+
div.textContent = 'Powered by ';
|
|
1149
|
+
const link = el('a', 'Content Credits');
|
|
1150
|
+
link.setAttribute('href', 'https://contentcredits.com');
|
|
1151
|
+
link.setAttribute('target', '_blank');
|
|
1152
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
1153
|
+
div.appendChild(link);
|
|
1154
|
+
return div;
|
|
1155
|
+
}
|
|
1156
|
+
function setButtonLoading(loading) {
|
|
1157
|
+
if (!overlay)
|
|
1158
|
+
return;
|
|
1159
|
+
const btn = overlay.querySelector('.cc-btn');
|
|
1160
|
+
if (!btn)
|
|
1161
|
+
return;
|
|
1162
|
+
btn.disabled = loading;
|
|
1163
|
+
if (loading) {
|
|
1164
|
+
const spinner = el('span');
|
|
1165
|
+
spinner.className = 'cc-spinner';
|
|
1166
|
+
setTextContent(btn, '');
|
|
1167
|
+
btn.appendChild(spinner);
|
|
1168
|
+
btn.appendChild(document.createTextNode(' Processing…'));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function destroy() {
|
|
1172
|
+
removeShadowHost(HOST_ID);
|
|
1173
|
+
root = null;
|
|
1174
|
+
overlay = null;
|
|
1175
|
+
}
|
|
1176
|
+
return { init, render, setButtonLoading, destroy };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const DETECTION_TIMEOUT_MS = 2000;
|
|
1180
|
+
/**
|
|
1181
|
+
* Detect whether the Content Credits browser extension is installed.
|
|
1182
|
+
*
|
|
1183
|
+
* Strategy 1: Load a known icon from the chrome-extension:// URL.
|
|
1184
|
+
* - Fastest and most reliable for Chrome/Edge.
|
|
1185
|
+
*
|
|
1186
|
+
* Strategy 2: Check a flag the extension sets on window.
|
|
1187
|
+
* - Extension can set `window.__CC_EXTENSION_LOADED = true` in a content script.
|
|
1188
|
+
*
|
|
1189
|
+
* Strategy 3: Timeout fallback — if neither resolves within DETECTION_TIMEOUT_MS,
|
|
1190
|
+
* assume not installed.
|
|
1191
|
+
*/
|
|
1192
|
+
function detectExtension(extensionId) {
|
|
1193
|
+
if (!extensionId || typeof extensionId !== 'string') {
|
|
1194
|
+
return Promise.resolve(false);
|
|
1195
|
+
}
|
|
1196
|
+
return new Promise(resolve => {
|
|
1197
|
+
let resolved = false;
|
|
1198
|
+
function done(result) {
|
|
1199
|
+
if (!resolved) {
|
|
1200
|
+
resolved = true;
|
|
1201
|
+
resolve(result);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Strategy 2: flag check (instantaneous if extension is loaded)
|
|
1205
|
+
if (window.__CC_EXTENSION_LOADED === true) {
|
|
1206
|
+
done(true);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
// Strategy 1: image load from chrome-extension:// protocol
|
|
1210
|
+
const img = new Image();
|
|
1211
|
+
img.onload = () => done(true);
|
|
1212
|
+
img.onerror = () => done(false);
|
|
1213
|
+
img.src = `chrome-extension://${extensionId}/icons/icon16.png`;
|
|
1214
|
+
// Strategy 3: timeout safety net
|
|
1215
|
+
setTimeout(() => done(false), DETECTION_TIMEOUT_MS);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* The set of origins we will accept postMessage messages from.
|
|
1221
|
+
* Only the accounts frontend and the current page itself (for extension relay).
|
|
1222
|
+
*/
|
|
1223
|
+
function getAllowedOrigins() {
|
|
1224
|
+
const origins = ["https://accounts.contentcredits.com"];
|
|
1225
|
+
// Allow current page origin (extension relays messages through the page)
|
|
1226
|
+
try {
|
|
1227
|
+
origins.push(window.location.origin);
|
|
1228
|
+
}
|
|
1229
|
+
catch (_a) {
|
|
1230
|
+
// ignore
|
|
1231
|
+
}
|
|
1232
|
+
return origins;
|
|
1233
|
+
}
|
|
1234
|
+
function createExtensionBridge() {
|
|
1235
|
+
let authHandler = null;
|
|
1236
|
+
let purchaseHandler = null;
|
|
1237
|
+
function handleMessage(event) {
|
|
1238
|
+
var _a, _b, _c, _d;
|
|
1239
|
+
// Security: validate the origin before trusting any message
|
|
1240
|
+
const allowed = getAllowedOrigins();
|
|
1241
|
+
if (!allowed.includes(event.origin) && event.origin !== window.location.origin) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const msg = event.data;
|
|
1245
|
+
if (!msg || typeof msg !== 'object' || !msg.type)
|
|
1246
|
+
return;
|
|
1247
|
+
switch (msg.type) {
|
|
1248
|
+
case 'authorization_response': {
|
|
1249
|
+
const data = ((_a = msg.data) !== null && _a !== void 0 ? _a : (_b = event.detail) === null || _b === void 0 ? void 0 : _b.data);
|
|
1250
|
+
if (data && authHandler)
|
|
1251
|
+
authHandler(data);
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
case 'purchase_response': {
|
|
1255
|
+
const data = ((_c = msg.data) !== null && _c !== void 0 ? _c : (_d = event.detail) === null || _d === void 0 ? void 0 : _d.data);
|
|
1256
|
+
if (data && purchaseHandler)
|
|
1257
|
+
purchaseHandler(data);
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function attach() {
|
|
1263
|
+
window.addEventListener('message', handleMessage);
|
|
1264
|
+
// Extension also dispatches as CustomEvents on window
|
|
1265
|
+
window.addEventListener('authorization_response', (e) => {
|
|
1266
|
+
const detail = e.detail;
|
|
1267
|
+
if ((detail === null || detail === void 0 ? void 0 : detail.data) && authHandler)
|
|
1268
|
+
authHandler(detail.data);
|
|
1269
|
+
});
|
|
1270
|
+
window.addEventListener('purchase_response', (e) => {
|
|
1271
|
+
const detail = e.detail;
|
|
1272
|
+
if ((detail === null || detail === void 0 ? void 0 : detail.data) && purchaseHandler)
|
|
1273
|
+
purchaseHandler(detail.data);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
function detach() {
|
|
1277
|
+
window.removeEventListener('message', handleMessage);
|
|
1278
|
+
}
|
|
1279
|
+
function requestAuthorization(articleId, hostName) {
|
|
1280
|
+
window.postMessage({ type: 'request_authorization', data: { articleId, hostName } }, window.location.origin);
|
|
1281
|
+
}
|
|
1282
|
+
function requestPurchase(params) {
|
|
1283
|
+
window.postMessage({ type: 'request_purchase', data: params }, window.location.origin);
|
|
1284
|
+
}
|
|
1285
|
+
function requestLogin(hostName) {
|
|
1286
|
+
window.postMessage({ type: 'request_login', data: { hostName } }, window.location.origin);
|
|
1287
|
+
}
|
|
1288
|
+
function onAuthorizationResponse(handler) {
|
|
1289
|
+
authHandler = handler;
|
|
1290
|
+
}
|
|
1291
|
+
function onPurchaseResponse(handler) {
|
|
1292
|
+
purchaseHandler = handler;
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
attach,
|
|
1296
|
+
detach,
|
|
1297
|
+
requestAuthorization,
|
|
1298
|
+
requestPurchase,
|
|
1299
|
+
requestLogin,
|
|
1300
|
+
onAuthorizationResponse,
|
|
1301
|
+
onPurchaseResponse,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const POPUP_NAME = 'ccAuthPopup';
|
|
1306
|
+
const POPUP_SPECS = 'scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=600,height=650';
|
|
1307
|
+
function centeredSpecs() {
|
|
1308
|
+
const width = 600;
|
|
1309
|
+
const height = 650;
|
|
1310
|
+
const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
|
|
1311
|
+
const top = Math.round(window.screenY + (window.outerHeight - height) / 2);
|
|
1312
|
+
return `${POPUP_SPECS},left=${left},top=${top}`;
|
|
1313
|
+
}
|
|
1314
|
+
function isMobileDevice() {
|
|
1315
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|
1316
|
+
|| window.innerWidth < 768;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Scrub `token` and `cc_token` query parameters from the current URL
|
|
1320
|
+
* so the token doesn't appear in browser history or server logs.
|
|
1321
|
+
*/
|
|
1322
|
+
function scrubTokenFromUrl() {
|
|
1323
|
+
try {
|
|
1324
|
+
const url = new URL(window.location.href);
|
|
1325
|
+
let changed = false;
|
|
1326
|
+
['token', 'cc_token'].forEach(param => {
|
|
1327
|
+
if (url.searchParams.has(param)) {
|
|
1328
|
+
url.searchParams.delete(param);
|
|
1329
|
+
changed = true;
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
if (changed) {
|
|
1333
|
+
history.replaceState(null, '', url.toString());
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
catch (_a) {
|
|
1337
|
+
// ignore in environments without history API
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Read and store a token that may have been placed in the current URL
|
|
1342
|
+
* (e.g. after a mobile redirect back from the accounts site).
|
|
1343
|
+
* Always scrubs the token from the URL after reading it.
|
|
1344
|
+
*/
|
|
1345
|
+
function consumeTokenFromUrl() {
|
|
1346
|
+
var _a;
|
|
1347
|
+
try {
|
|
1348
|
+
const url = new URL(window.location.href);
|
|
1349
|
+
const token = (_a = url.searchParams.get('token')) !== null && _a !== void 0 ? _a : url.searchParams.get('cc_token');
|
|
1350
|
+
scrubTokenFromUrl();
|
|
1351
|
+
if (token) {
|
|
1352
|
+
tokenStorage.set(token);
|
|
1353
|
+
return token;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
catch (_b) {
|
|
1357
|
+
// ignore
|
|
1358
|
+
}
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Open a centered auth popup and poll for the token callback.
|
|
1363
|
+
* Returns a promise that resolves with the token when login completes,
|
|
1364
|
+
* or null if the popup is closed without completing login.
|
|
1365
|
+
*/
|
|
1366
|
+
function openAuthPopup(authUrl) {
|
|
1367
|
+
return new Promise(resolve => {
|
|
1368
|
+
let popup = null;
|
|
1369
|
+
try {
|
|
1370
|
+
popup = window.open(authUrl, POPUP_NAME, centeredSpecs());
|
|
1371
|
+
}
|
|
1372
|
+
catch (_a) {
|
|
1373
|
+
// popup blocked — fall through to null
|
|
1374
|
+
}
|
|
1375
|
+
// Popup blocked
|
|
1376
|
+
if (!popup || popup.closed) {
|
|
1377
|
+
resolve(null);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const POLL_MS = 200;
|
|
1381
|
+
const MAX_WAIT_MS = 5 * 60 * 1000; // 5 minutes
|
|
1382
|
+
let elapsed = 0;
|
|
1383
|
+
const timer = setInterval(() => {
|
|
1384
|
+
var _a;
|
|
1385
|
+
elapsed += POLL_MS;
|
|
1386
|
+
if (!popup || popup.closed) {
|
|
1387
|
+
clearInterval(timer);
|
|
1388
|
+
resolve(tokenStorage.get()); // may have been set just before close
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (elapsed > MAX_WAIT_MS) {
|
|
1392
|
+
clearInterval(timer);
|
|
1393
|
+
try {
|
|
1394
|
+
popup.close();
|
|
1395
|
+
}
|
|
1396
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
1397
|
+
resolve(null);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
// Only readable once popup navigates back to our origin
|
|
1402
|
+
const popupUrl = popup.location.href;
|
|
1403
|
+
if (popupUrl.includes('/auth/callback') || popupUrl.includes('cc_token=') || popupUrl.includes('token=')) {
|
|
1404
|
+
const params = new URLSearchParams(popup.location.search);
|
|
1405
|
+
const token = (_a = params.get('token')) !== null && _a !== void 0 ? _a : params.get('cc_token');
|
|
1406
|
+
if (token) {
|
|
1407
|
+
tokenStorage.set(token);
|
|
1408
|
+
clearInterval(timer);
|
|
1409
|
+
try {
|
|
1410
|
+
popup.close();
|
|
1411
|
+
}
|
|
1412
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
1413
|
+
resolve(token);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
catch (_d) {
|
|
1418
|
+
// cross-origin error — popup is on accounts domain, not ours yet; keep polling
|
|
1419
|
+
}
|
|
1420
|
+
}, POLL_MS);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function createPaywall(config, creditsApi, state, emitter) {
|
|
1425
|
+
const gate = createGate({
|
|
1426
|
+
selector: config.contentSelector,
|
|
1427
|
+
teaserParagraphs: config.teaserParagraphs,
|
|
1428
|
+
});
|
|
1429
|
+
const renderer = createPaywallRenderer(config);
|
|
1430
|
+
const bridge = createExtensionBridge();
|
|
1431
|
+
let extensionAvailable = false;
|
|
1432
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
1433
|
+
function buildAuthUrl() {
|
|
1434
|
+
const redirect = encodeURIComponent(config.articleUrl);
|
|
1435
|
+
return `${"https://accounts.contentcredits.com"}/authenticate/extension?redirect=${redirect}`;
|
|
1436
|
+
}
|
|
1437
|
+
function handleAccessGranted(creditsSpent = 0, balance = 0) {
|
|
1438
|
+
var _a;
|
|
1439
|
+
state.set({ hasAccess: true, isLoaded: true, isLoading: false });
|
|
1440
|
+
gate.reveal();
|
|
1441
|
+
renderer.render('granted', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1442
|
+
emitter.emit('paywall:hidden', {});
|
|
1443
|
+
emitter.emit('article:purchased', { creditsSpent, remainingBalance: balance });
|
|
1444
|
+
(_a = config.onAccessGranted) === null || _a === void 0 ? void 0 : _a.call(config);
|
|
1445
|
+
}
|
|
1446
|
+
// ── Login ─────────────────────────────────────────────────────────────────
|
|
1447
|
+
async function doLogin() {
|
|
1448
|
+
const authUrl = buildAuthUrl();
|
|
1449
|
+
if (extensionAvailable) {
|
|
1450
|
+
bridge.requestLogin(config.hostName);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (isMobileDevice()) {
|
|
1454
|
+
// Full-page redirect — popup is blocked on mobile
|
|
1455
|
+
window.location.href = authUrl;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1459
|
+
const token = await openAuthPopup(authUrl);
|
|
1460
|
+
if (token) {
|
|
1461
|
+
state.set({ isLoggedIn: true });
|
|
1462
|
+
await checkAccess();
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
// Popup closed without login
|
|
1466
|
+
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
// ── Purchase ──────────────────────────────────────────────────────────────
|
|
1470
|
+
async function doPurchase() {
|
|
1471
|
+
var _a, _b, _c;
|
|
1472
|
+
if (!tokenStorage.has()) {
|
|
1473
|
+
await doLogin();
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (extensionAvailable) {
|
|
1477
|
+
bridge.requestPurchase({
|
|
1478
|
+
articleId: config.apiKey,
|
|
1479
|
+
hostName: config.hostName,
|
|
1480
|
+
location: config.articleUrl,
|
|
1481
|
+
title: config.pageTitle,
|
|
1482
|
+
});
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
renderer.render('loading', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1486
|
+
state.set({ isLoading: true });
|
|
1487
|
+
try {
|
|
1488
|
+
const result = await creditsApi.purchaseArticle({
|
|
1489
|
+
apiKey: config.apiKey,
|
|
1490
|
+
postUrl: config.articleUrl,
|
|
1491
|
+
postName: config.pageTitle,
|
|
1492
|
+
hostName: config.hostName,
|
|
1493
|
+
});
|
|
1494
|
+
if (result.success) {
|
|
1495
|
+
handleAccessGranted(0, 0);
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
state.set({ isLoading: false });
|
|
1499
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1500
|
+
emitter.emit('error', { message: (_a = result.message) !== null && _a !== void 0 ? _a : 'Purchase failed' });
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
catch (err) {
|
|
1504
|
+
state.set({ isLoading: false });
|
|
1505
|
+
if (err instanceof ApiError && err.status === 402) {
|
|
1506
|
+
// Insufficient credits
|
|
1507
|
+
renderer.render('insufficient', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
|
|
1508
|
+
requiredCredits: state.get().requiredCredits,
|
|
1509
|
+
creditBalance: state.get().creditBalance,
|
|
1510
|
+
});
|
|
1511
|
+
emitter.emit('credits:insufficient', {
|
|
1512
|
+
required: (_b = state.get().requiredCredits) !== null && _b !== void 0 ? _b : 0,
|
|
1513
|
+
available: (_c = state.get().creditBalance) !== null && _c !== void 0 ? _c : 0,
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1518
|
+
emitter.emit('error', { message: 'Purchase failed', error: err });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function doBuyMoreCredits() {
|
|
1523
|
+
window.open(`${"https://accounts.contentcredits.com"}/consumer/dashboard`, '_blank', 'noopener,noreferrer');
|
|
1524
|
+
}
|
|
1525
|
+
// ── Access Check ──────────────────────────────────────────────────────────
|
|
1526
|
+
async function checkAccess() {
|
|
1527
|
+
state.set({ isLoading: true });
|
|
1528
|
+
renderer.render('checking', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1529
|
+
if (extensionAvailable) {
|
|
1530
|
+
bridge.requestAuthorization(config.apiKey, config.hostName);
|
|
1531
|
+
return; // response handled in onAuthorizationResponse
|
|
1532
|
+
}
|
|
1533
|
+
if (!tokenStorage.has()) {
|
|
1534
|
+
state.set({ isLoading: false, isLoaded: true });
|
|
1535
|
+
gate.hide();
|
|
1536
|
+
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1537
|
+
emitter.emit('paywall:shown', {});
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const result = await creditsApi.checkAccess({
|
|
1542
|
+
apiKey: config.apiKey,
|
|
1543
|
+
postUrl: config.articleUrl,
|
|
1544
|
+
postName: config.pageTitle,
|
|
1545
|
+
hostName: config.hostName,
|
|
1546
|
+
});
|
|
1547
|
+
state.set({
|
|
1548
|
+
isLoading: false,
|
|
1549
|
+
isLoaded: true,
|
|
1550
|
+
hasAccess: result.success,
|
|
1551
|
+
});
|
|
1552
|
+
if (result.success) {
|
|
1553
|
+
handleAccessGranted(0, 0);
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
gate.hide();
|
|
1557
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1558
|
+
emitter.emit('paywall:shown', {});
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
catch (err) {
|
|
1562
|
+
state.set({ isLoading: false, isLoaded: true });
|
|
1563
|
+
gate.hide();
|
|
1564
|
+
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1565
|
+
if (!(err instanceof ApiError && err.status === 401)) {
|
|
1566
|
+
emitter.emit('error', { message: 'Access check failed', error: err });
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// ── Init ──────────────────────────────────────────────────────────────────
|
|
1571
|
+
async function init() {
|
|
1572
|
+
// Check if user just returned from a mobile login redirect
|
|
1573
|
+
const redirectToken = consumeTokenFromUrl();
|
|
1574
|
+
if (redirectToken) {
|
|
1575
|
+
state.set({ isLoggedIn: true });
|
|
1576
|
+
}
|
|
1577
|
+
// Detect extension
|
|
1578
|
+
extensionAvailable = await detectExtension(config.extensionId);
|
|
1579
|
+
state.set({ isExtensionAvailable: extensionAvailable });
|
|
1580
|
+
if (extensionAvailable) {
|
|
1581
|
+
bridge.attach();
|
|
1582
|
+
bridge.onAuthorizationResponse(data => {
|
|
1583
|
+
var _a, _b, _c;
|
|
1584
|
+
state.set({
|
|
1585
|
+
isLoggedIn: data.isAuthenticated,
|
|
1586
|
+
hasAccess: data.doesHaveAccess,
|
|
1587
|
+
isLoaded: true,
|
|
1588
|
+
isLoading: false,
|
|
1589
|
+
creditBalance: (_a = data.creditBalance) !== null && _a !== void 0 ? _a : null,
|
|
1590
|
+
requiredCredits: (_b = data.requiredCredits) !== null && _b !== void 0 ? _b : null,
|
|
1591
|
+
});
|
|
1592
|
+
if (!data.isAuthenticated) {
|
|
1593
|
+
gate.hide();
|
|
1594
|
+
renderer.render('login', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1595
|
+
emitter.emit('paywall:shown', {});
|
|
1596
|
+
}
|
|
1597
|
+
else if (data.doesHaveAccess) {
|
|
1598
|
+
handleAccessGranted(0, (_c = data.creditBalance) !== null && _c !== void 0 ? _c : 0);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
gate.hide();
|
|
1602
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits }, {
|
|
1603
|
+
requiredCredits: data.requiredCredits,
|
|
1604
|
+
creditBalance: data.creditBalance,
|
|
1605
|
+
});
|
|
1606
|
+
emitter.emit('paywall:shown', {});
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
bridge.onPurchaseResponse(data => {
|
|
1610
|
+
var _a, _b;
|
|
1611
|
+
state.set({ isLoading: false, isLoaded: true, hasAccess: data.doesHaveAccess });
|
|
1612
|
+
if (data.doesHaveAccess) {
|
|
1613
|
+
handleAccessGranted((_a = data.creditsSpent) !== null && _a !== void 0 ? _a : 0, (_b = data.creditBalance) !== null && _b !== void 0 ? _b : 0);
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
renderer.render('purchase', { onLogin: doLogin, onPurchase: doPurchase, onBuyMoreCredits: doBuyMoreCredits });
|
|
1617
|
+
emitter.emit('error', { message: 'Purchase failed via extension' });
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
await checkAccess();
|
|
1622
|
+
}
|
|
1623
|
+
function destroy() {
|
|
1624
|
+
bridge.detach();
|
|
1625
|
+
renderer.destroy();
|
|
1626
|
+
gate.reveal();
|
|
1627
|
+
}
|
|
1628
|
+
return { init, checkAccess, destroy };
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const POSITION_KEY = 'cc-widget-pos';
|
|
1632
|
+
const WIDGET_HEIGHT = 60;
|
|
1633
|
+
function createCommentWidget(primaryColor, onOpen) {
|
|
1634
|
+
let widget = null;
|
|
1635
|
+
let badgeEl = null;
|
|
1636
|
+
function mount() {
|
|
1637
|
+
if (document.getElementById('cc-comment-widget'))
|
|
1638
|
+
return;
|
|
1639
|
+
// Restore saved vertical position
|
|
1640
|
+
let topPercent = 50;
|
|
1641
|
+
try {
|
|
1642
|
+
const saved = localStorage.getItem(POSITION_KEY);
|
|
1643
|
+
if (saved)
|
|
1644
|
+
topPercent = JSON.parse(saved);
|
|
1645
|
+
}
|
|
1646
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
1647
|
+
widget = document.createElement('div');
|
|
1648
|
+
widget.id = 'cc-comment-widget';
|
|
1649
|
+
// Inline styles so no external stylesheet dependency and no shadow DOM needed
|
|
1650
|
+
// (widget is a minimal host-page element, panel uses shadow DOM)
|
|
1651
|
+
widget.style.cssText = `
|
|
1652
|
+
position:fixed;top:${topPercent}%;right:0;transform:translateY(-50%);
|
|
1653
|
+
height:${WIDGET_HEIGHT}px;background:${primaryColor};border-radius:10px 0 0 10px;
|
|
1654
|
+
display:flex;align-items:center;gap:8px;padding:0 8px 0 12px;
|
|
1655
|
+
z-index:2147483646;box-shadow:-2px 0 16px rgba(0,0,0,.12);
|
|
1656
|
+
cursor:pointer;user-select:none;
|
|
1657
|
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
1658
|
+
transition:filter .2s;
|
|
1659
|
+
`;
|
|
1660
|
+
widget.setAttribute('role', 'button');
|
|
1661
|
+
widget.setAttribute('aria-label', 'Open comments');
|
|
1662
|
+
widget.tabIndex = 0;
|
|
1663
|
+
// Chat icon
|
|
1664
|
+
const icon = document.createElement('div');
|
|
1665
|
+
icon.style.cssText = 'color:#fff;display:flex;align-items:center;flex-shrink:0;';
|
|
1666
|
+
icon.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
|
|
1667
|
+
// Comment count badge
|
|
1668
|
+
badgeEl = document.createElement('span');
|
|
1669
|
+
badgeEl.style.cssText = `
|
|
1670
|
+
background:#fff;color:${primaryColor};border-radius:12px;
|
|
1671
|
+
padding:2px 7px;font-size:12px;font-weight:700;min-width:20px;
|
|
1672
|
+
text-align:center;display:none;
|
|
1673
|
+
`;
|
|
1674
|
+
// Drag handle
|
|
1675
|
+
const handle = document.createElement('div');
|
|
1676
|
+
handle.style.cssText = 'color:rgba(255,255,255,.65);cursor:grab;display:flex;align-items:center;padding:0 2px;flex-shrink:0;';
|
|
1677
|
+
handle.innerHTML = `<svg width="8" height="22" viewBox="0 0 8 22" fill="none"><circle cx="4" cy="4" r="2" fill="currentColor"/><circle cx="4" cy="11" r="2" fill="currentColor"/><circle cx="4" cy="18" r="2" fill="currentColor"/></svg>`;
|
|
1678
|
+
// Click to open panel
|
|
1679
|
+
icon.addEventListener('click', onOpen);
|
|
1680
|
+
badgeEl.addEventListener('click', onOpen);
|
|
1681
|
+
// Drag to reposition
|
|
1682
|
+
let dragging = false;
|
|
1683
|
+
let startY = 0;
|
|
1684
|
+
let startTop = 0;
|
|
1685
|
+
function beginDrag(y) {
|
|
1686
|
+
dragging = true;
|
|
1687
|
+
startY = y;
|
|
1688
|
+
startTop = widget.getBoundingClientRect().top;
|
|
1689
|
+
handle.style.cursor = 'grabbing';
|
|
1690
|
+
}
|
|
1691
|
+
function moveDrag(y) {
|
|
1692
|
+
if (!dragging || !widget)
|
|
1693
|
+
return;
|
|
1694
|
+
const delta = y - startY;
|
|
1695
|
+
const newTop = Math.max(0, Math.min(window.innerHeight - WIDGET_HEIGHT, startTop + delta));
|
|
1696
|
+
const pct = (newTop / window.innerHeight) * 100;
|
|
1697
|
+
widget.style.top = `${pct}%`;
|
|
1698
|
+
}
|
|
1699
|
+
function endDrag() {
|
|
1700
|
+
if (!dragging || !widget)
|
|
1701
|
+
return;
|
|
1702
|
+
dragging = false;
|
|
1703
|
+
handle.style.cursor = 'grab';
|
|
1704
|
+
const rect = widget.getBoundingClientRect();
|
|
1705
|
+
const pct = (rect.top / window.innerHeight) * 100;
|
|
1706
|
+
try {
|
|
1707
|
+
localStorage.setItem(POSITION_KEY, JSON.stringify(pct));
|
|
1708
|
+
}
|
|
1709
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
1710
|
+
}
|
|
1711
|
+
handle.addEventListener('mousedown', e => { e.preventDefault(); e.stopPropagation(); beginDrag(e.clientY); });
|
|
1712
|
+
handle.addEventListener('touchstart', e => { e.preventDefault(); beginDrag(e.touches[0].clientY); }, { passive: false });
|
|
1713
|
+
document.addEventListener('mousemove', e => moveDrag(e.clientY));
|
|
1714
|
+
document.addEventListener('touchmove', e => moveDrag(e.touches[0].clientY), { passive: true });
|
|
1715
|
+
document.addEventListener('mouseup', endDrag);
|
|
1716
|
+
document.addEventListener('touchend', endDrag);
|
|
1717
|
+
widget.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ')
|
|
1718
|
+
onOpen(); });
|
|
1719
|
+
widget.appendChild(icon);
|
|
1720
|
+
widget.appendChild(badgeEl);
|
|
1721
|
+
widget.appendChild(handle);
|
|
1722
|
+
document.body.appendChild(widget);
|
|
1723
|
+
}
|
|
1724
|
+
function setCount(n) {
|
|
1725
|
+
if (!badgeEl)
|
|
1726
|
+
return;
|
|
1727
|
+
if (n > 0) {
|
|
1728
|
+
badgeEl.textContent = n > 99 ? '99+' : String(n);
|
|
1729
|
+
badgeEl.style.display = 'inline-block';
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
badgeEl.style.display = 'none';
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function show() {
|
|
1736
|
+
if (widget)
|
|
1737
|
+
widget.style.display = 'flex';
|
|
1738
|
+
}
|
|
1739
|
+
function hide() {
|
|
1740
|
+
if (widget)
|
|
1741
|
+
widget.style.display = 'none';
|
|
1742
|
+
}
|
|
1743
|
+
function destroy() {
|
|
1744
|
+
widget === null || widget === void 0 ? void 0 : widget.remove();
|
|
1745
|
+
widget = null;
|
|
1746
|
+
badgeEl = null;
|
|
1747
|
+
}
|
|
1748
|
+
return { mount, setCount, show, hide, destroy };
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const PANEL_HOST_ID = 'cc-comments-host';
|
|
1752
|
+
const AVATAR_COLORS = ['#6366f1', '#ec4899', '#8b5cf6', '#14b8a6', '#f59e0b', '#ef4444'];
|
|
1753
|
+
function avatarColor(name) {
|
|
1754
|
+
return AVATAR_COLORS[(name || 'A').charCodeAt(0) % AVATAR_COLORS.length];
|
|
1755
|
+
}
|
|
1756
|
+
function initials(first, last) {
|
|
1757
|
+
var _a, _b, _c, _d;
|
|
1758
|
+
return `${(_b = (_a = first === null || first === void 0 ? void 0 : first[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : ''}${(_d = (_c = last === null || last === void 0 ? void 0 : last[0]) === null || _c === void 0 ? void 0 : _c.toUpperCase()) !== null && _d !== void 0 ? _d : ''}` || '?';
|
|
1759
|
+
}
|
|
1760
|
+
function formatDate(iso) {
|
|
1761
|
+
try {
|
|
1762
|
+
const d = new Date(iso);
|
|
1763
|
+
const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
1764
|
+
const day = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
1765
|
+
return `${time} · ${day}`;
|
|
1766
|
+
}
|
|
1767
|
+
catch (_a) {
|
|
1768
|
+
return '';
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function createCommentPanel(config, commentsApi, emitter, onClose) {
|
|
1772
|
+
let root = null;
|
|
1773
|
+
let currentUserId = null;
|
|
1774
|
+
let currentThreadId = null;
|
|
1775
|
+
let currentComments = [];
|
|
1776
|
+
let currentSort = 'TOP';
|
|
1777
|
+
let viewingSubthreadFor = null;
|
|
1778
|
+
let replyingToCommentId = null;
|
|
1779
|
+
let editingCommentId = null;
|
|
1780
|
+
// ── Shadow DOM setup ──────────────────────────────────────────────────────
|
|
1781
|
+
function ensureRoot() {
|
|
1782
|
+
if (root)
|
|
1783
|
+
return root;
|
|
1784
|
+
const { root: r } = createShadowHost(PANEL_HOST_ID);
|
|
1785
|
+
root = r;
|
|
1786
|
+
injectStyles(root, getCommentStyles(config.theme.primaryColor, config.theme.fontFamily));
|
|
1787
|
+
return root;
|
|
1788
|
+
}
|
|
1789
|
+
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
1790
|
+
function refreshUser() {
|
|
1791
|
+
const token = tokenStorage.get();
|
|
1792
|
+
if (token) {
|
|
1793
|
+
currentUserId = getUserIdFromToken(token);
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
currentUserId = null;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async function doLogin() {
|
|
1800
|
+
const redirectUrl = encodeURIComponent(config.articleUrl);
|
|
1801
|
+
const authUrl = `${"https://accounts.contentcredits.com"}/authenticate/extension?redirect=${redirectUrl}`;
|
|
1802
|
+
if (isMobileDevice()) {
|
|
1803
|
+
window.location.href = authUrl;
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const token = await openAuthPopup(authUrl);
|
|
1807
|
+
if (token) {
|
|
1808
|
+
refreshUser();
|
|
1809
|
+
updateLoginOverlay();
|
|
1810
|
+
void loadComments();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
// ── Thread & Comments Loading ─────────────────────────────────────────────
|
|
1814
|
+
async function loadComments() {
|
|
1815
|
+
var _a;
|
|
1816
|
+
const r = ensureRoot();
|
|
1817
|
+
const listEl = r.getElementById('cc-comments-list');
|
|
1818
|
+
if (!listEl)
|
|
1819
|
+
return;
|
|
1820
|
+
renderLoading(listEl);
|
|
1821
|
+
try {
|
|
1822
|
+
// 1. Ensure a thread exists for this page (backend returns thread object directly)
|
|
1823
|
+
const threadRes = await commentsApi.ensureThread({
|
|
1824
|
+
pageUrl: config.articleUrl,
|
|
1825
|
+
hostname: config.hostName,
|
|
1826
|
+
});
|
|
1827
|
+
if (!threadRes._id) {
|
|
1828
|
+
renderError(listEl, 'Comments are not available for this page.');
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
currentThreadId = threadRes._id;
|
|
1832
|
+
// 2. Fetch comments (backend returns { thread, comments } — no success wrapper)
|
|
1833
|
+
const commentsRes = await commentsApi.getComments({
|
|
1834
|
+
pageUrl: config.articleUrl,
|
|
1835
|
+
sortBy: currentSort,
|
|
1836
|
+
});
|
|
1837
|
+
currentComments = (_a = commentsRes.comments) !== null && _a !== void 0 ? _a : [];
|
|
1838
|
+
if (commentsRes.thread)
|
|
1839
|
+
currentThreadId = commentsRes.thread._id;
|
|
1840
|
+
// Update count badge in header
|
|
1841
|
+
const countEl = r.getElementById('cc-header-count');
|
|
1842
|
+
if (countEl)
|
|
1843
|
+
setTextContent(countEl, String(currentComments.length));
|
|
1844
|
+
renderComments(listEl);
|
|
1845
|
+
}
|
|
1846
|
+
catch (_b) {
|
|
1847
|
+
renderError(listEl, 'Unable to reach the server. Check your connection.');
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// ── Comment Tree Building ─────────────────────────────────────────────────
|
|
1851
|
+
function buildTree(comments) {
|
|
1852
|
+
const map = new Map();
|
|
1853
|
+
const roots = [];
|
|
1854
|
+
comments.forEach(c => map.set(c._id, Object.assign(Object.assign({}, c), { replies: [] })));
|
|
1855
|
+
comments.forEach(c => {
|
|
1856
|
+
const node = map.get(c._id);
|
|
1857
|
+
if (c.parentCommentId) {
|
|
1858
|
+
const parent = map.get(c.parentCommentId);
|
|
1859
|
+
if (parent && !parent.parentCommentId)
|
|
1860
|
+
parent.replies.push(node);
|
|
1861
|
+
}
|
|
1862
|
+
else {
|
|
1863
|
+
roots.push(node);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
return roots;
|
|
1867
|
+
}
|
|
1868
|
+
function sortTree(roots) {
|
|
1869
|
+
const sorted = [...roots];
|
|
1870
|
+
if (currentSort === 'NEWEST') {
|
|
1871
|
+
sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
// TOP — sort by reply count then likes
|
|
1875
|
+
sorted.sort((a, b) => {
|
|
1876
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1877
|
+
return (((_b = (_a = b.replies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) - ((_d = (_c = a.replies) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0)) ||
|
|
1878
|
+
(((_e = b.likeCount) !== null && _e !== void 0 ? _e : 0) - ((_f = a.likeCount) !== null && _f !== void 0 ? _f : 0));
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
sorted.forEach(c => {
|
|
1882
|
+
var _a;
|
|
1883
|
+
if ((_a = c.replies) === null || _a === void 0 ? void 0 : _a.length) {
|
|
1884
|
+
c.replies.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
return sorted;
|
|
1888
|
+
}
|
|
1889
|
+
// ── DOM Rendering ─────────────────────────────────────────────────────────
|
|
1890
|
+
function renderLoading(container) {
|
|
1891
|
+
container.innerHTML = '';
|
|
1892
|
+
const div = el('div');
|
|
1893
|
+
div.className = 'cc-loading-state';
|
|
1894
|
+
const spinner = el('div');
|
|
1895
|
+
spinner.className = 'cc-spinner-lg';
|
|
1896
|
+
div.appendChild(spinner);
|
|
1897
|
+
div.appendChild(el('p', 'Loading comments…'));
|
|
1898
|
+
container.appendChild(div);
|
|
1899
|
+
}
|
|
1900
|
+
function renderError(container, message) {
|
|
1901
|
+
container.innerHTML = '';
|
|
1902
|
+
const div = el('div');
|
|
1903
|
+
div.className = 'cc-error-state';
|
|
1904
|
+
const icon = el('div');
|
|
1905
|
+
icon.className = 'cc-error-icon';
|
|
1906
|
+
icon.innerHTML = `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`;
|
|
1907
|
+
div.appendChild(icon);
|
|
1908
|
+
div.appendChild(el('p', message));
|
|
1909
|
+
const retry = el('button', 'Try Again');
|
|
1910
|
+
retry.className = 'cc-retry-btn';
|
|
1911
|
+
retry.addEventListener('click', () => void loadComments());
|
|
1912
|
+
div.appendChild(retry);
|
|
1913
|
+
container.appendChild(div);
|
|
1914
|
+
}
|
|
1915
|
+
function renderComments(container) {
|
|
1916
|
+
container.innerHTML = '';
|
|
1917
|
+
const r = ensureRoot();
|
|
1918
|
+
// Update back button and title
|
|
1919
|
+
const backBtn = r.getElementById('cc-back-btn');
|
|
1920
|
+
const titleEl = r.getElementById('cc-panel-title');
|
|
1921
|
+
if (viewingSubthreadFor) {
|
|
1922
|
+
backBtn === null || backBtn === void 0 ? void 0 : backBtn.classList.add('cc-visible');
|
|
1923
|
+
if (titleEl)
|
|
1924
|
+
setTextContent(titleEl, 'Replies');
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
backBtn === null || backBtn === void 0 ? void 0 : backBtn.classList.remove('cc-visible');
|
|
1928
|
+
if (titleEl)
|
|
1929
|
+
setTextContent(titleEl, 'Comments');
|
|
1930
|
+
}
|
|
1931
|
+
if (viewingSubthreadFor) {
|
|
1932
|
+
renderSubthread(container, viewingSubthreadFor);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (currentComments.length === 0) {
|
|
1936
|
+
const empty = el('div');
|
|
1937
|
+
empty.className = 'cc-empty-state';
|
|
1938
|
+
empty.appendChild(el('p', 'No comments yet'));
|
|
1939
|
+
const sub = el('span', 'Be the first to share your thoughts');
|
|
1940
|
+
empty.appendChild(sub);
|
|
1941
|
+
container.appendChild(empty);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const tree = sortTree(buildTree(currentComments));
|
|
1945
|
+
tree.forEach(c => container.appendChild(buildCommentEl(c, false)));
|
|
1946
|
+
}
|
|
1947
|
+
function renderSubthread(container, parentId) {
|
|
1948
|
+
var _a, _b, _c, _d, _e;
|
|
1949
|
+
const tree = buildTree(currentComments);
|
|
1950
|
+
const parent = tree.find(c => c._id === parentId);
|
|
1951
|
+
if (!parent)
|
|
1952
|
+
return;
|
|
1953
|
+
container.appendChild(buildCommentEl(parent, false));
|
|
1954
|
+
const label = el('div', `${(_b = (_a = parent.replies) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0} ${((_d = (_c = parent.replies) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) === 1 ? 'REPLY' : 'REPLIES'}`);
|
|
1955
|
+
label.className = 'cc-subthread-label';
|
|
1956
|
+
container.appendChild(label);
|
|
1957
|
+
if (!((_e = parent.replies) === null || _e === void 0 ? void 0 : _e.length)) {
|
|
1958
|
+
const empty = el('div');
|
|
1959
|
+
empty.className = 'cc-empty-state';
|
|
1960
|
+
empty.style.paddingTop = '20px';
|
|
1961
|
+
empty.appendChild(el('p', 'No replies yet'));
|
|
1962
|
+
container.appendChild(empty);
|
|
1963
|
+
}
|
|
1964
|
+
else {
|
|
1965
|
+
parent.replies.forEach(r => container.appendChild(buildCommentEl(r, true)));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function buildCommentEl(comment, isReply) {
|
|
1969
|
+
var _a, _b;
|
|
1970
|
+
const isOwn = !!(currentUserId && comment.authorId === currentUserId);
|
|
1971
|
+
const author = comment.author;
|
|
1972
|
+
const authorName = author ? `${author.firstName} ${author.lastName}`.trim() : 'Anonymous';
|
|
1973
|
+
const avatarBg = avatarColor(authorName);
|
|
1974
|
+
const inis = author ? initials(author.firstName, author.lastName) : '?';
|
|
1975
|
+
const card = el('div');
|
|
1976
|
+
card.className = `cc-comment-card${isReply ? ' cc-reply' : ''}`;
|
|
1977
|
+
card.dataset.commentId = comment._id;
|
|
1978
|
+
// Header row
|
|
1979
|
+
const header = el('div');
|
|
1980
|
+
header.className = 'cc-comment-header';
|
|
1981
|
+
const authorRow = el('div');
|
|
1982
|
+
authorRow.className = 'cc-comment-author-row';
|
|
1983
|
+
// Avatar
|
|
1984
|
+
const avatar = el('div');
|
|
1985
|
+
avatar.className = 'cc-avatar';
|
|
1986
|
+
avatar.style.background = avatarBg;
|
|
1987
|
+
if (author === null || author === void 0 ? void 0 : author.profilePicture) {
|
|
1988
|
+
const safeUrl = sanitizeUrl(author.profilePicture.startsWith('http')
|
|
1989
|
+
? author.profilePicture
|
|
1990
|
+
: `${config.apiBaseUrl}${author.profilePicture}`);
|
|
1991
|
+
if (safeUrl) {
|
|
1992
|
+
const img = el('img');
|
|
1993
|
+
img.setAttribute('src', safeUrl);
|
|
1994
|
+
img.setAttribute('alt', authorName);
|
|
1995
|
+
img.addEventListener('error', () => {
|
|
1996
|
+
img.remove();
|
|
1997
|
+
setTextContent(avatar, inis);
|
|
1998
|
+
});
|
|
1999
|
+
avatar.appendChild(img);
|
|
2000
|
+
}
|
|
2001
|
+
else {
|
|
2002
|
+
setTextContent(avatar, inis);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
setTextContent(avatar, inis);
|
|
2007
|
+
}
|
|
2008
|
+
const authorMeta = el('div');
|
|
2009
|
+
const nameEl = el('div', authorName);
|
|
2010
|
+
nameEl.className = 'cc-author-name';
|
|
2011
|
+
const timeEl = el('div', formatDate(comment.createdAt));
|
|
2012
|
+
timeEl.className = 'cc-comment-time';
|
|
2013
|
+
authorMeta.appendChild(nameEl);
|
|
2014
|
+
authorMeta.appendChild(timeEl);
|
|
2015
|
+
authorRow.appendChild(avatar);
|
|
2016
|
+
authorRow.appendChild(authorMeta);
|
|
2017
|
+
header.appendChild(authorRow);
|
|
2018
|
+
card.appendChild(header);
|
|
2019
|
+
// Body — safe DOM construction, never innerHTML of user content
|
|
2020
|
+
const body = el('div');
|
|
2021
|
+
body.className = 'cc-comment-body';
|
|
2022
|
+
body.appendChild(renderCommentContent(comment.content));
|
|
2023
|
+
card.appendChild(body);
|
|
2024
|
+
// Actions row
|
|
2025
|
+
const actions = el('div');
|
|
2026
|
+
actions.className = 'cc-comment-actions';
|
|
2027
|
+
// Reply button (only on top-level)
|
|
2028
|
+
if (!isReply) {
|
|
2029
|
+
const replyBtn = el('button');
|
|
2030
|
+
replyBtn.className = 'cc-action-btn';
|
|
2031
|
+
replyBtn.dataset.commentId = comment._id;
|
|
2032
|
+
replyBtn.dataset.action = 'reply';
|
|
2033
|
+
replyBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:scaleX(-1)"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
|
|
2034
|
+
replyBtn.appendChild(document.createTextNode(` ${((_a = comment.replies) === null || _a === void 0 ? void 0 : _a.length) || 'Reply'}`));
|
|
2035
|
+
actions.appendChild(replyBtn);
|
|
2036
|
+
}
|
|
2037
|
+
// Like button
|
|
2038
|
+
const likeBtn = el('button');
|
|
2039
|
+
likeBtn.className = `cc-action-btn${comment.hasLiked ? ' cc-liked' : ''}`;
|
|
2040
|
+
likeBtn.dataset.commentId = comment._id;
|
|
2041
|
+
likeBtn.dataset.action = 'like';
|
|
2042
|
+
likeBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="${comment.hasLiked ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`;
|
|
2043
|
+
likeBtn.appendChild(document.createTextNode(` ${(_b = comment.likeCount) !== null && _b !== void 0 ? _b : 0}`));
|
|
2044
|
+
actions.appendChild(likeBtn);
|
|
2045
|
+
// Owner controls
|
|
2046
|
+
if (isOwn) {
|
|
2047
|
+
const ownerDiv = el('div');
|
|
2048
|
+
ownerDiv.className = 'cc-action-btn cc-owner-actions';
|
|
2049
|
+
ownerDiv.style.cssText = 'margin-left:auto;display:flex;gap:4px;background:transparent;border:none;padding:0;';
|
|
2050
|
+
const editBtn = el('button', 'Edit');
|
|
2051
|
+
editBtn.className = 'cc-action-btn';
|
|
2052
|
+
editBtn.dataset.commentId = comment._id;
|
|
2053
|
+
editBtn.dataset.action = 'edit';
|
|
2054
|
+
const deleteBtn = el('button', 'Delete');
|
|
2055
|
+
deleteBtn.className = 'cc-action-btn cc-danger';
|
|
2056
|
+
deleteBtn.dataset.commentId = comment._id;
|
|
2057
|
+
deleteBtn.dataset.action = 'delete';
|
|
2058
|
+
ownerDiv.appendChild(editBtn);
|
|
2059
|
+
ownerDiv.appendChild(deleteBtn);
|
|
2060
|
+
actions.appendChild(ownerDiv);
|
|
2061
|
+
}
|
|
2062
|
+
card.appendChild(actions);
|
|
2063
|
+
return card;
|
|
2064
|
+
}
|
|
2065
|
+
// ── Panel DOM Structure ───────────────────────────────────────────────────
|
|
2066
|
+
function buildPanel() {
|
|
2067
|
+
const panel = el('div');
|
|
2068
|
+
panel.className = 'cc-panel';
|
|
2069
|
+
panel.id = 'cc-comments-panel';
|
|
2070
|
+
// Header
|
|
2071
|
+
const header = el('div');
|
|
2072
|
+
header.className = 'cc-panel-header';
|
|
2073
|
+
const backBtn = el('button');
|
|
2074
|
+
backBtn.className = 'cc-back-btn';
|
|
2075
|
+
backBtn.id = 'cc-back-btn';
|
|
2076
|
+
backBtn.setAttribute('aria-label', 'Back');
|
|
2077
|
+
backBtn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>`;
|
|
2078
|
+
backBtn.addEventListener('click', () => {
|
|
2079
|
+
viewingSubthreadFor = null;
|
|
2080
|
+
replyingToCommentId = null;
|
|
2081
|
+
const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
|
|
2082
|
+
if (listEl)
|
|
2083
|
+
renderComments(listEl);
|
|
2084
|
+
});
|
|
2085
|
+
const titleGroup = el('div');
|
|
2086
|
+
titleGroup.style.cssText = 'flex:1;display:flex;align-items:center;gap:6px;';
|
|
2087
|
+
const titleEl = el('span', 'Comments');
|
|
2088
|
+
titleEl.className = 'cc-panel-title';
|
|
2089
|
+
titleEl.id = 'cc-panel-title';
|
|
2090
|
+
const countEl = el('span', '');
|
|
2091
|
+
countEl.className = 'cc-panel-count';
|
|
2092
|
+
countEl.id = 'cc-header-count';
|
|
2093
|
+
titleGroup.appendChild(titleEl);
|
|
2094
|
+
titleGroup.appendChild(countEl);
|
|
2095
|
+
const closeBtn = el('button');
|
|
2096
|
+
closeBtn.className = 'cc-panel-close-btn';
|
|
2097
|
+
closeBtn.setAttribute('aria-label', 'Close comments');
|
|
2098
|
+
closeBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
|
2099
|
+
closeBtn.addEventListener('click', closePanel);
|
|
2100
|
+
header.appendChild(backBtn);
|
|
2101
|
+
header.appendChild(titleGroup);
|
|
2102
|
+
header.appendChild(closeBtn);
|
|
2103
|
+
panel.appendChild(header);
|
|
2104
|
+
// Sort bar
|
|
2105
|
+
const sortBar = el('div');
|
|
2106
|
+
sortBar.className = 'cc-sort-bar';
|
|
2107
|
+
const sortLabel = el('span', 'Sort:');
|
|
2108
|
+
sortLabel.className = 'cc-sort-label';
|
|
2109
|
+
sortBar.appendChild(sortLabel);
|
|
2110
|
+
['TOP', 'NEWEST'].forEach(sort => {
|
|
2111
|
+
const btn = el('button', sort);
|
|
2112
|
+
btn.className = `cc-sort-btn${currentSort === sort ? ' cc-active' : ''}`;
|
|
2113
|
+
btn.dataset.sort = sort;
|
|
2114
|
+
btn.addEventListener('click', () => {
|
|
2115
|
+
if (currentSort === sort)
|
|
2116
|
+
return;
|
|
2117
|
+
currentSort = sort;
|
|
2118
|
+
sortBar.querySelectorAll('.cc-sort-btn').forEach(b => b.classList.remove('cc-active'));
|
|
2119
|
+
btn.classList.add('cc-active');
|
|
2120
|
+
void loadComments();
|
|
2121
|
+
});
|
|
2122
|
+
sortBar.appendChild(btn);
|
|
2123
|
+
});
|
|
2124
|
+
panel.appendChild(sortBar);
|
|
2125
|
+
// Comments list
|
|
2126
|
+
const list = el('div');
|
|
2127
|
+
list.className = 'cc-comments-list';
|
|
2128
|
+
list.id = 'cc-comments-list';
|
|
2129
|
+
list.addEventListener('click', handleListClick);
|
|
2130
|
+
panel.appendChild(list);
|
|
2131
|
+
// Compose area
|
|
2132
|
+
panel.appendChild(buildCompose());
|
|
2133
|
+
return panel;
|
|
2134
|
+
}
|
|
2135
|
+
function buildCompose() {
|
|
2136
|
+
const compose = el('div');
|
|
2137
|
+
compose.className = 'cc-compose';
|
|
2138
|
+
compose.id = 'cc-compose';
|
|
2139
|
+
const textarea = el('textarea');
|
|
2140
|
+
textarea.className = 'cc-compose-textarea';
|
|
2141
|
+
textarea.id = 'cc-compose-textarea';
|
|
2142
|
+
textarea.setAttribute('placeholder', 'Write a comment…');
|
|
2143
|
+
textarea.setAttribute('rows', '3');
|
|
2144
|
+
compose.appendChild(textarea);
|
|
2145
|
+
const actions = el('div');
|
|
2146
|
+
actions.className = 'cc-compose-actions';
|
|
2147
|
+
const cancelBtn = el('button', 'Cancel');
|
|
2148
|
+
cancelBtn.className = 'cc-compose-cancel';
|
|
2149
|
+
cancelBtn.id = 'cc-compose-cancel';
|
|
2150
|
+
cancelBtn.addEventListener('click', cancelEdit);
|
|
2151
|
+
actions.appendChild(cancelBtn);
|
|
2152
|
+
const submitBtn = el('button', 'Post');
|
|
2153
|
+
submitBtn.className = 'cc-compose-submit';
|
|
2154
|
+
submitBtn.id = 'cc-compose-submit';
|
|
2155
|
+
submitBtn.addEventListener('click', () => void handleSubmit());
|
|
2156
|
+
actions.appendChild(submitBtn);
|
|
2157
|
+
compose.appendChild(actions);
|
|
2158
|
+
// Login overlay inside compose
|
|
2159
|
+
if (!tokenStorage.has()) {
|
|
2160
|
+
compose.appendChild(buildLoginOverlay());
|
|
2161
|
+
}
|
|
2162
|
+
return compose;
|
|
2163
|
+
}
|
|
2164
|
+
function buildLoginOverlay() {
|
|
2165
|
+
const overlay = el('div');
|
|
2166
|
+
overlay.className = 'cc-login-overlay';
|
|
2167
|
+
overlay.id = 'cc-login-overlay';
|
|
2168
|
+
overlay.appendChild(el('p', 'Sign in to join the conversation'));
|
|
2169
|
+
const btn = el('button', 'Login with Content Credits');
|
|
2170
|
+
btn.className = 'cc-login-overlay-btn';
|
|
2171
|
+
btn.addEventListener('click', () => void doLogin());
|
|
2172
|
+
overlay.appendChild(btn);
|
|
2173
|
+
return overlay;
|
|
2174
|
+
}
|
|
2175
|
+
// ── Interactions ──────────────────────────────────────────────────────────
|
|
2176
|
+
function handleListClick(e) {
|
|
2177
|
+
const target = e.target;
|
|
2178
|
+
const btn = target.closest('[data-action]');
|
|
2179
|
+
if (!btn)
|
|
2180
|
+
return;
|
|
2181
|
+
const action = btn.dataset.action;
|
|
2182
|
+
const commentId = btn.dataset.commentId;
|
|
2183
|
+
if (!commentId)
|
|
2184
|
+
return;
|
|
2185
|
+
switch (action) {
|
|
2186
|
+
case 'reply':
|
|
2187
|
+
handleReply(commentId);
|
|
2188
|
+
break;
|
|
2189
|
+
case 'like':
|
|
2190
|
+
void handleLike(commentId);
|
|
2191
|
+
break;
|
|
2192
|
+
case 'edit':
|
|
2193
|
+
handleEdit(commentId);
|
|
2194
|
+
break;
|
|
2195
|
+
case 'delete':
|
|
2196
|
+
void handleDelete(commentId);
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
function handleReply(commentId) {
|
|
2201
|
+
if (!viewingSubthreadFor) {
|
|
2202
|
+
// First click on Reply → drill into the subthread view
|
|
2203
|
+
viewingSubthreadFor = commentId;
|
|
2204
|
+
const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
|
|
2205
|
+
if (listEl)
|
|
2206
|
+
renderComments(listEl);
|
|
2207
|
+
}
|
|
2208
|
+
else {
|
|
2209
|
+
// Already in subthread → set the reply target and focus textarea
|
|
2210
|
+
replyingToCommentId = commentId;
|
|
2211
|
+
editingCommentId = null;
|
|
2212
|
+
const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
|
|
2213
|
+
if (textarea) {
|
|
2214
|
+
textarea.placeholder = 'Write a reply…';
|
|
2215
|
+
textarea.focus();
|
|
2216
|
+
}
|
|
2217
|
+
showCancel();
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
async function handleLike(commentId) {
|
|
2221
|
+
var _a, _b, _c, _d, _e;
|
|
2222
|
+
if (!tokenStorage.has()) {
|
|
2223
|
+
void doLogin();
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
// Optimistic update
|
|
2227
|
+
const comment = currentComments.find(c => c._id === commentId);
|
|
2228
|
+
if (!comment)
|
|
2229
|
+
return;
|
|
2230
|
+
const wasLiked = comment.hasLiked;
|
|
2231
|
+
comment.hasLiked = !wasLiked;
|
|
2232
|
+
comment.likeCount = ((_a = comment.likeCount) !== null && _a !== void 0 ? _a : 0) + (comment.hasLiked ? 1 : -1);
|
|
2233
|
+
const listEl = root === null || root === void 0 ? void 0 : root.getElementById('cc-comments-list');
|
|
2234
|
+
if (listEl)
|
|
2235
|
+
renderComments(listEl);
|
|
2236
|
+
try {
|
|
2237
|
+
// Backend returns { success: true, data: { _id, likeCount, hasLiked } }
|
|
2238
|
+
const res = await commentsApi.toggleLike(commentId);
|
|
2239
|
+
if (res.success) {
|
|
2240
|
+
if (typeof ((_b = res.data) === null || _b === void 0 ? void 0 : _b.hasLiked) === 'boolean')
|
|
2241
|
+
comment.hasLiked = res.data.hasLiked;
|
|
2242
|
+
if (typeof ((_c = res.data) === null || _c === void 0 ? void 0 : _c.likeCount) === 'number')
|
|
2243
|
+
comment.likeCount = res.data.likeCount;
|
|
2244
|
+
emitter.emit('comment:liked', { commentId, hasLiked: comment.hasLiked });
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
// Rollback
|
|
2248
|
+
comment.hasLiked = wasLiked;
|
|
2249
|
+
comment.likeCount = ((_d = comment.likeCount) !== null && _d !== void 0 ? _d : 0) + (wasLiked ? 1 : -1);
|
|
2250
|
+
}
|
|
2251
|
+
if (listEl)
|
|
2252
|
+
renderComments(listEl);
|
|
2253
|
+
}
|
|
2254
|
+
catch (_f) {
|
|
2255
|
+
comment.hasLiked = wasLiked;
|
|
2256
|
+
comment.likeCount = ((_e = comment.likeCount) !== null && _e !== void 0 ? _e : 0) + (wasLiked ? 1 : -1);
|
|
2257
|
+
if (listEl)
|
|
2258
|
+
renderComments(listEl);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function handleEdit(commentId) {
|
|
2262
|
+
const comment = currentComments.find(c => c._id === commentId);
|
|
2263
|
+
if (!comment)
|
|
2264
|
+
return;
|
|
2265
|
+
editingCommentId = commentId;
|
|
2266
|
+
replyingToCommentId = null;
|
|
2267
|
+
const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
|
|
2268
|
+
if (textarea) {
|
|
2269
|
+
textarea.value = comment.content;
|
|
2270
|
+
textarea.placeholder = 'Edit your comment…';
|
|
2271
|
+
textarea.focus();
|
|
2272
|
+
}
|
|
2273
|
+
const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
|
|
2274
|
+
if (submitBtn)
|
|
2275
|
+
setTextContent(submitBtn, 'Update');
|
|
2276
|
+
showCancel();
|
|
2277
|
+
}
|
|
2278
|
+
async function handleDelete(commentId) {
|
|
2279
|
+
if (!confirm('Delete this comment?'))
|
|
2280
|
+
return;
|
|
2281
|
+
try {
|
|
2282
|
+
// Backend returns the deleted comment object directly (check _id for success)
|
|
2283
|
+
const res = await commentsApi.deleteComment(commentId);
|
|
2284
|
+
if (res._id) {
|
|
2285
|
+
emitter.emit('comment:deleted', { commentId });
|
|
2286
|
+
void loadComments();
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
2290
|
+
}
|
|
2291
|
+
async function handleSubmit() {
|
|
2292
|
+
const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
|
|
2293
|
+
if (!textarea)
|
|
2294
|
+
return;
|
|
2295
|
+
const content = textarea.value.trim();
|
|
2296
|
+
if (!content)
|
|
2297
|
+
return;
|
|
2298
|
+
if (!tokenStorage.has()) {
|
|
2299
|
+
void doLogin();
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (!currentThreadId)
|
|
2303
|
+
return;
|
|
2304
|
+
const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
|
|
2305
|
+
if (submitBtn) {
|
|
2306
|
+
submitBtn.disabled = true;
|
|
2307
|
+
setTextContent(submitBtn, 'Posting…');
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
// Both editComment and postComment return the comment object directly
|
|
2311
|
+
let res;
|
|
2312
|
+
if (editingCommentId) {
|
|
2313
|
+
res = await commentsApi.editComment(editingCommentId, content);
|
|
2314
|
+
}
|
|
2315
|
+
else {
|
|
2316
|
+
res = await commentsApi.postComment({
|
|
2317
|
+
threadId: currentThreadId,
|
|
2318
|
+
content,
|
|
2319
|
+
parentCommentId: replyingToCommentId !== null && replyingToCommentId !== void 0 ? replyingToCommentId : viewingSubthreadFor,
|
|
2320
|
+
});
|
|
2321
|
+
emitter.emit('comment:posted', { comment: res });
|
|
2322
|
+
}
|
|
2323
|
+
if (res._id) {
|
|
2324
|
+
textarea.value = '';
|
|
2325
|
+
editingCommentId = null;
|
|
2326
|
+
replyingToCommentId = null;
|
|
2327
|
+
textarea.placeholder = viewingSubthreadFor ? 'Write a reply…' : 'Write a comment…';
|
|
2328
|
+
hideCancel();
|
|
2329
|
+
if (submitBtn)
|
|
2330
|
+
setTextContent(submitBtn, 'Post');
|
|
2331
|
+
void loadComments();
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
catch ( /* handled by loadComments */_a) { /* handled by loadComments */ }
|
|
2335
|
+
finally {
|
|
2336
|
+
if (submitBtn) {
|
|
2337
|
+
submitBtn.disabled = false;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
function showCancel() {
|
|
2342
|
+
const cancelBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-cancel');
|
|
2343
|
+
cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.classList.add('cc-visible');
|
|
2344
|
+
}
|
|
2345
|
+
function hideCancel() {
|
|
2346
|
+
const cancelBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-cancel');
|
|
2347
|
+
cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.classList.remove('cc-visible');
|
|
2348
|
+
}
|
|
2349
|
+
function cancelEdit() {
|
|
2350
|
+
editingCommentId = null;
|
|
2351
|
+
replyingToCommentId = null;
|
|
2352
|
+
const textarea = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-textarea');
|
|
2353
|
+
if (textarea) {
|
|
2354
|
+
textarea.value = '';
|
|
2355
|
+
textarea.placeholder = 'Write a comment…';
|
|
2356
|
+
}
|
|
2357
|
+
const submitBtn = root === null || root === void 0 ? void 0 : root.getElementById('cc-compose-submit');
|
|
2358
|
+
if (submitBtn)
|
|
2359
|
+
setTextContent(submitBtn, 'Post');
|
|
2360
|
+
hideCancel();
|
|
2361
|
+
}
|
|
2362
|
+
function updateLoginOverlay() {
|
|
2363
|
+
const r = ensureRoot();
|
|
2364
|
+
const existing = r.getElementById('cc-login-overlay');
|
|
2365
|
+
if (tokenStorage.has() && existing) {
|
|
2366
|
+
existing.remove();
|
|
2367
|
+
}
|
|
2368
|
+
else if (!tokenStorage.has() && !existing) {
|
|
2369
|
+
const compose = r.getElementById('cc-compose');
|
|
2370
|
+
compose === null || compose === void 0 ? void 0 : compose.appendChild(buildLoginOverlay());
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
// ── Open / Close ──────────────────────────────────────────────────────────
|
|
2374
|
+
function openPanel() {
|
|
2375
|
+
const r = ensureRoot();
|
|
2376
|
+
// Check if token arrived from redirect
|
|
2377
|
+
const redirectToken = consumeTokenFromUrl();
|
|
2378
|
+
if (redirectToken)
|
|
2379
|
+
refreshUser();
|
|
2380
|
+
refreshUser();
|
|
2381
|
+
// Build panel if not already present
|
|
2382
|
+
let backdrop = r.getElementById('cc-panel-backdrop');
|
|
2383
|
+
if (!backdrop) {
|
|
2384
|
+
backdrop = el('div');
|
|
2385
|
+
backdrop.className = 'cc-panel-backdrop';
|
|
2386
|
+
backdrop.id = 'cc-panel-backdrop';
|
|
2387
|
+
backdrop.addEventListener('click', closePanel);
|
|
2388
|
+
r.appendChild(backdrop);
|
|
2389
|
+
r.appendChild(buildPanel());
|
|
2390
|
+
}
|
|
2391
|
+
// Animate open
|
|
2392
|
+
requestAnimationFrame(() => {
|
|
2393
|
+
var _a;
|
|
2394
|
+
backdrop.classList.add('cc-visible');
|
|
2395
|
+
(_a = r.getElementById('cc-comments-panel')) === null || _a === void 0 ? void 0 : _a.classList.add('cc-open');
|
|
2396
|
+
});
|
|
2397
|
+
updateLoginOverlay();
|
|
2398
|
+
void loadComments();
|
|
2399
|
+
}
|
|
2400
|
+
function closePanel() {
|
|
2401
|
+
const r = root;
|
|
2402
|
+
if (!r)
|
|
2403
|
+
return;
|
|
2404
|
+
const backdrop = r.getElementById('cc-panel-backdrop');
|
|
2405
|
+
const panel = r.getElementById('cc-comments-panel');
|
|
2406
|
+
backdrop === null || backdrop === void 0 ? void 0 : backdrop.classList.remove('cc-visible');
|
|
2407
|
+
panel === null || panel === void 0 ? void 0 : panel.classList.remove('cc-open');
|
|
2408
|
+
setTimeout(() => {
|
|
2409
|
+
backdrop === null || backdrop === void 0 ? void 0 : backdrop.remove();
|
|
2410
|
+
panel === null || panel === void 0 ? void 0 : panel.remove();
|
|
2411
|
+
onClose();
|
|
2412
|
+
}, 280);
|
|
2413
|
+
}
|
|
2414
|
+
function destroy() {
|
|
2415
|
+
removeShadowHost(PANEL_HOST_ID);
|
|
2416
|
+
root = null;
|
|
2417
|
+
}
|
|
2418
|
+
return { openPanel, closePanel, destroy };
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function createComments(config, commentsApi, emitter) {
|
|
2422
|
+
const panel = createCommentPanel(config, commentsApi, emitter, () => widget.show());
|
|
2423
|
+
const widget = createCommentWidget(config.theme.primaryColor, () => {
|
|
2424
|
+
widget.hide();
|
|
2425
|
+
panel.openPanel();
|
|
2426
|
+
});
|
|
2427
|
+
function init() {
|
|
2428
|
+
widget.mount();
|
|
2429
|
+
}
|
|
2430
|
+
function open() {
|
|
2431
|
+
widget.hide();
|
|
2432
|
+
panel.openPanel();
|
|
2433
|
+
}
|
|
2434
|
+
function close() {
|
|
2435
|
+
panel.closePanel();
|
|
2436
|
+
}
|
|
2437
|
+
function destroy() {
|
|
2438
|
+
panel.destroy();
|
|
2439
|
+
widget.destroy();
|
|
2440
|
+
}
|
|
2441
|
+
return { init, open, close, destroy };
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/**
|
|
2445
|
+
* Content Credits JS SDK v2
|
|
2446
|
+
*
|
|
2447
|
+
* Drop-in paywall and comments for any website.
|
|
2448
|
+
*
|
|
2449
|
+
* CDN (script tag):
|
|
2450
|
+
* <script src="https://cdn.contentcredits.com/sdk/v2/content-credits.umd.min.js"></script>
|
|
2451
|
+
* <script>
|
|
2452
|
+
* ContentCreditsSDK.init({ apiKey: 'YOUR_API_KEY', contentSelector: '#article-body' });
|
|
2453
|
+
* </script>
|
|
2454
|
+
*
|
|
2455
|
+
* npm:
|
|
2456
|
+
* import { ContentCredits } from '@contentcredits/sdk';
|
|
2457
|
+
* ContentCredits.init({ apiKey: 'YOUR_API_KEY', contentSelector: '#article-body' });
|
|
2458
|
+
*/
|
|
2459
|
+
class ContentCredits {
|
|
2460
|
+
constructor(config) {
|
|
2461
|
+
this.config = config;
|
|
2462
|
+
this.state = createState();
|
|
2463
|
+
this.emitter = createEventEmitter();
|
|
2464
|
+
this.paywallModule = null;
|
|
2465
|
+
this.commentsModule = null;
|
|
2466
|
+
this.client = createApiClient(config.apiBaseUrl, this.emitter);
|
|
2467
|
+
this.creditsApi = createCreditsApi(this.client);
|
|
2468
|
+
this.commentsApi = createCommentsApi(this.client);
|
|
2469
|
+
}
|
|
2470
|
+
// ── Factory ───────────────────────────────────────────────────────────────
|
|
2471
|
+
/**
|
|
2472
|
+
* Initialise the SDK and immediately start the access check.
|
|
2473
|
+
*
|
|
2474
|
+
* @example
|
|
2475
|
+
* const cc = ContentCredits.init({
|
|
2476
|
+
* apiKey: 'pub_abc123',
|
|
2477
|
+
* contentSelector: '#premium-content',
|
|
2478
|
+
* });
|
|
2479
|
+
*/
|
|
2480
|
+
static init(rawConfig) {
|
|
2481
|
+
const config = resolveConfig(rawConfig);
|
|
2482
|
+
const instance = new ContentCredits(config);
|
|
2483
|
+
void instance._start();
|
|
2484
|
+
return instance;
|
|
2485
|
+
}
|
|
2486
|
+
// ── Internal start ────────────────────────────────────────────────────────
|
|
2487
|
+
async _start() {
|
|
2488
|
+
// Handle token that may have arrived in the URL (mobile redirect flow)
|
|
2489
|
+
consumeTokenFromUrl();
|
|
2490
|
+
this.paywallModule = createPaywall(this.config, this.creditsApi, this.state, this.emitter);
|
|
2491
|
+
if (this.config.enableComments) {
|
|
2492
|
+
this.commentsModule = createComments(this.config, this.commentsApi, this.emitter);
|
|
2493
|
+
this.commentsModule.init();
|
|
2494
|
+
}
|
|
2495
|
+
await this.paywallModule.init();
|
|
2496
|
+
this.emitter.emit('ready', { state: this.state.get() });
|
|
2497
|
+
}
|
|
2498
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
2499
|
+
/** Subscribe to an SDK event. Returns an unsubscribe function. */
|
|
2500
|
+
on(event, handler) {
|
|
2501
|
+
return this.emitter.on(event, handler);
|
|
2502
|
+
}
|
|
2503
|
+
/** Unsubscribe from an SDK event. */
|
|
2504
|
+
off(event, handler) {
|
|
2505
|
+
this.emitter.off(event, handler);
|
|
2506
|
+
}
|
|
2507
|
+
/** Get a snapshot of the current SDK state. */
|
|
2508
|
+
getState() {
|
|
2509
|
+
return this.state.get();
|
|
2510
|
+
}
|
|
2511
|
+
/** Programmatically trigger an article access check. */
|
|
2512
|
+
async checkAccess() {
|
|
2513
|
+
var _a;
|
|
2514
|
+
await ((_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.checkAccess());
|
|
2515
|
+
}
|
|
2516
|
+
/** Open the comment panel programmatically. */
|
|
2517
|
+
openComments() {
|
|
2518
|
+
var _a;
|
|
2519
|
+
(_a = this.commentsModule) === null || _a === void 0 ? void 0 : _a.open();
|
|
2520
|
+
}
|
|
2521
|
+
/** Close the comment panel programmatically. */
|
|
2522
|
+
closeComments() {
|
|
2523
|
+
var _a;
|
|
2524
|
+
(_a = this.commentsModule) === null || _a === void 0 ? void 0 : _a.close();
|
|
2525
|
+
}
|
|
2526
|
+
/** Check if the user is currently authenticated. */
|
|
2527
|
+
isLoggedIn() {
|
|
2528
|
+
return tokenStorage.has();
|
|
2529
|
+
}
|
|
2530
|
+
/** Tear down the SDK — removes all UI, event listeners, and stored state. */
|
|
2531
|
+
destroy() {
|
|
2532
|
+
var _a, _b;
|
|
2533
|
+
(_a = this.paywallModule) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
2534
|
+
(_b = this.commentsModule) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
2535
|
+
this.emitter.removeAll();
|
|
2536
|
+
this.state.reset();
|
|
2537
|
+
}
|
|
2538
|
+
/** SDK version string. */
|
|
2539
|
+
static get version() {
|
|
2540
|
+
return "2.0.0";
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
// ── Auto-init from script data attributes (CDN usage) ────────────────────────
|
|
2544
|
+
// Partners can include the script like:
|
|
2545
|
+
// <script src="..." data-api-key="pub_abc" data-content-selector="#body"></script>
|
|
2546
|
+
// and the SDK will auto-initialise without any additional JS.
|
|
2547
|
+
function autoInit() {
|
|
2548
|
+
var _a, _b, _c;
|
|
2549
|
+
const script = (_a = document.currentScript) !== null && _a !== void 0 ? _a : document.querySelector('script[data-cc-api-key], script[data-api-key]');
|
|
2550
|
+
if (!script)
|
|
2551
|
+
return;
|
|
2552
|
+
const ds = script.dataset;
|
|
2553
|
+
const apiKey = (_b = ds.ccApiKey) !== null && _b !== void 0 ? _b : ds.apiKey;
|
|
2554
|
+
if (!apiKey)
|
|
2555
|
+
return;
|
|
2556
|
+
const rawConfig = {
|
|
2557
|
+
apiKey,
|
|
2558
|
+
contentSelector: (_c = ds.ccContentSelector) !== null && _c !== void 0 ? _c : ds.contentSelector,
|
|
2559
|
+
teaserParagraphs: ds.ccTeaserParagraphs ? parseInt(ds.ccTeaserParagraphs, 10) : undefined,
|
|
2560
|
+
enableComments: ds.ccEnableComments !== 'false',
|
|
2561
|
+
extensionId: ds.ccExtensionId,
|
|
2562
|
+
debug: ds.ccDebug === 'true',
|
|
2563
|
+
};
|
|
2564
|
+
if (document.readyState === 'loading') {
|
|
2565
|
+
document.addEventListener('DOMContentLoaded', () => ContentCredits.init(rawConfig));
|
|
2566
|
+
}
|
|
2567
|
+
else {
|
|
2568
|
+
ContentCredits.init(rawConfig);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
autoInit();
|
|
2572
|
+
|
|
2573
|
+
exports.ContentCredits = ContentCredits;
|
|
2574
|
+
//# sourceMappingURL=content-credits.cjs.js.map
|