@jjlmoya/utils-home 1.34.0 → 1.36.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 +1 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/humidityCalculator/bibliography.astro +14 -0
- package/src/tool/humidityCalculator/bibliography.ts +14 -0
- package/src/tool/humidityCalculator/component.astro +296 -0
- package/src/tool/humidityCalculator/entry.ts +29 -0
- package/src/tool/humidityCalculator/humidity-calculator.css +469 -0
- package/src/tool/humidityCalculator/i18n/de.ts +217 -0
- package/src/tool/humidityCalculator/i18n/en.ts +217 -0
- package/src/tool/humidityCalculator/i18n/es.ts +217 -0
- package/src/tool/humidityCalculator/i18n/fr.ts +217 -0
- package/src/tool/humidityCalculator/i18n/id.ts +217 -0
- package/src/tool/humidityCalculator/i18n/it.ts +217 -0
- package/src/tool/humidityCalculator/i18n/ja.ts +217 -0
- package/src/tool/humidityCalculator/i18n/ko.ts +217 -0
- package/src/tool/humidityCalculator/i18n/nl.ts +217 -0
- package/src/tool/humidityCalculator/i18n/pl.ts +217 -0
- package/src/tool/humidityCalculator/i18n/pt.ts +217 -0
- package/src/tool/humidityCalculator/i18n/ru.ts +217 -0
- package/src/tool/humidityCalculator/i18n/sv.ts +217 -0
- package/src/tool/humidityCalculator/i18n/tr.ts +217 -0
- package/src/tool/humidityCalculator/i18n/zh.ts +217 -0
- package/src/tool/humidityCalculator/index.ts +9 -0
- package/src/tool/humidityCalculator/logic.ts +60 -0
- package/src/tool/humidityCalculator/seo.astro +15 -0
- package/src/tool/humidityCalculator/ui.ts +29 -0
- package/src/tool/waterSoftener/bibliography.astro +14 -0
- package/src/tool/waterSoftener/bibliography.ts +14 -0
- package/src/tool/waterSoftener/component.astro +321 -0
- package/src/tool/waterSoftener/entry.ts +29 -0
- package/src/tool/waterSoftener/i18n/de.ts +222 -0
- package/src/tool/waterSoftener/i18n/en.ts +222 -0
- package/src/tool/waterSoftener/i18n/es.ts +222 -0
- package/src/tool/waterSoftener/i18n/fr.ts +222 -0
- package/src/tool/waterSoftener/i18n/id.ts +222 -0
- package/src/tool/waterSoftener/i18n/it.ts +222 -0
- package/src/tool/waterSoftener/i18n/ja.ts +222 -0
- package/src/tool/waterSoftener/i18n/ko.ts +222 -0
- package/src/tool/waterSoftener/i18n/nl.ts +222 -0
- package/src/tool/waterSoftener/i18n/pl.ts +222 -0
- package/src/tool/waterSoftener/i18n/pt.ts +222 -0
- package/src/tool/waterSoftener/i18n/ru.ts +222 -0
- package/src/tool/waterSoftener/i18n/sv.ts +222 -0
- package/src/tool/waterSoftener/i18n/tr.ts +222 -0
- package/src/tool/waterSoftener/i18n/zh.ts +222 -0
- package/src/tool/waterSoftener/index.ts +9 -0
- package/src/tool/waterSoftener/logic.ts +103 -0
- package/src/tool/waterSoftener/seo.astro +15 -0
- package/src/tool/waterSoftener/ui.ts +34 -0
- package/src/tool/waterSoftener/water-softener.css +449 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { WaterSoftenerUI } from './ui';
|
|
3
|
+
import { calculateScale, calculateSalt, calculateApplianceLifespan, toGpg, getHardnessCategory, fmt1 } from './logic';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui = {} } = Astro.props;
|
|
10
|
+
const wUI = ui as WaterSoftenerUI;
|
|
11
|
+
|
|
12
|
+
const DEF_HARDNESS = 12;
|
|
13
|
+
const DEF_UNIT = 'gpg';
|
|
14
|
+
const DEF_OCCUPANTS = 3;
|
|
15
|
+
const DEF_CAPACITY = 32000;
|
|
16
|
+
|
|
17
|
+
const gpg = toGpg(DEF_HARDNESS, DEF_UNIT as 'gpg' | 'fH');
|
|
18
|
+
const cat = getHardnessCategory(gpg);
|
|
19
|
+
const scale = calculateScale(gpg);
|
|
20
|
+
const salt = calculateSalt({ hardnessValue: DEF_HARDNESS, hardnessUnit: DEF_UNIT as 'gpg' | 'fH', occupants: DEF_OCCUPANTS, softenerGrains: DEF_CAPACITY });
|
|
21
|
+
const apps = calculateApplianceLifespan(gpg);
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<div class="ws-root">
|
|
25
|
+
<div class="ws-card"
|
|
26
|
+
data-bl={wUI.applianceBaseline}
|
|
27
|
+
data-cl={wUI.applianceWithHardness}
|
|
28
|
+
data-sl={wUI.badgeSaved}
|
|
29
|
+
data-ul={wUI.applianceSaved}
|
|
30
|
+
data-um={wUI.scaleUnitMetric}
|
|
31
|
+
data-ui={wUI.scaleUnitImperial}
|
|
32
|
+
>
|
|
33
|
+
<div class="ws-stage">
|
|
34
|
+
<div class="ws-stage-glow"></div>
|
|
35
|
+
<div class="ws-unit-system" id="ws-unit-system">
|
|
36
|
+
<span class="ws-unit-label">{wUI.labelUnitSystem}</span>
|
|
37
|
+
<div class="ws-pill-track" id="ws-sys-pill-track">
|
|
38
|
+
<button class="ws-pill-btn ws-active" data-sys="metric" type="button">{wUI.btnMetric}</button>
|
|
39
|
+
<button class="ws-pill-btn" data-sys="imperial" type="button">{wUI.btnImperial}</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<p class="ws-section-label">{wUI.labelHardness}</p>
|
|
43
|
+
<div class="ws-hero">
|
|
44
|
+
<p class="ws-hero-value">
|
|
45
|
+
<span id="ws-hero-num">{DEF_HARDNESS}</span>
|
|
46
|
+
<span class="ws-hero-unit" id="ws-hero-unit">{wUI.unitGpg}</span>
|
|
47
|
+
</p>
|
|
48
|
+
<p class="ws-hero-label" id="ws-hero-label">{cat.label}</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="ws-slider-track" id="ws-slider-track">
|
|
51
|
+
<div class="ws-slider-fill" id="ws-slider-fill"></div>
|
|
52
|
+
<div class="ws-slider-thumb" id="ws-slider-thumb"></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="ws-pill-track" id="ws-pill-track" style="margin-top:1rem">
|
|
55
|
+
<button class="ws-pill-btn ws-active" data-unit="gpg" type="button">{wUI.unitGpg}</button>
|
|
56
|
+
<button class="ws-pill-btn" data-unit="fH" type="button">{wUI.unitFH}</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="ws-body">
|
|
61
|
+
<div class="ws-fields">
|
|
62
|
+
<div class="ws-field">
|
|
63
|
+
<label class="ws-field-label" for="ws-hard">{wUI.labelHardness}</label>
|
|
64
|
+
<div class="ws-field-row">
|
|
65
|
+
<input type="number" id="ws-hard" class="ws-field-input" value={DEF_HARDNESS} min="0" max="50" step="0.5" />
|
|
66
|
+
<span class="ws-field-unit" id="ws-hard-unit">{wUI.unitGpg}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="ws-field">
|
|
70
|
+
<label class="ws-field-label" for="ws-occ">{wUI.labelOccupants}</label>
|
|
71
|
+
<div class="ws-field-row">
|
|
72
|
+
<input type="number" id="ws-occ" class="ws-field-input" value={DEF_OCCUPANTS} min="1" max="20" step="1" />
|
|
73
|
+
<span class="ws-field-unit">{wUI.unitPeople}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="ws-field">
|
|
77
|
+
<label class="ws-field-label" for="ws-cap">{wUI.labelSoftenerCapacity}</label>
|
|
78
|
+
<div class="ws-field-row">
|
|
79
|
+
<input type="number" id="ws-cap" class="ws-field-input" value={DEF_CAPACITY} min="5000" max="100000" step="1000" />
|
|
80
|
+
<span class="ws-field-unit">{wUI.unitGrains}</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="ws-pipe">
|
|
86
|
+
<p class="ws-section-label">{wUI.scaleTitle}</p>
|
|
87
|
+
<div class="ws-pipe-visual">
|
|
88
|
+
<svg viewBox="0 0 200 120">
|
|
89
|
+
<defs>
|
|
90
|
+
<clipPath id="ws-pipe-clip">
|
|
91
|
+
<rect x="14" y="14" width="172" height="92" rx="46" />
|
|
92
|
+
</clipPath>
|
|
93
|
+
</defs>
|
|
94
|
+
<rect x="10" y="10" width="180" height="100" rx="50" fill="none" stroke="var(--ws-border)" stroke-width="4" />
|
|
95
|
+
<rect x="14" y="14" width="172" height="92" rx="46" fill="var(--ws-surface)" />
|
|
96
|
+
<rect id="ws-scale-fill" x="14" y="106" width="172" height="0" fill="#e5e7eb" opacity="0.9" clip-path="url(#ws-pipe-clip)" />
|
|
97
|
+
<line x1="100" y1="10" x2="100" y2="110" stroke="var(--ws-border)" stroke-width="2" stroke-dasharray="4 4" opacity="0.5" />
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
<p class="ws-pipe-num" id="ws-pipe-num">{fmt1(scale.rateMmPerYear)} <span class="ws-pipe-unit" id="ws-pipe-unit">{wUI.scaleUnitMetric}</span></p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="ws-salt">
|
|
104
|
+
<p class="ws-section-label">{wUI.saltTitle}</p>
|
|
105
|
+
<div class="ws-salt-grid" id="ws-salt-grid">
|
|
106
|
+
{Array.from({ length: 10 }, (_, i) => (
|
|
107
|
+
<div class={`ws-salt-bag ${i < Math.min(10, Math.ceil(salt.bagsPerYear)) ? 'ws-filled' : ''}`} data-idx={i}></div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
<div class="ws-salt-info">
|
|
111
|
+
<div class="ws-salt-item">
|
|
112
|
+
<span class="ws-salt-num" id="ws-salt-mass">{fmt1(salt.annualSaltKg)}</span>
|
|
113
|
+
<span class="ws-salt-label" id="ws-salt-mass-label">{wUI.saltAnnual}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="ws-salt-item">
|
|
116
|
+
<span class="ws-salt-num" id="ws-salt-bags">{fmt1(salt.bagsPerYear)}</span>
|
|
117
|
+
<span class="ws-salt-label">{wUI.saltBags}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="ws-salt-item">
|
|
120
|
+
<span class="ws-salt-num" id="ws-salt-days">{salt.daysPerBag}</span>
|
|
121
|
+
<span class="ws-salt-label">{wUI.saltDaysPerBag}</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="ws-salt-item">
|
|
124
|
+
<span class="ws-salt-num" id="ws-salt-weeks">{fmt1(salt.weeksPerBag)}</span>
|
|
125
|
+
<span class="ws-salt-label">{wUI.saltWeeksPerBag}</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="ws-appliances" id="ws-appliances">
|
|
131
|
+
<div class="ws-app">
|
|
132
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M12 14v-2"/></svg>
|
|
133
|
+
<p class="ws-app-name">{wUI.applianceWasher}</p>
|
|
134
|
+
<p class="ws-app-years" id="ws-app-w-base">{apps.washer.baseline}</p>
|
|
135
|
+
<p class="ws-app-sub" id="ws-app-w-sub">{wUI.applianceBaseline}</p>
|
|
136
|
+
<p class="ws-app-years" id="ws-app-w-cur">{apps.washer.withHardness}</p>
|
|
137
|
+
<p class="ws-app-sub">{wUI.applianceWithHardness}</p>
|
|
138
|
+
<span class="ws-app-saved" id="ws-app-w-saved">{wUI.badgeSaved} {fmt1(apps.washer.saved)} {wUI.applianceSaved}</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="ws-app">
|
|
141
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
142
|
+
<p class="ws-app-name">{wUI.applianceHeater}</p>
|
|
143
|
+
<p class="ws-app-years" id="ws-app-h-base">{apps.heater.baseline}</p>
|
|
144
|
+
<p class="ws-app-sub" id="ws-app-h-sub">{wUI.applianceBaseline}</p>
|
|
145
|
+
<p class="ws-app-years" id="ws-app-h-cur">{apps.heater.withHardness}</p>
|
|
146
|
+
<p class="ws-app-sub">{wUI.applianceWithHardness}</p>
|
|
147
|
+
<span class="ws-app-saved" id="ws-app-h-saved">{wUI.badgeSaved} {fmt1(apps.heater.saved)} {wUI.applianceSaved}</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="ws-app">
|
|
150
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
|
|
151
|
+
<p class="ws-app-name">{wUI.applianceCoffee}</p>
|
|
152
|
+
<p class="ws-app-years" id="ws-app-c-base">{apps.coffee.baseline}</p>
|
|
153
|
+
<p class="ws-app-sub" id="ws-app-c-sub">{wUI.applianceBaseline}</p>
|
|
154
|
+
<p class="ws-app-years" id="ws-app-c-cur">{apps.coffee.withHardness}</p>
|
|
155
|
+
<p class="ws-app-sub">{wUI.applianceWithHardness}</p>
|
|
156
|
+
<span class="ws-app-saved" id="ws-app-c-saved">{wUI.badgeSaved} {fmt1(apps.coffee.saved)} {wUI.applianceSaved}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<script>
|
|
164
|
+
import { calculateScale, calculateSalt, calculateApplianceLifespan, toGpg, getHardnessCategory, mmToInches, kgToLbs, fmt1 } from './logic';
|
|
165
|
+
|
|
166
|
+
function el(id: string) { return document.getElementById(id); }
|
|
167
|
+
function setTxt(id: string, val: string) { const e = el(id); if (e) e.textContent = val; }
|
|
168
|
+
function getNum(id: string): number { const e = el(id) as HTMLInputElement | null; return e ? parseFloat(e.value) || 0 : 0; }
|
|
169
|
+
|
|
170
|
+
const MAX_GPG_VISUAL = 35;
|
|
171
|
+
|
|
172
|
+
let unitSystem: 'metric' | 'imperial' = 'metric';
|
|
173
|
+
|
|
174
|
+
function readHardUnit(): 'gpg' | 'fH' {
|
|
175
|
+
const track = el('ws-pill-track');
|
|
176
|
+
const active = track?.querySelector('.ws-pill-btn.ws-active') as HTMLElement | null;
|
|
177
|
+
return (active?.dataset.unit || 'gpg') as 'gpg' | 'fH';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function setHardUnit(unit: 'gpg' | 'fH') {
|
|
181
|
+
const track = el('ws-pill-track');
|
|
182
|
+
if (!track) return;
|
|
183
|
+
track.querySelectorAll('.ws-pill-btn').forEach((b) => b.classList.toggle('ws-active', (b as HTMLElement).dataset.unit === unit));
|
|
184
|
+
setTxt('ws-hard-unit', unit === 'gpg' ? 'GPG' : 'fH');
|
|
185
|
+
setTxt('ws-hero-unit', unit === 'gpg' ? 'GPG' : 'fH');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function setSys(sys: 'metric' | 'imperial') {
|
|
189
|
+
const track = el('ws-sys-pill-track');
|
|
190
|
+
if (!track) return;
|
|
191
|
+
track.querySelectorAll('.ws-pill-btn').forEach((b) => b.classList.toggle('ws-active', (b as HTMLElement).dataset.sys === sys));
|
|
192
|
+
unitSystem = sys;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function setSlider(gpg: number) {
|
|
196
|
+
const pct = Math.min(100, (gpg / MAX_GPG_VISUAL) * 100);
|
|
197
|
+
const fill = el('ws-slider-fill') as HTMLElement | null;
|
|
198
|
+
const thumb = el('ws-slider-thumb') as HTMLElement | null;
|
|
199
|
+
if (fill) fill.style.width = `${pct}%`;
|
|
200
|
+
if (thumb) thumb.style.left = `${pct}%`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setHero(val: number, unit: string, label: string) {
|
|
204
|
+
setTxt('ws-hero-num', fmt1(val));
|
|
205
|
+
setTxt('ws-hero-unit', unit);
|
|
206
|
+
setTxt('ws-hero-label', label);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function setPipe(gpg: number) {
|
|
210
|
+
const scale = calculateScale(gpg);
|
|
211
|
+
const isMetric = unitSystem === 'metric';
|
|
212
|
+
const rate = isMetric ? scale.rateMmPerYear : mmToInches(scale.rateMmPerYear);
|
|
213
|
+
const unit = isMetric ? 'mm/yr' : 'in/yr';
|
|
214
|
+
setTxt('ws-pipe-num', `${fmt1(rate)} ${unit}`);
|
|
215
|
+
const maxRate = isMetric ? 5 : mmToInches(5);
|
|
216
|
+
const h = Math.min(92, (rate / maxRate) * 92);
|
|
217
|
+
const rect = el('ws-scale-fill') as SVGRectElement | null;
|
|
218
|
+
if (rect) {
|
|
219
|
+
rect.setAttribute('y', String(106 - h));
|
|
220
|
+
rect.setAttribute('height', String(h));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function setSalt(salt: { annualSaltKg: number; bagsPerYear: number; daysPerBag: number; weeksPerBag: number }) {
|
|
225
|
+
const isMetric = unitSystem === 'metric';
|
|
226
|
+
const mass = isMetric ? salt.annualSaltKg : kgToLbs(salt.annualSaltKg);
|
|
227
|
+
const massUnit = isMetric ? 'kg' : 'lbs';
|
|
228
|
+
setTxt('ws-salt-mass', `${fmt1(mass)} ${massUnit}`);
|
|
229
|
+
setTxt('ws-salt-bags', fmt1(salt.bagsPerYear));
|
|
230
|
+
setTxt('ws-salt-days', String(salt.daysPerBag));
|
|
231
|
+
setTxt('ws-salt-weeks', fmt1(salt.weeksPerBag));
|
|
232
|
+
const fullBags = Math.floor(salt.bagsPerYear);
|
|
233
|
+
const frac = salt.bagsPerYear - fullBags;
|
|
234
|
+
document.querySelectorAll('.ws-salt-bag').forEach((b, i) => {
|
|
235
|
+
const bag = b as HTMLElement;
|
|
236
|
+
if (i < fullBags) {
|
|
237
|
+
bag.classList.add('ws-filled');
|
|
238
|
+
bag.style.setProperty('--fill', '100%');
|
|
239
|
+
} else if (i === fullBags && frac > 0) {
|
|
240
|
+
bag.classList.add('ws-filled');
|
|
241
|
+
bag.style.setProperty('--fill', `${frac * 100}%`);
|
|
242
|
+
} else {
|
|
243
|
+
bag.classList.remove('ws-filled');
|
|
244
|
+
bag.style.setProperty('--fill', '0%');
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface AppArgs { idPrefix: string; r: { baseline: number; withHardness: number; saved: number }; baseLabel: string; curLabel: string; savedLabel: string; unitLabel: string }
|
|
250
|
+
|
|
251
|
+
function setApp(a: AppArgs) {
|
|
252
|
+
setTxt(`ws-app-${a.idPrefix}-base`, fmt1(a.r.baseline));
|
|
253
|
+
setTxt(`ws-app-${a.idPrefix}-sub`, a.baseLabel);
|
|
254
|
+
setTxt(`ws-app-${a.idPrefix}-cur`, fmt1(a.r.withHardness));
|
|
255
|
+
const savedEl = el(`ws-app-${a.idPrefix}-saved`) as HTMLElement | null;
|
|
256
|
+
if (savedEl) savedEl.textContent = `${a.savedLabel} ${fmt1(a.r.saved)} ${a.unitLabel}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readState() {
|
|
260
|
+
return {
|
|
261
|
+
hardnessValue: getNum('ws-hard'),
|
|
262
|
+
hardnessUnit: readHardUnit(),
|
|
263
|
+
occupants: getNum('ws-occ'),
|
|
264
|
+
softenerGrains: getNum('ws-cap'),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function update() {
|
|
269
|
+
const s = readState();
|
|
270
|
+
const gpg = toGpg(s.hardnessValue, s.hardnessUnit);
|
|
271
|
+
const cat = getHardnessCategory(gpg);
|
|
272
|
+
setSlider(gpg);
|
|
273
|
+
setHero(s.hardnessValue, s.hardnessUnit === 'gpg' ? 'GPG' : 'fH', cat.label);
|
|
274
|
+
setPipe(gpg);
|
|
275
|
+
setSalt(calculateSalt(s));
|
|
276
|
+
const apps = calculateApplianceLifespan(gpg);
|
|
277
|
+
const card = document.querySelector('.ws-card') as HTMLElement | null;
|
|
278
|
+
const bl = card?.dataset.bl || 'With softener';
|
|
279
|
+
const cl = card?.dataset.cl || 'With hard water';
|
|
280
|
+
const sl = card?.dataset.sl || 'Extended by';
|
|
281
|
+
const ul = card?.dataset.ul || 'yrs';
|
|
282
|
+
setApp({ idPrefix: 'w', r: apps.washer, baseLabel: bl, curLabel: cl, savedLabel: sl, unitLabel: ul });
|
|
283
|
+
setApp({ idPrefix: 'h', r: apps.heater, baseLabel: bl, curLabel: cl, savedLabel: sl, unitLabel: ul });
|
|
284
|
+
setApp({ idPrefix: 'c', r: apps.coffee, baseLabel: bl, curLabel: cl, savedLabel: sl, unitLabel: ul });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function bindSysPills() {
|
|
288
|
+
const track = el('ws-sys-pill-track');
|
|
289
|
+
if (!track) return;
|
|
290
|
+
track.querySelectorAll('.ws-pill-btn').forEach((b) => b.addEventListener('click', () => {
|
|
291
|
+
setSys((b as HTMLElement).dataset.sys as 'metric' | 'imperial');
|
|
292
|
+
update();
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function bindHardPills() {
|
|
297
|
+
const track = el('ws-pill-track');
|
|
298
|
+
if (!track) return;
|
|
299
|
+
track.querySelectorAll('.ws-pill-btn').forEach((b) => b.addEventListener('click', () => {
|
|
300
|
+
setHardUnit((b as HTMLElement).dataset.unit as 'gpg' | 'fH');
|
|
301
|
+
update();
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function bindInputs() {
|
|
306
|
+
['ws-hard', 'ws-occ', 'ws-cap'].forEach((id) => {
|
|
307
|
+
const i = el(id) as HTMLInputElement | null;
|
|
308
|
+
if (i) i.addEventListener('input', update);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function init() {
|
|
313
|
+
bindSysPills();
|
|
314
|
+
bindHardPills();
|
|
315
|
+
bindInputs();
|
|
316
|
+
update();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
document.addEventListener('astro:page-load', init);
|
|
320
|
+
init();
|
|
321
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HomeToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { WaterSoftenerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type WaterSoftenerLocaleContent = ToolLocaleContent<WaterSoftenerUI>;
|
|
5
|
+
|
|
6
|
+
export const waterSoftener: HomeToolEntry<WaterSoftenerUI> = {
|
|
7
|
+
id: 'water-softener',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:water-check',
|
|
10
|
+
fg: 'mdi:shaker-outline',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
en: async () => (await import('./i18n/en')).content,
|
|
14
|
+
de: async () => (await import('./i18n/de')).content,
|
|
15
|
+
es: async () => (await import('./i18n/es')).content,
|
|
16
|
+
fr: async () => (await import('./i18n/fr')).content,
|
|
17
|
+
id: async () => (await import('./i18n/id')).content,
|
|
18
|
+
it: async () => (await import('./i18n/it')).content,
|
|
19
|
+
ja: async () => (await import('./i18n/ja')).content,
|
|
20
|
+
ko: async () => (await import('./i18n/ko')).content,
|
|
21
|
+
nl: async () => (await import('./i18n/nl')).content,
|
|
22
|
+
pl: async () => (await import('./i18n/pl')).content,
|
|
23
|
+
pt: async () => (await import('./i18n/pt')).content,
|
|
24
|
+
ru: async () => (await import('./i18n/ru')).content,
|
|
25
|
+
sv: async () => (await import('./i18n/sv')).content,
|
|
26
|
+
tr: async () => (await import('./i18n/tr')).content,
|
|
27
|
+
zh: async () => (await import('./i18n/zh')).content,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { WaterSoftenerUI } from '../ui';
|
|
4
|
+
import { bibliography } from '../bibliography';
|
|
5
|
+
|
|
6
|
+
const slug = 'wasserenthaertungsrechner';
|
|
7
|
+
const title = 'Haushaltswasserenthärter und Salzverbrauchsoptimierer';
|
|
8
|
+
const description =
|
|
9
|
+
'Analysieren Sie Ihren Wasserhärtegrad, berechnen Sie optimale Enthärtereinstellungen und schätzen Sie den jährlichen Salzverbrauch. Sehen Sie Kalkablagerungsprognosen, Salznachfüllzeitpläne und Gerätelebensdauervorhersagen in einem interaktiven Tool.';
|
|
10
|
+
|
|
11
|
+
const faqData = [
|
|
12
|
+
{
|
|
13
|
+
question: 'Was ist Wasserhärte?',
|
|
14
|
+
answer:
|
|
15
|
+
'Wasserhärte ist die Konzentration gelöster Kalzium- und Magnesiumminerale in Ihrem Leitungswasser. Sie wird in Körnern pro Gallone oder französischen Graden gemessen. Hartes Wasser verursacht Kalkablagerungen in Rohren, reduziert die Heizeffizienz und verkürzt die Lebensdauer von Geräten.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
question: 'Wie funktioniert ein Wasserenthärter?',
|
|
19
|
+
answer:
|
|
20
|
+
'Ein Wasserenthärter verwendet Ionenaustauscherharzperlen, um Kalzium- und Magnesiumionen gegen Natriumionen auszutauschen. Wenn das Harz gesättigt ist, regeneriert das System durch Spülen einer Salzlösung durch den Tank, wodurch die Perlen wiederhergestellt und die harten Mineralien abgeführt werden.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
question: 'Wie viel Salz verbraucht ein Enthärter pro Jahr?',
|
|
24
|
+
answer:
|
|
25
|
+
'Ein typischer Vierpersonenhaushalt mit mäßig hartem Wasser verbraucht zwischen 80 und 120 Kilogramm Salz pro Jahr. Sehr hartes Wasser oder größere Haushalte können diesen Wert auf über 200 Kilogramm steigern. Moderne hochenergieeffiziente Enthärter verbrauchen etwa 30 Prozent weniger Salz als ältere Modelle.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: 'Wie oft sollte ich den Salztank auffüllen?',
|
|
29
|
+
answer:
|
|
30
|
+
'Die meisten Salztanks müssen alle 4 bis 8 Wochen aufgefüllt werden. Überprüfen Sie den Salzstand monatlich. Wenn der Tank weniger als ein Drittel voll ist, fügen Sie eine neue 25 Kilogramm Tüte hinzu. Lassen Sie das Salz niemals vollständig ausgehen, da sonst das Harz wieder aushärten und an Wirksamkeit verlieren wird.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: 'Schadet hartes Wasser Geräten wirklich?',
|
|
34
|
+
answer:
|
|
35
|
+
'Ja. Kalkablagerungen auf Heizelementen zwingen diese zu härterer Arbeit, was die Energiekosten erhöht und vorzeitige Ausfälle verursacht. Ein Warmwasserbereiter in einem Gebiet mit sehr hartem Wasser kann bis zu 45 Prozent seiner erwarteten Lebensdauer verlieren. Waschmaschinen und Geschirrspüler leiden ebenfalls unter verstopften Ventilen und verkalkten Trommeln.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
question: 'Kann ich enthärtetes Wasser trinken?',
|
|
39
|
+
answer:
|
|
40
|
+
'Enthärtetes Wasser ist für die meisten Menschen zum Trinken sicher. Der Natriumzuwachs ist gering und entspricht in etwa einer Scheibe Brot pro Tag. Menschen mit strenger natriumarmer Diät bevorzugen jedoch möglicherweise einen separaten nichtenthärteten Wasserhahn für Trinkwasser oder wählen ein kaliumbasiertes Enthärtersalz.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const howToData = [
|
|
45
|
+
{
|
|
46
|
+
name: 'Geben Sie Ihre Wasserhärte ein',
|
|
47
|
+
text: 'Geben Sie den Härtegrad aus Ihrem Wasserqualitätsbericht oder Teststreifen ein. Wählen Sie Körner pro Gallone oder französische Grade im Einheitenauswahlmenü.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Setzen Sie die Haushaltsgröße',
|
|
51
|
+
text: 'Wählen Sie die Anzahl der Personen in Ihrem Zuhause aus. Mehr Bewohner bedeuten höheren Wasserverbrauch und schnelleren Salzverbrauch.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Wählen Sie die Enthärterkapazität',
|
|
55
|
+
text: 'Geben Sie die Kornkapazität des Harztanks Ihres Enthärters ein. Diese steht normalerweise auf dem Steuerventil oder im Handbuch. Übliche Werte sind 24.000 oder 32.000 Körner.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Überprüfen Sie die Härtekategorie',
|
|
59
|
+
text: 'Lesen Sie die interaktive Härteskala, um zu verstehen, ob Ihr Wasser weich, mäßig hart oder extrem hart ist.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Prüfen Sie die Salzprognose',
|
|
63
|
+
text: 'Betrachten Sie den Salzsäckesimulator, um zu sehen, wie viele 25 Kilogramm Säcke Sie pro Jahr benötigen und wann die nächste Nachfüllung fällig ist.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Untersuchen Sie die Geräteauswirkung',
|
|
67
|
+
text: 'Vergleichen Sie die Basislebensdauer Ihrer Waschmaschine, Ihres Warmwasserbereiters und Ihrer Kaffeemaschine mit ihrer geschätzten Lebensdauer bei Ihrem aktuellen unbehandelten Wasser.',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
72
|
+
'@context': 'https://schema.org',
|
|
73
|
+
'@type': 'FAQPage',
|
|
74
|
+
mainEntity: faqData.map((item) => ({
|
|
75
|
+
'@type': 'Question',
|
|
76
|
+
name: item.question,
|
|
77
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const howToSchema: WithContext<HowTo> = {
|
|
82
|
+
'@context': 'https://schema.org',
|
|
83
|
+
'@type': 'HowTo',
|
|
84
|
+
name: title,
|
|
85
|
+
description,
|
|
86
|
+
step: howToData.map((step) => ({
|
|
87
|
+
'@type': 'HowToStep',
|
|
88
|
+
name: step.name,
|
|
89
|
+
text: step.text,
|
|
90
|
+
})),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
94
|
+
'@context': 'https://schema.org',
|
|
95
|
+
'@type': 'SoftwareApplication',
|
|
96
|
+
name: title,
|
|
97
|
+
description,
|
|
98
|
+
applicationCategory: 'UtilityApplication',
|
|
99
|
+
operatingSystem: 'All',
|
|
100
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
101
|
+
inLanguage: 'de',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const content: ToolLocaleContent<WaterSoftenerUI> = {
|
|
105
|
+
slug,
|
|
106
|
+
title,
|
|
107
|
+
description,
|
|
108
|
+
faq: faqData,
|
|
109
|
+
bibliography,
|
|
110
|
+
howTo: howToData,
|
|
111
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
112
|
+
seo: [
|
|
113
|
+
{
|
|
114
|
+
type: 'title',
|
|
115
|
+
text: 'Die unsichtbaren Kosten harten Wassers',
|
|
116
|
+
level: 2,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'paragraph',
|
|
120
|
+
html: 'Hartes Wasser ist eines der teuersten versteckten Probleme in einem Zuhause. Jedes Mal, wenn Sie den Wasserhahn aufdrehen, fließen gelöste Mineralien durch Ihre Rohre und Geräte. Über Monate und Jahre kristallisieren sich diese Mineralien zu Kalk, einer harten weißen Kruste, die Heizelemente verstopft, Durchflussraten reduziert und Gummidichtungen zerstört. Das Ergebnis sind höhere Energierechnungen, kürzere Gerätelebensdauer und häufigere Wartungsanrufe. Ein Wasserenthärter beseitigt diesen Schaden an der Quelle.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'stats',
|
|
124
|
+
items: [
|
|
125
|
+
{ value: '80kg', label: 'Durchschn. Jahressalzverbrauch', icon: 'mdi:shaker-outline' },
|
|
126
|
+
{ value: '11yr', label: 'Waschmaschine Basislebensdauer', icon: 'mdi:washing-machine' },
|
|
127
|
+
{ value: '0.15', label: 'mm Kalk pro GPG/Jahr', icon: 'mdi:water-check' },
|
|
128
|
+
],
|
|
129
|
+
columns: 3,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'title',
|
|
133
|
+
text: 'Warum Kalk so zerstörerisch ist',
|
|
134
|
+
level: 3,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'paragraph',
|
|
138
|
+
html: 'Kalk ist nicht nur ein kosmetischer Fleck auf Ihrem Wasserkocher. In einem Warmwasserbereiter bildet er eine isolierende Schicht auf dem Heizelement. Für jeden Millimeter Kalk sinkt die Energieübertragungseffizienz um bis zu 10 Prozent. Das bedeutet, dass eine 3 Millimeter Schicht Ihre Wasserheizungsrechnung um fast 30 Prozent erhöhen kann. In Waschmaschinen verhindert Kalk die Aktivierung von Waschmitteln, sodass Sie mehr Pulver für schlechtere Ergebnisse verwenden. In Kaffeemaschinen zerstört er das Thermostat und führt zu bitterem, ungleichmäßigem Extrakt.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'comparative',
|
|
142
|
+
items: [
|
|
143
|
+
{
|
|
144
|
+
title: 'Unbehandeltes hartes Wasser',
|
|
145
|
+
description: 'Mineralien lagern sich frei in der gesamten Installation und jedem angeschlossenen Gerät ab.',
|
|
146
|
+
icon: 'mdi:alert',
|
|
147
|
+
points: ['Schneller Kalkaufbau in Heizungen und Boilern', 'Erhöhter Waschmittel- und Seifenverbrauch', 'Verkürzte Gerätelebensdauer um 30 bis 45 Prozent', 'Höhere Energierechnungen durch Isolationswirkung von Kalk'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
title: 'Enthärtetes Wasser',
|
|
151
|
+
description: 'Kalzium und Magnesium werden am Eintrittspunkt entfernt, bevor sie Wasserhähne und Geräte erreichen.',
|
|
152
|
+
icon: 'mdi:check-circle',
|
|
153
|
+
points: ['Null Kalkablagerung auf Heizelementen', 'Normale Waschmitteldosen liefern bessere Ergebnisse', 'Geräte erreichen ihre volle konstruktive Lebensdauer', 'Niedrigerer Energieverbrauch für die Wassererwärmung'],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
columns: 2,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'diagnostic',
|
|
160
|
+
variant: 'info',
|
|
161
|
+
title: 'Schnelltest für hartes Wasser',
|
|
162
|
+
icon: 'mdi:clipboard-check',
|
|
163
|
+
badge: 'Aktion',
|
|
164
|
+
html: '<p style="margin:0">Füllen Sie eine klare Flasche mit Leitungswasser und geben Sie einige Tropfen Flüssigseife hinzu. Schütteln Sie kräftig. Wenn das Wasser trüb bleibt und sehr wenig Schaum bildet, ist Ihr Wasser hart. Kristallklares Wasser mit dichtem, stabilem Schaum weist auf weiches Wasser hin. Für eine präzise Messung verwenden Sie einen Gesamthärte-Teststreifen oder fordern Sie einen Bericht bei Ihrem Wasserversorger an.</p>',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'title',
|
|
168
|
+
text: 'Die Salzversorgung richtig bemessen',
|
|
169
|
+
level: 3,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'paragraph',
|
|
173
|
+
html: 'Salzmangel ist der schnellste Weg, einen Enthärter zu ruinieren. Wenn der Salztank leer ist, härtet das Harzbett innerhalb von Tagen wieder aus und das System hört auf, Ihr Zuhause zu schützen. Verwenden Sie diesen Rechner, um genau vorherzusagen, wie viele 25 Kilogramm Säcke Sie pro Jahr benötigen. Wenn das Ergebnis mehr als 10 Säcke ist, erwägen Sie ein Upgrade auf einen größeren Harztank oder ein hocheffizientes Ventil, das pro Regenerationszyklus weniger Salz verbraucht.',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'summary',
|
|
177
|
+
title: 'So schützen Sie Ihr Zuhause vor hartem Wasser',
|
|
178
|
+
items: [
|
|
179
|
+
'Verwenden Sie diesen Rechner, um Ihre genaue Wasserhärtekategorie und Salzanforderungen zu ermitteln.',
|
|
180
|
+
'Installieren Sie einen richtig dimensionierten Wasserenthärter am Haupteintrittspunkt der Wasserversorgung.',
|
|
181
|
+
'Füllen Sie den Salztank auf, bevor er unter ein Drittel fällt.',
|
|
182
|
+
'Verwenden Sie hochreine verdunstete Salzpellets für die beste Harzleistung.',
|
|
183
|
+
'Warten Sie Ventil und Harzbett alle 3 bis 5 Jahre.',
|
|
184
|
+
'Überwachen Sie die Energierechnungen der Geräte auf plötzliche Anstiege, die auf Kalkaufbau hindeuten könnten.',
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
ui: {
|
|
189
|
+
labelHardness: 'Wasserhärte',
|
|
190
|
+
unitGpg: 'GPG',
|
|
191
|
+
unitFH: 'fH',
|
|
192
|
+
labelOccupants: 'Haushaltsgröße',
|
|
193
|
+
unitPeople: 'Personen',
|
|
194
|
+
labelSoftenerCapacity: 'Enthärterkapazität',
|
|
195
|
+
unitGrains: 'Körner',
|
|
196
|
+
hardnessSoft: 'Weich',
|
|
197
|
+
hardnessSlightly: 'Leicht hart',
|
|
198
|
+
hardnessModerate: 'Mäßig hart',
|
|
199
|
+
hardnessHard: 'Hart',
|
|
200
|
+
hardnessVery: 'Sehr hart',
|
|
201
|
+
hardnessExtreme: 'Extrem hart',
|
|
202
|
+
scaleTitle: 'Kalkablagerungsrate',
|
|
203
|
+
scaleUnitMetric: 'mm/Jahr',
|
|
204
|
+
scaleUnitImperial: 'in/Jahr',
|
|
205
|
+
saltTitle: 'Jährliche Salzprognose',
|
|
206
|
+
saltAnnual: 'Jährlicher Salzverbrauch',
|
|
207
|
+
saltBags: 'Säcke pro Jahr',
|
|
208
|
+
saltDaysPerBag: 'Tage pro Sack',
|
|
209
|
+
saltWeeksPerBag: 'Wochen pro Sack',
|
|
210
|
+
applianceTitle: 'Gerätelebensdauer',
|
|
211
|
+
applianceWasher: 'Waschmaschine',
|
|
212
|
+
applianceHeater: 'Warmwasserbereiter',
|
|
213
|
+
applianceCoffee: 'Kaffeemaschine',
|
|
214
|
+
applianceBaseline: 'Mit Enthärter',
|
|
215
|
+
applianceWithHardness: 'Mit hartem Wasser',
|
|
216
|
+
applianceSaved: 'Jahre',
|
|
217
|
+
badgeSaved: 'Verlängert um',
|
|
218
|
+
labelUnitSystem: 'Einheiten',
|
|
219
|
+
btnMetric: 'Metrisch',
|
|
220
|
+
btnImperial: 'Imperial',
|
|
221
|
+
},
|
|
222
|
+
};
|