@jjlmoya/utils-chrono 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +65 -0
- package/scripts/postinstall.mjs +27 -0
- package/src/category/ChronoCategorySEO.astro +8 -0
- package/src/category/i18n/de.ts +23 -0
- package/src/category/i18n/en.ts +23 -0
- package/src/category/i18n/es.ts +23 -0
- package/src/category/i18n/fr.ts +23 -0
- package/src/category/i18n/id.ts +23 -0
- package/src/category/i18n/it.ts +23 -0
- package/src/category/i18n/ja.ts +23 -0
- package/src/category/i18n/ko.ts +23 -0
- package/src/category/i18n/nl.ts +23 -0
- package/src/category/i18n/pl.ts +23 -0
- package/src/category/i18n/pt.ts +23 -0
- package/src/category/i18n/ru.ts +23 -0
- package/src/category/i18n/sv.ts +23 -0
- package/src/category/i18n/tr.ts +23 -0
- package/src/category/i18n/zh.ts +23 -0
- package/src/category/index.ts +42 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/entries.ts +32 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +28 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +161 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/i18n_coverage.test.ts +36 -0
- package/src/tests/locale_completeness.test.ts +29 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/slug_language_code_format.test.ts +23 -0
- package/src/tests/slug_uniqueness.test.ts +81 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +18 -0
- package/src/tool/beat-rate-converter/beat-rate-converter.css +301 -0
- package/src/tool/beat-rate-converter/bibliography.astro +16 -0
- package/src/tool/beat-rate-converter/bibliography.ts +12 -0
- package/src/tool/beat-rate-converter/client.ts +46 -0
- package/src/tool/beat-rate-converter/component.astro +15 -0
- package/src/tool/beat-rate-converter/components/ConverterPanel.astro +87 -0
- package/src/tool/beat-rate-converter/entry.ts +42 -0
- package/src/tool/beat-rate-converter/i18n/de.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/en.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/es.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/fr.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/id.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/it.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/ja.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/ko.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/nl.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/pl.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/pt.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/ru.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/sv.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/tr.ts +138 -0
- package/src/tool/beat-rate-converter/i18n/zh.ts +138 -0
- package/src/tool/beat-rate-converter/index.ts +11 -0
- package/src/tool/beat-rate-converter/seo.astro +16 -0
- package/src/tool/crown-reference-guide/bibliography.astro +16 -0
- package/src/tool/crown-reference-guide/bibliography.ts +20 -0
- package/src/tool/crown-reference-guide/client.ts +193 -0
- package/src/tool/crown-reference-guide/component.astro +69 -0
- package/src/tool/crown-reference-guide/components/CrownView.astro +40 -0
- package/src/tool/crown-reference-guide/components/MovementSelector.astro +26 -0
- package/src/tool/crown-reference-guide/components/PositionPanel.astro +59 -0
- package/src/tool/crown-reference-guide/crown-reference-guide.css +383 -0
- package/src/tool/crown-reference-guide/entry.ts +60 -0
- package/src/tool/crown-reference-guide/i18n/de.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/en.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/es.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/fr.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/id.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/it.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/ja.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/ko.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/nl.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/pl.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/pt.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/ru.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/sv.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/tr.ts +207 -0
- package/src/tool/crown-reference-guide/i18n/zh.ts +207 -0
- package/src/tool/crown-reference-guide/index.ts +11 -0
- package/src/tool/crown-reference-guide/seo.astro +16 -0
- package/src/tool/crown-reference-guide/utils.ts +0 -0
- package/src/tool/demagnetizing-timer/bibliography.astro +16 -0
- package/src/tool/demagnetizing-timer/bibliography.ts +12 -0
- package/src/tool/demagnetizing-timer/client.ts +221 -0
- package/src/tool/demagnetizing-timer/component.astro +15 -0
- package/src/tool/demagnetizing-timer/components/TimerPanel.astro +92 -0
- package/src/tool/demagnetizing-timer/demagnetizing-timer.css +389 -0
- package/src/tool/demagnetizing-timer/entry.ts +50 -0
- package/src/tool/demagnetizing-timer/helpers/field.ts +134 -0
- package/src/tool/demagnetizing-timer/i18n/de.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/en.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/es.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/fr.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/id.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/it.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/ja.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/ko.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/nl.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/pl.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/pt.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/ru.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/sv.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/tr.ts +143 -0
- package/src/tool/demagnetizing-timer/i18n/zh.ts +143 -0
- package/src/tool/demagnetizing-timer/index.ts +11 -0
- package/src/tool/demagnetizing-timer/seo.astro +16 -0
- package/src/tool/demagnetizing-timer/utils.ts +0 -0
- package/src/tool/power-reserve-estimator/bibliography.astro +16 -0
- package/src/tool/power-reserve-estimator/bibliography.ts +16 -0
- package/src/tool/power-reserve-estimator/client.ts +139 -0
- package/src/tool/power-reserve-estimator/component.astro +15 -0
- package/src/tool/power-reserve-estimator/components/EstimatorPanel.astro +150 -0
- package/src/tool/power-reserve-estimator/entry.ts +51 -0
- package/src/tool/power-reserve-estimator/i18n/de.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/en.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/es.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/fr.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/id.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/it.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/ja.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/ko.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/nl.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/pl.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/pt.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/ru.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/sv.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/tr.ts +158 -0
- package/src/tool/power-reserve-estimator/i18n/zh.ts +158 -0
- package/src/tool/power-reserve-estimator/index.ts +11 -0
- package/src/tool/power-reserve-estimator/power-reserve-estimator.css +402 -0
- package/src/tool/power-reserve-estimator/seo.astro +16 -0
- package/src/tool/strap-taper-calculator/bibliography.astro +16 -0
- package/src/tool/strap-taper-calculator/bibliography.ts +12 -0
- package/src/tool/strap-taper-calculator/client.ts +129 -0
- package/src/tool/strap-taper-calculator/component.astro +15 -0
- package/src/tool/strap-taper-calculator/components/CalculatorPanel.astro +114 -0
- package/src/tool/strap-taper-calculator/entry.ts +56 -0
- package/src/tool/strap-taper-calculator/i18n/de.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/en.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/es.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/fr.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/id.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/it.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/ja.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/ko.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/nl.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/pl.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/pt.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/ru.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/sv.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/tr.ts +152 -0
- package/src/tool/strap-taper-calculator/i18n/zh.ts +152 -0
- package/src/tool/strap-taper-calculator/index.ts +11 -0
- package/src/tool/strap-taper-calculator/seo.astro +16 -0
- package/src/tool/strap-taper-calculator/strap-taper-calculator.css +320 -0
- package/src/tool/watch-accuracy-tracker/bibliography.astro +16 -0
- package/src/tool/watch-accuracy-tracker/bibliography.ts +12 -0
- package/src/tool/watch-accuracy-tracker/chart.ts +126 -0
- package/src/tool/watch-accuracy-tracker/client.ts +287 -0
- package/src/tool/watch-accuracy-tracker/component.astro +35 -0
- package/src/tool/watch-accuracy-tracker/components/AccuracyTracker.astro +96 -0
- package/src/tool/watch-accuracy-tracker/components/DriftPredictor.astro +126 -0
- package/src/tool/watch-accuracy-tracker/components/LogHistory.astro +66 -0
- package/src/tool/watch-accuracy-tracker/entry.ts +94 -0
- package/src/tool/watch-accuracy-tracker/i18n/de.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/en.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/es.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/fr.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/id.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/it.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/ja.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/ko.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/nl.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/pl.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/pt.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/ru.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/sv.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/tr.ts +167 -0
- package/src/tool/watch-accuracy-tracker/i18n/zh.ts +167 -0
- package/src/tool/watch-accuracy-tracker/index.ts +9 -0
- package/src/tool/watch-accuracy-tracker/logger.ts +105 -0
- package/src/tool/watch-accuracy-tracker/seo.astro +16 -0
- package/src/tool/watch-accuracy-tracker/utils.ts +99 -0
- package/src/tool/watch-accuracy-tracker/watch-accuracy-tracker.css +564 -0
- package/src/tool/watch-savings-planner/bibliography.astro +16 -0
- package/src/tool/watch-savings-planner/bibliography.ts +8 -0
- package/src/tool/watch-savings-planner/client.ts +271 -0
- package/src/tool/watch-savings-planner/component.astro +33 -0
- package/src/tool/watch-savings-planner/components/AddGoalForm.astro +49 -0
- package/src/tool/watch-savings-planner/components/GoalCard.astro +58 -0
- package/src/tool/watch-savings-planner/entry.ts +62 -0
- package/src/tool/watch-savings-planner/i18n/de.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/en.ts +155 -0
- package/src/tool/watch-savings-planner/i18n/es.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/fr.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/id.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/it.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/ja.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/ko.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/nl.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/pl.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/pt.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/ru.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/sv.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/tr.ts +153 -0
- package/src/tool/watch-savings-planner/i18n/zh.ts +153 -0
- package/src/tool/watch-savings-planner/index.ts +11 -0
- package/src/tool/watch-savings-planner/seo.astro +16 -0
- package/src/tool/watch-savings-planner/utils.ts +0 -0
- package/src/tool/watch-savings-planner/watch-savings-planner.css +460 -0
- package/src/tool/water-resistance-converter/bibliography.astro +16 -0
- package/src/tool/water-resistance-converter/bibliography.ts +16 -0
- package/src/tool/water-resistance-converter/client.ts +56 -0
- package/src/tool/water-resistance-converter/component.astro +15 -0
- package/src/tool/water-resistance-converter/components/ConverterPanel.astro +113 -0
- package/src/tool/water-resistance-converter/entry.ts +52 -0
- package/src/tool/water-resistance-converter/i18n/de.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/en.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/es.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/fr.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/id.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/it.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/ja.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/ko.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/nl.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/pl.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/pt.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/ru.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/sv.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/tr.ts +148 -0
- package/src/tool/water-resistance-converter/i18n/zh.ts +148 -0
- package/src/tool/water-resistance-converter/index.ts +11 -0
- package/src/tool/water-resistance-converter/seo.astro +16 -0
- package/src/tool/water-resistance-converter/water-resistance-converter.css +254 -0
- package/src/tool/wrist-presence-calculator/bibliography.astro +16 -0
- package/src/tool/wrist-presence-calculator/bibliography.ts +12 -0
- package/src/tool/wrist-presence-calculator/client.ts +180 -0
- package/src/tool/wrist-presence-calculator/component.astro +23 -0
- package/src/tool/wrist-presence-calculator/components/CalculatorInputs.astro +97 -0
- package/src/tool/wrist-presence-calculator/components/FitResult.astro +68 -0
- package/src/tool/wrist-presence-calculator/components/Visualizer.astro +16 -0
- package/src/tool/wrist-presence-calculator/entry.ts +59 -0
- package/src/tool/wrist-presence-calculator/helpers/canvas.ts +189 -0
- package/src/tool/wrist-presence-calculator/helpers/results.ts +78 -0
- package/src/tool/wrist-presence-calculator/i18n/de.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/en.ts +155 -0
- package/src/tool/wrist-presence-calculator/i18n/es.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/fr.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/id.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/it.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/ja.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/ko.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/nl.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/pl.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/pt.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/ru.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/sv.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/tr.ts +139 -0
- package/src/tool/wrist-presence-calculator/i18n/zh.ts +155 -0
- package/src/tool/wrist-presence-calculator/index.ts +11 -0
- package/src/tool/wrist-presence-calculator/seo.astro +16 -0
- package/src/tool/wrist-presence-calculator/utils.ts +30 -0
- package/src/tool/wrist-presence-calculator/wrist-presence-calculator.css +372 -0
- package/src/tools.ts +26 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
interface Goal {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
price: number;
|
|
5
|
+
saved: number;
|
|
6
|
+
monthly: number;
|
|
7
|
+
_celebrated?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const mainEl = document.querySelector('.tool-main-card') as HTMLElement;
|
|
11
|
+
const ui = mainEl ? JSON.parse(mainEl.dataset.ui || '{}') : {};
|
|
12
|
+
|
|
13
|
+
let goals: Goal[] = [];
|
|
14
|
+
let editingId: string | null = null;
|
|
15
|
+
|
|
16
|
+
const totalSavedEl = document.getElementById('total-saved') as HTMLElement;
|
|
17
|
+
const totalGoalsEl = document.getElementById('total-goals') as HTMLElement;
|
|
18
|
+
const goalsList = document.getElementById('goals-list') as HTMLElement;
|
|
19
|
+
const triggerBtn = document.getElementById('add-goal-trigger') as HTMLButtonElement;
|
|
20
|
+
const form = document.getElementById('add-goal-form') as HTMLElement;
|
|
21
|
+
const formClose = document.getElementById('form-close') as HTMLButtonElement;
|
|
22
|
+
const formSubmit = document.getElementById('form-submit') as HTMLButtonElement;
|
|
23
|
+
const nameInput = document.getElementById('goal-name') as HTMLInputElement;
|
|
24
|
+
const priceInput = document.getElementById('goal-price') as HTMLInputElement;
|
|
25
|
+
const savedInput = document.getElementById('goal-saved') as HTMLInputElement;
|
|
26
|
+
const monthlySlider = document.getElementById('goal-monthly') as HTMLInputElement;
|
|
27
|
+
const monthlyDisplay = document.getElementById('monthly-display') as HTMLElement;
|
|
28
|
+
const template = document.getElementById('goal-card-template') as HTMLTemplateElement;
|
|
29
|
+
|
|
30
|
+
const STORAGE_KEY = 'watch-savings-goals';
|
|
31
|
+
const TIMESTAMP_KEY = 'watch-savings-timestamp';
|
|
32
|
+
const CURRENCY_CODE = ui.currency || 'USD';
|
|
33
|
+
const FMT = (n: number) => n.toLocaleString(undefined, { style: 'currency', currency: CURRENCY_CODE, minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
34
|
+
const confettiColors = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#3b82f6', '#ec4899'];
|
|
35
|
+
|
|
36
|
+
function autoSimulate() {
|
|
37
|
+
const last = parseInt(localStorage.getItem(TIMESTAMP_KEY) || '0', 10);
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (!last || goals.length === 0) { localStorage.setItem(TIMESTAMP_KEY, String(now)); return; }
|
|
40
|
+
const msPerMonth = 30.44 * 24 * 60 * 60 * 1000;
|
|
41
|
+
const monthsPassed = Math.floor((now - last) / msPerMonth);
|
|
42
|
+
if (monthsPassed < 1) return;
|
|
43
|
+
for (const g of goals) {
|
|
44
|
+
if (g.saved < g.price) {
|
|
45
|
+
g.saved = Math.min(g.saved + g.monthly * monthsPassed, g.price);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
localStorage.setItem(TIMESTAMP_KEY, String(now));
|
|
49
|
+
saveGoals();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function loadGoals() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
55
|
+
goals = raw ? JSON.parse(raw) : [];
|
|
56
|
+
} catch {
|
|
57
|
+
goals = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveGoals() {
|
|
62
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(goals));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fmt(amount: number): string {
|
|
66
|
+
if (amount >= 1000) return FMT(amount);
|
|
67
|
+
return FMT(amount);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fmtFull(amount: number): string {
|
|
71
|
+
return FMT(amount);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function calcMonths(g: Goal): number {
|
|
75
|
+
const remaining = g.price - g.saved;
|
|
76
|
+
if (remaining <= 0) return 0;
|
|
77
|
+
if (g.monthly <= 0) return Infinity;
|
|
78
|
+
return Math.ceil(remaining / g.monthly);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function calcDate(months: number): string {
|
|
82
|
+
if (months === 0) return ui.now || 'Now';
|
|
83
|
+
if (!isFinite(months)) return '\u2014';
|
|
84
|
+
const d = new Date();
|
|
85
|
+
d.setMonth(d.getMonth() + months);
|
|
86
|
+
return d.toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function updateSummary() {
|
|
90
|
+
const total = goals.reduce((sum, g) => sum + g.saved, 0);
|
|
91
|
+
totalSavedEl.textContent = fmt(total);
|
|
92
|
+
totalGoalsEl.textContent = String(goals.length);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderEmptyState() {
|
|
96
|
+
goalsList.innerHTML = `
|
|
97
|
+
<div class="goals-empty">
|
|
98
|
+
<svg viewBox="0 0 24 24" width="48" height="48" opacity="0.3">
|
|
99
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
|
|
100
|
+
</svg>
|
|
101
|
+
<p class="empty-title">${ui.goalsEmpty || 'No savings goals yet'}</p>
|
|
102
|
+
<p class="empty-sub">${ui.goalsEmptySub || 'Set your first watch goal and start saving!'}</p>
|
|
103
|
+
</div>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function setCardText(clone: DocumentFragment, sel: string, text: string) {
|
|
107
|
+
(clone.querySelector(sel) as HTMLElement).textContent = text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderGoalCard(goal: Goal) {
|
|
111
|
+
const clone = template.content.cloneNode(true) as DocumentFragment;
|
|
112
|
+
const card = clone.querySelector('.goal-card') as HTMLElement;
|
|
113
|
+
card.dataset.id = goal.id;
|
|
114
|
+
|
|
115
|
+
const achieved = goal.saved >= goal.price;
|
|
116
|
+
const percent = Math.min(100, Math.round((goal.saved / goal.price) * 100));
|
|
117
|
+
|
|
118
|
+
setCardText(clone, '.goal-name', goal.name);
|
|
119
|
+
setCardText(clone, '.goal-price', fmtFull(goal.price));
|
|
120
|
+
setCardText(clone, '.goal-saved', fmtFull(Math.min(goal.saved, goal.price)));
|
|
121
|
+
setCardText(clone, '.goal-monthly', fmtFull(goal.monthly));
|
|
122
|
+
setCardText(clone, '.goal-date', achieved ? (ui.now || 'Now') : calcDate(calcMonths(goal)));
|
|
123
|
+
|
|
124
|
+
(clone.querySelector('.goal-ring-fill') as SVGElement).setAttribute('stroke-dashoffset', String(326.73 * (1 - percent / 100)));
|
|
125
|
+
setCardText(clone, '.goal-percent', `${percent}%`);
|
|
126
|
+
|
|
127
|
+
if (achieved) {
|
|
128
|
+
card.classList.add('goal-achieved');
|
|
129
|
+
const badge = clone.querySelector('.goal-achieved-badge') as HTMLElement;
|
|
130
|
+
badge.style.display = 'inline-flex';
|
|
131
|
+
setCardText(clone, '.goal-ring-label', ui.goalAchieved || 'Achieved!');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const slider = clone.querySelector('.goal-slider') as HTMLInputElement;
|
|
135
|
+
slider.value = String(goal.monthly);
|
|
136
|
+
|
|
137
|
+
(clone.querySelector('.goal-delete') as HTMLButtonElement).addEventListener('click', () => deleteGoal(goal.id));
|
|
138
|
+
slider.addEventListener('input', () => {
|
|
139
|
+
goal.monthly = parseInt(slider.value, 10);
|
|
140
|
+
saveGoals();
|
|
141
|
+
renderGoals();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
goalsList.appendChild(clone);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderGoals() {
|
|
148
|
+
goalsList.innerHTML = '';
|
|
149
|
+
if (goals.length === 0) { renderEmptyState(); return; }
|
|
150
|
+
for (const goal of goals) renderGoalCard(goal);
|
|
151
|
+
checkAchievements();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function checkAchievements() {
|
|
155
|
+
for (const g of goals) {
|
|
156
|
+
if (g.saved >= g.price && !g._celebrated) {
|
|
157
|
+
celebrate();
|
|
158
|
+
g._celebrated = true;
|
|
159
|
+
saveGoals();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function celebrate() {
|
|
165
|
+
const rect = goalsList.getBoundingClientRect();
|
|
166
|
+
const cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
|
|
167
|
+
const parts = [];
|
|
168
|
+
for (let i = 0; i < 60; i++) {
|
|
169
|
+
const a = Math.random() * Math.PI * 2, s = Math.random() * 8 + 4;
|
|
170
|
+
parts.push({ x: cx, y: cy, vx: Math.cos(a) * s, vy: Math.sin(a) * s - 3, color: confettiColors[Math.floor(Math.random() * confettiColors.length)], life: 0, maxLife: Math.random() * 80 + 60, size: Math.random() * 6 + 3 });
|
|
171
|
+
}
|
|
172
|
+
const canvas = document.createElement('canvas');
|
|
173
|
+
Object.assign(canvas.style, { position: 'fixed', inset: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '9999' });
|
|
174
|
+
canvas.width = innerWidth; canvas.height = innerHeight;
|
|
175
|
+
document.body.appendChild(canvas);
|
|
176
|
+
const ctx = canvas.getContext('2d')!;
|
|
177
|
+
(function frame() {
|
|
178
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
179
|
+
for (const p of parts) { p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life++; const a = 1 - p.life / p.maxLife; if (a <= 0) continue; ctx.save(); ctx.globalAlpha = a; ctx.fillStyle = p.color; ctx.translate(p.x, p.y); ctx.rotate(p.life * 0.1); ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6); ctx.restore(); }
|
|
180
|
+
if (parts.some(p => p.life < p.maxLife)) requestAnimationFrame(frame); else canvas.remove();
|
|
181
|
+
})();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function deleteGoal(id: string) {
|
|
185
|
+
goals = goals.filter((g) => g.id !== id);
|
|
186
|
+
saveGoals();
|
|
187
|
+
renderGoals();
|
|
188
|
+
updateSummary();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getGoalDefaults(goal?: Goal) {
|
|
192
|
+
const g = goal;
|
|
193
|
+
return {
|
|
194
|
+
name: g ? g.name : '',
|
|
195
|
+
price: g ? g.price : 5000,
|
|
196
|
+
saved: g ? g.saved : 0,
|
|
197
|
+
monthly: g ? g.monthly : 200,
|
|
198
|
+
id: g ? g.id : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function openForm(goal?: Goal) {
|
|
203
|
+
const d = getGoalDefaults(goal);
|
|
204
|
+
editingId = d.id;
|
|
205
|
+
nameInput.value = d.name;
|
|
206
|
+
priceInput.value = String(d.price);
|
|
207
|
+
savedInput.value = String(d.saved);
|
|
208
|
+
monthlySlider.value = String(d.monthly);
|
|
209
|
+
monthlyDisplay.textContent = FMT(parseInt(monthlySlider.value, 10));
|
|
210
|
+
formSubmit.textContent = editingId ? ui.saveGoal || 'Save' : ui.addButton || 'Add Goal';
|
|
211
|
+
form.style.display = 'block';
|
|
212
|
+
triggerBtn.style.display = 'none';
|
|
213
|
+
nameInput.focus();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function closeForm() {
|
|
217
|
+
form.style.display = 'none';
|
|
218
|
+
triggerBtn.style.display = 'flex';
|
|
219
|
+
editingId = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getFormValues() {
|
|
223
|
+
return {
|
|
224
|
+
name: nameInput.value.trim() || 'Watch',
|
|
225
|
+
price: Math.max(1, parseInt(priceInput.value, 10) || 5000),
|
|
226
|
+
saved: Math.max(0, parseInt(savedInput.value, 10) || 0),
|
|
227
|
+
monthly: Math.max(10, parseInt(monthlySlider.value, 10) || 200),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
monthlySlider.addEventListener('input', () => {
|
|
232
|
+
monthlyDisplay.textContent = FMT(parseInt(monthlySlider.value, 10));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
triggerBtn.addEventListener('click', () => openForm());
|
|
236
|
+
|
|
237
|
+
formClose.addEventListener('click', closeForm);
|
|
238
|
+
|
|
239
|
+
formSubmit.addEventListener('click', () => {
|
|
240
|
+
const { name, price, saved, monthly } = getFormValues();
|
|
241
|
+
|
|
242
|
+
if (editingId) {
|
|
243
|
+
const goal = goals.find((g) => g.id === editingId);
|
|
244
|
+
if (goal) { goal.name = name; goal.price = price; goal.saved = saved; goal.monthly = monthly; }
|
|
245
|
+
} else {
|
|
246
|
+
goals.push({
|
|
247
|
+
id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
248
|
+
name, price, saved, monthly, _celebrated: false,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
saveGoals();
|
|
253
|
+
closeForm();
|
|
254
|
+
renderGoals();
|
|
255
|
+
updateSummary();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const simBtn = document.getElementById('sim-month-btn') as HTMLButtonElement;
|
|
259
|
+
if (simBtn) {
|
|
260
|
+
simBtn.addEventListener('click', () => {
|
|
261
|
+
for (const g of goals) {
|
|
262
|
+
if (g.saved < g.price) g.saved = Math.min(g.saved + g.monthly, g.price);
|
|
263
|
+
}
|
|
264
|
+
localStorage.setItem(TIMESTAMP_KEY, String(Date.now()));
|
|
265
|
+
saveGoals();
|
|
266
|
+
renderGoals();
|
|
267
|
+
updateSummary();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
loadGoals(); autoSimulate(); renderGoals(); updateSummary();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AddGoalForm from './components/AddGoalForm.astro';
|
|
3
|
+
import GoalCard from './components/GoalCard.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}>
|
|
13
|
+
<div class="savings-summary" id="savings-summary">
|
|
14
|
+
<div class="summary-stat">
|
|
15
|
+
<span class="summary-value" id="total-saved">0</span>
|
|
16
|
+
<span class="summary-label">{ui.totalSaved || "Total Saved"}</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="summary-stat">
|
|
19
|
+
<span class="summary-value" id="total-goals">0</span>
|
|
20
|
+
<span class="summary-label">{ui.totalGoals || "Goals"}</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<AddGoalForm labels={ui} />
|
|
25
|
+
<GoalCard labels={ui} />
|
|
26
|
+
<button type="button" class="sim-month-btn" id="sim-month-btn">
|
|
27
|
+
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11z" fill="currentColor"/></svg>
|
|
28
|
+
{ui.simMonth || "+1 month"}
|
|
29
|
+
</button>
|
|
30
|
+
<div class="goals-list" id="goals-list"></div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="add-goal-form" id="add-goal-form" style="display: none;">
|
|
10
|
+
<div class="form-header">
|
|
11
|
+
<h3>{labels.addGoalTitle || "New Savings Goal"}</h3>
|
|
12
|
+
<button type="button" class="form-close" id="form-close">
|
|
13
|
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
14
|
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor" />
|
|
15
|
+
</svg>
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-body">
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<label class="form-label" for="goal-name">{labels.goalNameLabel || "Watch"}</label>
|
|
21
|
+
<input type="text" id="goal-name" class="form-input" placeholder={labels.goalNamePlaceholder || "e.g. Rolex Submariner"} maxlength="40" />
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<div class="form-group">
|
|
25
|
+
<label class="form-label" for="goal-price">{labels.targetPriceLabel || "Target Price"}</label>
|
|
26
|
+
<input type="number" id="goal-price" class="form-input" min="1" step="100" value="5000" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="form-group">
|
|
29
|
+
<label class="form-label" for="goal-saved">{labels.currentSavingsLabel || "Saved So Far"}</label>
|
|
30
|
+
<input type="number" id="goal-saved" class="form-input" min="0" step="100" value="0" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-group">
|
|
34
|
+
<label class="form-label" for="goal-monthly">{labels.monthlyLabel || "Per Month"}</label>
|
|
35
|
+
<div class="range-group">
|
|
36
|
+
<input type="range" id="goal-monthly" class="form-range" min="10" max="5000" step="10" value="200" />
|
|
37
|
+
<span class="range-value" id="monthly-display">$200</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<button type="button" class="form-submit" id="form-submit">{labels.addButton || "Add Goal"}</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<button type="button" class="add-goal-trigger" id="add-goal-trigger">
|
|
45
|
+
<svg viewBox="0 0 24 24" width="22" height="22">
|
|
46
|
+
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" />
|
|
47
|
+
</svg>
|
|
48
|
+
{labels.addGoalTitle || "New Savings Goal"}
|
|
49
|
+
</button>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<template id="goal-card-template">
|
|
10
|
+
<div class="goal-card" data-id="">
|
|
11
|
+
<div class="goal-header">
|
|
12
|
+
<div class="goal-name-wrap">
|
|
13
|
+
<span class="goal-name"></span>
|
|
14
|
+
<span class="goal-achieved-badge" style="display: none;">{labels.achieved || "Achieved!"}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<button type="button" class="goal-delete" title={labels.deleteGoal || "Delete"}>
|
|
17
|
+
<svg viewBox="0 0 24 24" width="18" height="18">
|
|
18
|
+
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor" />
|
|
19
|
+
</svg>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="goal-ring-section">
|
|
23
|
+
<div class="goal-ring">
|
|
24
|
+
<svg viewBox="0 0 120 120">
|
|
25
|
+
<circle class="goal-ring-bg" cx="60" cy="60" r="52" />
|
|
26
|
+
<circle class="goal-ring-fill" cx="60" cy="60" r="52"
|
|
27
|
+
stroke-dasharray="326.73" stroke-dashoffset="326.73" />
|
|
28
|
+
</svg>
|
|
29
|
+
<div class="goal-ring-text">
|
|
30
|
+
<span class="goal-percent">0%</span>
|
|
31
|
+
<span class="goal-ring-label">{labels.percentLabel || "Saved"}</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="goal-meta">
|
|
35
|
+
<div class="meta-row">
|
|
36
|
+
<span class="meta-label">{labels.targetPriceLabel || "Target"}</span>
|
|
37
|
+
<span class="meta-value goal-price"></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="meta-row">
|
|
40
|
+
<span class="meta-label">{labels.currentSavingsLabel || "Saved"}</span>
|
|
41
|
+
<span class="meta-value goal-saved"></span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="meta-row">
|
|
44
|
+
<span class="meta-label">{labels.monthlyLabel || "Monthly"}</span>
|
|
45
|
+
<span class="meta-value goal-monthly"></span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="meta-row">
|
|
48
|
+
<span class="meta-label">{labels.targetDate || "Target Date"}</span>
|
|
49
|
+
<span class="meta-value goal-date"></span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="goal-monthly-adjust">
|
|
54
|
+
<span class="adjust-label">{labels.adjustMonthly || "Adjust monthly"}</span>
|
|
55
|
+
<input type="range" class="goal-slider" min="10" max="5000" step="10" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ChronoToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
|
|
3
|
+
export type WatchSavingsPlannerUI = {
|
|
4
|
+
title: string;
|
|
5
|
+
addGoalTitle: string;
|
|
6
|
+
goalNameLabel: string;
|
|
7
|
+
goalNamePlaceholder: string;
|
|
8
|
+
targetPriceLabel: string;
|
|
9
|
+
currentSavingsLabel: string;
|
|
10
|
+
monthlyLabel: string;
|
|
11
|
+
addButton: string;
|
|
12
|
+
cancelButton: string;
|
|
13
|
+
goalsEmpty: string;
|
|
14
|
+
goalsEmptySub: string;
|
|
15
|
+
monthsToGoal: string;
|
|
16
|
+
yearsToGoal: string;
|
|
17
|
+
month: string;
|
|
18
|
+
months: string;
|
|
19
|
+
year: string;
|
|
20
|
+
totalSaved: string;
|
|
21
|
+
totalGoals: string;
|
|
22
|
+
achieved: string;
|
|
23
|
+
percentLabel: string;
|
|
24
|
+
deleteGoal: string;
|
|
25
|
+
editGoal: string;
|
|
26
|
+
saveGoal: string;
|
|
27
|
+
currency: string;
|
|
28
|
+
summaryTitle: string;
|
|
29
|
+
goalAchieved: string;
|
|
30
|
+
goalProgress: string;
|
|
31
|
+
congratsTitle: string;
|
|
32
|
+
congratsDesc: string;
|
|
33
|
+
monthlyContribution: string;
|
|
34
|
+
targetDate: string;
|
|
35
|
+
adjustMonthly: string;
|
|
36
|
+
simMonth: string;
|
|
37
|
+
now: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type WatchSavingsPlannerLocaleContent = ToolLocaleContent<WatchSavingsPlannerUI>;
|
|
41
|
+
|
|
42
|
+
export const watchSavingsPlanner: ChronoToolEntry<WatchSavingsPlannerUI> = {
|
|
43
|
+
id: 'watch-savings-planner',
|
|
44
|
+
icons: { bg: 'mdi:wallet-outline', fg: 'mdi:watch-variant' },
|
|
45
|
+
i18n: {
|
|
46
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
47
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
48
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
49
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
50
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
51
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
52
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
53
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
54
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
55
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
56
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
57
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
58
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
59
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
60
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { WatchSavingsPlannerUI } from '../entry';
|
|
3
|
+
import { bibliography } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<WatchSavingsPlannerUI> = {
|
|
6
|
+
slug: 'uhren-sparplaner',
|
|
7
|
+
title: 'Sparzielplaner für Uhren',
|
|
8
|
+
description: 'Lege Sparziele für deinen nächsten Uhrenkauf fest und verfolge sie. Visualisiere Fortschritte, berechne Zeitpläne und bleib motiviert.',
|
|
9
|
+
ui: {
|
|
10
|
+
title: 'Sparzieleinstellungen',
|
|
11
|
+
addGoalTitle: 'Neues Sparziel',
|
|
12
|
+
goalNameLabel: 'Uhr',
|
|
13
|
+
goalNamePlaceholder: 'z.B. Rolex Submariner',
|
|
14
|
+
targetPriceLabel: 'Zielpreis',
|
|
15
|
+
currentSavingsLabel: 'Gespart',
|
|
16
|
+
monthlyLabel: 'Pro Monat',
|
|
17
|
+
addButton: 'Ziel hinzufügen',
|
|
18
|
+
cancelButton: 'Abbrechen',
|
|
19
|
+
goalsEmpty: 'Noch keine Sparziele',
|
|
20
|
+
goalsEmptySub: 'Setz dein erstes Uhrenziel und leg los!',
|
|
21
|
+
monthsToGoal: 'Zeit bis zum Ziel',
|
|
22
|
+
yearsToGoal: 'Jahre bis zum Ziel',
|
|
23
|
+
month: 'Mo',
|
|
24
|
+
months: 'Mte',
|
|
25
|
+
year: 'J',
|
|
26
|
+
totalSaved: 'Gesamt gespart',
|
|
27
|
+
totalGoals: 'Ziele',
|
|
28
|
+
achieved: 'Erreicht',
|
|
29
|
+
percentLabel: 'Gespart',
|
|
30
|
+
deleteGoal: 'Löschen',
|
|
31
|
+
editGoal: 'Bearbeiten',
|
|
32
|
+
saveGoal: 'Speichern',
|
|
33
|
+
currency: 'EUR',
|
|
34
|
+
summaryTitle: 'Zusammenfassung',
|
|
35
|
+
goalAchieved: 'Erreicht!',
|
|
36
|
+
goalProgress: 'Fortschritt',
|
|
37
|
+
congratsTitle: 'Glückwunsch!',
|
|
38
|
+
congratsDesc: 'Du hast dein Sparziel erreicht!',
|
|
39
|
+
monthlyContribution: 'Monatlich',
|
|
40
|
+
targetDate: 'Zieldatum',
|
|
41
|
+
adjustMonthly: 'Monatsrate anpassen',
|
|
42
|
+
},
|
|
43
|
+
seo: [
|
|
44
|
+
{ type: 'title', text: 'Sparzielplaner für Uhren \u2014 Verfolge deinen nächsten Uhrenkauf', level: 2 },
|
|
45
|
+
{ type: 'paragraph', html: 'F\u00fcr eine Uhr zu sparen ist eine Reise. Ob es eine <strong>Vintage Speedmaster</strong>, eine <strong>Submariner</strong> oder eine <strong>Grand Seiko</strong> ist \u2014 ein klarer Sparplan verwandelt den Traum in einen Zeitplan. Dieses Tool hilft dir, deinen Fortschritt zu visualisieren, deine monatlichen Beitr\u00e4ge anzupassen und genau zu sehen, wann du mit deinem heiligen Gral aus dem Laden gehen wirst.' },
|
|
46
|
+
{ type: 'title', text: 'Warum ein Sparplan f\u00fcr Uhrensammler wichtig ist', level: 3 },
|
|
47
|
+
{ type: 'paragraph', html: 'Uhren sammeln ist ein Geduldsspiel. Die Preise begehrter Modelle steigen stetig, und Impulsk\u00e4ufe f\u00fchren oft zu Reue. Ein strukturierter Sparansatz h\u00e4lt dich diszipliniert, verhindert finanzielle Belastung und l\u00e4sst den endg\u00fcltigen Kauf wie eine Belohnung wirken. Au\u00dferdem steigert die t\u00e4gliche Verfolgung deines Fortschritts die Vorfreude und macht das Auspacken noch s\u00fc\u00dfer.' },
|
|
48
|
+
{ type: 'title', text: 'Wie du realistische Sparziele f\u00fcr Uhren setzt', level: 3 },
|
|
49
|
+
{ type: 'paragraph', html: 'Beginne mit dem Gesamtpreis inklusive Steuern und Versand. Teile ihn dann durch den Betrag, den du bequem pro Monat zur\u00fccklegen kannst. Als Faustregel gilt: Widme nicht mehr als <strong>10\u201315 % deines verf\u00fcgbaren Einkommens</strong> dem Uhrensparen. Falls der Zeitraum zu lang erscheint, unterteile ihn in kleinere Meilensteine \u2014 oder erkunde g\u00fcnstigere Alternativen in derselben Stilrichtung.' },
|
|
50
|
+
{ type: 'title', text: 'Die Psychologie der Zielverfolgung', level: 3 },
|
|
51
|
+
{ type: 'paragraph', html: 'Visuelle Fortschrittsanzeigen l\u00f6sen Dopaminaussch\u00fcttung aus \u2014 denselben Nervenbotenstoff, der das Sammeln so befriedigend macht. Jedes Mal, wenn du einen neuen Beitrag erfasst und den Fortschrittsring sich f\u00fcllen siehst, verst\u00e4rkst du die Gewohnheit. Deshalb wirken regelm\u00e4\u00dfige kleine Sparbetr\u00e4ge oft besser als unregelm\u00e4\u00dfige Gro\u00dfeinzahlungen \u2014 das Ritual selbst wird Teil des Sammelerlebnisses.' },
|
|
52
|
+
],
|
|
53
|
+
faq: [
|
|
54
|
+
{
|
|
55
|
+
question: 'Wie viel sollte ich pro Monat f\u00fcr eine Uhr sparen?',
|
|
56
|
+
answer: 'Strebe 10\u201315 % deines verf\u00fcgbaren monatlichen Einkommens an. Der Schl\u00fcssel ist Best\u00e4ndigkeit \u2014 selbst 100 $ pro Monat ergeben 1.200 $ pro Jahr. Passe den Betrag an deinen Zeitplan an: k\u00fcrzere Ziele erfordern h\u00f6here monatliche Beitr\u00e4ge.',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
question: 'Sollte ich f\u00fcr eine Uhr auf einmal sparen oder f\u00fcr mehrere?',
|
|
60
|
+
answer: 'Eine nach der anderen ist meist effektiver. Konzentration schafft Schwung. Sobald du dein erstes Ziel erreichst, \u00fcbertr\u00e4gst du die Disziplin auf das n\u00e4chste. Dieser Planer unterst\u00fctzt jedoch mehrere Ziele, damit du Zeitpl\u00e4ne vergleichen kannst.',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
question: 'Was ist, wenn der Uhrenpreis steigt, w\u00e4hrend ich spare?',
|
|
64
|
+
answer: 'Gute Frage. \u00dcberpr\u00fcfe den aktuellen Marktpreis alle paar Monate und aktualisiere dein Ziel. Wenn die Preise schnell steigen, erw\u00e4ge, deinen monatlichen Betrag zu erh\u00f6hen oder den Zeitplan zu verk\u00fcrzen, um den aktuellen Preis zu sichern.',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
bibliography,
|
|
68
|
+
howTo: [
|
|
69
|
+
{
|
|
70
|
+
name: 'Ziel setzen',
|
|
71
|
+
text: 'Gib den Namen der Uhr und den Gesamtpreis inklusive Steuern und Versand ein.',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Sparen verfolgen',
|
|
75
|
+
text: 'Erfasse, wie viel du bereits gespart hast und wie viel du monatlich zur Seite legen kannst.',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Fortschritt \u00fcberwachen',
|
|
79
|
+
text: 'Beobachte, wie sich der Fortschrittsring f\u00fcllt, w\u00e4hrend du deine Ersparnisse aktualisierst. Passe die monatlichen Betr\u00e4ge jederzeit an.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Feiern',
|
|
83
|
+
text: 'Wenn der Ring 100 % erreicht, hast du dein Ziel geschafft. Zeit, diese Uhr zu kaufen!',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
schemas: [
|
|
87
|
+
{
|
|
88
|
+
'@context': 'https://schema.org',
|
|
89
|
+
'@type': 'FAQPage',
|
|
90
|
+
'mainEntity': [
|
|
91
|
+
{
|
|
92
|
+
'@type': 'Question',
|
|
93
|
+
'name': 'Wie viel sollte ich pro Monat f\u00fcr eine Uhr sparen?',
|
|
94
|
+
'acceptedAnswer': {
|
|
95
|
+
'@type': 'Answer',
|
|
96
|
+
'text': 'Strebe 10\u201315 % deines verf\u00fcgbaren monatlichen Einkommens an. Der Schl\u00fcssel ist Best\u00e4ndigkeit \u2014 selbst 100 $ pro Monat ergeben 1.200 $ pro Jahr.',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
'@type': 'Question',
|
|
101
|
+
'name': 'Sollte ich f\u00fcr eine Uhr auf einmal sparen oder f\u00fcr mehrere?',
|
|
102
|
+
'acceptedAnswer': {
|
|
103
|
+
'@type': 'Answer',
|
|
104
|
+
'text': 'Eine nach der anderen ist meist effektiver. Konzentration schafft Schwung. Sobald du dein erstes Ziel erreichst, \u00fcbertr\u00e4gst du diese Disziplin auf das n\u00e4chste.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
'@type': 'Question',
|
|
109
|
+
'name': 'Was ist, wenn der Uhrenpreis steigt, w\u00e4hrend ich spare?',
|
|
110
|
+
'acceptedAnswer': {
|
|
111
|
+
'@type': 'Answer',
|
|
112
|
+
'text': '\u00dcberpr\u00fcfe den aktuellen Marktpreis alle paar Monate und aktualisiere dein Ziel. Erw\u00e4ge, deinen monatlichen Betrag zu erh\u00f6hen, wenn die Preise schnell steigen.',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
'@context': 'https://schema.org',
|
|
119
|
+
'@type': 'SoftwareApplication',
|
|
120
|
+
'name': 'Sparzielplaner f\u00fcr Uhren',
|
|
121
|
+
'operatingSystem': 'Alle',
|
|
122
|
+
'applicationCategory': 'FinanceApplication',
|
|
123
|
+
'browserRequirements': 'Erfordert HTML5. Erfordert JavaScript.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
'@context': 'https://schema.org',
|
|
127
|
+
'@type': 'HowTo',
|
|
128
|
+
'name': 'Wie man f\u00fcr einen Uhrenkauf spart',
|
|
129
|
+
'step': [
|
|
130
|
+
{
|
|
131
|
+
'@type': 'HowToStep',
|
|
132
|
+
'name': 'Ziel setzen',
|
|
133
|
+
'text': 'Gib den Namen der Uhr und den Gesamtpreis inklusive Steuern und Versand ein.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
'@type': 'HowToStep',
|
|
137
|
+
'name': 'Sparen verfolgen',
|
|
138
|
+
'text': 'Erfasse, wie viel du bereits gespart hast und wie viel du monatlich zur Seite legen kannst.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
'@type': 'HowToStep',
|
|
142
|
+
'name': 'Fortschritt \u00fcberwachen',
|
|
143
|
+
'text': 'Beobachte, wie sich der Fortschrittsring f\u00fcllt, w\u00e4hrend du deine Ersparnisse aktualisierst.',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
'@type': 'HowToStep',
|
|
147
|
+
'name': 'Feiern',
|
|
148
|
+
'text': 'Wenn der Ring 100 % erreicht, hast du dein Ziel geschafft. Zeit, diese Uhr zu kaufen!',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|