@jjlmoya/utils-home 1.26.0 → 1.27.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/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/vampireDrawSimulator/bibliography.astro +14 -0
- package/src/tool/vampireDrawSimulator/bibliography.ts +14 -0
- package/src/tool/vampireDrawSimulator/component.astro +320 -0
- package/src/tool/vampireDrawSimulator/entry.ts +29 -0
- package/src/tool/vampireDrawSimulator/i18n/de.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/en.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/es.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/fr.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/id.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/it.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/ja.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/ko.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/nl.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/pl.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/pt.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/ru.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/sv.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/tr.ts +211 -0
- package/src/tool/vampireDrawSimulator/i18n/zh.ts +211 -0
- package/src/tool/vampireDrawSimulator/index.ts +9 -0
- package/src/tool/vampireDrawSimulator/logic.ts +31 -0
- package/src/tool/vampireDrawSimulator/seo.astro +15 -0
- package/src/tool/vampireDrawSimulator/ui.ts +32 -0
- package/src/tool/vampireDrawSimulator/vampire-draw-simulator.css +542 -0
- package/src/tool/wallPaintingCalculator/bibliography.ts +4 -4
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/entries.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { acTonnageCalculator } from './tool/acTonnageCalculator/entry';
|
|
|
18
18
|
export type { AcTonnageCalculatorLocaleContent } from './tool/acTonnageCalculator/entry';
|
|
19
19
|
export { wallPaintingCalculator } from './tool/wallPaintingCalculator/entry';
|
|
20
20
|
export type { WallPaintingCalculatorLocaleContent } from './tool/wallPaintingCalculator/entry';
|
|
21
|
+
export { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
|
|
22
|
+
export type { VampireDrawSimulatorLocaleContent } from './tool/vampireDrawSimulator/entry';
|
|
21
23
|
export { homeCategory } from './category';
|
|
22
24
|
import { dewPointCalculator } from './tool/dewPointCalculator/entry';
|
|
23
25
|
import { heatingComparator } from './tool/heatingComparator/entry';
|
|
@@ -29,4 +31,5 @@ import { tariffComparator } from './tool/tariffComparator/entry';
|
|
|
29
31
|
import { wifiRangeSimulator } from './tool/wifiRangeSimulator/entry';
|
|
30
32
|
import { acTonnageCalculator } from './tool/acTonnageCalculator/entry';
|
|
31
33
|
import { wallPaintingCalculator } from './tool/wallPaintingCalculator/entry';
|
|
32
|
-
|
|
34
|
+
import { vampireDrawSimulator } from './tool/vampireDrawSimulator/entry';
|
|
35
|
+
export const ALL_ENTRIES = [dewPointCalculator, heatingComparator, ledSavingCalculator, projectorCalculator, qrGenerator, solarCalculator, tariffComparator, wifiRangeSimulator, acTonnageCalculator, wallPaintingCalculator, vampireDrawSimulator];
|
|
@@ -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 11 tools registered', () => {
|
|
21
|
+
expect(ALL_TOOLS.length).toBe(11);
|
|
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 11 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(11);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('homeCategory should be defined', () => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { vampireDrawSimulator } 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 vampireDrawSimulator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Standby Power and Vampire Draw: Lawrence Berkeley National Laboratory',
|
|
4
|
+
url: 'https://standby.lbl.gov/',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Energy Consumption of Household Appliances: U.S. Department of Energy',
|
|
8
|
+
url: 'https://www.energy.gov/energysaver/energy-saver',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'Phantom Load Calculator: Natural Resources Canada',
|
|
12
|
+
url: 'https://www.nrcan.gc.ca/energy-efficiency/energy-efficiency-residential/14227',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { VampireDrawSimulatorUI } from './ui';
|
|
3
|
+
import { calculateVampireDraw, fmt2 } from './logic';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui = {} } = Astro.props;
|
|
10
|
+
const vUI = ui as VampireDrawSimulatorUI;
|
|
11
|
+
|
|
12
|
+
const DEFAULT_DEVICES = [
|
|
13
|
+
{ name: 'Smart TV', watts: 12, hours: 24 },
|
|
14
|
+
{ name: 'Router / WiFi', watts: 8, hours: 24 },
|
|
15
|
+
{ name: 'Game Console', watts: 15, hours: 24 },
|
|
16
|
+
{ name: 'Desktop PC (sleep)', watts: 15, hours: 24 },
|
|
17
|
+
{ name: 'Set Top Box', watts: 20, hours: 24 },
|
|
18
|
+
{ name: 'Microwave (clock)', watts: 3, hours: 24 },
|
|
19
|
+
{ name: 'Printer', watts: 5, hours: 24 },
|
|
20
|
+
{ name: 'Phone Charger', watts: 0.5, hours: 24 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const INITIAL_PRICE = 0.18;
|
|
24
|
+
|
|
25
|
+
const initial = calculateVampireDraw(DEFAULT_DEVICES.map((d) => ({ name: d.name, watts: d.watts, hoursPerDay: d.hours })), INITIAL_PRICE);
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class="vamp-wrapper">
|
|
29
|
+
<div class="vamp-card"
|
|
30
|
+
data-currency-sign={vUI.currencySign}
|
|
31
|
+
data-curr-usd={vUI.btnCurrUSD}
|
|
32
|
+
data-curr-eur={vUI.btnCurrEUR}
|
|
33
|
+
data-curr-gbp={vUI.btnCurrGBP}
|
|
34
|
+
data-curr-jpy={vUI.btnCurrJPY}
|
|
35
|
+
data-cat-low={vUI.categoryLow}
|
|
36
|
+
data-cat-moderate={vUI.categoryModerate}
|
|
37
|
+
data-cat-high={vUI.categoryHigh}
|
|
38
|
+
data-cat-extreme={vUI.categoryExtreme}
|
|
39
|
+
>
|
|
40
|
+
<div class="vamp-hero">
|
|
41
|
+
<div class="vamp-result-badge">{vUI.resultBadge}</div>
|
|
42
|
+
<p class="vamp-annual-label">{vUI.labelAnnualCost}</p>
|
|
43
|
+
<p class="vamp-annual-value">
|
|
44
|
+
<span id="vamp-annual-num">{fmt2(initial.annualCost)}</span>
|
|
45
|
+
<span id="vamp-annual-unit" class="vamp-annual-unit">{vUI.currencySign}</span>
|
|
46
|
+
</p>
|
|
47
|
+
<div id="vamp-category-badge" class="vamp-category-badge vamp-cat-{initial.category}">{vUI[`category${initial.category.charAt(0).toUpperCase()}${initial.category.slice(1)}` as keyof VampireDrawSimulatorUI]}</div>
|
|
48
|
+
|
|
49
|
+
<div class="vamp-stats">
|
|
50
|
+
<div class="vamp-stat">
|
|
51
|
+
<p class="vamp-stat-label">{vUI.labelAnnualKwh}</p>
|
|
52
|
+
<p id="vamp-kwh" class="vamp-stat-value">{fmt2(initial.annualKwh)} kWh</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="vamp-stat">
|
|
55
|
+
<p class="vamp-stat-label">{vUI.labelMonthlyCost}</p>
|
|
56
|
+
<p id="vamp-monthly" class="vamp-stat-value">{vUI.currencySign}{fmt2(initial.monthlyCost)}</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="vamp-stat">
|
|
59
|
+
<p class="vamp-stat-label">{vUI.labelTotalWatts}</p>
|
|
60
|
+
<p id="vamp-watts" class="vamp-stat-value">{initial.totalWatts} {vUI.unitWatts}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="vamp-body">
|
|
66
|
+
<p class="vamp-section-title">{vUI.sectionTitle}</p>
|
|
67
|
+
<div class="vamp-device-list" id="vamp-device-list">
|
|
68
|
+
{DEFAULT_DEVICES.map((d) => (
|
|
69
|
+
<div class="vamp-device-row" data-name={d.name} data-watts={d.watts}>
|
|
70
|
+
<span class="vamp-device-name">{d.name}</span>
|
|
71
|
+
<span class="vamp-device-watts">{d.watts} {vUI.unitWatts}</span>
|
|
72
|
+
<input type="number" class="vamp-device-hours" value={d.hours} min="0" max="24" step="1" />
|
|
73
|
+
<span class="vamp-device-watts">{vUI.unitHours}</span>
|
|
74
|
+
<button class="vamp-remove-btn">{vUI.removeDevice}</button>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
<button class="vamp-add-btn" id="vamp-add-btn">+ {vUI.addDevice}</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="vamp-footer">
|
|
82
|
+
<div class="vamp-field">
|
|
83
|
+
<label class="vamp-label" for="vamp-price">{vUI.labelPrice}</label>
|
|
84
|
+
<div class="vamp-price-row">
|
|
85
|
+
<input type="number" id="vamp-price" value={INITIAL_PRICE} min="0.01" step="0.01" class="vamp-price-input" />
|
|
86
|
+
<span class="vamp-price-unit" id="vamp-price-unit">{vUI.currencySign}{vUI.unitPrice}</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="vamp-field vamp-currency-toggle">
|
|
90
|
+
<span class="vamp-currency-label">{vUI.labelCurrency}</span>
|
|
91
|
+
<div class="vamp-currency-btns">
|
|
92
|
+
<button class="vamp-currency-btn vamp-currency-active" data-currency="usd">{vUI.btnCurrUSD}</button>
|
|
93
|
+
<button class="vamp-currency-btn" data-currency="eur">{vUI.btnCurrEUR}</button>
|
|
94
|
+
<button class="vamp-currency-btn" data-currency="gbp">{vUI.btnCurrGBP}</button>
|
|
95
|
+
<button class="vamp-currency-btn" data-currency="jpy">{vUI.btnCurrJPY}</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="vamp-modal" id="vamp-modal" role="dialog" aria-modal="true" aria-labelledby="vamp-modal-title" style="display:none">
|
|
102
|
+
<div class="vamp-modal-backdrop" id="vamp-modal-backdrop"></div>
|
|
103
|
+
<div class="vamp-modal-card">
|
|
104
|
+
<h3 id="vamp-modal-title" class="vamp-modal-title">{vUI.modalAddTitle}</h3>
|
|
105
|
+
<div class="vamp-modal-body">
|
|
106
|
+
<div class="vamp-modal-field">
|
|
107
|
+
<label class="vamp-modal-label" for="vamp-modal-name">{vUI.labelDeviceName}</label>
|
|
108
|
+
<input type="text" id="vamp-modal-name" class="vamp-modal-input" placeholder="Smart Speaker" autocomplete="off" />
|
|
109
|
+
</div>
|
|
110
|
+
<div class="vamp-modal-field">
|
|
111
|
+
<label class="vamp-modal-label" for="vamp-modal-watts">{vUI.labelDeviceWatts}</label>
|
|
112
|
+
<input type="number" id="vamp-modal-watts" class="vamp-modal-input" value="5" min="0" step="0.1" />
|
|
113
|
+
</div>
|
|
114
|
+
<div class="vamp-modal-field">
|
|
115
|
+
<label class="vamp-modal-label" for="vamp-modal-hours">{vUI.labelHours}</label>
|
|
116
|
+
<input type="number" id="vamp-modal-hours" class="vamp-modal-input" value="24" min="0" max="24" step="1" />
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="vamp-modal-actions">
|
|
120
|
+
<button class="vamp-modal-btn vamp-modal-btn-secondary" id="vamp-modal-cancel">{vUI.btnCancel}</button>
|
|
121
|
+
<button class="vamp-modal-btn vamp-modal-btn-primary" id="vamp-modal-save">{vUI.btnSave}</button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
import { calculateVampireDraw, fmt2 } from './logic';
|
|
129
|
+
|
|
130
|
+
const LS_KEY_CURRENCY = 'vampire-draw-currency';
|
|
131
|
+
let currency: 'usd' | 'eur' | 'gbp' | 'jpy' = 'usd';
|
|
132
|
+
|
|
133
|
+
function el(id: string) { return document.getElementById(id); }
|
|
134
|
+
function setTxt(id: string, val: string) { const e = el(id); if (e) e.textContent = val; }
|
|
135
|
+
function getNum(id: string): number { const e = el(id) as HTMLInputElement | null; return e ? parseFloat(e.value) || 0 : 0; }
|
|
136
|
+
function getStr(id: string): string { const e = el(id) as HTMLInputElement | null; return e ? e.value.trim() : ''; }
|
|
137
|
+
function getDataset(card: HTMLElement, key: string): string { return card.dataset[key] ?? ''; }
|
|
138
|
+
|
|
139
|
+
function getCurrencySign(card: HTMLElement): string {
|
|
140
|
+
const key = `curr${currency.charAt(0).toUpperCase()}${currency.slice(1)}`;
|
|
141
|
+
return getDataset(card, key) || getDataset(card, 'currencySign') || '$';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readDevices(): { name: string; watts: number; hoursPerDay: number }[] {
|
|
145
|
+
const list = el('vamp-device-list');
|
|
146
|
+
if (!list) return [];
|
|
147
|
+
return Array.from(list.querySelectorAll('.vamp-device-row')).map((row) => {
|
|
148
|
+
const name = (row.querySelector('.vamp-device-name') as HTMLElement)?.textContent ?? '';
|
|
149
|
+
const watts = parseFloat((row as HTMLElement).dataset.watts ?? '0');
|
|
150
|
+
const hours = parseFloat((row.querySelector('.vamp-device-hours') as HTMLInputElement)?.value ?? '24');
|
|
151
|
+
return { name, watts, hoursPerDay: hours };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function updateCategoryBadge(card: HTMLElement, category: string) {
|
|
156
|
+
const badge = el('vamp-category-badge');
|
|
157
|
+
if (!badge) return;
|
|
158
|
+
badge.className = `vamp-category-badge vamp-cat-${category}`;
|
|
159
|
+
const key = `cat${category.charAt(0).toUpperCase()}${category.slice(1)}`;
|
|
160
|
+
badge.textContent = getDataset(card, key) || category;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function calculate(card: HTMLElement) {
|
|
164
|
+
const devices = readDevices();
|
|
165
|
+
const price = getNum('vamp-price');
|
|
166
|
+
const r = calculateVampireDraw(devices, price);
|
|
167
|
+
const currencySign = getCurrencySign(card);
|
|
168
|
+
setTxt('vamp-annual-num', fmt2(r.annualCost));
|
|
169
|
+
setTxt('vamp-annual-unit', currencySign);
|
|
170
|
+
setTxt('vamp-kwh', `${fmt2(r.annualKwh)} kWh`);
|
|
171
|
+
setTxt('vamp-monthly', `${currencySign}${fmt2(r.monthlyCost)}`);
|
|
172
|
+
setTxt('vamp-watts', `${r.totalWatts} W`);
|
|
173
|
+
updateCategoryBadge(card, r.category);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function clampHours(input: HTMLInputElement) {
|
|
177
|
+
let v = parseFloat(input.value);
|
|
178
|
+
if (Number.isNaN(v)) v = 0;
|
|
179
|
+
if (v < 0) v = 0;
|
|
180
|
+
if (v > 24) v = 24;
|
|
181
|
+
input.value = String(v);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function attachRowListeners(row: HTMLElement, card: HTMLElement) {
|
|
185
|
+
const hoursInput = row.querySelector('.vamp-device-hours') as HTMLInputElement | null;
|
|
186
|
+
const removeBtn = row.querySelector('.vamp-remove-btn') as HTMLButtonElement | null;
|
|
187
|
+
if (hoursInput) {
|
|
188
|
+
hoursInput.addEventListener('input', () => {
|
|
189
|
+
clampHours(hoursInput);
|
|
190
|
+
calculate(card);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (removeBtn) {
|
|
194
|
+
removeBtn.addEventListener('click', () => {
|
|
195
|
+
row.remove();
|
|
196
|
+
calculate(card);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let unitW = 'W';
|
|
202
|
+
let unitH = 'h';
|
|
203
|
+
let removeLabel = 'Remove';
|
|
204
|
+
|
|
205
|
+
function textOf(sel: string, wrapper: HTMLElement | null, fallback: string): string {
|
|
206
|
+
return (wrapper?.querySelector(sel) as HTMLElement | null)?.textContent ?? fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function grabUnits(wrapper: HTMLElement | null) {
|
|
210
|
+
const firstWatts = textOf('.vamp-device-watts', wrapper, '');
|
|
211
|
+
unitW = firstWatts.split(' ')[1] || 'W';
|
|
212
|
+
const allWatts = wrapper?.querySelectorAll('.vamp-device-watts');
|
|
213
|
+
unitH = (allWatts && allWatts.length > 0)
|
|
214
|
+
? (allWatts[allWatts.length - 1] as HTMLElement).textContent ?? 'h'
|
|
215
|
+
: 'h';
|
|
216
|
+
removeLabel = textOf('.vamp-remove-btn', wrapper, 'Remove');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createDeviceRow(name: string, watts: number, hours: number, card: HTMLElement) {
|
|
220
|
+
const list = el('vamp-device-list');
|
|
221
|
+
if (!list) return;
|
|
222
|
+
const row = document.createElement('div');
|
|
223
|
+
row.className = 'vamp-device-row';
|
|
224
|
+
row.dataset.name = name;
|
|
225
|
+
row.dataset.watts = String(watts);
|
|
226
|
+
row.innerHTML = `<span class="vamp-device-name">${name}</span><span class="vamp-device-watts">${watts} ${unitW}</span><input type="number" class="vamp-device-hours" value="${hours}" min="0" max="24" step="1" /><span class="vamp-device-watts">${unitH}</span><button class="vamp-remove-btn">${removeLabel}</button>`;
|
|
227
|
+
list.appendChild(row);
|
|
228
|
+
attachRowListeners(row, card);
|
|
229
|
+
calculate(card);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function openModal() {
|
|
233
|
+
const modal = el('vamp-modal');
|
|
234
|
+
if (!modal) return;
|
|
235
|
+
modal.style.display = '';
|
|
236
|
+
const nameIn = el('vamp-modal-name') as HTMLInputElement | null;
|
|
237
|
+
if (nameIn) { nameIn.value = ''; nameIn.focus(); }
|
|
238
|
+
const wattsIn = el('vamp-modal-watts') as HTMLInputElement | null;
|
|
239
|
+
if (wattsIn) wattsIn.value = '5';
|
|
240
|
+
const hoursIn = el('vamp-modal-hours') as HTMLInputElement | null;
|
|
241
|
+
if (hoursIn) hoursIn.value = '24';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function closeModal() {
|
|
245
|
+
const modal = el('vamp-modal');
|
|
246
|
+
if (modal) modal.style.display = 'none';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function saveModal(card: HTMLElement) {
|
|
250
|
+
const name = getStr('vamp-modal-name') || 'Device';
|
|
251
|
+
const watts = getNum('vamp-modal-watts');
|
|
252
|
+
let hours = getNum('vamp-modal-hours');
|
|
253
|
+
if (hours < 0) hours = 0;
|
|
254
|
+
if (hours > 24) hours = 24;
|
|
255
|
+
createDeviceRow(name, watts, hours, card);
|
|
256
|
+
closeModal();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function bindModalEvents(card: HTMLElement) {
|
|
260
|
+
const addBtn = el('vamp-add-btn');
|
|
261
|
+
if (addBtn) addBtn.addEventListener('click', openModal);
|
|
262
|
+
const modalBackdrop = el('vamp-modal-backdrop');
|
|
263
|
+
if (modalBackdrop) modalBackdrop.addEventListener('click', closeModal);
|
|
264
|
+
const cancelBtn = el('vamp-modal-cancel');
|
|
265
|
+
if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
|
|
266
|
+
const saveBtn = el('vamp-modal-save');
|
|
267
|
+
if (saveBtn) saveBtn.addEventListener('click', () => saveModal(card));
|
|
268
|
+
const modalHoursIn = el('vamp-modal-hours') as HTMLInputElement | null;
|
|
269
|
+
if (modalHoursIn) modalHoursIn.addEventListener('input', () => clampHours(modalHoursIn));
|
|
270
|
+
document.addEventListener('keydown', (e) => {
|
|
271
|
+
if (e.key === 'Escape') closeModal();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function bindCurrencyEvents(card: HTMLElement) {
|
|
276
|
+
document.querySelectorAll('.vamp-currency-btn').forEach((btn) => {
|
|
277
|
+
btn.addEventListener('click', () => {
|
|
278
|
+
const c = (btn as HTMLElement).dataset.currency as 'usd' | 'eur' | 'gbp' | 'jpy';
|
|
279
|
+
if (c) applyCurrency(c, card);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function bindPriceEvent(card: HTMLElement) {
|
|
285
|
+
const priceInput = el('vamp-price');
|
|
286
|
+
if (priceInput) priceInput.addEventListener('input', () => calculate(card));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function init() {
|
|
290
|
+
const card = document.querySelector('.vamp-card') as HTMLElement | null;
|
|
291
|
+
if (!card) return;
|
|
292
|
+
grabUnits(card.closest('.vamp-wrapper') as HTMLElement | null);
|
|
293
|
+
const savedCurr = localStorage.getItem(LS_KEY_CURRENCY) as 'usd' | 'eur' | 'gbp' | 'jpy' | null;
|
|
294
|
+
if (savedCurr) currency = savedCurr;
|
|
295
|
+
toggleClass('.vamp-currency-btn', 'currency', currency, 'vamp-currency-active');
|
|
296
|
+
const currencySign = getCurrencySign(card);
|
|
297
|
+
setTxt('vamp-price-unit', `${currencySign}/kWh`);
|
|
298
|
+
document.querySelectorAll('.vamp-device-row').forEach((row) => attachRowListeners(row as HTMLElement, card));
|
|
299
|
+
bindModalEvents(card);
|
|
300
|
+
bindCurrencyEvents(card);
|
|
301
|
+
bindPriceEvent(card);
|
|
302
|
+
calculate(card);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function toggleClass(sel: string, attr: string, val: string, cls: string) {
|
|
306
|
+
document.querySelectorAll(sel).forEach((b) => b.classList.toggle(cls, (b as HTMLElement).dataset[attr] === val));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function applyCurrency(curr: 'usd' | 'eur' | 'gbp' | 'jpy', card: HTMLElement) {
|
|
310
|
+
currency = curr;
|
|
311
|
+
localStorage.setItem(LS_KEY_CURRENCY, curr);
|
|
312
|
+
toggleClass('.vamp-currency-btn', 'currency', curr, 'vamp-currency-active');
|
|
313
|
+
const currencySign = getCurrencySign(card);
|
|
314
|
+
setTxt('vamp-price-unit', `${currencySign}/kWh`);
|
|
315
|
+
calculate(card);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
document.addEventListener('astro:page-load', init);
|
|
319
|
+
init();
|
|
320
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HomeToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { VampireDrawSimulatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type VampireDrawSimulatorLocaleContent = ToolLocaleContent<VampireDrawSimulatorUI>;
|
|
5
|
+
|
|
6
|
+
export const vampireDrawSimulator: HomeToolEntry<VampireDrawSimulatorUI> = {
|
|
7
|
+
id: 'vampire-draw-simulator',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:power-plug',
|
|
10
|
+
fg: 'mdi:ghost',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
en: async () => (await import('./i18n/en')).content,
|
|
14
|
+
es: async () => (await import('./i18n/es')).content,
|
|
15
|
+
de: async () => (await import('./i18n/de')).content,
|
|
16
|
+
fr: async () => (await import('./i18n/fr')).content,
|
|
17
|
+
it: async () => (await import('./i18n/it')).content,
|
|
18
|
+
pt: async () => (await import('./i18n/pt')).content,
|
|
19
|
+
nl: async () => (await import('./i18n/nl')).content,
|
|
20
|
+
pl: async () => (await import('./i18n/pl')).content,
|
|
21
|
+
sv: async () => (await import('./i18n/sv')).content,
|
|
22
|
+
ru: async () => (await import('./i18n/ru')).content,
|
|
23
|
+
tr: async () => (await import('./i18n/tr')).content,
|
|
24
|
+
zh: async () => (await import('./i18n/zh')).content,
|
|
25
|
+
ja: async () => (await import('./i18n/ja')).content,
|
|
26
|
+
ko: async () => (await import('./i18n/ko')).content,
|
|
27
|
+
id: async () => (await import('./i18n/id')).content,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
import type { VampireDrawSimulatorUI } from '../ui';
|
|
4
|
+
import { bibliography } from '../bibliography';
|
|
5
|
+
|
|
6
|
+
const slug = 'standby-verbrauch-simulator';
|
|
7
|
+
const title = 'Standby Verbrauch Simulator';
|
|
8
|
+
const description =
|
|
9
|
+
'Finden Sie heraus, wie viel Strom Ihre Geräte im Standby-Modus verschwenden. Berechnen Sie die versteckten jährlichen Kosten von Phantomlasten von Fernsehern, Routern, Ladegeräten und mehr.';
|
|
10
|
+
|
|
11
|
+
const faqData = [
|
|
12
|
+
{
|
|
13
|
+
question: 'Was ist Vampire Power oder Phantomlast?',
|
|
14
|
+
answer:
|
|
15
|
+
'Vampire Power ist die Elektrizität, die elektronische Geräte verbrauchen, wenn sie ausgeschaltet oder im Standby-Modus sind. Dies geschieht, weil viele Geräte teilweise aktiv bleiben, um auf Fernbedienungen zu reagieren, Netzwerkverbindungen aufrechtzuerhalten oder interne Uhren laufen zu lassen.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
question: 'Wie viel kostet Standby-Strom pro Jahr?',
|
|
19
|
+
answer:
|
|
20
|
+
'Der durchschnittliche Haushalt verschwendet allein für Standby-Strom zwischen 50 und 150 Euro pro Jahr. Ein einzelner moderner Fernseher kann kontinuierlich 10 bis 20 Watt verbrauchen, was je nach Strompreis auf etwa 15 bis 30 Euro pro Jahr hinausläuft.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
question: 'Welche Geräte verbrauchen im Standby am meisten?',
|
|
24
|
+
answer:
|
|
25
|
+
'Die größten Verbraucher sind Set-Top-Boxen, Spielekonsolen, Desktop-Computer im Schlafmodus, Smart-TVs und ältere externe Netzteile. Kabel- und Satellitenboxen sind besonders verschwenderisch, weil sie selten in den echten Standby wechseln.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: 'Bringt das Ausstecken von Geräten wirklich Geld?',
|
|
29
|
+
answer:
|
|
30
|
+
'Ja. Das Ausstecken von Geräten oder die Verwendung von Advanced Power Strips mit Master-Steckdosen eliminiert den Standby-Verbrauch vollständig. Bei einem typischen Haushalt mit 20 Standby-Geräten kann die jährliche Einsparung zwischen 40 und 120 Euro liegen.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: 'Lohnen sich smarte Steckdosenleisten?',
|
|
34
|
+
answer:
|
|
35
|
+
'Smarte oder Advanced Power Strips schalten Strom zu Peripheriegeräten ab, wenn ein Master-Gerät ausgeschaltet wird. Sie amortisieren sich innerhalb weniger Monate, wenn man sie mit Entertainment-Centern oder Computerarbeitsplätzen nutzt.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
question: 'Wie kann ich den Standby-Verbrauch zu Hause messen?',
|
|
39
|
+
answer:
|
|
40
|
+
'Sie können ein preiswertes Steckdosen-Leistungsmessgerät (Wattmeter) verwenden, um exakt zu messen, wie viele Watt jedes Gerät im ausgeschalteten oder im Leerlaufzustand zieht. Alternativ liefert dieser Simulator standardisierte Schätzungen basierend auf Herstellermittelwerten und Labormessungen.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const howToData = [
|
|
45
|
+
{
|
|
46
|
+
name: 'Gehen Sie jeden Raum durch',
|
|
47
|
+
text: 'Zählen Sie jedes Gerät, das rund um die Uhr eingesteckt bleibt. Dazu gehören Fernseher, Mikrowellen, Router, Drucker, Spielekonsolen und Handy-Ladegeräte.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Schätzen Sie die täglichen Standby-Stunden',
|
|
51
|
+
text: 'Die meisten Geräte sind 20 bis 24 Stunden pro Tag im Standby. Geben Sie den Durchschnitt für jedes Gerät ein oder verwenden Sie den Standardwert von 24 Stunden.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Geben Sie Ihren Strompreis ein',
|
|
55
|
+
text: 'Geben Sie den Preis ein, den Sie pro Kilowattstunde zahlen. Er steht auf Ihrer Stromrechnung.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Lesen Sie Ihre Vampire-Draw-Zusammenfassung',
|
|
59
|
+
text: 'Der Simulator zeigt die insgesamt verschwendeten Watt, die jährlichen Kilowattstunden und die versteckten jährlichen Kosten. Verwenden Sie das Kategorie-Abzeichen, um zu sehen, ob Ihr Haushalt niedrig, moderat, hoch oder extrem ist.',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
64
|
+
'@context': 'https://schema.org',
|
|
65
|
+
'@type': 'FAQPage',
|
|
66
|
+
mainEntity: faqData.map((item) => ({
|
|
67
|
+
'@type': 'Question',
|
|
68
|
+
name: item.question,
|
|
69
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const howToSchema: WithContext<HowTo> = {
|
|
74
|
+
'@context': 'https://schema.org',
|
|
75
|
+
'@type': 'HowTo',
|
|
76
|
+
name: title,
|
|
77
|
+
description,
|
|
78
|
+
step: howToData.map((step) => ({
|
|
79
|
+
'@type': 'HowToStep',
|
|
80
|
+
name: step.name,
|
|
81
|
+
text: step.text,
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
86
|
+
'@context': 'https://schema.org',
|
|
87
|
+
'@type': 'SoftwareApplication',
|
|
88
|
+
name: title,
|
|
89
|
+
description,
|
|
90
|
+
applicationCategory: 'UtilityApplication',
|
|
91
|
+
operatingSystem: 'All',
|
|
92
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
|
93
|
+
inLanguage: 'de',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const content: ToolLocaleContent<VampireDrawSimulatorUI> = {
|
|
97
|
+
slug,
|
|
98
|
+
title,
|
|
99
|
+
description,
|
|
100
|
+
faq: faqData,
|
|
101
|
+
bibliography,
|
|
102
|
+
howTo: howToData,
|
|
103
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
104
|
+
seo: [
|
|
105
|
+
{
|
|
106
|
+
type: 'title',
|
|
107
|
+
text: 'Phantomlast: Die Elektrizität, die Sie bezahlen, aber nie nutzen',
|
|
108
|
+
level: 2,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'paragraph',
|
|
112
|
+
html: 'Jede Steckdose in Ihrem Zuhause, an der ein Kabel angeschlossen ist, ist eine potenzielle Quelle von <strong>Phantomelektrizität</strong>. Auch wenn der Schalter ausgeschaltet ist, zieht das Gerät weiterhin Strom. Es sind vielleicht nur wenige Watt, multipliziert mit 24 Stunden am Tag und 365 Tagen im Jahr, werden diese Watt zu Hunderten von Kilowattstunden und zu echtem Geld, das aus Ihrer Tasche geht.',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'stats',
|
|
116
|
+
items: [
|
|
117
|
+
{ value: '5-10%', label: 'des Haushaltsbudgets', icon: 'mdi:flash' },
|
|
118
|
+
{ value: '23h', label: 'Ø Standby pro Tag', icon: 'mdi:clock-outline' },
|
|
119
|
+
{ value: '30W', label: 'Ø Top-Gerät', icon: 'mdi:television' },
|
|
120
|
+
],
|
|
121
|
+
columns: 3,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: 'title',
|
|
125
|
+
text: 'Die Geräte, die niemals schlafen',
|
|
126
|
+
level: 3,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'paragraph',
|
|
130
|
+
html: 'Moderne Haushalte enthalten durchschnittlich 40 elektronische Geräte. Etwa die Hälfte davon ist durchgehend mit dem Stromnetz verbunden. Während Energievorschriften die Standby-Effizienz in den letzten Jahren verbessert haben, ist die absolute Zahl der Geräte gestiegen. Eine einzelne Spielekonsole im Instant-On-Modus kann 15 Watt ziehen. Ein Fernseher mit Quick-Start kann 12 Watt verbrauchen. Addieren Sie Router, Modems, smarte Lautsprecher, Mikrowellen mit Uhren, Drucker und Ladegeräte, und die Summe übersteigt schnell 100 Watt dauerhaften Hintergrundverbrauchs.',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
type: 'comparative',
|
|
134
|
+
items: [
|
|
135
|
+
{
|
|
136
|
+
title: 'Haushalte mit geringem Verbrauch',
|
|
137
|
+
description: 'Haushalte, die den Standby-Verbrauch aktiv durch smarte Steckdosenleisten und Aussteckgewohnheiten steuern.',
|
|
138
|
+
icon: 'mdi:leaf',
|
|
139
|
+
points: ['Unter 50 Watt Gesamt-Standby', 'Jährliche Kosten unter 50 Euro', 'Verwendet Advanced Power Strips'],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: 'Haushalte mit hohem Verbrauch',
|
|
143
|
+
description: 'Typische Haushalte mit Entertainment-Centern, mehreren Spielekonsolen und immer eingeschalteten Computern.',
|
|
144
|
+
icon: 'mdi:lightning-bolt',
|
|
145
|
+
points: ['Über 150 Watt Gesamt-Standby', 'Jährliche Kosten über 150 Euro', 'Viele Geräte im Quick-Start-Modus'],
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
columns: 2,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'diagnostic',
|
|
152
|
+
variant: 'info',
|
|
153
|
+
title: 'Schnelle Spar Checkliste',
|
|
154
|
+
icon: 'mdi:check-circle',
|
|
155
|
+
badge: 'Aktion',
|
|
156
|
+
html: '<p style="margin:0">Ziehen Sie Handy-Ladegeräte bei Nichtgebrauch aus der Steckdose. Verwenden Sie eine Master-gesteuerte Steckdosenleiste für Ihr Entertainment-Center. Deaktivieren Sie den Quick-Start- oder Instant-On-Modus bei Fernsehern und Konsolen. Schalten Sie Desktop-Computer aus, anstatt sie im Schlafmodus zu lassen.</p>',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'title',
|
|
160
|
+
text: 'Warum Watt sich schneller summieren, als Sie denken',
|
|
161
|
+
level: 3,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'paragraph',
|
|
165
|
+
html: 'Ein Watt ist eine Rate des Energieverbrauchs. Ein Watt, das eine Stunde lang läuft, ist eine Wattstunde. Ein Gerät, das kontinuierlich 10 Watt zieht, verbraucht 87,6 Kilowattstunden pro Jahr. Bei einem Durchschnittspreis von 0,25 Euro pro Kilowattstunde kostet dieses einzelne Gerät über 21 Euro pro Jahr. Multiplizieren Sie das nun mit jedem eingesteckten Gadget in Ihrem Zuhause.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'summary',
|
|
169
|
+
title: 'So senken Sie Ihre Phantomlast',
|
|
170
|
+
items: [
|
|
171
|
+
'Ermitteln Sie die größten Verbraucher mit diesem Simulator oder einem Steckdosen-Messgerät.',
|
|
172
|
+
'Gruppieren Sie Geräte auf smarten Steckdosenleisten, die den Standby abschalten, wenn das Hauptgerät aus ist.',
|
|
173
|
+
'Deaktivieren Sie Always-On-, Quick-Start- und Netzwerk-Standby-Funktionen, wo möglich.',
|
|
174
|
+
'Ziehen Sie Ladegeräte und kleine Geräte ab, die nicht täglich genutzt werden.',
|
|
175
|
+
'Erwägen Sie den Ersatz sehr alter externer Netzteile durch moderne, effiziente Modelle.',
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
ui: {
|
|
180
|
+
sectionTitle: 'Phantom-Last-Audit',
|
|
181
|
+
labelDevices: 'Angeschlossene Standby-Geräte',
|
|
182
|
+
labelHours: 'Standby-Stunden pro Tag',
|
|
183
|
+
unitHours: 'h',
|
|
184
|
+
labelPrice: 'Strompreis',
|
|
185
|
+
unitPrice: '/kWh',
|
|
186
|
+
resultBadge: 'Vampire-Draw-Ergebnis',
|
|
187
|
+
labelAnnualKwh: 'Pro Jahr verschwendet',
|
|
188
|
+
labelAnnualCost: 'Versteckte jährliche Kosten',
|
|
189
|
+
labelMonthlyCost: 'Monatliches Äquivalent',
|
|
190
|
+
labelDevicesCount: 'Geräte gezählt',
|
|
191
|
+
labelTotalWatts: 'Gesamt-Standby-Watt',
|
|
192
|
+
unitWatts: 'W',
|
|
193
|
+
currencySign: '$',
|
|
194
|
+
labelCurrency: 'Währung',
|
|
195
|
+
btnCurrUSD: '$',
|
|
196
|
+
btnCurrEUR: '€',
|
|
197
|
+
btnCurrGBP: '£',
|
|
198
|
+
btnCurrJPY: '¥',
|
|
199
|
+
categoryLow: 'Geringer Verbrauch',
|
|
200
|
+
categoryModerate: 'Moderater Verbrauch',
|
|
201
|
+
categoryHigh: 'Hoher Verbrauch',
|
|
202
|
+
categoryExtreme: 'Extremer Verbrauch',
|
|
203
|
+
addDevice: 'Gerät hinzufügen',
|
|
204
|
+
removeDevice: 'Entfernen',
|
|
205
|
+
modalAddTitle: 'Standby-Gerät hinzufügen',
|
|
206
|
+
labelDeviceName: 'Gerätename',
|
|
207
|
+
labelDeviceWatts: 'Watt im Standby',
|
|
208
|
+
btnSave: 'Speichern',
|
|
209
|
+
btnCancel: 'Abbrechen',
|
|
210
|
+
},
|
|
211
|
+
};
|