Repository navigation
Expand file tree
/
Copy pathscript.js
More file actions
807 lines (741 loc) · 38.7 KB
/
Copy pathscript.js
File metadata and controls
807 lines (741 loc) · 38.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
// Cache de Elementos del DOM
const elements = {
toggleBtn: document.querySelector('.toggle-btn'),
langToggleBtn: document.getElementById('langToggleBtn'),
bgColor: document.querySelector('.bg-color'),
loader: document.querySelector('.loader'),
placeholder: document.querySelector('.chart_placeholder'),
copyContainer: document.querySelector('.copy_container'),
textArea: document.querySelector('.text'),
copyText: document.querySelector('.copy_text'),
copyBtn: document.getElementById('copyBtn') || document.querySelector('.copy-btn'),
submitButton: document.getElementById('submit-button'),
username: document.getElementById('username'),
themeSelect: document.getElementById('themeSelect'),
chartContainer: document.querySelector('.ct-chart'),
stageCard: document.querySelector('.rect.stage-card'),
liveIndicator: document.getElementById('live-indicator'),
useCustomColors: document.getElementById('useCustomColors'),
resetColorsBtn: document.getElementById('resetColorsBtn'),
themeActiveNotice: document.getElementById('themeActiveNotice'),
colorGridWrapper: document.getElementById('colorGridWrapper'),
};
// Diccionario de Traducciones (Inglés predeterminado / Español)
const translations = {
en: {
portfolioTitle: 'My Portfolio',
originalRepoTitle: 'Original Repository',
forkTitle: 'My Fork (v2.0)',
repoText: 'Repo',
themesTitle: 'Themes Catalog on GitHub',
themesText: 'Themes',
themeToggleAria: 'Toggle theme',
langToggleAria: 'Switch language / Cambiar idioma',
heroEyebrow: '// DYNAMIC METRICS GENERATOR',
heroTitle: 'GitHub Activity Graph',
heroDesc: 'Activity graph generator for your GitHub profile. Customize colors, periods, multi-user comparisons, borders, and dimensions in real time.',
folderPath: 'WORKSPACE / GRAPH_BUILDER / CONFIGURATION',
folderBadge: 'FULL CONTROLS',
usernamePlaceholder: 'Username (e.g. ema28pro or ema28pro,NachoOFC~_)',
generateGraphBtn: 'Generate Graph',
tabColors: 'Colors & Theme',
tabVisuals: 'Options & Style',
tabDates: 'Periods & Dates',
tabTitles: 'Titles & Friends',
themeSelectLabel: 'Preset Theme',
viewThemesCatalog: 'View Catalog (37 themes) ↗',
customColorsTitle: 'Customize colors manually',
customColorsDesc: 'Overrides theme colors. Disabled by default to use the native palette.',
resetColorsBtn: 'Reset to theme',
themeNoticeBadge: 'NATIVE THEME PALETTE',
themeNoticeText: 'The graph will automatically use all official colors from the theme without adding color parameters to the URL.',
labelBgColor: 'Background (bg_color)',
labelBorderColor: 'Border (border_color)',
labelLineColor: 'Line (line)',
labelPointColor: 'Points (point)',
labelTextColor: 'Text / Axes (color)',
labelTitleColor: 'Title (title_color)',
labelAreaColor: 'Area (area_color)',
hexEmpty: 'empty',
clearHexTitle: 'Leave empty',
optAreaTitle: 'Shaded area (area=true)',
optAreaDesc: 'Gradient fill under the chart curve',
optHideBorderTitle: 'Hide border (hide_border=true)',
optHideBorderDesc: 'Makes the outer SVG frame transparent',
optHideTitleTitle: 'Hide title (hide_title=true)',
optHideTitleDesc: 'Removes the top header with username',
optGridTitle: 'Grid lines (grid=true)',
optGridDesc: 'Shows dotted background grid lines',
optHideLegendTitle: 'Hide legend (hide_legend=true)',
optHideLegendDesc: 'Hides user row in multi-user graphs',
optFrontTitle: 'Primary user on top (front=true)',
optFrontDesc: 'Keeps your curves above your friends\' curves',
labelHeight: 'Graph height (height):',
labelRadius: 'Border radius (radius):',
labelPeriodSelect: 'Preset period or days (days / period):',
optPeriod31: 'Last 31 days (default)',
optPeriodLastWeek: 'Last week (last-week / 7 days)',
optPeriodLast15: 'Last 15 days (last-15-days)',
optPeriodCustom: 'Custom number of days...',
labelCustomDays: 'Specific days (1 to 90):',
customDaysPlaceholder: 'e.g. 45',
labelDateFrom: 'Start date (from):',
labelDateTo: 'End date (to):',
labelCustomTitle: 'Custom title (custom_title):',
customTitlePlaceholder: 'e.g. My Contributions This Month',
helpCustomTitle: 'Completely replaces the auto-generated title.',
labelVsFriends: 'Friends title format (vs_friends):',
vsFriendsPlaceholder: 'true, _ or custom text (e.g. _vs friends)',
helpVsFriends: '• Use <code>true</code> for "User vs Friends".<br/>• Use <code>_</code> to remove "vs" ("User Friends").<br/>• Use <code>_with friends</code> for "User with friends".',
tipsTitle: 'EXTRA USER CUSTOMIZATION:',
tipsContent: 'You can assign individual colors to friends in the username input:<br/><code>user1,user2~_</code> (Original line, white points).<br/><code>user1,user2~ff0000_</code> (Red line, white points).<br/><code>user1,user2~ff0000_00ff00</code> (Red line, green points).',
stageTag: '// REAL-TIME PREVIEW (FULL WIDTH)',
liveReady: 'READY TO RENDER',
loaderText: 'FETCHING GITHUB API...',
placeholderText: 'Your activity graph will be rendered here in full size.',
placeholderSubtext: 'Enter your username above and click "Generate Graph"',
copyLabel: 'MARKDOWN LINK FOR YOUR PROFILE OR README:',
copyInputPlaceholder: 'Generate your graph to get the Markdown snippet.',
copyBtnTitle: 'Copy to clipboard',
copyTextBtn: 'Copy',
footerFork: 'FORK OF',
statusRendering: 'RENDERING...',
statusUpdated: 'UPDATED',
statusFallback: 'ONLINE (FALLBACK)',
statusReady: 'READY TO RENDER',
copySuccess: 'Copied!',
alertEmptyUsername: 'Please enter a GitHub username.',
alertFetchError: 'Could not fetch GitHub user data.',
},
es: {
portfolioTitle: 'Mi Portafolio',
originalRepoTitle: 'Repositorio Original',
forkTitle: 'Mi Fork (v2.0)',
repoText: 'Repo',
themesTitle: 'Catálogo de Temas en GitHub',
themesText: 'Temas',
themeToggleAria: 'Cambiar tema',
langToggleAria: 'Switch language / Cambiar idioma',
heroEyebrow: '// GENERADOR DE MÉTRICAS DINÁMICAS',
heroTitle: 'GitHub Activity Graph',
heroDesc: 'Generador de gráficas de actividad para tu perfil. Personaliza colores, periodos, comparativas multi-usuario, bordes y dimensiones en tiempo real.',
folderPath: 'ESPACIO DE TRABAJO / CREADOR DE GRÁFICO / CONFIGURACIÓN',
folderBadge: 'CONTROLES COMPLETOS',
usernamePlaceholder: 'Usuario (ej: ema28pro o ema28pro,NachoOFC~_)',
generateGraphBtn: 'Generar Gráfico',
tabColors: 'Colores & Tema',
tabVisuals: 'Opciones & Estilo',
tabDates: 'Periodos & Fechas',
tabTitles: 'Títulos & Amigos',
themeSelectLabel: 'Tema Predefinido',
viewThemesCatalog: 'Ver Catálogo (37 temas) ↗',
customColorsTitle: 'Personalizar colores manualmente',
customColorsDesc: 'Sobrescribe los colores del tema. Desactivado por defecto para usar la paleta nativa.',
resetColorsBtn: 'Restablecer al tema',
themeNoticeBadge: 'PALETA NATIVA DEL TEMA',
themeNoticeText: 'El gráfico utilizará automáticamente todos los colores oficiales del tema sin añadir parámetros de color a la URL.',
labelBgColor: 'Fondo (bg_color)',
labelBorderColor: 'Borde (border_color)',
labelLineColor: 'Línea (line)',
labelPointColor: 'Puntos (point)',
labelTextColor: 'Texto / Ejes (color)',
labelTitleColor: 'Título (title_color)',
labelAreaColor: 'Área (area_color)',
hexEmpty: 'vacío',
clearHexTitle: 'Dejar vacío',
optAreaTitle: 'Área sombreada (area=true)',
optAreaDesc: 'Relleno degradado bajo la curva del gráfico',
optHideBorderTitle: 'Ocultar borde (hide_border=true)',
optHideBorderDesc: 'Hace transparente el marco exterior del SVG',
optHideTitleTitle: 'Ocultar título (hide_title=true)',
optHideTitleDesc: 'Remueve el encabezado superior con el nombre',
optGridTitle: 'Cuadrícula (grid=true)',
optGridDesc: 'Muestra las líneas punteadas de fondo',
optHideLegendTitle: 'Ocultar leyenda (hide_legend=true)',
optHideLegendDesc: 'Oculta la fila de usuarios en gráficos multi-usuario',
optFrontTitle: 'Usuario principal al frente (front=true)',
optFrontDesc: 'Mantiene tus curvas arriba de las de tus amigos',
labelHeight: 'Altura del gráfico (height):',
labelRadius: 'Radio de bordes (radius):',
labelPeriodSelect: 'Periodo predefinido o días (days / period):',
optPeriod31: 'Últimos 31 días (default)',
optPeriodLastWeek: 'Última semana (last-week / 7 días)',
optPeriodLast15: 'Últimos 15 días (last-15-days)',
optPeriodCustom: 'Número de días personalizado...',
labelCustomDays: 'Días específicos (1 a 90):',
customDaysPlaceholder: 'Ej: 45',
labelDateFrom: 'Fecha de inicio (from):',
labelDateTo: 'Fecha de fin (to):',
labelCustomTitle: 'Título personalizado (custom_title):',
customTitlePlaceholder: 'Ej: Mis Contribuciones del Mes',
helpCustomTitle: 'Reemplaza completamente el título generado automáticamente.',
labelVsFriends: 'Formato de amigos (vs_friends):',
vsFriendsPlaceholder: 'true, _ o texto (ej: _contra amigos)',
helpVsFriends: '• Usa <code>true</code> para "Usuario vs Friends".<br/>• Usa <code>_</code> para quitar el "vs" ("Usuario Friends").<br/>• Usa <code>_contra amigos</code> para "Usuario contra amigos".',
tipsTitle: 'PERSONALIZACIÓN DE USUARIOS EXTRA:',
tipsContent: 'Puedes asignar colores individuales a tus amigos en el campo de usuario:<br/><code>user1,user2~_</code> (Línea original, puntos blancos).<br/><code>user1,user2~ff0000_</code> (Línea roja, puntos blancos).<br/><code>user1,user2~ff0000_00ff00</code> (Línea roja, puntos verdes).',
stageTag: '// VISTA PREVIA EN TIEMPO REAL (FULL WIDTH)',
liveReady: 'LISTO PARA RENDERIZAR',
loaderText: 'CONSULTANDO GITHUB API...',
placeholderText: 'Tu gráfico de actividad se renderizará aquí en tamaño completo.',
placeholderSubtext: 'Ingresa tu usuario arriba y presiona "Generar Gráfico"',
copyLabel: 'ENLACE MARKDOWN PARA TU PERFIL O README:',
copyInputPlaceholder: 'Genera tu gráfico para obtener el código Markdown.',
copyBtnTitle: 'Copiar al portapapeles',
copyTextBtn: 'Copiar',
footerFork: 'FORK DE',
statusRendering: 'RENDERIZANDO...',
statusUpdated: 'ACTUALIZADO',
statusFallback: 'ONLINE (FALLBACK)',
statusReady: 'LISTO PARA RENDERIZAR',
copySuccess: '¡Copiado!',
alertEmptyUsername: 'Por favor, ingresa un nombre de usuario de GitHub.',
alertFetchError: 'No se pudieron obtener datos del usuario de GitHub.',
},
};
// Variable de estado de idioma actual (inglés por defecto)
let currentLang = 'en';
// Actualizar aviso del tema según idioma y tema seleccionado
const updateThemeNotice = () => {
if (elements.themeActiveNotice) {
const p = elements.themeActiveNotice.querySelector('p');
const selected = elements.themeSelect ? elements.themeSelect.value : 'default';
if (p) {
p.textContent = currentLang === 'es'
? `El gráfico utilizará automáticamente todos los colores oficiales del tema "${selected}" sin añadir parámetros de color a la URL.`
: `The graph will automatically use all official colors from theme "${selected}" without adding color parameters to the URL.`;
}
}
};
// Actualizar textos según el idioma seleccionado
const setLanguage = (lang) => {
currentLang = lang === 'es' ? 'es' : 'en';
localStorage.setItem('lang', currentLang);
document.documentElement.lang = currentLang;
// Actualizar estado activo en los botones del selector
document.querySelectorAll('.lang-opt').forEach((opt) => {
if (opt.getAttribute('data-lang') === currentLang) {
opt.classList.add('active');
} else {
opt.classList.remove('active');
}
});
const dict = translations[currentLang] || translations.en;
// Reemplazar contenido de texto o HTML de elementos con data-i18n
document.querySelectorAll('[data-i18n]').forEach((el) => {
const key = el.getAttribute('data-i18n');
if (dict[key] !== undefined) {
el.innerHTML = dict[key];
}
});
// Reemplazar placeholders de inputs
document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
const key = el.getAttribute('data-i18n-placeholder');
if (dict[key] !== undefined) {
el.placeholder = dict[key];
}
});
// Reemplazar atributos title y aria-label
document.querySelectorAll('[data-i18n-title]').forEach((el) => {
const key = el.getAttribute('data-i18n-title');
if (dict[key] !== undefined) {
el.title = dict[key];
if (el.hasAttribute('aria-label')) {
el.setAttribute('aria-label', dict[key]);
}
}
});
// Actualizar aviso del tema y regenerar enlace con placeholder acorde
updateThemeNotice();
generateLink();
};
// Alternar entre inglés y español
const toggleLanguage = () => {
setLanguage(currentLang === 'en' ? 'es' : 'en');
};
// Inputs de Colores (swatches)
const inputElements = {
bgInput: document.getElementById('bgColor'),
borderInput: document.getElementById('borderColor'),
lineInput: document.getElementById('line'),
pointInput: document.getElementById('point'),
ltgInput: document.getElementById('color'),
titleInput: document.getElementById('titleColor'),
areaInput: document.getElementById('areaColor'),
};
// Inputs de Texto Hexadecimal (editables o vacíos)
const hexInputs = {
bgHex: document.getElementById('bgHex'),
borderHex: document.getElementById('borderHex'),
lineHex: document.getElementById('lineHex'),
pointHex: document.getElementById('pointHex'),
colorHex: document.getElementById('colorHex'),
titleHex: document.getElementById('titleHex'),
areaHex: document.getElementById('areaHex'),
};
// Opciones Visuales y Dimensiones
const visualElements = {
area: document.getElementById('optArea'),
hideBorder: document.getElementById('optHideBorder'),
hideTitle: document.getElementById('optHideTitle'),
grid: document.getElementById('optGrid'),
hideLegend: document.getElementById('optHideLegend'),
front: document.getElementById('optFront'),
height: document.getElementById('optHeight'),
heightVal: document.getElementById('heightVal'),
radius: document.getElementById('optRadius'),
radiusVal: document.getElementById('radiusVal'),
};
// Opciones de Fecha y Periodo
const dateElements = {
periodSelect: document.getElementById('periodSelect'),
customDaysGroup: document.getElementById('customDaysGroup'),
customDays: document.getElementById('customDays'),
dateFrom: document.getElementById('dateFrom'),
dateTo: document.getElementById('dateTo'),
};
// Opciones de Título y Comparativa
const titleElements = {
customTitle: document.getElementById('customTitle'),
vsFriends: document.getElementById('vsFriends'),
};
// Definiciones de Temas
const themes = {
default: { bgColor: '#ffcfe9', line: '#9e4c98', point: '#403d3d', color: '#9e4c98', title: '#9e4c98', area: '#9e4c98', border: '#ffffff' },
github: { bgColor: '#293036', line: '#9ecbff', point: '#f97583', color: '#ffffff', title: '#ffffff', area: '#9ecbff', border: '#ffffff' },
'github-light': { bgColor: '#ffffff', line: '#9be9a8', point: '#40c463', color: '#000000', title: '#000000', area: '#9be9a8', border: '#ffffff' },
'github-compact': { bgColor: '#00000000', line: '#26a641', point: '#8b949e', color: '#8b949e', title: '#8b949e', area: '#26a641', border: '#8b949e' },
'github-dark': { bgColor: '#0D1117', line: '#1F6FEB', point: '#58A6FF', color: '#58A6FF', title: '#58A6FF', area: '#1F6FEB', border: '#ffffff' },
'github-dark-dimmed': { bgColor: '#24292F', line: '#ADBAC7', point: '#539bf5', color: '#ADBAC7', title: '#539bf5', area: '#ADBAC7', border: '#444c56' },
dracula: { bgColor: '#44475a', line: '#ff79c6', point: '#bd93f9', color: '#f8f8f2', title: '#f8f8f2', area: '#ff79c6', border: '#ffffff' },
gruvbox: { bgColor: '#504945', line: '#d8a657', point: '#e78a4e', color: '#d4be98', title: '#d4be98', area: '#d8a657', border: '#ffffff' },
gotham: { bgColor: '#0c1014', line: '#599cab', point: '#99d1ce', color: '#2aa889', title: '#2aa889', area: '#599cab', border: '#ffffff' },
rogue: { bgColor: '#172030', line: '#b18bb1', point: '#c6797e', color: '#a3b09a', title: '#a3b09a', area: '#b18bb1', border: '#ffffff' },
xcode: { bgColor: '#202124', line: '#c4e3ff', point: '#ff8070', color: '#fcfcfa', title: '#fcfcfa', area: '#c4e3ff', border: '#ffffff' },
radical: { bgColor: '#141321', line: '#fe428e', point: '#f8d847', color: '#a9fef7', title: '#a9fef7', area: '#fe428e', border: '#ffffff' },
coral: { bgColor: '#9a3838', line: '#f4e23d', point: '#f4e7e7', color: '#f9fae9', title: '#f9fae9', area: '#f4e23d', border: '#ffffff' },
react: { bgColor: '#282c34', line: '#61dafb', point: '#61dafb', color: '#ffffff', title: '#ffffff', area: '#61dafb', border: '#ffffff' },
'react-dark': { bgColor: '#0d1117', line: '#5bcdec', point: '#ffffff', color: '#5bcdec', title: '#5bcdec', area: '#5bcdec', border: '#ffffff' },
nord: { bgColor: '#2e3440', line: '#88c0d0', point: '#ffffff', color: '#88c0d0', title: '#88c0d0', area: '#88c0d0', border: '#ffffff' },
lucent: { bgColor: '#cccccc', line: '#ffd369', point: '#faf3e0', color: '#000000', title: '#000000', area: '#ffd369', border: '#ffffff' },
'chartreuse-dark': { bgColor: '#000000', line: '#00adfe', point: '#7ffe00', color: '#7ffe00', title: '#7ffe00', area: '#00adfe', border: '#ffffff' },
minimal: { bgColor: '#ffffff', line: '#d3e6fa', point: '#3f99ed', color: '#000000', title: '#000000', area: '#d3e6fa', border: '#ffffff' },
'material-palenight': { bgColor: '#292d3e', line: '#c792ea', point: '#89ddff', color: '#a6accd', title: '#a6accd', area: '#c792ea', border: '#ffffff' },
green: { bgColor: '#dad7cd', line: '#588157', point: '#344e41', color: '#3a5a40', title: '#3a5a40', area: '#588157', border: '#ffffff' },
'noctis-minimus': { bgColor: '#1b2932', line: '#72b7c0', point: '#c5cdd3', color: '#d3b692', title: '#d3b692', area: '#72b7c0', border: '#ffffff' },
'one-dark': { bgColor: '#282C34', line: '#e5c17c', point: '#e06c75', color: '#abb2bf', title: '#abb2bf', area: '#e5c17c', border: '#ffffff' },
monokai: { bgColor: '#2D2A2E', line: '#ff6188', point: '#ffd866', color: '#fcfcfa', title: '#fcfcfa', area: '#ff6188', border: '#ffffff' },
elegant: { bgColor: '#161e2d', line: '#fb8500', point: '#42002b', color: '#d8d5d5', title: '#d8d5d5', area: '#fb8500', border: '#ffffff' },
aqua: { bgColor: '#52b69a', line: '#226d64', point: '#156f69', color: '#187177', title: '#187177', area: '#226d64', border: '#ffffff' },
'synthwave-84': { bgColor: '#2C223B', line: '#F7F645', point: '#22E5F4', color: '#FF3CA2', title: '#FF3CA2', area: '#F7F645', border: '#ffffff' },
merko: { bgColor: '#0a0f0b', line: '#abd200', point: '#f6f8fa', color: '#f6f8fa', title: '#f6f8fa', area: '#abd200', border: '#ffffff' },
vue: { bgColor: '#2c3e50', line: '#41b883', point: '#f6f8fa', color: '#41b883', title: '#41b883', area: '#41b883', border: '#ffffff' },
'tokyo-day': { bgColor: '#e5e8d8', line: '#8f5a02', point: '#562e49', color: '#8f5a02', title: '#8f5a02', area: '#8f5a02', border: '#ffffff' },
'tokyo-night': { bgColor: '#1a1b27', line: '#70a5fd', point: '#a9b1d6', color: '#70a5fd', title: '#70a5fd', area: '#70a5fd', border: '#ffffff' },
'high-contrast': { bgColor: '#000000', line: '#e7e7e7', point: '#e7e7e7', color: '#e7e7e7', title: '#e7e7e7', area: '#e7e7e7', border: '#ffffff' },
cobalt: { bgColor: '#193549', line: '#d19a66', point: '#e7e7e7', color: '#d19a66', title: '#d19a66', area: '#d19a66', border: '#ffffff' },
material: { bgColor: '#263238', line: '#80cbc4', point: '#ffab91', color: '#80cbc4', title: '#80cbc4', area: '#80cbc4', border: '#ffffff' },
nightowl: { bgColor: '#011627', line: '#c792ea', point: '#ffeb95', color: '#c792ea', title: '#c792ea', area: '#c792ea', border: '#ffffff' },
'modern-lilac': { bgColor: '#0a0e12', line: '#5d417a', point: '#fab795', color: '#5d417a', title: '#5d417a', area: '#5d417a', border: '#ffffff' },
arctic: { bgColor: '#03203C', line: '#5DA3FA', point: '#2827CC', color: '#5DA3FA', title: '#5DA3FA', area: '#5DA3FA', border: '#ffffff' },
};
// Manejo de Pestañas (Tabs)
document.querySelectorAll('.tab-btn').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach((p) => p.classList.remove('active'));
btn.classList.add('active');
const targetId = btn.getAttribute('data-tab');
const targetPane = document.getElementById(targetId);
if (targetPane) targetPane.classList.add('active');
});
});
// Actualizar Valores Hexadecimales en los Inputs de Texto
const updateHexBadges = () => {
const pairs = [
[hexInputs.bgHex, inputElements.bgInput],
[hexInputs.borderHex, inputElements.borderInput],
[hexInputs.lineHex, inputElements.lineInput],
[hexInputs.pointHex, inputElements.pointInput],
[hexInputs.colorHex, inputElements.ltgInput],
[hexInputs.titleHex, inputElements.titleInput],
[hexInputs.areaHex, inputElements.areaInput],
];
pairs.forEach(([hexInput, colorInput]) => {
if (hexInput && colorInput) hexInput.value = colorInput.value;
});
};
// Manejo de cambio de Tema
const onThemeChange = () => {
const selected = elements.themeSelect ? elements.themeSelect.value : 'default';
const theme = themes[selected] || themes.default;
const setPair = (colorInput, hexInput, value) => {
if (colorInput) colorInput.value = value;
if (hexInput) hexInput.value = value;
};
setPair(inputElements.bgInput, hexInputs.bgHex, theme.bgColor);
setPair(inputElements.borderInput, hexInputs.borderHex, theme.border || '#ffffff');
setPair(inputElements.lineInput, hexInputs.lineHex, theme.line);
setPair(inputElements.pointInput, hexInputs.pointHex, theme.point);
setPair(inputElements.ltgInput, hexInputs.colorHex, theme.color);
setPair(inputElements.titleInput, hexInputs.titleHex, theme.title || theme.color);
setPair(inputElements.areaInput, hexInputs.areaHex, theme.area || theme.line);
if (elements.stageCard) elements.stageCard.style.backgroundColor = theme.bgColor;
// Actualizar aviso del tema según idioma activo
updateThemeNotice();
generateLink();
};
// Manejo del Switch de Colores Personalizados (activar/desactivar colores extra)
const updateCustomColorsState = () => {
const isCustom = elements.useCustomColors && elements.useCustomColors.checked;
if (elements.colorGridWrapper) {
if (isCustom) {
elements.colorGridWrapper.classList.remove('disabled');
} else {
elements.colorGridWrapper.classList.add('disabled');
}
}
if (elements.themeActiveNotice) {
if (isCustom) {
elements.themeActiveNotice.classList.add('hidden');
} else {
elements.themeActiveNotice.classList.remove('hidden');
}
}
if (elements.resetColorsBtn) {
elements.resetColorsBtn.style.display = isCustom ? 'inline-block' : 'none';
}
generateLink();
};
// Restablecer colores a los valores nativos del tema actual
const resetColorsToTheme = () => {
onThemeChange();
};
// Construir Query String con todas las opciones
const buildQueryParams = () => {
const params = new URLSearchParams();
const cleanHex = (val) => (val || '').replace('#', '').trim();
// Tema
const theme = elements.themeSelect ? elements.themeSelect.value : 'default';
if (theme && theme !== 'default') {
params.append('theme', theme);
}
// Colores personalizados: SÓLO si el usuario los activa explícitamente y tienen valor no vacío
if (elements.useCustomColors && elements.useCustomColors.checked) {
const currentBg = cleanHex(hexInputs.bgHex ? hexInputs.bgHex.value : '');
const currentBorder = cleanHex(hexInputs.borderHex ? hexInputs.borderHex.value : '');
const currentLine = cleanHex(hexInputs.lineHex ? hexInputs.lineHex.value : '');
const currentPoint = cleanHex(hexInputs.pointHex ? hexInputs.pointHex.value : '');
const currentColor = cleanHex(hexInputs.colorHex ? hexInputs.colorHex.value : '');
const currentTitle = cleanHex(hexInputs.titleHex ? hexInputs.titleHex.value : '');
const currentArea = cleanHex(hexInputs.areaHex ? hexInputs.areaHex.value : '');
if (currentBg) params.append('bg_color', currentBg);
if (currentBorder) params.append('border_color', currentBorder);
if (currentLine) params.append('line', currentLine);
if (currentPoint) params.append('point', currentPoint);
if (currentColor) params.append('color', currentColor);
if (currentTitle) params.append('title_color', currentTitle);
if (currentArea) params.append('area_color', currentArea);
}
// Opciones Visuales
if (visualElements.area) {
params.append('area', visualElements.area.checked ? 'true' : 'false');
}
if (visualElements.hideBorder && visualElements.hideBorder.checked) {
params.append('hide_border', 'true');
}
if (visualElements.hideTitle && visualElements.hideTitle.checked) {
params.append('hide_title', 'true');
}
if (visualElements.grid && !visualElements.grid.checked) {
params.append('grid', 'false');
}
if (visualElements.hideLegend && visualElements.hideLegend.checked) {
params.append('hide_legend', 'true');
}
if (visualElements.front && !visualElements.front.checked) {
params.append('front', 'false');
}
// Dimensiones
if (visualElements.height && visualElements.height.value !== '420') {
params.append('height', visualElements.height.value);
}
if (visualElements.radius && visualElements.radius.value !== '8') {
params.append('radius', visualElements.radius.value);
}
// Periodos y Fechas
const periodVal = dateElements.periodSelect ? dateElements.periodSelect.value : '31';
if (periodVal === 'last-week' || periodVal === 'last-15-days') {
params.append('days', periodVal);
} else if (periodVal === 'custom' && dateElements.customDays && dateElements.customDays.value) {
params.append('days', dateElements.customDays.value);
}
if (dateElements.dateFrom && dateElements.dateFrom.value) {
params.append('from', dateElements.dateFrom.value);
}
if (dateElements.dateTo && dateElements.dateTo.value) {
params.append('to', dateElements.dateTo.value);
}
// Títulos y Comparativa
if (titleElements.customTitle && titleElements.customTitle.value.trim()) {
params.append('custom_title', titleElements.customTitle.value.trim());
}
if (titleElements.vsFriends && titleElements.vsFriends.value.trim()) {
params.append('vs_friends', titleElements.vsFriends.value.trim());
}
return params.toString();
};
// Generar Enlace Markdown
const generateLink = () => {
const origin =
typeof window !== 'undefined' && window.location.hostname === 'localhost' && window.location.port === '5100'
? window.location.origin
: 'https://graf-activity-github.vercel.app';
const defaultPlaceholderUser = currentLang === 'es' ? 'tu_usuario' : 'your_username';
const rawUser = (elements.username.value || '').trim() || defaultPlaceholderUser;
const query = buildQueryParams();
const queryPart = query ? `&${query}` : '';
const graphUrl = `${origin}/graph?username=${encodeURIComponent(rawUser)}${queryPart}`;
const markdown = `[](${origin})`;
if (elements.textArea) {
elements.textArea.value = markdown;
}
return markdown;
};
// Mostrar / Ocultar Loader y Placeholder
const setLoader = () => {
if (elements.loader) elements.loader.classList.add('active');
};
const removeLoader = () => {
if (elements.loader) elements.loader.classList.remove('active');
};
const removePlaceholder = () => {
if (elements.placeholder) elements.placeholder.classList.remove('active');
};
// Renderizar Gráfico SVG Real en Ancho Completo
const getGraph = async (username) => {
setLoader();
const dict = translations[currentLang] || translations.en;
if (elements.liveIndicator) elements.liveIndicator.textContent = dict.statusRendering;
const origin =
typeof window !== 'undefined' && window.location.hostname === 'localhost' && window.location.port === '5100'
? window.location.origin
: 'https://graf-activity-github.vercel.app';
const query = buildQueryParams();
const queryPart = query ? `&${query}` : '';
const graphUrl = `${origin}/graph?username=${encodeURIComponent(username)}${queryPart}`;
try {
const response = await fetch(graphUrl);
const svgContent = await response.text();
if (svgContent && svgContent.includes('<svg')) {
if (elements.chartContainer) {
elements.chartContainer.innerHTML = svgContent;
const svg = elements.chartContainer.querySelector('svg');
if (svg) {
svg.style.width = '100%';
svg.style.maxWidth = '1200px';
svg.style.height = 'auto';
svg.style.display = 'block';
svg.style.margin = '0 auto';
}
}
removeLoader();
removePlaceholder();
generateLink();
if (elements.liveIndicator) elements.liveIndicator.textContent = dict.statusUpdated;
return;
}
} catch (err) {
console.warn('Fallo la consulta directa del SVG, intentando fallback:', err);
}
// Fallback: Si no responde directamente el SVG, consultar endpoint /data con Chartist
const firstUser = username.split(',')[0].split(/[#~]/)[0].trim();
axios({
url: `${origin}/data?username=${firstUser}`,
method: 'GET',
})
.then((contributionData) => {
const days = contributionData.data.contributions;
const data = {
labels: days.map((day) => day.date),
series: [{ value: days.map((day) => day.contributionCount) }],
};
const options = {
height: 380,
axisY: { title: 'Contributions', onlyInteger: true, offset: 60 },
axisX: { title: 'Days', offset: 45 },
showArea: true,
fullWidth: true,
};
new Chartist.Line('.ct-chart', data, options);
removeLoader();
removePlaceholder();
generateLink();
if (elements.liveIndicator) elements.liveIndicator.textContent = dict.statusFallback;
})
.catch((err) => {
console.error(err);
removeLoader();
alert(dict.alertFetchError);
});
};
// Enviar formulario
const onSubmit = (event) => {
if (event) event.preventDefault();
const dict = translations[currentLang] || translations.en;
const username = (elements.username.value || '').trim();
if (username.length > 0) {
getGraph(username);
} else {
alert(dict.alertEmptyUsername);
}
};
// Copiar al Portapapeles (solo al hacer clic en el botón de copiar)
const copyLink = (event) => {
if (event) event.stopPropagation();
if (elements.textArea && elements.textArea.value !== '') {
const copyBtn = elements.copyBtn || (elements.copyText ? elements.copyText.querySelector('button') : null);
const textToCopy = elements.textArea.value;
navigator.clipboard.writeText(textToCopy);
const dict = translations[currentLang] || translations.en;
if (elements.copyText) elements.copyText.classList.add('active');
const copyLabel = copyBtn ? copyBtn.querySelector('.copy-text-btn') : null;
if (copyLabel) copyLabel.textContent = dict.copySuccess;
setTimeout(() => {
if (elements.copyText) elements.copyText.classList.remove('active');
if (copyLabel) copyLabel.textContent = dict.copyTextBtn;
}, 2500);
}
};
// Manejo del Modo Oscuro
let darkMode = localStorage.getItem('darkMode');
const enableDarkMode = () => {
document.documentElement.removeAttribute('data-theme');
if (elements.bgColor) elements.bgColor.classList.add('active');
localStorage.setItem('darkMode', 'enabled');
};
const disableDarkMode = () => {
document.documentElement.setAttribute('data-theme', 'light');
if (elements.bgColor) elements.bgColor.classList.remove('active');
localStorage.setItem('darkMode', 'disabled');
};
if (darkMode === 'disabled') {
disableDarkMode();
} else {
enableDarkMode();
}
const handleDarkMode = () => {
darkMode = localStorage.getItem('darkMode');
if (darkMode === 'disabled') {
enableDarkMode();
} else {
disableDarkMode();
}
};
// Controladores de Eventos para Inputs Dinámicos
const bindLiveUpdate = () => {
// Sincronización: Al cambiar el selector de color gráfico (picker)
document.querySelectorAll('.picker-row input[type="color"]').forEach((colorInput) => {
colorInput.addEventListener('input', () => {
const pickerRow = colorInput.closest('.picker-row');
const hexInput = pickerRow ? pickerRow.querySelector('.hex-input') : null;
if (hexInput) {
hexInput.value = colorInput.value;
}
if (colorInput.id === 'bgColor' && elements.stageCard) {
elements.stageCard.style.backgroundColor = colorInput.value;
}
generateLink();
});
});
// Sincronización: Al escribir o borrar en el input de texto hex (permite dejar vacío)
document.querySelectorAll('.hex-input').forEach((hexInput) => {
hexInput.addEventListener('input', () => {
const pickerRow = hexInput.closest('.picker-row');
const colorInput = pickerRow ? pickerRow.querySelector('input[type="color"]') : null;
if (colorInput) {
const val = hexInput.value.trim();
const formatted = val.startsWith('#') ? val : `#${val}`;
if (/^#[0-9A-Fa-f]{6}$/.test(formatted)) {
colorInput.value = formatted;
}
}
if (hexInput.id === 'bgHex' && elements.stageCard) {
const selected = elements.themeSelect ? elements.themeSelect.value : 'default';
const theme = themes[selected] || themes.default;
elements.stageCard.style.backgroundColor = hexInput.value.trim() || theme.bgColor;
}
generateLink();
});
});
// Botones para dejar vacío un color individual (×)
document.querySelectorAll('.clear-hex-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.getAttribute('data-target');
const targetInput = document.getElementById(targetId);
if (targetInput) {
targetInput.value = '';
targetInput.focus();
if (targetId === 'bgHex' && elements.stageCard) {
const selected = elements.themeSelect ? elements.themeSelect.value : 'default';
const theme = themes[selected] || themes.default;
elements.stageCard.style.backgroundColor = theme.bgColor;
}
generateLink();
}
});
});
// Toggles y Opciones Visuales
Object.values(visualElements).forEach((el) => {
if (el) {
el.addEventListener('change', generateLink);
}
});
// Sliders
if (visualElements.height) {
visualElements.height.addEventListener('input', () => {
if (visualElements.heightVal) visualElements.heightVal.textContent = `${visualElements.height.value}px`;
generateLink();
});
}
if (visualElements.radius) {
visualElements.radius.addEventListener('input', () => {
if (visualElements.radiusVal) visualElements.radiusVal.textContent = `${visualElements.radius.value}px`;
generateLink();
});
}
// Periodo
if (dateElements.periodSelect) {
dateElements.periodSelect.addEventListener('change', () => {
if (dateElements.customDaysGroup) {
dateElements.customDaysGroup.style.display =
dateElements.periodSelect.value === 'custom' ? 'block' : 'none';
}
generateLink();
});
}
if (dateElements.customDays) dateElements.customDays.addEventListener('input', generateLink);
if (dateElements.dateFrom) dateElements.dateFrom.addEventListener('change', generateLink);
if (dateElements.dateTo) dateElements.dateTo.addEventListener('change', generateLink);
// Títulos
if (titleElements.customTitle) titleElements.customTitle.addEventListener('input', generateLink);
if (titleElements.vsFriends) titleElements.vsFriends.addEventListener('input', generateLink);
// Username
if (elements.username) elements.username.addEventListener('input', generateLink);
};
// Event Listeners Principales
if (elements.langToggleBtn) elements.langToggleBtn.addEventListener('click', toggleLanguage);
if (elements.toggleBtn) elements.toggleBtn.addEventListener('click', handleDarkMode);
if (elements.submitButton) elements.submitButton.addEventListener('click', onSubmit);
if (elements.copyBtn) elements.copyBtn.addEventListener('click', copyLink);
if (elements.themeSelect) elements.themeSelect.addEventListener('change', onThemeChange);
if (elements.useCustomColors) elements.useCustomColors.addEventListener('change', updateCustomColorsState);
if (elements.resetColorsBtn) elements.resetColorsBtn.addEventListener('click', resetColorsToTheme);
// Inicializar Idioma (Inglés predeterminado a menos que el usuario haya seleccionado español)
const initialLang = localStorage.getItem('lang') || 'en';
setLanguage(initialLang);
// Inicializar
updateCustomColorsState();
updateHexBadges();
bindLiveUpdate();
generateLink();