@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,689 @@
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="fire-container" id="fire-calculator-main" data-currency-symbol={ui.currencySymbol} data-currency-code={ui.currencyCode} data-currency-locale={ui.currencyLocale} data-time-format={ui.labelTimeRemainingFormat} data-already-fi={ui.labelAlreadyFI} data-unachievable={ui.labelUnachievable}>
14
+ <div class="fire-grid">
15
+ <div class="fire-panel">
16
+ <div class="fire-input-group">
17
+ <div class="fire-label-row">
18
+ <label class="fire-label">{ui.labelMonthlyExpenses}</label>
19
+ <span class="fire-value-display" id="val-expenses">2.500 €</span>
20
+ </div>
21
+ <input
22
+ type="range"
23
+ id="fire-expenses"
24
+ class="fire-range"
25
+ min="500"
26
+ max="10000"
27
+ step="50"
28
+ value="2500"
29
+ />
30
+ </div>
31
+
32
+ <div class="fire-input-group">
33
+ <div class="fire-label-row">
34
+ <label class="fire-label">
35
+ {ui.labelSWR}
36
+ <div class="fire-help">
37
+ <Icon name="mdi:information-outline" size={16} />
38
+ <span class="fire-tooltip">{ui.labelSWRTooltip}</span>
39
+ </div>
40
+ </label>
41
+ <span class="fire-value-display" id="val-swr">4.0 %</span>
42
+ </div>
43
+ <input
44
+ type="range"
45
+ id="fire-swr"
46
+ class="fire-range"
47
+ min="2.5"
48
+ max="6.0"
49
+ step="0.1"
50
+ value="4.0"
51
+ />
52
+ </div>
53
+
54
+ <div class="fire-input-group">
55
+ <div class="fire-label-row">
56
+ <label class="fire-label">{ui.labelCurrentWorth}</label>
57
+ <span class="fire-value-display" id="val-worth-display">50.000 €</span>
58
+ </div>
59
+ <input
60
+ type="range"
61
+ id="fire-worth"
62
+ class="fire-range"
63
+ min="0"
64
+ max="1000000"
65
+ step="1000"
66
+ value="50000"
67
+ />
68
+ </div>
69
+
70
+ <div class="fire-input-group">
71
+ <div class="fire-label-row">
72
+ <label class="fire-label">{ui.labelMonthlySavings}</label>
73
+ <span class="fire-value-display" id="val-savings">1.000 €</span>
74
+ </div>
75
+ <input
76
+ type="range"
77
+ id="fire-savings"
78
+ class="fire-range"
79
+ min="0"
80
+ max="5000"
81
+ step="50"
82
+ value="1000"
83
+ />
84
+ </div>
85
+
86
+ <div class="fire-input-group">
87
+ <div class="fire-label-row">
88
+ <label class="fire-label">
89
+ {ui.labelAnnualReturn}
90
+ <div class="fire-help">
91
+ <Icon name="mdi:information-outline" size={16} />
92
+ <span class="fire-tooltip">{ui.labelReturnTooltip}</span>
93
+ </div>
94
+ </label>
95
+ <span class="fire-value-display" id="val-return">7.0 %</span>
96
+ </div>
97
+ <input
98
+ type="range"
99
+ id="fire-return"
100
+ class="fire-range"
101
+ min="1"
102
+ max="12"
103
+ step="0.5"
104
+ value="7"
105
+ />
106
+ </div>
107
+
108
+ <div class="fire-checklist">
109
+ <h3>{ui.labelFreedomMilestones}</h3>
110
+ <div class="fire-milestones">
111
+ <div class="milestone-item lean">
112
+ <div class="milestone-header">
113
+ <span>{ui.labelLeanFIRE}</span>
114
+ <span id="label-lean">0%</span>
115
+ </div>
116
+ <div class="milestone-bar-bg">
117
+ <div class="milestone-bar-fill" id="bar-lean" style="width: 0%"></div>
118
+ </div>
119
+ </div>
120
+ <div class="milestone-item barista">
121
+ <div class="milestone-header">
122
+ <span>{ui.labelBaristaFIRE}</span>
123
+ <span id="label-barista">0%</span>
124
+ </div>
125
+ <div class="milestone-bar-bg">
126
+ <div class="milestone-bar-fill" id="bar-barista" style="width: 0%"></div>
127
+ </div>
128
+ </div>
129
+ <div class="milestone-item fat">
130
+ <div class="milestone-header">
131
+ <span>{ui.labelFatFIRE}</span>
132
+ <span id="label-fat">0%</span>
133
+ </div>
134
+ <div class="milestone-bar-bg">
135
+ <div class="milestone-bar-fill" id="bar-fat" style="width: 0%"></div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="fire-result-panel">
143
+ <div class="fire-main-result">
144
+ <span class="fire-magic-number-label">{ui.labelMagicNumber}</span>
145
+ <div class="fire-magic-number" id="fire-number">750.000 €</div>
146
+ <div class="fire-countdown" id="fire-time">{ui.labelTimeRemaining}</div>
147
+ </div>
148
+
149
+ <div class="fire-chart-container" id="fire-chart">
150
+ <svg class="fire-line-svg" preserveAspectRatio="none" id="svg-chart">
151
+ <path class="fire-path-savings" id="chart-area" d="" />
152
+ <path class="fire-path" id="chart-line" d="" />
153
+ </svg>
154
+ </div>
155
+
156
+ <div class="fire-checklist">
157
+ <h3>{ui.labelHiddenCostsChecklist}</h3>
158
+ <div class="checklist-grid" id="fire-checklist-grid">
159
+ <label class="checklist-item"><input type="checkbox" data-cost="200" /> {ui.labelHealth}</label>
160
+ <label class="checklist-item"><input type="checkbox" data-cost="150" /> {ui.labelTaxes}</label>
161
+ <label class="checklist-item"><input type="checkbox" data-cost="100" /> {ui.labelHome}</label>
162
+ <label class="checklist-item"><input type="checkbox" data-cost="300" /> {ui.labelTravel}</label>
163
+ <label class="checklist-item"><input type="checkbox" data-cost="100" /> {ui.labelEmergency}</label>
164
+ <label class="checklist-item"><input type="checkbox" data-cost="50" /> {ui.labelSubscriptions}</label>
165
+ </div>
166
+ <div id="checklist-feedback" class="checklist-feedback"></div>
167
+ </div>
168
+
169
+ <div class="fire-actions">
170
+ <button class="fire-btn fire-btn-primary" id="btn-export">
171
+ <Icon name="mdi:file-pdf-box" size={20} />
172
+ {ui.labelSavePlan}
173
+ </button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <style>
180
+ :root {
181
+ --fire-primary: #10b981;
182
+ --fire-primary-hover: #059669;
183
+ --fire-secondary: #3b82f6;
184
+ --fire-accent: #f59e0b;
185
+ --fire-bg: rgba(255, 255, 255, 0.7);
186
+ --fire-card-bg: rgba(255, 255, 255, 0.8);
187
+ --fire-text: #1f2937;
188
+ --fire-text-muted: #6b7280;
189
+ --fire-border: rgba(16, 185, 129, 0.2);
190
+ --fire-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
191
+ }
192
+
193
+ :global(.theme-dark) {
194
+ --fire-bg: rgba(17, 24, 39, 0.7);
195
+ --fire-card-bg: rgba(31, 41, 55, 0.8);
196
+ --fire-text: #f3f4f6;
197
+ --fire-text-muted: #9ca3af;
198
+ --fire-border: rgba(16, 185, 129, 0.3);
199
+ }
200
+
201
+ :global([data-theme='dark']) {
202
+ --fire-bg: rgba(17, 24, 39, 0.7);
203
+ --fire-card-bg: rgba(31, 41, 55, 0.8);
204
+ --fire-text: #f3f4f6;
205
+ --fire-text-muted: #9ca3af;
206
+ --fire-border: rgba(16, 185, 129, 0.3);
207
+ }
208
+
209
+ .fire-container {
210
+ max-width: 1000px;
211
+ margin: 0 auto;
212
+ padding: 2rem;
213
+ border-radius: 24px;
214
+ background: var(--fire-bg);
215
+ backdrop-filter: blur(12px);
216
+ -webkit-backdrop-filter: blur(12px);
217
+ border: 1px solid var(--fire-border);
218
+ box-shadow: var(--fire-shadow);
219
+ color: var(--fire-text);
220
+ }
221
+
222
+ .fire-grid {
223
+ display: grid;
224
+ grid-template-columns: 1fr 1fr;
225
+ gap: 2rem;
226
+ }
227
+
228
+ .fire-panel {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 1.5rem;
232
+ }
233
+
234
+ .fire-input-group {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 0.75rem;
238
+ }
239
+
240
+ .fire-label-row {
241
+ display: flex;
242
+ justify-content: space-between;
243
+ align-items: center;
244
+ }
245
+
246
+ .fire-label {
247
+ font-weight: 600;
248
+ font-size: 0.95rem;
249
+ }
250
+
251
+ .fire-value-display {
252
+ font-weight: 700;
253
+ color: var(--fire-primary);
254
+ background: var(--fire-card-bg);
255
+ padding: 2px 8px;
256
+ border-radius: 6px;
257
+ font-size: 1.1rem;
258
+ }
259
+
260
+ .fire-range {
261
+ -webkit-appearance: none;
262
+ appearance: none;
263
+ width: 100%;
264
+ height: 6px;
265
+ background: var(--fire-border);
266
+ border-radius: 5px;
267
+ outline: none;
268
+ }
269
+
270
+ .fire-range::-webkit-slider-thumb {
271
+ -webkit-appearance: none;
272
+ appearance: none;
273
+ width: 20px;
274
+ height: 20px;
275
+ background: var(--fire-primary);
276
+ cursor: pointer;
277
+ border-radius: 50%;
278
+ box-shadow: 0 2px 6px rgba(16, 185, 129, 0.4);
279
+ }
280
+
281
+ .fire-range::-moz-range-thumb {
282
+ width: 20px;
283
+ height: 20px;
284
+ background: var(--fire-primary);
285
+ cursor: pointer;
286
+ border-radius: 50%;
287
+ border: none;
288
+ box-shadow: 0 2px 6px rgba(16, 185, 129, 0.4);
289
+ }
290
+
291
+ .fire-help {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ gap: 4px;
295
+ cursor: help;
296
+ color: var(--fire-secondary);
297
+ position: relative;
298
+ }
299
+
300
+ .fire-tooltip {
301
+ display: none;
302
+ position: absolute;
303
+ bottom: 125%;
304
+ left: 50%;
305
+ transform: translateX(-50%);
306
+ background: var(--fire-card-bg);
307
+ border: 1px solid var(--fire-border);
308
+ color: var(--fire-text-muted);
309
+ padding: 8px 12px;
310
+ border-radius: 8px;
311
+ font-size: 0.85rem;
312
+ white-space: nowrap;
313
+ z-index: 10;
314
+ box-shadow: var(--fire-shadow);
315
+ }
316
+
317
+ .fire-help:hover .fire-tooltip {
318
+ display: block;
319
+ }
320
+
321
+ .fire-checklist {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 1rem;
325
+ }
326
+
327
+ .fire-checklist h3 {
328
+ margin: 0;
329
+ font-size: 1rem;
330
+ font-weight: 700;
331
+ }
332
+
333
+ .fire-milestones {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: 1rem;
337
+ }
338
+
339
+ .milestone-item {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 0.5rem;
343
+ }
344
+
345
+ .milestone-header {
346
+ display: flex;
347
+ justify-content: space-between;
348
+ font-size: 0.9rem;
349
+ font-weight: 600;
350
+ }
351
+
352
+ .milestone-item.lean .milestone-bar-fill {
353
+ background: linear-gradient(90deg, #ef4444, #f97316);
354
+ }
355
+
356
+ .milestone-item.barista .milestone-bar-fill {
357
+ background: linear-gradient(90deg, #f59e0b, #f59e0b);
358
+ }
359
+
360
+ .milestone-item.fat .milestone-bar-fill {
361
+ background: linear-gradient(90deg, #10b981, #14b8a6);
362
+ }
363
+
364
+ .milestone-bar-bg {
365
+ width: 100%;
366
+ height: 10px;
367
+ background: var(--fire-border);
368
+ border-radius: 5px;
369
+ overflow: hidden;
370
+ }
371
+
372
+ .milestone-bar-fill {
373
+ height: 100%;
374
+ transition: width 0.3s ease;
375
+ border-radius: 5px;
376
+ }
377
+
378
+ .fire-result-panel {
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 1.5rem;
382
+ }
383
+
384
+ .fire-main-result {
385
+ text-align: center;
386
+ padding: 1.5rem;
387
+ background: var(--fire-card-bg);
388
+ border-radius: 12px;
389
+ border: 1px solid var(--fire-border);
390
+ }
391
+
392
+ .fire-magic-number-label {
393
+ display: block;
394
+ font-size: 0.9rem;
395
+ color: var(--fire-text-muted);
396
+ margin-bottom: 0.5rem;
397
+ }
398
+
399
+ .fire-magic-number {
400
+ font-size: 2.5rem;
401
+ font-weight: 900;
402
+ color: var(--fire-primary);
403
+ margin-bottom: 0.5rem;
404
+ }
405
+
406
+ .fire-countdown {
407
+ font-size: 0.95rem;
408
+ color: var(--fire-text-muted);
409
+ }
410
+
411
+ .fire-chart-container {
412
+ width: 100%;
413
+ height: 200px;
414
+ background: var(--fire-card-bg);
415
+ border-radius: 12px;
416
+ border: 1px solid var(--fire-border);
417
+ padding: 1rem;
418
+ box-sizing: border-box;
419
+ }
420
+
421
+ .fire-line-svg {
422
+ width: 100%;
423
+ height: 100%;
424
+ }
425
+
426
+ .fire-path {
427
+ fill: none;
428
+ stroke: var(--fire-primary);
429
+ stroke-width: 2;
430
+ }
431
+
432
+ .fire-path-savings {
433
+ fill: rgba(16, 185, 129, 0.1);
434
+ }
435
+
436
+ .checklist-grid {
437
+ display: grid;
438
+ grid-template-columns: 1fr 1fr;
439
+ gap: 1rem;
440
+ }
441
+
442
+ .checklist-item {
443
+ display: flex;
444
+ align-items: center;
445
+ gap: 0.5rem;
446
+ font-size: 0.9rem;
447
+ cursor: pointer;
448
+ }
449
+
450
+ .checklist-item input[type="checkbox"] {
451
+ cursor: pointer;
452
+ accent-color: var(--fire-primary);
453
+ }
454
+
455
+ .checklist-feedback {
456
+ display: none;
457
+ padding: 0.75rem;
458
+ background: rgba(16, 185, 129, 0.1);
459
+ border: 1px solid rgba(16, 185, 129, 0.3);
460
+ border-radius: 8px;
461
+ color: var(--fire-primary);
462
+ font-size: 0.85rem;
463
+ font-weight: 600;
464
+ }
465
+
466
+ .checklist-feedback.visible {
467
+ display: block;
468
+ }
469
+
470
+ .fire-actions {
471
+ display: flex;
472
+ gap: 1rem;
473
+ }
474
+
475
+ .fire-btn {
476
+ padding: 0.75rem 1.5rem;
477
+ border: none;
478
+ border-radius: 8px;
479
+ font-weight: 600;
480
+ cursor: pointer;
481
+ display: flex;
482
+ align-items: center;
483
+ gap: 0.5rem;
484
+ transition: all 0.3s ease;
485
+ font-size: 0.9rem;
486
+ }
487
+
488
+ .fire-btn-primary {
489
+ background: var(--fire-primary);
490
+ color: white;
491
+ }
492
+
493
+ .fire-btn-primary:hover {
494
+ background: var(--fire-primary-hover);
495
+ transform: translateY(-2px);
496
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
497
+ }
498
+
499
+ @media (max-width: 768px) {
500
+ .fire-grid {
501
+ grid-template-columns: 1fr;
502
+ }
503
+
504
+ .fire-container {
505
+ padding: 1rem;
506
+ }
507
+
508
+ .checklist-grid {
509
+ grid-template-columns: 1fr;
510
+ }
511
+ }
512
+ </style>
513
+
514
+ <script>
515
+ import { FIRELogic } from './logic';
516
+
517
+ const expensesIn = document.getElementById('fire-expenses') as HTMLInputElement | null;
518
+ const swrIn = document.getElementById('fire-swr') as HTMLInputElement | null;
519
+ const worthIn = document.getElementById('fire-worth') as HTMLInputElement | null;
520
+ const savingsIn = document.getElementById('fire-savings') as HTMLInputElement | null;
521
+ const returnIn = document.getElementById('fire-return') as HTMLInputElement | null;
522
+
523
+ const valExpenses = document.getElementById('val-expenses');
524
+ const valSwr = document.getElementById('val-swr');
525
+ const valWorth = document.getElementById('val-worth-display');
526
+ const valSavings = document.getElementById('val-savings');
527
+ const valReturn = document.getElementById('val-return');
528
+
529
+ const fireNumber = document.getElementById('fire-number');
530
+ const fireTime = document.getElementById('fire-time');
531
+
532
+ const barLean = document.getElementById('bar-lean');
533
+ const barBarista = document.getElementById('bar-barista');
534
+ const barFat = document.getElementById('bar-fat');
535
+ const labelLean = document.getElementById('label-lean');
536
+ const labelBarista = document.getElementById('label-barista');
537
+ const labelFat = document.getElementById('label-fat');
538
+
539
+ const checklistGrid = document.getElementById('fire-checklist-grid');
540
+ const checklistFeedback = document.getElementById('checklist-feedback');
541
+
542
+ const chartLine = document.getElementById('chart-line') as SVGPathElement | null;
543
+ const chartArea = document.getElementById('chart-area') as SVGPathElement | null;
544
+
545
+ const container = document.querySelector('.fire-container') as HTMLElement;
546
+ const currencyLocale = container?.getAttribute('data-currency-locale') || 'es-ES';
547
+ const timeFormatTemplate = container?.getAttribute('data-time-format') || 'You have {years} years and {months} months left';
548
+ const labelAlreadyFI = container?.getAttribute('data-already-fi') || 'You have already achieved financial freedom!';
549
+ const labelUnachievable = container?.getAttribute('data-unachievable') || 'Goal unachievable with this savings rate';
550
+
551
+ function formatCurrency(val: number): string {
552
+ return new Intl.NumberFormat(currencyLocale, {
553
+ style: 'currency',
554
+ currency: container?.getAttribute('data-currency-code') || 'EUR',
555
+ maximumFractionDigits: 0,
556
+ }).format(val);
557
+ }
558
+
559
+ function updateDisplay(result: ReturnType<typeof FIRELogic.calculateFIRE>, extraCosts: number) {
560
+ if (fireNumber) fireNumber.textContent = formatCurrency(result.magicNumber);
561
+ if (valExpenses) {
562
+ valExpenses.textContent = formatCurrency(result.monthlyExpenses);
563
+ valExpenses.style.color = extraCosts > 0 ? 'var(--fire-accent)' : 'var(--fire-primary)';
564
+ }
565
+ if (valSwr) valSwr.textContent = result.swr.toFixed(1) + ' %';
566
+ if (valWorth) valWorth.textContent = formatCurrency(result.currentWorth);
567
+ if (valSavings) valSavings.textContent = formatCurrency(result.monthlySavings);
568
+ if (valReturn) valReturn.textContent = result.annualReturn.toFixed(1) + ' %';
569
+ }
570
+
571
+ function updateMilestones(result: ReturnType<typeof FIRELogic.calculateFIRE>) {
572
+ const update = (current: number, target: number, bar: HTMLElement | null, label: HTMLElement | null) => {
573
+ const pct = Math.min(100, (current / target) * 100);
574
+ if (bar) bar.style.width = pct + '%';
575
+ if (label) label.textContent = pct.toFixed(0) + '%';
576
+ };
577
+ update(result.currentWorth, result.leanTarget, barLean, labelLean);
578
+ update(result.currentWorth, result.baristaTarget, barBarista, labelBarista);
579
+ update(result.currentWorth, result.fatTarget, barFat, labelFat);
580
+ }
581
+
582
+ function updateChart(result: ReturnType<typeof FIRELogic.calculateFIRE>) {
583
+ if (!chartLine || !chartArea) return;
584
+ const { wealthHistory } = result;
585
+ const pointsCount = wealthHistory.length;
586
+ const maxVal = Math.max(result.magicNumber * 1.1, ...wealthHistory);
587
+ let d = '';
588
+ wealthHistory.forEach((v, i) => {
589
+ const x = (i / (pointsCount - 1)) * 100;
590
+ const y = 100 - (v / maxVal) * 100;
591
+ d += (i === 0 ? 'M ' : ' L ') + x + ' ' + y;
592
+ });
593
+ chartLine.setAttribute('d', d);
594
+ chartArea.setAttribute('d', d + ' L 100 100 L 0 100 Z');
595
+ document.getElementById('svg-chart')?.setAttribute('viewBox', '0 0 100 100');
596
+ }
597
+
598
+ function updateTime(result: ReturnType<typeof FIRELogic.calculateFIRE>) {
599
+ if (!fireTime) return;
600
+ if (result.isAlreadyFI) {
601
+ fireTime.textContent = labelAlreadyFI;
602
+ } else if (!result.isAchievable) {
603
+ fireTime.textContent = labelUnachievable;
604
+ } else {
605
+ const formatted = timeFormatTemplate
606
+ .split('{years}').join(String(result.yearsToFI))
607
+ .split('{months}').join(String(result.monthsToFI));
608
+ fireTime.textContent = formatted;
609
+ }
610
+ }
611
+
612
+ function calculateFIRE() {
613
+ if (!expensesIn || !swrIn || !worthIn || !savingsIn || !returnIn) return;
614
+
615
+ let extraCosts = 0;
616
+ const checks = checklistGrid?.querySelectorAll('input[type="checkbox"]:checked') as NodeListOf<HTMLInputElement>;
617
+ checks.forEach((check) => {
618
+ extraCosts += parseFloat(check.getAttribute('data-cost') || '0');
619
+ });
620
+
621
+ const result = FIRELogic.calculateFIRE({
622
+ monthlyExpenses: parseFloat(expensesIn.value),
623
+ swr: parseFloat(swrIn.value),
624
+ currentWorth: parseFloat(worthIn.value),
625
+ monthlySavings: parseFloat(savingsIn.value),
626
+ annualReturn: parseFloat(returnIn.value),
627
+ extraCosts,
628
+ });
629
+
630
+ updateDisplay(result, extraCosts);
631
+ updateMilestones(result);
632
+ updateTime(result);
633
+ updateChart(result);
634
+ }
635
+
636
+ [expensesIn, swrIn, worthIn, savingsIn, returnIn].forEach((el) => {
637
+ el?.addEventListener('input', calculateFIRE);
638
+ });
639
+
640
+ checklistGrid?.addEventListener('change', (e) => {
641
+ const target = e.target as HTMLInputElement;
642
+ if (target && checklistFeedback) {
643
+ if (target.checked) {
644
+ checklistFeedback.textContent = `Añadido: ${target.parentElement?.textContent?.trim()}`;
645
+ checklistFeedback.classList.add('visible');
646
+ setTimeout(() => checklistFeedback.classList.remove('visible'), 2000);
647
+ }
648
+ }
649
+ calculateFIRE();
650
+ });
651
+
652
+ async function exportPDF() {
653
+ const { default: jsPDF } = await import('jspdf');
654
+ const { default: html2canvas } = await import('html2canvas');
655
+ const element = document.getElementById('fire-calculator-main');
656
+ if (!element) return;
657
+ const canvas = await html2canvas(element, {
658
+ scale: 2,
659
+ useCORS: true,
660
+ backgroundColor: '#ffffff',
661
+ onclone: (clonedDoc) => {
662
+ const clonedEl = clonedDoc.getElementById('fire-calculator-main');
663
+ if (!clonedEl) return;
664
+ clonedEl.style.padding = '40px';
665
+ clonedEl.style.background = '#ffffff';
666
+ clonedEl.style.color = '#000000';
667
+ clonedEl.style.width = '1000px';
668
+ const results = clonedEl.querySelector('.fire-result-panel') as HTMLElement;
669
+ if (results) {
670
+ results.style.background = '#f9fafb';
671
+ results.style.color = '#000000';
672
+ }
673
+ const magic = clonedEl.querySelector('.fire-magic-number') as HTMLElement;
674
+ if (magic) magic.style.color = '#10b981';
675
+ },
676
+ });
677
+ const imgData = canvas.toDataURL('image/png');
678
+ const pdf = new jsPDF('p', 'pt', 'a4');
679
+ pdf.addImage(imgData, 'PNG', 0, 0, 595.28, (canvas.height * 595.28) / canvas.width);
680
+ pdf.save('mi-plan-fire.pdf');
681
+ }
682
+
683
+ const btnExport = document.getElementById('btn-export');
684
+ btnExport?.addEventListener('click', () => {
685
+ exportPDF().catch((error) => console.error('PDF error:', error));
686
+ });
687
+
688
+ calculateFIRE();
689
+ </script>