@jjlmoya/utils-social 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +23 -0
  3. package/src/category/i18n/es.ts +23 -0
  4. package/src/category/i18n/fr.ts +23 -0
  5. package/src/category/index.ts +13 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +27 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/redditFormatter/bibliography.astro +14 -0
  23. package/src/tool/redditFormatter/component.astro +246 -0
  24. package/src/tool/redditFormatter/i18n/en.ts +193 -0
  25. package/src/tool/redditFormatter/i18n/es.ts +193 -0
  26. package/src/tool/redditFormatter/i18n/fr.ts +193 -0
  27. package/src/tool/redditFormatter/index.ts +34 -0
  28. package/src/tool/redditFormatter/logic.ts +24 -0
  29. package/src/tool/redditFormatter/seo.astro +14 -0
  30. package/src/tool/redditFormatter/ui.ts +13 -0
  31. package/src/tool/safeZones/bibliography.astro +14 -0
  32. package/src/tool/safeZones/component.astro +857 -0
  33. package/src/tool/safeZones/i18n/en.ts +244 -0
  34. package/src/tool/safeZones/i18n/es.ts +244 -0
  35. package/src/tool/safeZones/i18n/fr.ts +244 -0
  36. package/src/tool/safeZones/index.ts +34 -0
  37. package/src/tool/safeZones/logic.ts +137 -0
  38. package/src/tool/safeZones/seo.astro +14 -0
  39. package/src/tool/safeZones/ui.ts +34 -0
  40. package/src/tool/socialImageResizer/bibliography.astro +14 -0
  41. package/src/tool/socialImageResizer/component.astro +498 -0
  42. package/src/tool/socialImageResizer/i18n/en.ts +211 -0
  43. package/src/tool/socialImageResizer/i18n/es.ts +211 -0
  44. package/src/tool/socialImageResizer/i18n/fr.ts +211 -0
  45. package/src/tool/socialImageResizer/index.ts +34 -0
  46. package/src/tool/socialImageResizer/logic.ts +133 -0
  47. package/src/tool/socialImageResizer/seo.astro +14 -0
  48. package/src/tool/socialImageResizer/ui.ts +7 -0
  49. package/src/tool/socialValueCalculator/bibliography.astro +14 -0
  50. package/src/tool/socialValueCalculator/component.astro +829 -0
  51. package/src/tool/socialValueCalculator/i18n/en.ts +310 -0
  52. package/src/tool/socialValueCalculator/i18n/es.ts +310 -0
  53. package/src/tool/socialValueCalculator/i18n/fr.ts +310 -0
  54. package/src/tool/socialValueCalculator/index.ts +34 -0
  55. package/src/tool/socialValueCalculator/logic.ts +60 -0
  56. package/src/tool/socialValueCalculator/seo.astro +14 -0
  57. package/src/tool/socialValueCalculator/ui.ts +31 -0
  58. package/src/tool/tinderPhotoOptimizer/bibliography.astro +14 -0
  59. package/src/tool/tinderPhotoOptimizer/component.astro +1514 -0
  60. package/src/tool/tinderPhotoOptimizer/i18n/en.ts +248 -0
  61. package/src/tool/tinderPhotoOptimizer/i18n/es.ts +248 -0
  62. package/src/tool/tinderPhotoOptimizer/i18n/fr.ts +248 -0
  63. package/src/tool/tinderPhotoOptimizer/index.ts +34 -0
  64. package/src/tool/tinderPhotoOptimizer/logic.ts +199 -0
  65. package/src/tool/tinderPhotoOptimizer/seo.astro +14 -0
  66. package/src/tool/tinderPhotoOptimizer/ui.ts +37 -0
  67. package/src/tool/youtubeThumbnail/bibliography.astro +14 -0
  68. package/src/tool/youtubeThumbnail/component.astro +419 -0
  69. package/src/tool/youtubeThumbnail/i18n/en.ts +207 -0
  70. package/src/tool/youtubeThumbnail/i18n/es.ts +207 -0
  71. package/src/tool/youtubeThumbnail/i18n/fr.ts +207 -0
  72. package/src/tool/youtubeThumbnail/index.ts +34 -0
  73. package/src/tool/youtubeThumbnail/logic.ts +7 -0
  74. package/src/tool/youtubeThumbnail/seo.astro +14 -0
  75. package/src/tool/youtubeThumbnail/ui.ts +13 -0
  76. package/src/tool/youtubeThumbnailPreviewer/bibliography.astro +14 -0
  77. package/src/tool/youtubeThumbnailPreviewer/component.astro +791 -0
  78. package/src/tool/youtubeThumbnailPreviewer/i18n/en.ts +321 -0
  79. package/src/tool/youtubeThumbnailPreviewer/i18n/es.ts +321 -0
  80. package/src/tool/youtubeThumbnailPreviewer/i18n/fr.ts +321 -0
  81. package/src/tool/youtubeThumbnailPreviewer/index.ts +34 -0
  82. package/src/tool/youtubeThumbnailPreviewer/seo.astro +14 -0
  83. package/src/tool/youtubeThumbnailPreviewer/ui.ts +19 -0
  84. package/src/tools.ts +10 -0
  85. package/src/types.ts +72 -0
@@ -0,0 +1,829 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { SocialValueCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as SocialValueCalculatorUI;
12
+
13
+ const ICON_VIDEO = `<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M14,2H4A2,2 0 0,0 2,4V16A2,2 0 0,0 4,18H14A2,2 0 0,0 16,16V4A2,2 0 0,0 14,2M14,16H4V4H14V16M22,4L18,8V16L22,20V4Z"/></svg>`;
14
+ const ICON_INSTAGRAM = `<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M7.8,2H16.2C19.4,2 22,4.6 22,7.8V16.2A5.8,5.8 0 0,1 16.2,22H7.8C4.6,22 2,19.4 2,16.2V7.8A5.8,5.8 0 0,1 7.8,2M7.6,4A3.6,3.6 0 0,0 4,7.6V16.4C4,18.39 5.61,20 7.6,20H16.4A3.6,3.6 0 0,0 20,16.4V7.6C20,5.61 18.39,4 16.4,4H7.6M17.25,5.5A1.25,1.25 0 0,1 18.5,6.75A1.25,1.25 0 0,1 17.25,8A1.25,1.25 0 0,1 16,6.75A1.25,1.25 0 0,1 17.25,5.5M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z"/></svg>`;
15
+ const ICON_EYE = `<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18"><path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z"/></svg>`;
16
+ const ICON_INFO = `<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z"/></svg>`;
17
+ ---
18
+
19
+ <div
20
+ class="svc-root"
21
+ data-label-views-tiktok={t.labelAvgViewsTiktok}
22
+ data-label-views-ig={t.labelAvgViewsIg}
23
+ data-range-label={t.rangeLabel}
24
+ data-currency-symbol={t.currencySymbol}
25
+ data-number-locale={t.numberLocale}
26
+ data-insight-text={t.insightText}
27
+ >
28
+ <div class="svc-glass">
29
+ <header class="svc-nav">
30
+ <div class="svc-toggle" role="tablist">
31
+ <button class="svc-toggle-btn svc-active" data-platform="tiktok" id="svc-btn-tiktok" role="tab">
32
+ <span class="svc-nav-icon" set:html={ICON_VIDEO} />
33
+ <span>TikTok</span>
34
+ </button>
35
+ <div class="svc-slider"></div>
36
+ <button class="svc-toggle-btn" data-platform="instagram" id="svc-btn-instagram" role="tab">
37
+ <span class="svc-nav-icon" set:html={ICON_INSTAGRAM} />
38
+ <span>Instagram</span>
39
+ </button>
40
+ </div>
41
+ </header>
42
+
43
+ <div class="svc-grid">
44
+ <aside class="svc-sidebar">
45
+ <div class="svc-section-tag">{t.sectionTag}</div>
46
+
47
+ <div class="svc-inputs">
48
+ <div class="svc-input-box">
49
+ <label for="svc-followers">
50
+ <svg class="svc-input-icon" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M16,13C15.71,13 15.38,13.04 15.07,13.07C16.03,13.86 16.75,14.82 16.75,16V19H23V16C23,14.17 19.33,13 16,13M8,13C4.67,13 1,14.17 1,16V19H15V16C15,14.17 11.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z"/></svg>
51
+ {t.labelFollowers}
52
+ </label>
53
+ <div class="svc-input-wrap">
54
+ <input type="number" id="svc-followers" placeholder="0" min="0" />
55
+ </div>
56
+ </div>
57
+
58
+ <div class="svc-input-box">
59
+ <label for="svc-avg-views" id="svc-label-views">
60
+ <span class="svc-label-icon" set:html={ICON_EYE} />
61
+ <span id="svc-label-views-text">{t.labelAvgViewsTiktok}</span>
62
+ <span class="svc-info-trigger">
63
+ <span set:html={ICON_INFO} />
64
+ <span class="svc-tooltip">{t.tooltipViews}</span>
65
+ </span>
66
+ </label>
67
+ <div class="svc-input-wrap">
68
+ <input type="number" id="svc-avg-views" placeholder="0" min="0" />
69
+ </div>
70
+ </div>
71
+
72
+ <div class="svc-input-box">
73
+ <label for="svc-avg-engagement">
74
+ <svg class="svc-input-icon" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,3.18L19,6.3V11C19,15.5 16.2,19.77 12,21.17C7.8,19.77 5,15.5 5,11V6.3L12,3.18M12,7L10.5,10H7.5L9.5,12.5L8.5,16L12,14L15.5,16L14.5,12.5L16.5,10H13.5L12,7Z"/></svg>
75
+ {t.labelEngagement}
76
+ <span class="svc-info-trigger">
77
+ <span set:html={ICON_INFO} />
78
+ <span class="svc-tooltip">{t.tooltipEngagement}</span>
79
+ </span>
80
+ </label>
81
+ <div class="svc-input-wrap">
82
+ <input type="number" id="svc-avg-engagement" placeholder="0" min="0" />
83
+ </div>
84
+ </div>
85
+
86
+ <div class="svc-input-box">
87
+ <label for="svc-niche">
88
+ <svg class="svc-input-icon" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M18,15H16V17H18M18,11H16V13H18M20,19H12V17H14V15H12V13H14V11H12V9H20M10,7H8V5H10M10,11H8V9H10M10,15H8V13H10M10,19H8V17H10M6,7H4V5H6M6,11H4V9H6M6,15H4V13H6M6,19H4V17H6M12,7V3H2V21H22V7H12Z"/></svg>
89
+ {t.labelNiche}
90
+ </label>
91
+ <div class="svc-input-wrap svc-select-wrap">
92
+ <select id="svc-niche">
93
+ <option value="entertainment">{t.nicheEntertainment}</option>
94
+ <option value="finance">{t.nicheFinance}</option>
95
+ <option value="tech">{t.nicheTech}</option>
96
+ <option value="fitness">{t.nicheFitness}</option>
97
+ <option value="fashion">{t.nicheFashion}</option>
98
+ <option value="travel">{t.nicheTravel}</option>
99
+ <option value="food">{t.nicheFood}</option>
100
+ <option value="gaming">{t.nicheGaming}</option>
101
+ <option value="home">{t.nicheHome}</option>
102
+ </select>
103
+ <svg class="svc-select-arrow" viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"/></svg>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="svc-sidebar-action">
109
+ <button id="svc-btn-calculate" class="svc-btn-primary">
110
+ {t.btnCalculate}
111
+ <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20" class="svc-sparkle"><path d="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,17.6L22,16L20.6,18.5L22,21L19.5,19.6L17,21L18.4,18.5L17,16L19.5,17.6M22,2L18,6L15,3L5,13L8,16L18,6L20,8L22,2M4.41,14.41L2,16.83L7.17,22L9.59,19.59L4.41,14.41Z"/></svg>
112
+ </button>
113
+ </div>
114
+ </aside>
115
+
116
+ <main class="svc-viewport" id="svc-viewport">
117
+ <div class="svc-empty" id="svc-empty">
118
+ <div class="svc-placeholder">
119
+ <div class="svc-circle svc-c1"></div>
120
+ <div class="svc-circle svc-c2"></div>
121
+ <svg class="svc-placeholder-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M6,16.5L3,19.44V11H6M11,14.66L9.43,13.32L8,14.64V7H11M16,13L13,16V3H16M18.81,12.81L17,11H22V16L20.21,14.21L13,21.36L9.53,18.34L5.75,22H3L9.47,15.66L13,18.64"/></svg>
122
+ </div>
123
+ <h3>{t.emptyTitle}</h3>
124
+ <p>{t.emptyDesc}</p>
125
+ </div>
126
+
127
+ <div class="svc-results svc-hidden" id="svc-results">
128
+ <div class="svc-valuation">
129
+ <div class="svc-roi-label" id="svc-roi-label">{t.roiLabel}</div>
130
+ <div class="svc-amount" id="svc-price">0</div>
131
+ <div class="svc-range" id="svc-range"></div>
132
+ </div>
133
+
134
+ <div class="svc-analytics">
135
+ <div class="svc-stat-card">
136
+ <div class="svc-stat-header">
137
+ <span class="svc-stat-title">{t.audienceQuality}</span>
138
+ <svg class="svc-stat-icon svc-icon-warn" viewBox="0 0 24 24" fill="currentColor" width="22" height="22"><path d="M7,2V13H10V22L17,10H13L17,2H7Z"/></svg>
139
+ </div>
140
+ <div class="svc-stat-value" id="svc-metric-er">0%</div>
141
+ <div class="svc-stat-desc">{t.metricER}</div>
142
+ <div class="svc-progress"><div class="svc-bar svc-bar-er" id="svc-er-bar"></div></div>
143
+ </div>
144
+
145
+ <div class="svc-stat-card">
146
+ <div class="svc-stat-header">
147
+ <span class="svc-stat-title">{t.investmentEfficiency}</span>
148
+ <svg class="svc-stat-icon svc-icon-ok" viewBox="0 0 24 24" fill="currentColor" width="22" height="22"><path d="M5,6H23V18H5V6M14,9A3,3 0 0,1 17,12A3,3 0 0,1 14,15A3,3 0 0,1 11,12A3,3 0 0,1 14,9M9,8A2,2 0 0,1 7,10V14A2,2 0 0,1 9,16H19A2,2 0 0,1 21,14V10A2,2 0 0,1 19,8H9M1,10H3V20H19V22H1V10Z"/></svg>
149
+ </div>
150
+ <div class="svc-stat-value" id="svc-metric-cpm">0</div>
151
+ <div class="svc-stat-desc">{t.metricCPM}</div>
152
+ <div class="svc-progress"><div class="svc-bar svc-bar-cpm" id="svc-cpm-bar"></div></div>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="svc-insight">
157
+ <svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20" class="svc-insight-icon"><path d="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z"/></svg>
158
+ <p id="svc-insight-text"></p>
159
+ </div>
160
+ </div>
161
+ </main>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <script>
167
+ import { calculateValue } from './logic';
168
+ import type { CalcResult } from './logic';
169
+
170
+ type Platform = 'tiktok' | 'instagram';
171
+
172
+ let platform: Platform = 'tiktok';
173
+ let labelViewsTiktok = '';
174
+ let labelViewsIg = '';
175
+ let rangeLabel = '';
176
+ let currencySymbol = '€';
177
+ let numberLocale = 'es-ES';
178
+
179
+ let btnTiktok: HTMLButtonElement | null = null;
180
+ let btnInstagram: HTMLButtonElement | null = null;
181
+ let inputFollowers: HTMLInputElement | null = null;
182
+ let inputAvgViews: HTMLInputElement | null = null;
183
+ let inputEngagement: HTMLInputElement | null = null;
184
+ let selectNiche: HTMLSelectElement | null = null;
185
+ let emptyState: HTMLElement | null = null;
186
+ let results: HTMLElement | null = null;
187
+ let priceEl: HTMLElement | null = null;
188
+ let rangeEl: HTMLElement | null = null;
189
+ let erEl: HTMLElement | null = null;
190
+ let cpmEl: HTMLElement | null = null;
191
+ let erBar: HTMLElement | null = null;
192
+ let cpmBar: HTMLElement | null = null;
193
+ let labelViewsText: HTMLElement | null = null;
194
+ let toggle: Element | null = null;
195
+
196
+ function applyTiktok() {
197
+ toggle?.classList.remove('svc-switch');
198
+ btnTiktok?.classList.add('svc-active');
199
+ btnInstagram?.classList.remove('svc-active');
200
+ if (labelViewsText) labelViewsText.textContent = labelViewsTiktok;
201
+ }
202
+
203
+ function applyInstagram() {
204
+ toggle?.classList.add('svc-switch');
205
+ btnInstagram?.classList.add('svc-active');
206
+ btnTiktok?.classList.remove('svc-active');
207
+ if (labelViewsText) labelViewsText.textContent = labelViewsIg;
208
+ }
209
+
210
+ function switchPlatform(p: Platform) {
211
+ platform = p;
212
+ if (p === 'tiktok') { applyTiktok(); } else { applyInstagram(); }
213
+ }
214
+
215
+ function fmt(n: number) {
216
+ return n.toLocaleString(numberLocale, { maximumFractionDigits: 0 });
217
+ }
218
+
219
+ function parseInputs() {
220
+ if (!inputFollowers || !inputAvgViews || !inputEngagement || !selectNiche) return null;
221
+ return {
222
+ followers: parseFloat(inputFollowers.value) || 0,
223
+ avgViews: parseFloat(inputAvgViews.value) || 0,
224
+ avgEngagement: parseFloat(inputEngagement.value) || 0,
225
+ niche: selectNiche.value,
226
+ };
227
+ }
228
+
229
+ function resolveEls() {
230
+ if (!emptyState || !results || !priceEl || !rangeEl || !erEl || !cpmEl) return null;
231
+ return { emptyState, results, priceEl, rangeEl, erEl, cpmEl };
232
+ }
233
+
234
+ function applyBars(result: CalcResult) {
235
+ if (erBar) erBar.style.width = `${Math.min(result.er * 8, 100)}%`;
236
+ if (cpmBar) cpmBar.style.width = `${Math.min(result.projectedCPM * 3, 100)}%`;
237
+ }
238
+
239
+ function renderResult(result: CalcResult | null) {
240
+ if (!result) return;
241
+ const els = resolveEls();
242
+ if (!els) return;
243
+ els.priceEl.textContent = `${fmt(result.tariff)} ${currencySymbol}`;
244
+ els.rangeEl.textContent = `${rangeLabel} ${fmt(result.lowRange)} ${currencySymbol} — ${fmt(result.highRange)} ${currencySymbol}`;
245
+ els.erEl.textContent = `${result.er.toFixed(1)}%`;
246
+ els.cpmEl.textContent = `${result.projectedCPM.toFixed(2)} ${currencySymbol}`;
247
+ applyBars(result);
248
+ els.emptyState.classList.add('svc-hidden');
249
+ els.results.classList.remove('svc-hidden');
250
+ els.results.classList.remove('svc-fadein');
251
+ void els.results.offsetWidth;
252
+ els.results.classList.add('svc-fadein');
253
+ }
254
+
255
+ function calculate() {
256
+ const inputs = parseInputs();
257
+ if (!inputs) return;
258
+ renderResult(calculateValue({ ...inputs, platform }));
259
+ }
260
+
261
+ function setupElements() {
262
+ btnTiktok = document.getElementById('svc-btn-tiktok') as HTMLButtonElement | null;
263
+ btnInstagram = document.getElementById('svc-btn-instagram') as HTMLButtonElement | null;
264
+ inputFollowers = document.getElementById('svc-followers') as HTMLInputElement | null;
265
+ inputAvgViews = document.getElementById('svc-avg-views') as HTMLInputElement | null;
266
+ inputEngagement = document.getElementById('svc-avg-engagement') as HTMLInputElement | null;
267
+ selectNiche = document.getElementById('svc-niche') as HTMLSelectElement | null;
268
+ emptyState = document.getElementById('svc-empty');
269
+ results = document.getElementById('svc-results');
270
+ priceEl = document.getElementById('svc-price');
271
+ rangeEl = document.getElementById('svc-range');
272
+ erEl = document.getElementById('svc-metric-er');
273
+ cpmEl = document.getElementById('svc-metric-cpm');
274
+ erBar = document.getElementById('svc-er-bar');
275
+ cpmBar = document.getElementById('svc-cpm-bar');
276
+ labelViewsText = document.getElementById('svc-label-views-text');
277
+ toggle = document.querySelector('.svc-toggle');
278
+ }
279
+
280
+ function setupData(root: HTMLElement) {
281
+ labelViewsTiktok = root.dataset.labelViewsTiktok || '';
282
+ labelViewsIg = root.dataset.labelViewsIg || '';
283
+ rangeLabel = root.dataset.rangeLabel || '';
284
+ currencySymbol = root.dataset.currencySymbol || '€';
285
+ numberLocale = root.dataset.numberLocale || 'es-ES';
286
+ const insightEl = document.getElementById('svc-insight-text');
287
+ if (insightEl) insightEl.textContent = root.dataset.insightText || '';
288
+ }
289
+
290
+ function init() {
291
+ const root = document.querySelector('.svc-root') as HTMLElement | null;
292
+ if (!root) return;
293
+ setupData(root);
294
+ setupElements();
295
+ btnTiktok?.addEventListener('click', () => switchPlatform('tiktok'));
296
+ btnInstagram?.addEventListener('click', () => switchPlatform('instagram'));
297
+ document.getElementById('svc-btn-calculate')?.addEventListener('click', calculate);
298
+ }
299
+
300
+ document.addEventListener('astro:page-load', init);
301
+ init();
302
+ </script>
303
+
304
+ <style>
305
+ .svc-root {
306
+ --glass-bg: rgba(255, 255, 255, 0.75);
307
+ --glass-border: rgba(255, 255, 255, 0.5);
308
+ --glass-shadow: rgba(0, 0, 0, 0.08);
309
+ --nav-bg: rgba(255, 255, 255, 0.3);
310
+ --nav-border: rgba(0, 0, 0, 0.05);
311
+ --toggle-bg: rgba(0, 0, 0, 0.04);
312
+ --slider-bg: #fff;
313
+ --slider-shadow: rgba(0, 0, 0, 0.1);
314
+ --btn-active-color: #0f172a;
315
+ --btn-color: #64748b;
316
+ --sidebar-bg: rgba(0, 0, 0, 0.015);
317
+ --sidebar-border: rgba(0, 0, 0, 0.05);
318
+ --label-color: #475569;
319
+ --input-bg: #fff;
320
+ --input-border: rgba(0, 0, 0, 0.1);
321
+ --input-text: #0f172a;
322
+ --tooltip-bg: #1e293b;
323
+ --card-bg: #fff;
324
+ --card-border: rgba(0, 0, 0, 0.05);
325
+ --stat-value: #0f172a;
326
+ --text-muted: #64748b;
327
+ --insight-bg: #f8fafc;
328
+ --progress-bg: rgba(0, 0, 0, 0.05);
329
+ --empty-title: #1e293b;
330
+
331
+ max-width: 1000px;
332
+ margin: 2rem auto 5rem;
333
+ }
334
+
335
+ :global(.theme-dark) .svc-root {
336
+ --glass-bg: rgba(15, 23, 42, 0.85);
337
+ --glass-border: rgba(255, 255, 255, 0.08);
338
+ --glass-shadow: rgba(0, 0, 0, 0.6);
339
+ --nav-bg: rgba(30, 41, 59, 0.3);
340
+ --nav-border: rgba(255, 255, 255, 0.05);
341
+ --toggle-bg: rgba(255, 255, 255, 0.05);
342
+ --slider-bg: #1e293b;
343
+ --slider-shadow: rgba(0, 0, 0, 0.3);
344
+ --btn-active-color: #fff;
345
+ --btn-color: #94a3b8;
346
+ --sidebar-bg: rgba(0, 0, 0, 0.2);
347
+ --sidebar-border: rgba(255, 255, 255, 0.05);
348
+ --label-color: #94a3b8;
349
+ --input-bg: #0f172a;
350
+ --input-border: rgba(255, 255, 255, 0.1);
351
+ --input-text: #fff;
352
+ --tooltip-bg: #0f172a;
353
+ --card-bg: rgba(255, 255, 255, 0.03);
354
+ --card-border: rgba(255, 255, 255, 0.05);
355
+ --stat-value: #fff;
356
+ --text-muted: #94a3b8;
357
+ --insight-bg: rgba(255, 255, 255, 0.02);
358
+ --progress-bg: rgba(255, 255, 255, 0.05);
359
+ --empty-title: #f1f5f9;
360
+ }
361
+
362
+ .svc-glass {
363
+ background: var(--glass-bg);
364
+ backdrop-filter: blur(25px) saturate(180%);
365
+ -webkit-backdrop-filter: blur(25px) saturate(180%);
366
+ border: 1px solid var(--glass-border);
367
+ border-radius: 32px;
368
+ box-shadow: 0 25px 50px -12px var(--glass-shadow), inset 0 0 0 1px var(--glass-border);
369
+ overflow: hidden;
370
+ }
371
+
372
+ .svc-nav {
373
+ padding: 1.5rem;
374
+ display: flex;
375
+ justify-content: center;
376
+ background: var(--nav-bg);
377
+ border-bottom: 1px solid var(--nav-border);
378
+ }
379
+
380
+ .svc-toggle {
381
+ position: relative;
382
+ display: flex;
383
+ background: var(--toggle-bg);
384
+ padding: 4px;
385
+ border-radius: 16px;
386
+ width: 340px;
387
+ }
388
+
389
+ .svc-toggle-btn {
390
+ flex: 1;
391
+ z-index: 2;
392
+ background: transparent;
393
+ border: none;
394
+ padding: 12px 0;
395
+ font-size: 0.95rem;
396
+ font-weight: 700;
397
+ color: var(--btn-color);
398
+ cursor: pointer;
399
+ display: flex;
400
+ align-items: center;
401
+ justify-content: center;
402
+ gap: 8px;
403
+ transition: color 0.4s ease;
404
+ }
405
+
406
+ .svc-toggle-btn.svc-active { color: var(--btn-active-color); }
407
+
408
+ .svc-slider {
409
+ position: absolute;
410
+ top: 4px;
411
+ left: 4px;
412
+ width: calc(50% - 4px);
413
+ height: calc(100% - 8px);
414
+ background: var(--slider-bg);
415
+ border-radius: 12px;
416
+ box-shadow: 0 4px 12px var(--slider-shadow);
417
+ transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
418
+ }
419
+
420
+ .svc-toggle.svc-switch .svc-slider { transform: translateX(100%); }
421
+
422
+ .svc-nav-icon {
423
+ display: flex;
424
+ align-items: center;
425
+ }
426
+
427
+ .svc-grid {
428
+ display: grid;
429
+ grid-template-columns: 400px 1fr;
430
+ min-height: 550px;
431
+ }
432
+
433
+ .svc-sidebar {
434
+ padding: 2.5rem;
435
+ background: var(--sidebar-bg);
436
+ border-right: 1px solid var(--sidebar-border);
437
+ display: flex;
438
+ flex-direction: column;
439
+ }
440
+
441
+ .svc-section-tag {
442
+ font-size: 0.75rem;
443
+ font-weight: 800;
444
+ text-transform: uppercase;
445
+ letter-spacing: 0.15em;
446
+ color: #94a3b8;
447
+ margin-bottom: 2rem;
448
+ }
449
+
450
+ .svc-inputs {
451
+ display: flex;
452
+ flex-direction: column;
453
+ gap: 1.75rem;
454
+ margin-bottom: 3rem;
455
+ }
456
+
457
+ .svc-input-box {
458
+ display: flex;
459
+ flex-direction: column;
460
+ gap: 0.75rem;
461
+ }
462
+
463
+ .svc-input-box label {
464
+ font-size: 0.9rem;
465
+ font-weight: 700;
466
+ color: var(--label-color);
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 10px;
470
+ }
471
+
472
+ .svc-input-icon {
473
+ color: #6366f1;
474
+ flex-shrink: 0;
475
+ }
476
+
477
+ .svc-label-icon {
478
+ display: flex;
479
+ align-items: center;
480
+ color: #6366f1;
481
+ flex-shrink: 0;
482
+ }
483
+
484
+ .svc-info-trigger {
485
+ position: relative;
486
+ color: #94a3b8;
487
+ cursor: help;
488
+ display: flex;
489
+ align-items: center;
490
+ transition: color 0.2s;
491
+ }
492
+
493
+ .svc-info-trigger:hover { color: #6366f1; }
494
+
495
+ .svc-tooltip {
496
+ position: absolute;
497
+ bottom: 125%;
498
+ left: 50%;
499
+ transform: translateX(-50%) translateY(10px);
500
+ background: var(--tooltip-bg);
501
+ color: white;
502
+ padding: 0.75rem 1rem;
503
+ border-radius: 12px;
504
+ font-size: 0.8rem;
505
+ font-weight: 500;
506
+ line-height: 1.4;
507
+ width: 200px;
508
+ text-align: center;
509
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
510
+ opacity: 0;
511
+ visibility: hidden;
512
+ pointer-events: none;
513
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
514
+ z-index: 100;
515
+ }
516
+
517
+ .svc-tooltip::after {
518
+ content: '';
519
+ position: absolute;
520
+ top: 100%;
521
+ left: 50%;
522
+ margin-left: -5px;
523
+ border-width: 5px;
524
+ border-style: solid;
525
+ border-color: var(--tooltip-bg) transparent transparent transparent;
526
+ }
527
+
528
+ .svc-info-trigger:hover .svc-tooltip {
529
+ opacity: 1;
530
+ visibility: visible;
531
+ transform: translateX(-50%) translateY(0);
532
+ }
533
+
534
+ .svc-input-wrap { position: relative; }
535
+
536
+ .svc-input-wrap input,
537
+ .svc-input-wrap select {
538
+ width: 100%;
539
+ padding: 14px 18px;
540
+ background: var(--input-bg);
541
+ border: 1px solid var(--input-border);
542
+ border-radius: 14px;
543
+ font-size: 1.05rem;
544
+ color: var(--input-text);
545
+ font-weight: 600;
546
+ transition: all 0.3s ease;
547
+ appearance: none;
548
+ }
549
+
550
+ .svc-input-wrap input:focus {
551
+ outline: none;
552
+ border-color: #6366f1;
553
+ box-shadow: 0 0 0 5px rgba(99, 102, 241, 0.1);
554
+ }
555
+
556
+ .svc-select-wrap { position: relative; }
557
+
558
+ .svc-select-arrow {
559
+ position: absolute;
560
+ right: 16px;
561
+ top: 50%;
562
+ transform: translateY(-50%);
563
+ pointer-events: none;
564
+ color: #94a3b8;
565
+ }
566
+
567
+ .svc-sidebar-action { margin-top: auto; }
568
+
569
+ .svc-btn-primary {
570
+ width: 100%;
571
+ padding: 18px;
572
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
573
+ border: none;
574
+ border-radius: 16px;
575
+ color: #fff;
576
+ font-weight: 800;
577
+ font-size: 1.1rem;
578
+ cursor: pointer;
579
+ display: flex;
580
+ align-items: center;
581
+ justify-content: center;
582
+ gap: 12px;
583
+ box-shadow: 0 12px 24px -6px rgba(79, 70, 229, 0.4);
584
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
585
+ }
586
+
587
+ .svc-btn-primary:hover {
588
+ transform: translateY(-3px);
589
+ box-shadow: 0 20px 30px -8px rgba(79, 70, 229, 0.5);
590
+ filter: brightness(1.1);
591
+ }
592
+
593
+ .svc-sparkle { animation: svc-pulse 2s infinite; }
594
+
595
+ .svc-viewport {
596
+ padding: 3rem;
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: center;
600
+ }
601
+
602
+ .svc-empty {
603
+ text-align: center;
604
+ max-width: 360px;
605
+ }
606
+
607
+ .svc-placeholder {
608
+ position: relative;
609
+ width: 120px;
610
+ height: 120px;
611
+ margin: 0 auto 2rem;
612
+ }
613
+
614
+ .svc-circle {
615
+ position: absolute;
616
+ top: 0; left: 0;
617
+ width: 100%; height: 100%;
618
+ border-radius: 100px;
619
+ }
620
+
621
+ .svc-c1 {
622
+ background: rgba(99, 102, 241, 0.05);
623
+ animation: svc-pulse 3s infinite;
624
+ }
625
+
626
+ .svc-c2 {
627
+ background: rgba(99, 102, 241, 0.08);
628
+ animation: svc-pulse 3s infinite 1.5s;
629
+ transform: scale(0.8);
630
+ }
631
+
632
+ .svc-placeholder-icon {
633
+ position: absolute;
634
+ top: 50%; left: 50%;
635
+ transform: translate(-50%, -50%);
636
+ width: 48px; height: 48px;
637
+ color: #6366f1;
638
+ }
639
+
640
+ .svc-empty h3 {
641
+ font-size: 1.4rem;
642
+ font-weight: 800;
643
+ color: var(--empty-title);
644
+ margin-bottom: 0.75rem;
645
+ }
646
+
647
+ .svc-empty p {
648
+ font-size: 1rem;
649
+ color: var(--text-muted);
650
+ line-height: 1.6;
651
+ }
652
+
653
+ .svc-results {
654
+ width: 100%;
655
+ display: flex;
656
+ flex-direction: column;
657
+ gap: 2.5rem;
658
+ }
659
+
660
+ .svc-valuation {
661
+ text-align: center;
662
+ background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
663
+ padding: 3rem;
664
+ border-radius: 28px;
665
+ color: #fff;
666
+ box-shadow: 0 20px 40px -10px rgba(15, 23, 42, 0.4);
667
+ }
668
+
669
+ .svc-roi-label {
670
+ font-size: 0.8rem;
671
+ font-weight: 800;
672
+ text-transform: uppercase;
673
+ color: #818cf8;
674
+ margin-bottom: 1.5rem;
675
+ letter-spacing: 0.1em;
676
+ }
677
+
678
+ .svc-amount {
679
+ font-size: 4.5rem;
680
+ font-weight: 900;
681
+ letter-spacing: -3px;
682
+ margin-bottom: 0.5rem;
683
+ }
684
+
685
+ .svc-range {
686
+ font-size: 1rem;
687
+ color: #94a3b8;
688
+ font-weight: 600;
689
+ opacity: 0.8;
690
+ }
691
+
692
+ .svc-analytics {
693
+ display: grid;
694
+ grid-template-columns: 1fr 1fr;
695
+ gap: 1.5rem;
696
+ }
697
+
698
+ .svc-stat-card {
699
+ background: var(--card-bg);
700
+ border: 1px solid var(--card-border);
701
+ border-radius: 20px;
702
+ padding: 1.75rem;
703
+ display: flex;
704
+ flex-direction: column;
705
+ gap: 0.5rem;
706
+ }
707
+
708
+ .svc-stat-header {
709
+ display: flex;
710
+ justify-content: space-between;
711
+ align-items: center;
712
+ margin-bottom: 0.5rem;
713
+ }
714
+
715
+ .svc-stat-title {
716
+ font-size: 0.8rem;
717
+ font-weight: 800;
718
+ color: #94a3b8;
719
+ text-transform: uppercase;
720
+ }
721
+
722
+ .svc-stat-value {
723
+ font-size: 2rem;
724
+ font-weight: 900;
725
+ color: var(--stat-value);
726
+ }
727
+
728
+ .svc-stat-desc {
729
+ font-size: 0.85rem;
730
+ font-weight: 600;
731
+ color: var(--text-muted);
732
+ margin-bottom: 0.75rem;
733
+ }
734
+
735
+ .svc-icon-warn { color: #f59e0b; }
736
+ .svc-icon-ok { color: #10b981; }
737
+
738
+ .svc-progress {
739
+ height: 8px;
740
+ background: var(--progress-bg);
741
+ border-radius: 100px;
742
+ overflow: hidden;
743
+ }
744
+
745
+ .svc-bar {
746
+ height: 100%;
747
+ width: 0%;
748
+ transition: width 1.2s cubic-bezier(0.19, 1, 0.22, 1);
749
+ border-radius: 100px;
750
+ }
751
+
752
+ .svc-bar-er { background: linear-gradient(to right, #6366f1, #c084fc); }
753
+ .svc-bar-cpm { background: #10b981; }
754
+
755
+ .svc-insight {
756
+ display: flex;
757
+ align-items: flex-start;
758
+ gap: 12px;
759
+ background: var(--insight-bg);
760
+ padding: 1rem 1.5rem;
761
+ border-radius: 14px;
762
+ border-left: 4px solid #6366f1;
763
+ }
764
+
765
+ .svc-insight-icon {
766
+ color: #6366f1;
767
+ flex-shrink: 0;
768
+ margin-top: 2px;
769
+ }
770
+
771
+ .svc-insight p {
772
+ font-size: 0.85rem;
773
+ color: var(--text-muted);
774
+ margin: 0;
775
+ line-height: 1.5;
776
+ font-weight: 500;
777
+ }
778
+
779
+ .svc-hidden {
780
+ display: none;
781
+ }
782
+
783
+ .svc-fadein { animation: svc-fadein 0.4s ease; }
784
+
785
+ @keyframes svc-pulse {
786
+ 0%, 100% {
787
+ opacity: 1;
788
+ transform: scale(1);
789
+ }
790
+
791
+ 50% {
792
+ opacity: 0.6;
793
+ transform: scale(1.1);
794
+ }
795
+ }
796
+
797
+ @keyframes svc-fadein {
798
+ from {
799
+ opacity: 0;
800
+ transform: translateY(10px);
801
+ }
802
+
803
+ to {
804
+ opacity: 1;
805
+ transform: translateY(0);
806
+ }
807
+ }
808
+
809
+ @media (max-width: 900px) {
810
+ .svc-grid {
811
+ grid-template-columns: 1fr;
812
+ }
813
+
814
+ .svc-sidebar {
815
+ border-right: none;
816
+ border-bottom: 1px solid var(--sidebar-border);
817
+ }
818
+ }
819
+
820
+ @media (max-width: 600px) {
821
+ .svc-analytics {
822
+ grid-template-columns: 1fr;
823
+ }
824
+
825
+ .svc-amount {
826
+ font-size: 3rem;
827
+ }
828
+ }
829
+ </style>