@jjlmoya/utils-home 1.35.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 +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/humidityCalculator/bibliography.ts +2 -2
- 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 +2 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -30,6 +30,8 @@ export { lightingCalculator } from './tool/lightingCalculator/entry';
|
|
|
30
30
|
export type { LightingCalculatorLocaleContent } from './tool/lightingCalculator/entry';
|
|
31
31
|
export { humidityCalculator } from './tool/humidityCalculator/entry';
|
|
32
32
|
export type { HumidityCalculatorLocaleContent } from './tool/humidityCalculator/entry';
|
|
33
|
+
export { waterSoftener } from './tool/waterSoftener/entry';
|
|
34
|
+
export type { WaterSoftenerLocaleContent } from './tool/waterSoftener/entry';
|
|
33
35
|
export { homeCategory } from './category';
|
|
34
36
|
import { dewPointCalculator } from './tool/dewPointCalculator/entry';
|
|
35
37
|
import { heatingComparator } from './tool/heatingComparator/entry';
|
|
@@ -47,4 +49,5 @@ import { applianceCostCalculator } from './tool/applianceCostCalculator/entry';
|
|
|
47
49
|
import { tileLayoutCalculator } from './tool/tileLayoutCalculator/entry';
|
|
48
50
|
import { lightingCalculator } from './tool/lightingCalculator/entry';
|
|
49
51
|
import { humidityCalculator } from './tool/humidityCalculator/entry';
|
|
50
|
-
|
|
52
|
+
import { waterSoftener } from './tool/waterSoftener/entry';
|
|
53
|
+
export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator, deskErgonomics, applianceCostCalculator, tileLayoutCalculator, lightingCalculator, humidityCalculator, waterSoftener];
|
package/src/index.ts
CHANGED
|
@@ -30,4 +30,5 @@ export { DESK_ERGONOMICS_TOOL } from './tool/deskErgonomics';
|
|
|
30
30
|
export { APPLIANCE_COST_CALCULATOR_TOOL } from './tool/applianceCostCalculator';
|
|
31
31
|
export { TILE_LAYOUT_CALCULATOR_TOOL } from './tool/tileLayoutCalculator';
|
|
32
32
|
export { HUMIDITY_CALCULATOR_TOOL } from './tool/humidityCalculator';
|
|
33
|
+
export { WATER_SOFTENER_TOOL } from './tool/waterSoftener';
|
|
33
34
|
|
|
@@ -17,8 +17,8 @@ describe('Locale Completeness Validation', () => {
|
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
it('should have
|
|
21
|
-
expect(ALL_TOOLS.length).toBe(
|
|
20
|
+
it('should have 17 tools registered', () => {
|
|
21
|
+
expect(ALL_TOOLS.length).toBe(17);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -4,8 +4,8 @@ import { homeCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 17 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(17);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('homeCategory should be defined', () => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export const bibliography = [
|
|
2
2
|
{
|
|
3
3
|
name: 'EPA: Mold Course Chapter 2: Mold and Moisture Dynamics',
|
|
4
|
-
url: 'https://www.epa.gov/mold/mold-course-
|
|
4
|
+
url: 'https://www.epa.gov/mold/mold-course-chapter-2',
|
|
5
5
|
},
|
|
6
6
|
{
|
|
7
7
|
name: 'ASHRAE Standard 55: Thermal Environmental Conditions for Human Occupancy',
|
|
8
|
-
url: 'https://www.ashrae.org/technical-resources/
|
|
8
|
+
url: 'https://www.ashrae.org/technical-resources/bookstore/standard-55-thermal-environmental-conditions-for-human-occupancy',
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
name: 'Energy Star: Choosing a Dehumidifier',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { waterSoftener } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await waterSoftener.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'WQA: Water Hardness and Softening Basics',
|
|
4
|
+
url: 'https://wqa.org/resources/getting-smart-with-softeners/',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'U.S. EPA: Home Water Treatment Facts',
|
|
8
|
+
url: 'https://www.epa.gov/ground-water-and-drinking-water/home-drinking-water-filtration-fact-sheet',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'Energy.gov: Water Heating and Hard Water Scaling',
|
|
12
|
+
url: 'https://www.energy.gov/energysaver/purchasing-and-maintaining-water-softener',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
@@ -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
|
+
};
|