@jjlmoya/utils-motor 1.4.0 → 1.6.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 +2 -2
- package/src/category/index.ts +2 -1
- package/src/entries.ts +2 -1
- package/src/index.ts +5 -0
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +76 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/tool_validation.test.ts +1 -1
- package/src/tool/evChargingTimeCostPlanner/bibliography.astro +6 -0
- package/src/tool/evChargingTimeCostPlanner/bibliography.ts +16 -0
- package/src/tool/evChargingTimeCostPlanner/component.astro +147 -0
- package/src/tool/evChargingTimeCostPlanner/controller.ts +214 -0
- package/src/tool/evChargingTimeCostPlanner/dom-views.ts +124 -0
- package/src/tool/evChargingTimeCostPlanner/entry.ts +30 -0
- package/src/tool/evChargingTimeCostPlanner/ev-charging-time-cost-planner.css +598 -0
- package/src/tool/evChargingTimeCostPlanner/evaluator.ts +10 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/de.ts +22 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/en.ts +161 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/es.ts +16 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/factory.ts +113 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/fr.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/id.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/it.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ja.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ko.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/nl.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/pl.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/pt.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/ru.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/sv.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/tr.ts +9 -0
- package/src/tool/evChargingTimeCostPlanner/i18n/zh.ts +13 -0
- package/src/tool/evChargingTimeCostPlanner/index.ts +10 -0
- package/src/tool/evChargingTimeCostPlanner/logic.test.ts +71 -0
- package/src/tool/evChargingTimeCostPlanner/logic.ts +174 -0
- package/src/tool/evChargingTimeCostPlanner/seo.astro +15 -0
- package/src/tool/evChargingTimeCostPlanner/storage.ts +27 -0
- package/src/tool/evChargingTimeCostPlanner/ui.ts +50 -0
- package/src/tools.ts +4 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { CHARGING_PRESETS, getCurrencyOption, type ChargingInputs, type ChargingPlan } from './logic';
|
|
2
|
+
import type { ChargingStatus } from './evaluator';
|
|
3
|
+
import type { EVChargingTimeCostPlannerUI } from './ui';
|
|
4
|
+
|
|
5
|
+
const numberFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
|
|
6
|
+
const priceFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
|
|
7
|
+
const factorFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 4 });
|
|
8
|
+
|
|
9
|
+
export function formatNumber(value: number): string {
|
|
10
|
+
return numberFormatter.format(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatCurrency(value: number, currency: ChargingInputs['currency']): string {
|
|
14
|
+
return new Intl.NumberFormat('en-US', {
|
|
15
|
+
style: 'currency',
|
|
16
|
+
currency,
|
|
17
|
+
maximumFractionDigits: 2,
|
|
18
|
+
}).format(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatPrice(value: number): string {
|
|
22
|
+
return priceFormatter.format(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatFactor(value: number): string {
|
|
26
|
+
return factorFormatter.format(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatDuration(minutes: number): string {
|
|
30
|
+
if (minutes <= 0) return '0 min';
|
|
31
|
+
const hours = Math.floor(minutes / 60);
|
|
32
|
+
const remainder = minutes % 60;
|
|
33
|
+
if (hours === 0) return `${remainder} min`;
|
|
34
|
+
if (remainder === 0) return `${hours} h`;
|
|
35
|
+
return `${hours} h ${remainder} min`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function statusText(ui: EVChargingTimeCostPlannerUI, status: ChargingStatus): string {
|
|
39
|
+
const labels: Record<ChargingStatus, string> = {
|
|
40
|
+
empty: ui.statusEmpty,
|
|
41
|
+
short: ui.statusShort,
|
|
42
|
+
standard: ui.statusStandard,
|
|
43
|
+
long: ui.statusLong,
|
|
44
|
+
};
|
|
45
|
+
return labels[status];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function updateText(root: HTMLElement, selector: string, value: string): void {
|
|
49
|
+
const element = root.querySelector<HTMLElement>(selector);
|
|
50
|
+
if (element) element.textContent = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function updateCurrencyContext(root: HTMLElement, ui: EVChargingTimeCostPlannerUI, inputs: ChargingInputs): void {
|
|
54
|
+
const currency = getCurrencyOption(inputs.currency);
|
|
55
|
+
updateText(root, '[data-ev-currency-code]', currency.code);
|
|
56
|
+
updateText(root, '[data-ev-price-unit]', `${currency.symbol} / kWh`);
|
|
57
|
+
updateText(root, '[data-ev-price-label]', `${ui.priceLabel} (${currency.code})`);
|
|
58
|
+
updateText(root, '[data-ev-currency-factor]', ui.currencyFactorTemplate
|
|
59
|
+
.replace('{factor}', formatFactor(currency.unitsPerEur))
|
|
60
|
+
.replace('{currency}', currency.code));
|
|
61
|
+
root.querySelectorAll<HTMLElement>('[data-ev-currency-option]').forEach((option) => {
|
|
62
|
+
const active = option.dataset.evCurrencyOption === currency.code;
|
|
63
|
+
option.setAttribute('aria-selected', String(active));
|
|
64
|
+
option.classList.toggle('is-active', active);
|
|
65
|
+
});
|
|
66
|
+
root.querySelectorAll<HTMLElement>('[data-ev-preset-meta]').forEach((meta) => {
|
|
67
|
+
const presetId = meta.dataset.evPresetMeta;
|
|
68
|
+
const preset = CHARGING_PRESETS.find((item) => item.id === presetId);
|
|
69
|
+
if (!preset) return;
|
|
70
|
+
const presetCurrency = getCurrencyOption(preset.inputs.currency);
|
|
71
|
+
const price = (preset.inputs.pricePerKwh / presetCurrency.unitsPerEur) * currency.unitsPerEur;
|
|
72
|
+
meta.textContent = ui.presetMetaTemplate
|
|
73
|
+
.replace('{power}', formatPrice(preset.inputs.chargerPowerKw))
|
|
74
|
+
.replace('{symbol}', currency.symbol)
|
|
75
|
+
.replace('{price}', formatPrice(price));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getAddedLevel(index: number, endSegment: number): string {
|
|
80
|
+
return index < endSegment ? 'added' : 'empty';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function updateBattery(root: HTMLElement, startSoc: number, endSoc: number): void {
|
|
84
|
+
const segments = Array.from(root.querySelectorAll<HTMLElement>('[data-ev-segment]'));
|
|
85
|
+
const startSegment = Math.round((startSoc / 100) * segments.length);
|
|
86
|
+
const endSegment = Math.round((endSoc / 100) * segments.length);
|
|
87
|
+
segments.forEach((segment, index) => {
|
|
88
|
+
const level = index < startSegment ? 'held' : getAddedLevel(index, endSegment);
|
|
89
|
+
segment.dataset.level = level;
|
|
90
|
+
});
|
|
91
|
+
root.style.setProperty('--ev-start-soc', `${startSoc}%`);
|
|
92
|
+
root.style.setProperty('--ev-end-soc', `${endSoc}%`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderChargingPlan(
|
|
96
|
+
root: HTMLElement,
|
|
97
|
+
view: {
|
|
98
|
+
ui: EVChargingTimeCostPlannerUI;
|
|
99
|
+
inputs: ChargingInputs;
|
|
100
|
+
plan: ChargingPlan;
|
|
101
|
+
status: ChargingStatus;
|
|
102
|
+
},
|
|
103
|
+
): void {
|
|
104
|
+
const { ui, inputs, plan, status } = view;
|
|
105
|
+
updateCurrencyContext(root, ui, inputs);
|
|
106
|
+
updateBattery(root, inputs.startSoc, inputs.endSoc);
|
|
107
|
+
root.dataset.status = status;
|
|
108
|
+
updateText(root, '[data-ev-start-soc]', `${formatNumber(inputs.startSoc)}%`);
|
|
109
|
+
updateText(root, '[data-ev-end-soc]', `${formatNumber(inputs.endSoc)}%`);
|
|
110
|
+
updateText(root, '[data-ev-battery-energy]', `${formatNumber(plan.batteryEnergyKwh)} kWh`);
|
|
111
|
+
updateText(root, '[data-ev-grid-energy]', `${formatNumber(plan.gridEnergyKwh)} kWh`);
|
|
112
|
+
updateText(root, '[data-ev-loss-energy]', `${formatNumber(plan.lossEnergyKwh)} kWh`);
|
|
113
|
+
updateText(root, '[data-ev-time]', formatDuration(plan.timeMinutes));
|
|
114
|
+
updateText(root, '[data-ev-cost]', formatCurrency(plan.cost, inputs.currency));
|
|
115
|
+
updateText(root, '[data-ev-result-range]', `${formatNumber(plan.recoveredRangeKm)} ${ui.rangeUnit}`);
|
|
116
|
+
updateText(root, '[data-ev-status]', statusText(ui, status));
|
|
117
|
+
updateText(root, '[data-ev-power]', `${formatNumber(inputs.chargerPowerKw)} kW`);
|
|
118
|
+
updateText(root, '[data-ev-summary]', ui.summaryTemplate
|
|
119
|
+
.replace('{battery}', formatNumber(plan.batteryEnergyKwh))
|
|
120
|
+
.replace('{range}', formatNumber(plan.recoveredRangeKm))
|
|
121
|
+
.replace('{power}', formatNumber(inputs.chargerPowerKw))
|
|
122
|
+
.replace('{time}', formatDuration(plan.timeMinutes))
|
|
123
|
+
.replace('{cost}', formatCurrency(plan.cost, inputs.currency)));
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MotorToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { EVChargingTimeCostPlannerUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { EVChargingTimeCostPlannerUI };
|
|
5
|
+
export type EVChargingTimeCostPlannerLocaleContent = ToolLocaleContent<EVChargingTimeCostPlannerUI>;
|
|
6
|
+
|
|
7
|
+
export const evChargingTimeCostPlanner: MotorToolEntry<EVChargingTimeCostPlannerUI> = {
|
|
8
|
+
id: 'ev-charging-time-cost-planner',
|
|
9
|
+
icons: {
|
|
10
|
+
bg: 'mdi:ev-station',
|
|
11
|
+
fg: 'mdi:flash-outline',
|
|
12
|
+
},
|
|
13
|
+
i18n: {
|
|
14
|
+
de: () => import('./i18n/de').then((module) => module.content),
|
|
15
|
+
en: () => import('./i18n/en').then((module) => module.content),
|
|
16
|
+
es: () => import('./i18n/es').then((module) => module.content),
|
|
17
|
+
fr: () => import('./i18n/fr').then((module) => module.content),
|
|
18
|
+
id: () => import('./i18n/id').then((module) => module.content),
|
|
19
|
+
it: () => import('./i18n/it').then((module) => module.content),
|
|
20
|
+
ja: () => import('./i18n/ja').then((module) => module.content),
|
|
21
|
+
ko: () => import('./i18n/ko').then((module) => module.content),
|
|
22
|
+
nl: () => import('./i18n/nl').then((module) => module.content),
|
|
23
|
+
pl: () => import('./i18n/pl').then((module) => module.content),
|
|
24
|
+
pt: () => import('./i18n/pt').then((module) => module.content),
|
|
25
|
+
ru: () => import('./i18n/ru').then((module) => module.content),
|
|
26
|
+
sv: () => import('./i18n/sv').then((module) => module.content),
|
|
27
|
+
tr: () => import('./i18n/tr').then((module) => module.content),
|
|
28
|
+
zh: () => import('./i18n/zh').then((module) => module.content),
|
|
29
|
+
},
|
|
30
|
+
};
|