@cloff/sdk 1.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/README.md +244 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +1049 -0
- package/dist/widget.d.ts +18 -0
- package/dist/widget.js +623 -0
- package/package.json +30 -0
- package/src/index.ts +1194 -0
- package/src/widget.ts +664 -0
- package/tsconfig.json +15 -0
package/src/widget.ts
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
// Default database connection credentials
|
|
2
|
+
const DEFAULT_SUPABASE_URL = 'https://edhkrzyiiznotsmblbrt.supabase.co';
|
|
3
|
+
const DEFAULT_SUPABASE_ANON_KEY = 'sb_publishable_EzjTe8cJu-8gZmdccQr4aA_iZY8WIxL';
|
|
4
|
+
|
|
5
|
+
export class ClofWidget extends HTMLElement {
|
|
6
|
+
private shadow: ShadowRoot;
|
|
7
|
+
private apiKey: string | null = null;
|
|
8
|
+
private days: number = 30;
|
|
9
|
+
private theme: 'light' | 'dark' = 'dark';
|
|
10
|
+
private accentColor: string = '#8b5cf6'; // Indigo/violet default
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.shadow = this.attachShadow({ mode: 'open' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static get observedAttributes() {
|
|
18
|
+
return ['api-key', 'days', 'theme', 'accent-color'];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
22
|
+
if (oldValue === newValue) return;
|
|
23
|
+
|
|
24
|
+
if (name === 'api-key') this.apiKey = newValue;
|
|
25
|
+
if (name === 'days') this.days = parseInt(newValue, 10) || 30;
|
|
26
|
+
if (name === 'theme') this.theme = newValue === 'light' ? 'light' : 'dark';
|
|
27
|
+
if (name === 'accent-color') this.accentColor = newValue || '#8b5cf6';
|
|
28
|
+
|
|
29
|
+
if (this.isConnected && this.apiKey) {
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
connectedCallback() {
|
|
35
|
+
this.apiKey = this.getAttribute('api-key');
|
|
36
|
+
this.days = parseInt(this.getAttribute('days') || '30', 10);
|
|
37
|
+
this.accentColor = this.getAttribute('accent-color') || '#8b5cf6';
|
|
38
|
+
|
|
39
|
+
const themeAttr = this.getAttribute('theme');
|
|
40
|
+
if (themeAttr === 'light' || themeAttr === 'dark') {
|
|
41
|
+
this.theme = themeAttr;
|
|
42
|
+
} else {
|
|
43
|
+
// Auto-detect dark mode from page classes or system preferences
|
|
44
|
+
const hasDarkClass = document.documentElement.classList.contains('dark') || document.body.classList.contains('dark');
|
|
45
|
+
const hasLightClass = document.documentElement.classList.contains('light') || document.body.classList.contains('light');
|
|
46
|
+
|
|
47
|
+
if (hasDarkClass) {
|
|
48
|
+
this.theme = 'dark';
|
|
49
|
+
} else if (hasLightClass) {
|
|
50
|
+
this.theme = 'light';
|
|
51
|
+
} else {
|
|
52
|
+
this.theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.render();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async fetchMetrics(): Promise<any> {
|
|
60
|
+
if (!this.apiKey) {
|
|
61
|
+
throw new Error('API Key is missing');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Intercept Demo/Mock keys for local sandboxes or developer previews
|
|
65
|
+
if (this.apiKey.startsWith('socio_dau_live_demo_key_')) {
|
|
66
|
+
const seed = this.apiKey.includes('socionetwork') ? 1.5 : 0.8;
|
|
67
|
+
const metrics = [];
|
|
68
|
+
const today = new Date();
|
|
69
|
+
|
|
70
|
+
for (let i = this.days - 1; i >= 0; i--) {
|
|
71
|
+
const date = new Date(today);
|
|
72
|
+
date.setDate(today.getDate() - i);
|
|
73
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
74
|
+
|
|
75
|
+
const base = 50 + (this.days - 1 - i) * 8 * seed;
|
|
76
|
+
const dayOfWeek = date.getDay();
|
|
77
|
+
const weekendDip = (dayOfWeek === 0 || dayOfWeek === 6) ? 0.7 : 1.0;
|
|
78
|
+
const randomNoise = 0.9 + Math.random() * 0.2;
|
|
79
|
+
|
|
80
|
+
metrics.push({
|
|
81
|
+
activity_date: dateStr,
|
|
82
|
+
active_users: Math.round(base * weekendDip * randomNoise)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const devices = [
|
|
87
|
+
{ device_type: 'Mobile', count: Math.round(1482 * seed) },
|
|
88
|
+
{ device_type: 'Desktop', count: Math.round(853 * seed) },
|
|
89
|
+
{ device_type: 'Tablet', count: Math.round(124 * seed) }
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const locations = [
|
|
93
|
+
{ location: 'America/New_York', count: Math.round(980 * seed) },
|
|
94
|
+
{ location: 'Europe/London', count: Math.round(642 * seed) },
|
|
95
|
+
{ location: 'Asia/Tokyo', count: Math.round(412 * seed) },
|
|
96
|
+
{ location: 'Europe/Paris', count: Math.round(280 * seed) },
|
|
97
|
+
{ location: 'Australia/Sydney', count: Math.round(144 * seed) }
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Simulate a small network delay
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
metrics,
|
|
106
|
+
devices,
|
|
107
|
+
locations
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = await fetch(`${DEFAULT_SUPABASE_URL}/rest/v1/rpc/get_public_dau_metrics`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'apikey': DEFAULT_SUPABASE_ANON_KEY,
|
|
115
|
+
'Authorization': `Bearer ${DEFAULT_SUPABASE_ANON_KEY}`,
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
p_api_key: this.apiKey,
|
|
120
|
+
p_days: this.days
|
|
121
|
+
})
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const errText = await response.text();
|
|
126
|
+
throw new Error(errText || 'Failed to fetch public metrics');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
if (data && data.ok === false) {
|
|
131
|
+
throw new Error(data.error || 'Server rejected metrics request');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private getStyles(): string {
|
|
138
|
+
const isDark = this.theme === 'dark';
|
|
139
|
+
const bg = isDark ? '#0f172a' : '#ffffff';
|
|
140
|
+
const text = isDark ? '#f8fafc' : '#0f172a';
|
|
141
|
+
const border = isDark ? '#1e293b' : '#e2e8f0';
|
|
142
|
+
const subtext = isDark ? '#94a3b8' : '#64748b';
|
|
143
|
+
const cardBg = isDark ? '#1e293b' : '#f8fafc';
|
|
144
|
+
const gridLine = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)';
|
|
145
|
+
|
|
146
|
+
return `
|
|
147
|
+
:host {
|
|
148
|
+
display: block;
|
|
149
|
+
width: 100%;
|
|
150
|
+
max-width: 600px;
|
|
151
|
+
font-family: 'Outfit', 'Inter', system-ui, -apple-system, sans-serif;
|
|
152
|
+
box-sizing: border-box;
|
|
153
|
+
}
|
|
154
|
+
.widget-container {
|
|
155
|
+
background: ${bg};
|
|
156
|
+
color: ${text};
|
|
157
|
+
border: 1px solid ${border};
|
|
158
|
+
border-radius: 16px;
|
|
159
|
+
padding: 20px;
|
|
160
|
+
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.08);
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
gap: 20px;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
transition: all 0.3s ease;
|
|
166
|
+
}
|
|
167
|
+
.header {
|
|
168
|
+
display: flex;
|
|
169
|
+
justify-content: space-between;
|
|
170
|
+
align-items: flex-start;
|
|
171
|
+
border-bottom: 1px solid ${border};
|
|
172
|
+
padding-bottom: 15px;
|
|
173
|
+
}
|
|
174
|
+
.title-section {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
gap: 4px;
|
|
178
|
+
}
|
|
179
|
+
.title {
|
|
180
|
+
font-size: 11px;
|
|
181
|
+
text-transform: uppercase;
|
|
182
|
+
font-weight: 700;
|
|
183
|
+
letter-spacing: 1.5px;
|
|
184
|
+
color: ${subtext};
|
|
185
|
+
}
|
|
186
|
+
.dau-count-container {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: baseline;
|
|
189
|
+
gap: 8px;
|
|
190
|
+
}
|
|
191
|
+
.dau-count {
|
|
192
|
+
font-size: 32px;
|
|
193
|
+
font-weight: 800;
|
|
194
|
+
letter-spacing: -1px;
|
|
195
|
+
background: linear-gradient(to right, ${this.accentColor}, #6366f1);
|
|
196
|
+
-webkit-background-clip: text;
|
|
197
|
+
-webkit-text-fill-color: transparent;
|
|
198
|
+
}
|
|
199
|
+
.dau-label {
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
color: ${subtext};
|
|
202
|
+
font-weight: 600;
|
|
203
|
+
}
|
|
204
|
+
.badge {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 4px;
|
|
208
|
+
background: ${cardBg};
|
|
209
|
+
border: 1px solid ${border};
|
|
210
|
+
padding: 6px 12px;
|
|
211
|
+
border-radius: 10px;
|
|
212
|
+
font-size: 11px;
|
|
213
|
+
font-weight: 700;
|
|
214
|
+
color: ${this.accentColor};
|
|
215
|
+
}
|
|
216
|
+
.badge-dot {
|
|
217
|
+
width: 6px;
|
|
218
|
+
height: 6px;
|
|
219
|
+
background: ${this.accentColor};
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
animation: pulse 2s infinite;
|
|
222
|
+
}
|
|
223
|
+
.chart-container {
|
|
224
|
+
position: relative;
|
|
225
|
+
width: 100%;
|
|
226
|
+
height: 180px;
|
|
227
|
+
}
|
|
228
|
+
svg {
|
|
229
|
+
width: 100%;
|
|
230
|
+
height: 100%;
|
|
231
|
+
overflow: visible;
|
|
232
|
+
}
|
|
233
|
+
.grid-line {
|
|
234
|
+
stroke: ${gridLine};
|
|
235
|
+
stroke-dasharray: 4;
|
|
236
|
+
}
|
|
237
|
+
.chart-line {
|
|
238
|
+
stroke: ${this.accentColor};
|
|
239
|
+
stroke-width: 3;
|
|
240
|
+
stroke-linecap: round;
|
|
241
|
+
stroke-linejoin: round;
|
|
242
|
+
fill: none;
|
|
243
|
+
}
|
|
244
|
+
.chart-area {
|
|
245
|
+
fill: url(#areaGradient);
|
|
246
|
+
}
|
|
247
|
+
.chart-dot {
|
|
248
|
+
fill: ${this.accentColor};
|
|
249
|
+
stroke: ${bg};
|
|
250
|
+
stroke-width: 2;
|
|
251
|
+
}
|
|
252
|
+
.stats-grid {
|
|
253
|
+
display: grid;
|
|
254
|
+
grid-template-columns: 1fr 1fr;
|
|
255
|
+
gap: 16px;
|
|
256
|
+
}
|
|
257
|
+
@media (max-width: 480px) {
|
|
258
|
+
.stats-grid {
|
|
259
|
+
grid-template-columns: 1fr;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
.card {
|
|
263
|
+
background: ${cardBg};
|
|
264
|
+
border: 1px solid ${border};
|
|
265
|
+
border-radius: 12px;
|
|
266
|
+
padding: 14px;
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-direction: column;
|
|
269
|
+
gap: 12px;
|
|
270
|
+
}
|
|
271
|
+
.card-title {
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
font-weight: 700;
|
|
274
|
+
color: ${subtext};
|
|
275
|
+
text-transform: uppercase;
|
|
276
|
+
letter-spacing: 0.5px;
|
|
277
|
+
}
|
|
278
|
+
.device-bar {
|
|
279
|
+
display: flex;
|
|
280
|
+
height: 8px;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
background: ${border};
|
|
284
|
+
}
|
|
285
|
+
.device-segment {
|
|
286
|
+
height: 100%;
|
|
287
|
+
transition: width 0.3s;
|
|
288
|
+
}
|
|
289
|
+
.legend-list {
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-direction: column;
|
|
292
|
+
gap: 6px;
|
|
293
|
+
}
|
|
294
|
+
.legend-item {
|
|
295
|
+
display: flex;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
font-size: 11px;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
}
|
|
300
|
+
.legend-label {
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
gap: 6px;
|
|
304
|
+
color: ${subtext};
|
|
305
|
+
}
|
|
306
|
+
.legend-dot {
|
|
307
|
+
width: 8px;
|
|
308
|
+
height: 8px;
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
}
|
|
311
|
+
.location-list {
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction: column;
|
|
314
|
+
gap: 8px;
|
|
315
|
+
}
|
|
316
|
+
.location-row {
|
|
317
|
+
display: flex;
|
|
318
|
+
flex-direction: column;
|
|
319
|
+
gap: 4px;
|
|
320
|
+
}
|
|
321
|
+
.location-meta {
|
|
322
|
+
display: flex;
|
|
323
|
+
justify-content: space-between;
|
|
324
|
+
font-size: 11px;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
}
|
|
327
|
+
.location-name {
|
|
328
|
+
color: ${text};
|
|
329
|
+
white-space: nowrap;
|
|
330
|
+
overflow: hidden;
|
|
331
|
+
text-overflow: ellipsis;
|
|
332
|
+
max-width: 140px;
|
|
333
|
+
}
|
|
334
|
+
.location-bar-bg {
|
|
335
|
+
height: 4px;
|
|
336
|
+
background: ${border};
|
|
337
|
+
border-radius: 2px;
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
}
|
|
340
|
+
.location-bar-fill {
|
|
341
|
+
height: 100%;
|
|
342
|
+
background: ${this.accentColor};
|
|
343
|
+
border-radius: 2px;
|
|
344
|
+
}
|
|
345
|
+
.footer {
|
|
346
|
+
display: flex;
|
|
347
|
+
justify-content: space-between;
|
|
348
|
+
font-size: 10px;
|
|
349
|
+
color: ${subtext};
|
|
350
|
+
border-top: 1px solid ${border};
|
|
351
|
+
padding-top: 12px;
|
|
352
|
+
margin-top: 5px;
|
|
353
|
+
}
|
|
354
|
+
.footer a {
|
|
355
|
+
color: ${this.accentColor};
|
|
356
|
+
text-decoration: none;
|
|
357
|
+
font-weight: 700;
|
|
358
|
+
}
|
|
359
|
+
.footer-badge {
|
|
360
|
+
display: flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
gap: 4px;
|
|
363
|
+
font-weight: 600;
|
|
364
|
+
}
|
|
365
|
+
.check-icon {
|
|
366
|
+
color: #10b981;
|
|
367
|
+
}
|
|
368
|
+
.loader-container {
|
|
369
|
+
display: flex;
|
|
370
|
+
flex-direction: column;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
height: 300px;
|
|
374
|
+
gap: 12px;
|
|
375
|
+
}
|
|
376
|
+
.spinner {
|
|
377
|
+
width: 32px;
|
|
378
|
+
height: 32px;
|
|
379
|
+
border: 3px solid ${border};
|
|
380
|
+
border-top-color: ${this.accentColor};
|
|
381
|
+
border-radius: 50%;
|
|
382
|
+
animation: spin 1s linear infinite;
|
|
383
|
+
}
|
|
384
|
+
.loader-text {
|
|
385
|
+
font-size: 13px;
|
|
386
|
+
color: ${subtext};
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
}
|
|
389
|
+
.error-container {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex-direction: column;
|
|
392
|
+
align-items: center;
|
|
393
|
+
justify-content: center;
|
|
394
|
+
height: 250px;
|
|
395
|
+
color: #ef4444;
|
|
396
|
+
text-align: center;
|
|
397
|
+
padding: 20px;
|
|
398
|
+
gap: 8px;
|
|
399
|
+
}
|
|
400
|
+
.error-title {
|
|
401
|
+
font-weight: 700;
|
|
402
|
+
font-size: 16px;
|
|
403
|
+
}
|
|
404
|
+
.error-desc {
|
|
405
|
+
font-size: 12px;
|
|
406
|
+
color: ${subtext};
|
|
407
|
+
}
|
|
408
|
+
@keyframes pulse {
|
|
409
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
410
|
+
50% { opacity: 0.4; transform: scale(1.3); }
|
|
411
|
+
}
|
|
412
|
+
@keyframes spin {
|
|
413
|
+
to { transform: rotate(360deg); }
|
|
414
|
+
}
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private renderError(message: string) {
|
|
419
|
+
const isDark = this.theme === 'dark';
|
|
420
|
+
const bg = isDark ? '#0f172a' : '#ffffff';
|
|
421
|
+
const border = isDark ? '#1e293b' : '#e2e8f0';
|
|
422
|
+
const subtext = isDark ? '#94a3b8' : '#64748b';
|
|
423
|
+
|
|
424
|
+
this.shadow.innerHTML = `
|
|
425
|
+
<style>
|
|
426
|
+
${this.getStyles()}
|
|
427
|
+
</style>
|
|
428
|
+
<div class="widget-container" style="background: ${bg}; border-color: ${border};">
|
|
429
|
+
<div class="error-container">
|
|
430
|
+
<div class="error-title">Unable to Load Metrics</div>
|
|
431
|
+
<div class="error-desc">${message}</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private renderLoading() {
|
|
438
|
+
const isDark = this.theme === 'dark';
|
|
439
|
+
const bg = isDark ? '#0f172a' : '#ffffff';
|
|
440
|
+
const border = isDark ? '#1e293b' : '#e2e8f0';
|
|
441
|
+
|
|
442
|
+
this.shadow.innerHTML = `
|
|
443
|
+
<style>
|
|
444
|
+
${this.getStyles()}
|
|
445
|
+
</style>
|
|
446
|
+
<div class="widget-container" style="background: ${bg}; border-color: ${border};">
|
|
447
|
+
<div class="loader-container">
|
|
448
|
+
<div class="spinner"></div>
|
|
449
|
+
<div class="loader-text">Fetching verified metrics...</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private renderWidget(data: any) {
|
|
456
|
+
const metrics: any[] = data.metrics || [];
|
|
457
|
+
const devices: any[] = data.devices || [];
|
|
458
|
+
const locations: any[] = data.locations || [];
|
|
459
|
+
|
|
460
|
+
// Chronological order for chart
|
|
461
|
+
const sortedMetrics = [...metrics].sort(
|
|
462
|
+
(a, b) => new Date(a.activity_date).getTime() - new Date(b.activity_date).getTime()
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
const latestDau = sortedMetrics.length > 0 ? sortedMetrics[sortedMetrics.length - 1].active_users : 0;
|
|
466
|
+
|
|
467
|
+
// SVG coordinates setup
|
|
468
|
+
const width = 500;
|
|
469
|
+
const height = 150;
|
|
470
|
+
const paddingLeft = 30;
|
|
471
|
+
const paddingRight = 10;
|
|
472
|
+
const paddingTop = 15;
|
|
473
|
+
const paddingBottom = 15;
|
|
474
|
+
|
|
475
|
+
const chartWidth = width - paddingLeft - paddingRight;
|
|
476
|
+
const chartHeight = height - paddingTop - paddingBottom;
|
|
477
|
+
|
|
478
|
+
const maxVal = Math.max(...sortedMetrics.map(m => m.active_users), 0);
|
|
479
|
+
const scaleMax = maxVal === 0 ? 10 : Math.ceil(maxVal * 1.25);
|
|
480
|
+
|
|
481
|
+
const coordPoints = sortedMetrics.map((p, i) => {
|
|
482
|
+
const x = paddingLeft + (i / Math.max(sortedMetrics.length - 1, 1)) * chartWidth;
|
|
483
|
+
const y = paddingTop + chartHeight - (p.active_users / scaleMax) * chartHeight;
|
|
484
|
+
return { x, y };
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Generate path strings
|
|
488
|
+
let linePath = '';
|
|
489
|
+
let areaPath = '';
|
|
490
|
+
if (coordPoints.length > 0) {
|
|
491
|
+
linePath = coordPoints.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
|
|
492
|
+
const lastX = coordPoints[coordPoints.length - 1].x;
|
|
493
|
+
const firstX = coordPoints[0].x;
|
|
494
|
+
const bottomY = paddingTop + chartHeight;
|
|
495
|
+
areaPath = `${linePath} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Grid lines calculation
|
|
499
|
+
const gridLinesY = [0.25, 0.5, 0.75, 1.0].map(pct => {
|
|
500
|
+
return paddingTop + chartHeight - pct * chartHeight;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Devices Calculations
|
|
504
|
+
const totalDevices = devices.reduce((sum, item) => sum + Number(item.count), 0);
|
|
505
|
+
const deviceColors = ['#8b5cf6', '#6366f1', '#10b981', '#f59e0b'];
|
|
506
|
+
|
|
507
|
+
// Locations Calculations
|
|
508
|
+
const totalLocations = locations.reduce((sum, item) => sum + Number(item.count), 0);
|
|
509
|
+
|
|
510
|
+
const isDark = this.theme === 'dark';
|
|
511
|
+
const subtext = isDark ? '#94a3b8' : '#64748b';
|
|
512
|
+
|
|
513
|
+
this.shadow.innerHTML = `
|
|
514
|
+
<style>
|
|
515
|
+
${this.getStyles()}
|
|
516
|
+
</style>
|
|
517
|
+
<div class="widget-container">
|
|
518
|
+
<div class="header">
|
|
519
|
+
<div class="title-section">
|
|
520
|
+
<div class="title">Active Users History</div>
|
|
521
|
+
<div class="dau-count-container">
|
|
522
|
+
<span class="dau-count">${latestDau.toLocaleString()}</span>
|
|
523
|
+
<span class="dau-label">DAU today</span>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
<div class="badge">
|
|
527
|
+
<div class="badge-dot"></div>
|
|
528
|
+
<span>Verified Metrics</span>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
<!-- SVG Interactive Area Chart -->
|
|
533
|
+
<div class="chart-container">
|
|
534
|
+
<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
|
|
535
|
+
<defs>
|
|
536
|
+
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
|
|
537
|
+
<stop offset="0%" stop-color="${this.accentColor}" stop-opacity="0.3"/>
|
|
538
|
+
<stop offset="100%" stop-color="${this.accentColor}" stop-opacity="0.0"/>
|
|
539
|
+
</linearGradient>
|
|
540
|
+
</defs>
|
|
541
|
+
|
|
542
|
+
<!-- Grid Lines -->
|
|
543
|
+
${gridLinesY.map((y) => `
|
|
544
|
+
<line class="grid-line" x1="${paddingLeft}" y1="${y}" x2="${width - paddingRight}" y2="${y}" />
|
|
545
|
+
`).join('')}
|
|
546
|
+
|
|
547
|
+
<!-- Y Axis bounds text -->
|
|
548
|
+
<text x="${paddingLeft - 5}" y="${paddingTop + 4}" text-anchor="end" font-size="8" fill="${subtext}" font-weight="600">${Math.round(scaleMax)}</text>
|
|
549
|
+
<text x="${paddingLeft - 5}" y="${paddingTop + chartHeight + 4}" text-anchor="end" font-size="8" fill="${subtext}" font-weight="600">0</text>
|
|
550
|
+
|
|
551
|
+
${coordPoints.length > 0 ? `
|
|
552
|
+
<!-- Area Path -->
|
|
553
|
+
<path class="chart-area" d="${areaPath}" />
|
|
554
|
+
<!-- Line Path -->
|
|
555
|
+
<path class="chart-line" d="${linePath}" />
|
|
556
|
+
<!-- Endpoint Dot -->
|
|
557
|
+
<circle class="chart-dot" cx="${coordPoints[coordPoints.length - 1].x}" cy="${coordPoints[coordPoints.length - 1].y}" r="5" />
|
|
558
|
+
` : ''}
|
|
559
|
+
</svg>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<!-- Breakdown Grid -->
|
|
563
|
+
<div class="stats-grid">
|
|
564
|
+
|
|
565
|
+
<!-- Devices Breakdown -->
|
|
566
|
+
<div class="card">
|
|
567
|
+
<div class="card-title">Environments</div>
|
|
568
|
+
${totalDevices === 0 ? `
|
|
569
|
+
<div style="font-size: 11px; color: ${subtext}; text-align: center; margin: auto;">No device data</div>
|
|
570
|
+
` : `
|
|
571
|
+
<div class="device-bar">
|
|
572
|
+
${devices.map((dev, idx) => {
|
|
573
|
+
const pct = (dev.count / totalDevices) * 100;
|
|
574
|
+
return `<div class="device-segment" style="width: ${pct}%; background: ${deviceColors[idx % deviceColors.length]}"></div>`;
|
|
575
|
+
}).join('')}
|
|
576
|
+
</div>
|
|
577
|
+
<div class="legend-list">
|
|
578
|
+
${devices.map((dev, idx) => {
|
|
579
|
+
const pct = Math.round((dev.count / totalDevices) * 100);
|
|
580
|
+
return `
|
|
581
|
+
<div class="legend-item">
|
|
582
|
+
<div class="legend-label">
|
|
583
|
+
<div class="legend-dot" style="background: ${deviceColors[idx % deviceColors.length]}"></div>
|
|
584
|
+
<span>${dev.device_type}</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div>${pct}%</div>
|
|
587
|
+
</div>
|
|
588
|
+
`;
|
|
589
|
+
}).join('')}
|
|
590
|
+
</div>
|
|
591
|
+
`}
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
<!-- Locations Breakdown -->
|
|
595
|
+
<div class="card">
|
|
596
|
+
<div class="card-title">Top Locations</div>
|
|
597
|
+
${totalLocations === 0 ? `
|
|
598
|
+
<div style="font-size: 11px; color: ${subtext}; text-align: center; margin: auto;">No location data</div>
|
|
599
|
+
` : `
|
|
600
|
+
<div class="location-list">
|
|
601
|
+
${locations.slice(0, 3).map((loc) => {
|
|
602
|
+
const pct = Math.round((loc.count / totalLocations) * 100);
|
|
603
|
+
const locVal = loc.location || loc.country || 'Unknown';
|
|
604
|
+
const shortName = locVal.split('/').pop()?.replace('_', ' ') || locVal;
|
|
605
|
+
return `
|
|
606
|
+
<div class="location-row">
|
|
607
|
+
<div class="location-meta">
|
|
608
|
+
<span class="location-name">${shortName}</span>
|
|
609
|
+
<span>${pct}%</span>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="location-bar-bg">
|
|
612
|
+
<div class="location-bar-fill" style="width: ${pct}%; background: ${this.accentColor}"></div>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
`;
|
|
616
|
+
}).join('')}
|
|
617
|
+
</div>
|
|
618
|
+
`}
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<!-- Verified Footer -->
|
|
624
|
+
<div class="footer">
|
|
625
|
+
<div class="footer-badge">
|
|
626
|
+
<span class="check-icon">✓</span> Verified by <strong>Analytics</strong>
|
|
627
|
+
</div>
|
|
628
|
+
<div>
|
|
629
|
+
Powered by <a href="https://socio.kim" target="_blank" rel="noopener">Socio.kim</a>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private async render() {
|
|
637
|
+
if (!this.apiKey) {
|
|
638
|
+
this.renderError('API key attribute "api-key" is required.');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
this.renderLoading();
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const data = await this.fetchMetrics();
|
|
646
|
+
this.renderWidget(data);
|
|
647
|
+
} catch (error: any) {
|
|
648
|
+
console.error('ClofWidget error:', error);
|
|
649
|
+
this.renderError(error.message || 'Network error fetching data.');
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Register the custom elements globally if in browser environment
|
|
655
|
+
if (typeof window !== 'undefined') {
|
|
656
|
+
if (!window.customElements.get('clof-widget')) {
|
|
657
|
+
window.customElements.define('clof-widget', ClofWidget);
|
|
658
|
+
}
|
|
659
|
+
if (!window.customElements.get('socio-dau-widget')) {
|
|
660
|
+
window.customElements.define('socio-dau-widget', ClofWidget);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export { ClofWidget as SocioDauWidget };
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2020",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["es2020", "dom"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "node"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|