@jjlmoya/utils-finance 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 (133) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +159 -0
  3. package/src/category/i18n/es.ts +159 -0
  4. package/src/category/i18n/fr.ts +159 -0
  5. package/src/category/index.ts +24 -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 +32 -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 +39 -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/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/compoundInterest/bibliography.astro +14 -0
  25. package/src/tool/compoundInterest/component.astro +697 -0
  26. package/src/tool/compoundInterest/i18n/en.ts +212 -0
  27. package/src/tool/compoundInterest/i18n/es.ts +212 -0
  28. package/src/tool/compoundInterest/i18n/fr.ts +212 -0
  29. package/src/tool/compoundInterest/index.ts +34 -0
  30. package/src/tool/compoundInterest/logic.ts +40 -0
  31. package/src/tool/compoundInterest/seo.astro +14 -0
  32. package/src/tool/compoundInterest/ui.ts +19 -0
  33. package/src/tool/courtFeeCalculator/bibliography.astro +14 -0
  34. package/src/tool/courtFeeCalculator/component.astro +427 -0
  35. package/src/tool/courtFeeCalculator/i18n/en.ts +268 -0
  36. package/src/tool/courtFeeCalculator/i18n/es.ts +268 -0
  37. package/src/tool/courtFeeCalculator/i18n/fr.ts +268 -0
  38. package/src/tool/courtFeeCalculator/index.ts +33 -0
  39. package/src/tool/courtFeeCalculator/logic.ts +86 -0
  40. package/src/tool/courtFeeCalculator/seo.astro +14 -0
  41. package/src/tool/courtFeeCalculator/ui.ts +31 -0
  42. package/src/tool/debtSnowball/bibliography.astro +14 -0
  43. package/src/tool/debtSnowball/component.astro +1052 -0
  44. package/src/tool/debtSnowball/i18n/en.ts +288 -0
  45. package/src/tool/debtSnowball/i18n/es.ts +288 -0
  46. package/src/tool/debtSnowball/i18n/fr.ts +288 -0
  47. package/src/tool/debtSnowball/index.ts +37 -0
  48. package/src/tool/debtSnowball/logic.ts +156 -0
  49. package/src/tool/debtSnowball/seo.astro +14 -0
  50. package/src/tool/debtSnowball/ui.ts +49 -0
  51. package/src/tool/fireCalculator/bibliography.astro +14 -0
  52. package/src/tool/fireCalculator/component.astro +689 -0
  53. package/src/tool/fireCalculator/i18n/en.ts +261 -0
  54. package/src/tool/fireCalculator/i18n/es.ts +261 -0
  55. package/src/tool/fireCalculator/i18n/fr.ts +261 -0
  56. package/src/tool/fireCalculator/index.ts +33 -0
  57. package/src/tool/fireCalculator/logic.ts +108 -0
  58. package/src/tool/fireCalculator/seo.astro +14 -0
  59. package/src/tool/fireCalculator/ui.ts +33 -0
  60. package/src/tool/ibanBicSwiftConverter/bibliography.astro +14 -0
  61. package/src/tool/ibanBicSwiftConverter/component.astro +581 -0
  62. package/src/tool/ibanBicSwiftConverter/i18n/en.ts +167 -0
  63. package/src/tool/ibanBicSwiftConverter/i18n/es.ts +216 -0
  64. package/src/tool/ibanBicSwiftConverter/i18n/fr.ts +167 -0
  65. package/src/tool/ibanBicSwiftConverter/index.ts +33 -0
  66. package/src/tool/ibanBicSwiftConverter/logic.ts +101 -0
  67. package/src/tool/ibanBicSwiftConverter/seo.astro +14 -0
  68. package/src/tool/ibanBicSwiftConverter/ui.ts +17 -0
  69. package/src/tool/inflation/bibliography.astro +14 -0
  70. package/src/tool/inflation/component.astro +400 -0
  71. package/src/tool/inflation/i18n/en.ts +301 -0
  72. package/src/tool/inflation/i18n/es.ts +301 -0
  73. package/src/tool/inflation/i18n/fr.ts +301 -0
  74. package/src/tool/inflation/index.ts +34 -0
  75. package/src/tool/inflation/logic.ts +92 -0
  76. package/src/tool/inflation/seo.astro +14 -0
  77. package/src/tool/inflation/ui.ts +17 -0
  78. package/src/tool/lateInterest/bibliography.astro +14 -0
  79. package/src/tool/lateInterest/component.astro +655 -0
  80. package/src/tool/lateInterest/i18n/en.ts +255 -0
  81. package/src/tool/lateInterest/i18n/es.ts +224 -0
  82. package/src/tool/lateInterest/i18n/fr.ts +224 -0
  83. package/src/tool/lateInterest/index.ts +34 -0
  84. package/src/tool/lateInterest/logic.ts +89 -0
  85. package/src/tool/lateInterest/seo.astro +14 -0
  86. package/src/tool/lateInterest/ui.ts +33 -0
  87. package/src/tool/legalInterestRate/bibliography.astro +14 -0
  88. package/src/tool/legalInterestRate/component.astro +650 -0
  89. package/src/tool/legalInterestRate/i18n/en.ts +159 -0
  90. package/src/tool/legalInterestRate/i18n/es.ts +159 -0
  91. package/src/tool/legalInterestRate/i18n/fr.ts +159 -0
  92. package/src/tool/legalInterestRate/index.ts +33 -0
  93. package/src/tool/legalInterestRate/logic.ts +29 -0
  94. package/src/tool/legalInterestRate/seo.astro +14 -0
  95. package/src/tool/legalInterestRate/ui.ts +23 -0
  96. package/src/tool/lotteryOptimizer/bibliography.astro +14 -0
  97. package/src/tool/lotteryOptimizer/component.astro +863 -0
  98. package/src/tool/lotteryOptimizer/i18n/en.ts +188 -0
  99. package/src/tool/lotteryOptimizer/i18n/es.ts +281 -0
  100. package/src/tool/lotteryOptimizer/i18n/fr.ts +188 -0
  101. package/src/tool/lotteryOptimizer/index.ts +33 -0
  102. package/src/tool/lotteryOptimizer/logic.ts +133 -0
  103. package/src/tool/lotteryOptimizer/seo.astro +14 -0
  104. package/src/tool/lotteryOptimizer/ui.ts +33 -0
  105. package/src/tool/mortgage/bibliography.astro +14 -0
  106. package/src/tool/mortgage/component.astro +1030 -0
  107. package/src/tool/mortgage/i18n/en.ts +227 -0
  108. package/src/tool/mortgage/i18n/es.ts +227 -0
  109. package/src/tool/mortgage/i18n/fr.ts +227 -0
  110. package/src/tool/mortgage/index.ts +34 -0
  111. package/src/tool/mortgage/logic.ts +118 -0
  112. package/src/tool/mortgage/seo.astro +14 -0
  113. package/src/tool/mortgage/ui.ts +35 -0
  114. package/src/tool/percentageCalculator/bibliography.astro +14 -0
  115. package/src/tool/percentageCalculator/component.astro +640 -0
  116. package/src/tool/percentageCalculator/i18n/en.ts +233 -0
  117. package/src/tool/percentageCalculator/i18n/es.ts +233 -0
  118. package/src/tool/percentageCalculator/i18n/fr.ts +233 -0
  119. package/src/tool/percentageCalculator/index.ts +34 -0
  120. package/src/tool/percentageCalculator/logic.ts +48 -0
  121. package/src/tool/percentageCalculator/seo.astro +14 -0
  122. package/src/tool/percentageCalculator/ui.ts +30 -0
  123. package/src/tool/rentIncreaseCalculator/bibliography.astro +14 -0
  124. package/src/tool/rentIncreaseCalculator/component.astro +587 -0
  125. package/src/tool/rentIncreaseCalculator/i18n/en.ts +159 -0
  126. package/src/tool/rentIncreaseCalculator/i18n/es.ts +220 -0
  127. package/src/tool/rentIncreaseCalculator/i18n/fr.ts +159 -0
  128. package/src/tool/rentIncreaseCalculator/index.ts +33 -0
  129. package/src/tool/rentIncreaseCalculator/logic.ts +43 -0
  130. package/src/tool/rentIncreaseCalculator/seo.astro +14 -0
  131. package/src/tool/rentIncreaseCalculator/ui.ts +19 -0
  132. package/src/tools.ts +16 -0
  133. package/src/types.ts +74 -0
@@ -0,0 +1,863 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { KnownLocale } from '../../types';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="lo-container" data-currency-symbol={ui.currencySymbol} data-currency-code={ui.currencyCode} data-currency-locale={ui.currencyLocale} data-label-units={ui.labelUnits} data-label-low={ui.labelLow} data-label-medium={ui.labelMedium} data-label-high={ui.labelHigh} data-label-accessible={ui.labelAccessible} data-label-difficult={ui.labelDifficult} data-label-extreme={ui.labelExtreme} data-label-optimal={ui.labelOptimalCutoff} data-game-translations={JSON.stringify(ui.gameTranslations)}>
14
+ <div class="lo-games-grid">
15
+ {['gordo', 'nino', 'euromillones', 'primitiva', 'bonoloto'].map((gameId) => (
16
+ <button class="lo-game-btn" data-game-btn data-id={gameId}>
17
+ <div class="lo-game-icon">
18
+ {gameId === 'gordo' || gameId === 'nino' ? (
19
+ <Icon name="mdi:gift" />
20
+ ) : (
21
+ <Icon name="mdi:dice-multiple" />
22
+ )}
23
+ </div>
24
+ <h4 class="lo-game-name">-</h4>
25
+ <p class="lo-game-description">-</p>
26
+ <div class="lo-game-footer">
27
+ <span class="lo-game-odds">-</span>
28
+ <Icon name="mdi:arrow-right" class="lo-arrow-icon" />
29
+ </div>
30
+ </button>
31
+ ))}
32
+ </div>
33
+
34
+ <div id="calculator-panel" class="lo-panel lo-hidden">
35
+ <div class="lo-panel-card">
36
+ <div class="lo-panel-header">
37
+ <div>
38
+ <h3 class="lo-panel-title">{ui.labelConfigurePlay}</h3>
39
+ <p class="lo-panel-subtitle">{ui.labelAdjustQuantity}</p>
40
+ </div>
41
+ <div class="lo-investment-badge">
42
+ <div>
43
+ <p class="lo-badge-label">{ui.labelTotalInvestment}</p>
44
+ <p class="lo-badge-value" id="display-total-cost">0,00€</p>
45
+ </div>
46
+ <div class="lo-badge-icon">
47
+ <Icon name="mdi:credit-card" />
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="lo-panel-content">
53
+ <div class="lo-input-section">
54
+ <div class="lo-slider-group">
55
+ <div class="lo-slider-header">
56
+ <label>{ui.labelTickets}</label>
57
+ <span class="lo-slider-value" id="ticket-count-display">1</span>
58
+ </div>
59
+ <input type="range" id="ticket-slider" min="1" max="100" value="1" class="lo-slider" />
60
+ <div class="lo-slider-labels">
61
+ <span>1 {ui.labelUnits}</span>
62
+ <span>100 {ui.labelUnits}</span>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="lo-recommendation-box">
67
+ <div class="lo-recommendation-header">
68
+ <Icon name="mdi:alert-circle" class="lo-alert-icon" />
69
+ <h4>{ui.labelExposureAnalysis}</h4>
70
+ </div>
71
+ <p class="lo-recommendation-text" id="recommendation-text">{ui.labelSelectLottery}</p>
72
+ <div id="optimal-tag" class="lo-optimal-tag lo-hidden">
73
+ <span>{ui.labelOptimalCutoff}: <span id="optimal-count">0</span> {ui.labelUnits}</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="lo-stats-grid">
79
+ <div class="lo-stat-card">
80
+ <p class="lo-stat-label">{ui.labelProbabilitySuccess}</p>
81
+ <p class="lo-stat-value" id="stat-probability">0.00%</p>
82
+ <div class="lo-stat-bar">
83
+ <div class="lo-stat-bar-fill" id="prob-bar"></div>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="lo-stat-card">
88
+ <p class="lo-stat-label">{ui.labelExpectedValue}</p>
89
+ <p class="lo-stat-value" id="stat-ev">-0.00€</p>
90
+ <p class="lo-stat-hint">{ui.labelReturnTheoretical}</p>
91
+ </div>
92
+
93
+ <div class="lo-stat-card lo-stat-card-full">
94
+ <div class="lo-stat-header">
95
+ <p class="lo-stat-label">{ui.labelSocialReturnIndex}</p>
96
+ <span class="lo-efficiency-label" id="efficiency-label">{ui.labelLow}</span>
97
+ </div>
98
+ <div class="lo-efficiency-value">
99
+ <p class="lo-stat-value" id="stat-efficiency">0.0</p>
100
+ <p class="lo-efficiency-max">/ 100</p>
101
+ </div>
102
+ <div class="lo-efficiency-segments" id="efficiency-segments">
103
+ {[...Array(10)].map((_, i) => (
104
+ <div class="lo-efficiency-segment" data-index={i}></div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="lo-comparison-section">
114
+ <div class="lo-comparison-header">
115
+ <div class="lo-comparison-icon">
116
+ <Icon name="mdi:chart-bar" />
117
+ </div>
118
+ <div>
119
+ <h3>{ui.labelComparison}</h3>
120
+ <p>{ui.labelCost} vs {ui.labelTypicalPrize}</p>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="lo-table-wrapper">
125
+ <table class="lo-table">
126
+ <thead>
127
+ <tr>
128
+ <th>{ui.labelLottery}</th>
129
+ <th class="lo-table-right">{ui.labelCost}</th>
130
+ <th class="lo-table-right">{ui.labelTypicalPrize}</th>
131
+ <th class="lo-table-right">{ui.labelDifficulty}</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody id="comparison-tbody">
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+
139
+ <p class="lo-table-note">{ui.labelNote}</p>
140
+ </div>
141
+ </div>
142
+
143
+ <style>
144
+ :root {
145
+ --lo-bg-card: #fff;
146
+ --lo-border-card: #eef2f6;
147
+ --lo-shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.02), 0 20px 40px -8px rgba(0, 0, 0, 0.05);
148
+ --lo-text-title: #0f172a;
149
+ --lo-text-secondary: #64748b;
150
+ --lo-text-primary: #1e293b;
151
+ --lo-bg-input: #f8fafc;
152
+ --lo-border-input: #f1f5f9;
153
+ --lo-focus-border: #10b981;
154
+ --lo-focus-ring: rgba(16, 185, 129, 0.15);
155
+ --lo-icon-color: #10b981;
156
+ --lo-bg-button: #0f172a;
157
+ --lo-text-button: #fff;
158
+ --lo-bg-hover: #1e293b;
159
+ --lo-bg-stat: #fff;
160
+ --lo-border-stat: #eef2f6;
161
+ --lo-bar-bg: #f1f5f9;
162
+ --lo-bar-fill: #10b981;
163
+ --lo-recommendation-bg: #eef5ff;
164
+ --lo-recommendation-border: #d0e4ff;
165
+ --lo-recommendation-icon: #3b82f6;
166
+ }
167
+
168
+ :global(.theme-dark) {
169
+ --lo-bg-card: #0f172a;
170
+ --lo-border-card: rgba(255, 255, 255, 0.05);
171
+ --lo-text-title: #f8fafc;
172
+ --lo-text-secondary: #94a3b8;
173
+ --lo-text-primary: #f8fafc;
174
+ --lo-bg-input: #1e293b;
175
+ --lo-border-input: #334155;
176
+ --lo-focus-border: #10b981;
177
+ --lo-focus-ring: rgba(16, 185, 129, 0.15);
178
+ --lo-icon-color: #34d399;
179
+ --lo-bg-button: #10b981;
180
+ --lo-text-button: #fff;
181
+ --lo-bg-hover: #059669;
182
+ --lo-bg-stat: #1e293b;
183
+ --lo-border-stat: rgba(255, 255, 255, 0.05);
184
+ --lo-bar-bg: #1e293b;
185
+ --lo-bar-fill: #10b981;
186
+ --lo-recommendation-bg: rgba(59, 130, 246, 0.1);
187
+ --lo-recommendation-border: rgba(59, 130, 246, 0.2);
188
+ --lo-recommendation-icon: #60a5fa;
189
+ }
190
+
191
+ :global([data-theme='dark']) {
192
+ --lo-bg-card: #0f172a;
193
+ --lo-border-card: rgba(255, 255, 255, 0.05);
194
+ --lo-text-title: #f8fafc;
195
+ --lo-text-secondary: #94a3b8;
196
+ --lo-text-primary: #f8fafc;
197
+ --lo-bg-input: #1e293b;
198
+ --lo-border-input: #334155;
199
+ --lo-focus-border: #10b981;
200
+ --lo-focus-ring: rgba(16, 185, 129, 0.15);
201
+ --lo-icon-color: #34d399;
202
+ --lo-bg-button: #10b981;
203
+ --lo-text-button: #fff;
204
+ --lo-bg-hover: #059669;
205
+ --lo-bg-stat: #1e293b;
206
+ --lo-border-stat: rgba(255, 255, 255, 0.05);
207
+ --lo-bar-bg: #1e293b;
208
+ --lo-bar-fill: #10b981;
209
+ --lo-recommendation-bg: rgba(59, 130, 246, 0.1);
210
+ --lo-recommendation-border: rgba(59, 130, 246, 0.2);
211
+ --lo-recommendation-icon: #60a5fa;
212
+ }
213
+
214
+ .lo-container {
215
+ width: 100%;
216
+ max-width: 1200px;
217
+ margin: 0 auto;
218
+ padding: 2rem 1rem;
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 3rem;
222
+ }
223
+
224
+ :global(.lo-games-grid) {
225
+ display: grid;
226
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
227
+ gap: 1.5rem;
228
+ }
229
+
230
+ :global(.lo-game-btn) {
231
+ display: flex;
232
+ flex-direction: column;
233
+ padding: 1.5rem;
234
+ background: var(--lo-bg-card);
235
+ border: 2px solid var(--lo-border-card);
236
+ border-radius: 1.5rem;
237
+ cursor: pointer;
238
+ transition: all 0.2s;
239
+ text-align: left;
240
+ color: inherit;
241
+ outline: none;
242
+ }
243
+
244
+ :global(.lo-game-btn:hover) {
245
+ border-color: var(--lo-focus-border);
246
+ box-shadow: var(--lo-shadow-card);
247
+ }
248
+
249
+ :global(.lo-game-icon) {
250
+ width: 3rem;
251
+ height: 3rem;
252
+ border-radius: 1rem;
253
+ background: var(--lo-bg-input);
254
+ margin-bottom: 1rem;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ color: var(--lo-icon-color);
259
+ }
260
+
261
+ :global(.lo-game-icon svg) {
262
+ width: 1.5rem;
263
+ height: 1.5rem;
264
+ }
265
+
266
+ :global(.lo-game-name) {
267
+ font-size: 1.125rem;
268
+ font-weight: 700;
269
+ color: var(--lo-text-title);
270
+ margin-bottom: 0.5rem;
271
+ }
272
+
273
+ :global(.lo-game-description) {
274
+ font-size: 0.875rem;
275
+ color: var(--lo-text-secondary);
276
+ margin-bottom: 1rem;
277
+ flex-grow: 1;
278
+ }
279
+
280
+ :global(.lo-game-footer) {
281
+ display: flex;
282
+ justify-content: space-between;
283
+ align-items: center;
284
+ padding-top: 1rem;
285
+ border-top: 1px solid var(--lo-border-card);
286
+ font-size: 0.75rem;
287
+ font-weight: 700;
288
+ color: var(--lo-text-secondary);
289
+ }
290
+
291
+ :global(.lo-arrow-icon) {
292
+ width: 1rem;
293
+ height: 1rem;
294
+ }
295
+
296
+ .lo-hidden {
297
+ display: none;
298
+ }
299
+
300
+ .lo-panel {
301
+ animation: slide-up 0.5s ease-out;
302
+ }
303
+
304
+ @keyframes slide-up {
305
+ from {
306
+ opacity: 0;
307
+ transform: translateY(20px);
308
+ }
309
+ to {
310
+ opacity: 1;
311
+ transform: translateY(0);
312
+ }
313
+ }
314
+
315
+ .lo-panel-card {
316
+ background: var(--lo-bg-card);
317
+ border: 1px solid var(--lo-border-card);
318
+ border-radius: 2rem;
319
+ padding: 2rem;
320
+ box-shadow: var(--lo-shadow-card);
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 2rem;
324
+ }
325
+
326
+ .lo-panel-header {
327
+ display: flex;
328
+ justify-content: space-between;
329
+ align-items: flex-start;
330
+ gap: 1rem;
331
+ }
332
+
333
+ .lo-panel-title {
334
+ font-size: 1.25rem;
335
+ font-weight: 700;
336
+ color: var(--lo-text-title);
337
+ margin-bottom: 0.25rem;
338
+ }
339
+
340
+ .lo-panel-subtitle {
341
+ font-size: 0.875rem;
342
+ color: var(--lo-text-secondary);
343
+ }
344
+
345
+ .lo-investment-badge {
346
+ display: flex;
347
+ gap: 1rem;
348
+ padding: 1rem;
349
+ background: var(--lo-bg-input);
350
+ border-radius: 1.5rem;
351
+ min-width: 200px;
352
+ }
353
+
354
+ .lo-investment-badge > div:first-child {
355
+ flex: 1;
356
+ }
357
+
358
+ .lo-badge-label {
359
+ font-size: 0.75rem;
360
+ font-weight: 700;
361
+ color: var(--lo-text-secondary);
362
+ text-transform: uppercase;
363
+ margin-bottom: 0.25rem;
364
+ }
365
+
366
+ .lo-badge-value {
367
+ font-size: 1.25rem;
368
+ font-weight: 900;
369
+ color: var(--lo-text-title);
370
+ }
371
+
372
+ .lo-badge-icon {
373
+ display: flex;
374
+ align-items: center;
375
+ color: var(--lo-icon-color);
376
+ }
377
+
378
+ .lo-badge-icon :global(svg) {
379
+ width: 1.5rem;
380
+ height: 1.5rem;
381
+ }
382
+
383
+ .lo-panel-content {
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: 2rem;
387
+ }
388
+
389
+ .lo-input-section {
390
+ display: flex;
391
+ flex-direction: column;
392
+ gap: 1.5rem;
393
+ }
394
+
395
+ .lo-slider-group {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 1rem;
399
+ }
400
+
401
+ .lo-slider-header {
402
+ display: flex;
403
+ justify-content: space-between;
404
+ align-items: center;
405
+ }
406
+
407
+ .lo-slider-header label {
408
+ font-weight: 700;
409
+ color: var(--lo-text-title);
410
+ }
411
+
412
+ .lo-slider-value {
413
+ font-weight: 700;
414
+ color: var(--lo-focus-border);
415
+ }
416
+
417
+ .lo-slider {
418
+ width: 100%;
419
+ height: 0.5rem;
420
+ border-radius: 0.25rem;
421
+ background: var(--lo-bar-bg);
422
+ outline: none;
423
+ -webkit-appearance: none;
424
+ appearance: none;
425
+ }
426
+
427
+ .lo-slider::-webkit-slider-thumb {
428
+ -webkit-appearance: none;
429
+ appearance: none;
430
+ width: 1.25rem;
431
+ height: 1.25rem;
432
+ background: var(--lo-focus-border);
433
+ border-radius: 50%;
434
+ cursor: pointer;
435
+ box-shadow: 0 0 10px var(--lo-focus-ring);
436
+ }
437
+
438
+ .lo-slider::-moz-range-thumb {
439
+ width: 1.25rem;
440
+ height: 1.25rem;
441
+ background: var(--lo-focus-border);
442
+ border-radius: 50%;
443
+ cursor: pointer;
444
+ border: none;
445
+ box-shadow: 0 0 10px var(--lo-focus-ring);
446
+ }
447
+
448
+ .lo-slider-labels {
449
+ display: flex;
450
+ justify-content: space-between;
451
+ font-size: 0.75rem;
452
+ font-weight: 700;
453
+ color: var(--lo-text-secondary);
454
+ }
455
+
456
+ .lo-recommendation-box {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 1rem;
460
+ padding: 1.5rem;
461
+ background: var(--lo-recommendation-bg);
462
+ border: 1px solid var(--lo-recommendation-border);
463
+ border-radius: 1rem;
464
+ }
465
+
466
+ .lo-recommendation-header {
467
+ display: flex;
468
+ align-items: center;
469
+ gap: 0.75rem;
470
+ }
471
+
472
+ .lo-alert-icon {
473
+ width: 1.25rem;
474
+ height: 1.25rem;
475
+ color: var(--lo-recommendation-icon);
476
+ flex-shrink: 0;
477
+ }
478
+
479
+ .lo-recommendation-header h4 {
480
+ font-size: 0.875rem;
481
+ font-weight: 700;
482
+ color: var(--lo-text-title);
483
+ }
484
+
485
+ .lo-recommendation-text {
486
+ font-size: 0.875rem;
487
+ line-height: 1.5;
488
+ color: var(--lo-text-primary);
489
+ }
490
+
491
+ .lo-optimal-tag {
492
+ font-size: 0.75rem;
493
+ font-weight: 700;
494
+ padding: 0.75rem;
495
+ background: var(--lo-bg-input);
496
+ border-radius: 0.5rem;
497
+ color: var(--lo-text-secondary);
498
+ }
499
+
500
+ .lo-stats-grid {
501
+ display: grid;
502
+ grid-template-columns: 1fr 1fr;
503
+ gap: 1rem;
504
+ }
505
+
506
+ .lo-stat-card {
507
+ padding: 1.5rem;
508
+ background: var(--lo-bg-stat);
509
+ border: 1px solid var(--lo-border-stat);
510
+ border-radius: 1.5rem;
511
+ display: flex;
512
+ flex-direction: column;
513
+ gap: 0.75rem;
514
+ }
515
+
516
+ .lo-stat-card-full {
517
+ grid-column: span 2;
518
+ }
519
+
520
+ .lo-stat-label {
521
+ font-size: 0.75rem;
522
+ font-weight: 700;
523
+ color: var(--lo-text-secondary);
524
+ text-transform: uppercase;
525
+ }
526
+
527
+ .lo-stat-value {
528
+ font-size: 1.5rem;
529
+ font-weight: 900;
530
+ color: var(--lo-text-title);
531
+ }
532
+
533
+ .lo-stat-bar {
534
+ width: 100%;
535
+ height: 0.25rem;
536
+ background: var(--lo-bar-bg);
537
+ border-radius: 0.25rem;
538
+ overflow: hidden;
539
+ }
540
+
541
+ .lo-stat-bar-fill {
542
+ height: 100%;
543
+ background: var(--lo-bar-fill);
544
+ transition: width 0.3s ease;
545
+ }
546
+
547
+ .lo-stat-hint {
548
+ font-size: 0.75rem;
549
+ color: var(--lo-text-secondary);
550
+ font-style: italic;
551
+ }
552
+
553
+ .lo-stat-header {
554
+ display: flex;
555
+ justify-content: space-between;
556
+ align-items: center;
557
+ }
558
+
559
+ .lo-efficiency-label {
560
+ font-size: 0.75rem;
561
+ font-weight: 700;
562
+ padding: 0.25rem 0.75rem;
563
+ border-radius: 0.5rem;
564
+ background: var(--lo-bar-bg);
565
+ color: var(--lo-text-secondary);
566
+ }
567
+
568
+ .lo-efficiency-value {
569
+ display: flex;
570
+ align-items: baseline;
571
+ gap: 0.5rem;
572
+ }
573
+
574
+ .lo-efficiency-max {
575
+ font-size: 0.875rem;
576
+ color: var(--lo-text-secondary);
577
+ }
578
+
579
+ .lo-efficiency-segments {
580
+ display: grid;
581
+ grid-template-columns: repeat(10, 1fr);
582
+ gap: 0.5rem;
583
+ }
584
+
585
+ .lo-efficiency-segment {
586
+ height: 0.5rem;
587
+ background: var(--lo-bar-bg);
588
+ border-radius: 0.25rem;
589
+ }
590
+
591
+ .lo-comparison-section {
592
+ display: flex;
593
+ flex-direction: column;
594
+ gap: 2rem;
595
+ padding: 2rem;
596
+ background: var(--lo-bg-card);
597
+ border: 1px solid var(--lo-border-card);
598
+ border-radius: 2rem;
599
+ box-shadow: var(--lo-shadow-card);
600
+ }
601
+
602
+ .lo-comparison-header {
603
+ display: flex;
604
+ align-items: flex-start;
605
+ gap: 1.5rem;
606
+ }
607
+
608
+ .lo-comparison-icon {
609
+ display: flex;
610
+ align-items: center;
611
+ justify-content: center;
612
+ width: 3rem;
613
+ height: 3rem;
614
+ background: var(--lo-bg-input);
615
+ border-radius: 1rem;
616
+ color: var(--lo-icon-color);
617
+ flex-shrink: 0;
618
+ }
619
+
620
+ .lo-comparison-icon :global(svg) {
621
+ width: 1.5rem;
622
+ height: 1.5rem;
623
+ }
624
+
625
+ .lo-comparison-header > div:last-child h3 {
626
+ font-size: 1.25rem;
627
+ font-weight: 700;
628
+ color: var(--lo-text-title);
629
+ margin-bottom: 0.25rem;
630
+ }
631
+
632
+ .lo-comparison-header > div:last-child p {
633
+ font-size: 0.875rem;
634
+ color: var(--lo-text-secondary);
635
+ }
636
+
637
+ :global(.lo-table-wrapper) {
638
+ overflow-x: auto;
639
+ }
640
+
641
+ :global(.lo-table) {
642
+ width: 100%;
643
+ border-collapse: collapse;
644
+ }
645
+
646
+ :global(.lo-table thead) {
647
+ background: var(--lo-bg-input);
648
+ }
649
+
650
+ :global(.lo-table th) {
651
+ padding: 1rem;
652
+ text-align: left;
653
+ font-weight: 700;
654
+ font-size: 0.875rem;
655
+ color: var(--lo-text-secondary);
656
+ text-transform: uppercase;
657
+ border-bottom: 2px solid var(--lo-border-card);
658
+ }
659
+
660
+ :global(.lo-table-right) {
661
+ text-align: right;
662
+ }
663
+
664
+ :global(.lo-table tbody tr) {
665
+ border-bottom: 1px solid var(--lo-border-card);
666
+ }
667
+
668
+ :global(.lo-table td) {
669
+ padding: 1rem;
670
+ font-size: 0.875rem;
671
+ color: var(--lo-text-primary);
672
+ }
673
+
674
+ :global(.lo-table tbody tr:hover) {
675
+ background: var(--lo-bg-input);
676
+ }
677
+
678
+ :global(.lo-table-note) {
679
+ font-size: 0.75rem;
680
+ color: var(--lo-text-secondary);
681
+ font-style: italic;
682
+ text-align: center;
683
+ }
684
+ </style>
685
+
686
+ <script>
687
+ import { LOTTERY_DATA, LotteryLogic, type LotteryId } from './logic';
688
+
689
+ const btns = document.querySelectorAll('[data-game-btn]');
690
+ const panel = document.getElementById('calculator-panel');
691
+ const slider = document.getElementById('ticket-slider') as HTMLInputElement;
692
+ const ticketCountDisplay = document.getElementById('ticket-count-display');
693
+ const totalCostDisplay = document.getElementById('display-total-cost');
694
+ const probDisplay = document.getElementById('stat-probability');
695
+ const probBar = document.getElementById('prob-bar');
696
+ const evDisplay = document.getElementById('stat-ev');
697
+ const efficiencyDisplay = document.getElementById('stat-efficiency');
698
+ const efficiencyLabel = document.getElementById('efficiency-label');
699
+ const recommendationText = document.getElementById('recommendation-text');
700
+ const optimalCount = document.getElementById('optimal-count');
701
+ const comparisonTbody = document.getElementById('comparison-tbody');
702
+ const segments = document.querySelectorAll('.lo-efficiency-segment');
703
+ const container = document.querySelector('.lo-container') as HTMLElement;
704
+
705
+ const currencySymbol = container?.getAttribute('data-currency-symbol') || '€';
706
+ const currencyCode = container?.getAttribute('data-currency-code') || 'EUR';
707
+ const currencyLocale = container?.getAttribute('data-currency-locale') || 'es-ES';
708
+ const labelLow = container?.getAttribute('data-label-low') || 'Baja';
709
+ const labelMedium = container?.getAttribute('data-label-medium') || 'Media';
710
+ const labelHigh = container?.getAttribute('data-label-high') || 'Alta';
711
+ const labelAccessible = container?.getAttribute('data-label-accessible') || 'Accesible';
712
+ const labelDifficult = container?.getAttribute('data-label-difficult') || 'Difícil';
713
+ const labelExtreme = container?.getAttribute('data-label-extreme') || 'Extrema';
714
+
715
+ let gameTranslations: Record<string, { name: string; description: string }> = {};
716
+ try {
717
+ const translationsJson = container?.getAttribute('data-game-translations');
718
+ if (translationsJson) gameTranslations = JSON.parse(translationsJson);
719
+ } catch {
720
+ void 0;
721
+ }
722
+
723
+ function formatCurrency(val: number): string {
724
+ return new Intl.NumberFormat(currencyLocale, {
725
+ style: 'currency',
726
+ currency: currencyCode,
727
+ }).format(val);
728
+ }
729
+
730
+ function getDifficulty(odds: number): string {
731
+ if (odds < 200000) return labelAccessible;
732
+ if (odds < 20000000) return labelDifficult;
733
+ return labelExtreme;
734
+ }
735
+
736
+ function formatPrize(jackpot: number): string {
737
+ if (jackpot > 1000000) {
738
+ return `${(jackpot / 1000000).toFixed(1)}M${currencySymbol}`;
739
+ }
740
+ return `${(jackpot / 1000).toFixed(0)}k${currencySymbol}`;
741
+ }
742
+
743
+ function getGameColor(gameId: LotteryId): string {
744
+ return gameId === 'gordo' || gameId === 'nino' ? '#fbbf24' : '#10b981';
745
+ }
746
+
747
+ function initializeGames() {
748
+ Object.entries(LOTTERY_DATA).forEach(([id, game]) => {
749
+ const btn = document.querySelector(`[data-game-btn][data-id="${id}"]`) as HTMLElement;
750
+ if (btn) {
751
+ const name = btn.querySelector('.lo-game-name') as HTMLElement;
752
+ const desc = btn.querySelector('.lo-game-description') as HTMLElement;
753
+ const odds = btn.querySelector('.lo-game-odds') as HTMLElement;
754
+ const translation = gameTranslations[id] || { name: game.name, description: game.description };
755
+ if (name) name.textContent = translation.name;
756
+ if (desc) desc.textContent = translation.description;
757
+ if (odds) odds.textContent = `1/${game.odds.toLocaleString()}`;
758
+ }
759
+ });
760
+
761
+ Object.values(LOTTERY_DATA).forEach((game) => {
762
+ const row = document.createElement('tr');
763
+ const difficulty = getDifficulty(game.odds);
764
+ const prize = formatPrize(game.jackpot);
765
+ const color = getGameColor(game.id as LotteryId);
766
+ row.innerHTML = `
767
+ <td><span style="display:inline-block;width:0.5rem;height:0.5rem;border-radius:50%;background:${color};margin-right:0.75rem"></span>${game.name}</td>
768
+ <td class="lo-table-right">${game.cost}${currencySymbol}</td>
769
+ <td class="lo-table-right">${prize}</td>
770
+ <td class="lo-table-right">${difficulty}</td>
771
+ `;
772
+ comparisonTbody?.appendChild(row);
773
+ });
774
+ }
775
+
776
+ function getEfficiencyLabel(eff: number): string {
777
+ if (eff > 65) return labelHigh;
778
+ if (eff > 50) return labelMedium;
779
+ return labelLow;
780
+ }
781
+
782
+ function updateEfficiencySegments(eff: number) {
783
+ segments.forEach((seg, i) => {
784
+ if (eff >= (i + 1) * 10) {
785
+ seg.classList.add('lo-active');
786
+ } else {
787
+ seg.classList.remove('lo-active');
788
+ }
789
+ });
790
+ }
791
+
792
+ function updateExpectedValue(expectedValue: number) {
793
+ if (!evDisplay) return;
794
+ evDisplay.textContent = formatCurrency(expectedValue);
795
+ const className = expectedValue < 0 ? 'lo-negative' : 'lo-positive';
796
+ evDisplay.className = `lo-stat-value ${className}`;
797
+ }
798
+
799
+ function updateProbability(winProbability: number) {
800
+ if (probDisplay) probDisplay.textContent = `${winProbability.toFixed(5)}%`;
801
+ if (probBar) probBar.style.width = `${Math.min(winProbability * 10, 100)}%`;
802
+ }
803
+
804
+ function updateStats(tickets: number, stats: ReturnType<typeof LotteryLogic.calculateStats>) {
805
+ if (ticketCountDisplay) ticketCountDisplay.textContent = tickets.toString();
806
+ if (totalCostDisplay) totalCostDisplay.textContent = formatCurrency(stats.totalCost);
807
+ updateProbability(stats.winProbability);
808
+ updateExpectedValue(stats.expectedValue);
809
+ if (recommendationText) recommendationText.textContent = stats.recommendation;
810
+ if (optimalCount) optimalCount.textContent = stats.optimalTickets.toString();
811
+ }
812
+
813
+ function updateEfficiency(selectedId: LotteryId) {
814
+ const eff = Math.min(LOTTERY_DATA[selectedId].payoutRate, 100);
815
+ if (efficiencyDisplay) efficiencyDisplay.textContent = eff.toFixed(1);
816
+ if (efficiencyLabel) efficiencyLabel.textContent = getEfficiencyLabel(eff);
817
+ updateEfficiencySegments(eff);
818
+ }
819
+
820
+ function update() {
821
+ const selectedBtn = document.querySelector('.lo-selected') as HTMLElement;
822
+ if (!selectedBtn) return;
823
+
824
+ const selectedId = selectedBtn.getAttribute('data-id') as LotteryId;
825
+ const tickets = parseInt(slider.value);
826
+ const stats = LotteryLogic.calculateStats(selectedId, tickets);
827
+
828
+ updateStats(tickets, stats);
829
+ updateEfficiency(selectedId);
830
+ }
831
+
832
+ btns.forEach((btn) => {
833
+ btn.addEventListener('click', () => {
834
+ btns.forEach((b) => b.classList.remove('lo-selected'));
835
+ btn.classList.add('lo-selected');
836
+ if (panel) panel.classList.remove('lo-hidden');
837
+ update();
838
+ });
839
+ });
840
+
841
+ slider?.addEventListener('input', update);
842
+ initializeGames();
843
+ </script>
844
+
845
+ <style is:global>
846
+ .lo-game-btn.lo-selected {
847
+ border-color: var(--lo-focus-border);
848
+ background: rgba(16, 185, 129, 0.05);
849
+ box-shadow: 0 0 0 4px var(--lo-focus-ring);
850
+ }
851
+
852
+ .lo-stat-value.lo-negative {
853
+ color: #ef4444;
854
+ }
855
+
856
+ .lo-stat-value.lo-positive {
857
+ color: #10b981;
858
+ }
859
+
860
+ .lo-efficiency-segment.lo-active {
861
+ background: var(--lo-bar-fill);
862
+ }
863
+ </style>