@jjlmoya/utils-alcohol 1.24.0 → 1.26.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 +1 -0
- package/src/pages/[locale]/[slug].astro +7 -3
- package/src/tests/locale_completeness.test.ts +4 -9
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/alcoholClearance/alcohol-clearance-calculator.css +23 -23
- package/src/tool/alcoholClearance/bibliography.ts +6 -0
- package/src/tool/alcoholClearance/entry.ts +1 -0
- package/src/tool/alcoholClearance/i18n/de.ts +1 -12
- package/src/tool/alcoholClearance/i18n/en.ts +1 -12
- package/src/tool/alcoholClearance/i18n/es.ts +1 -12
- package/src/tool/alcoholClearance/i18n/fr.ts +1 -12
- package/src/tool/alcoholClearance/i18n/id.ts +1 -12
- package/src/tool/alcoholClearance/i18n/it.ts +1 -12
- package/src/tool/alcoholClearance/i18n/ja.ts +1 -12
- package/src/tool/alcoholClearance/i18n/ko.ts +1 -12
- package/src/tool/alcoholClearance/i18n/nl.ts +1 -12
- package/src/tool/alcoholClearance/i18n/pl.ts +1 -12
- package/src/tool/alcoholClearance/i18n/pt.ts +1 -12
- package/src/tool/alcoholClearance/i18n/ru.ts +1 -12
- package/src/tool/alcoholClearance/i18n/sv.ts +1 -12
- package/src/tool/alcoholClearance/i18n/tr.ts +1 -12
- package/src/tool/alcoholClearance/i18n/zh.ts +1 -12
- package/src/tool/beerCooler/beer-cooler.css +22 -22
- package/src/tool/beerCooler/bibliography.ts +6 -0
- package/src/tool/beerCooler/entry.ts +1 -0
- package/src/tool/beerCooler/i18n/de.ts +1 -12
- package/src/tool/beerCooler/i18n/en.ts +1 -12
- package/src/tool/beerCooler/i18n/es.ts +1 -12
- package/src/tool/beerCooler/i18n/fr.ts +1 -12
- package/src/tool/beerCooler/i18n/id.ts +1 -12
- package/src/tool/beerCooler/i18n/it.ts +1 -12
- package/src/tool/beerCooler/i18n/ja.ts +1 -12
- package/src/tool/beerCooler/i18n/ko.ts +1 -12
- package/src/tool/beerCooler/i18n/nl.ts +1 -12
- package/src/tool/beerCooler/i18n/pl.ts +1 -12
- package/src/tool/beerCooler/i18n/pt.ts +1 -12
- package/src/tool/beerCooler/i18n/ru.ts +1 -12
- package/src/tool/beerCooler/i18n/sv.ts +1 -12
- package/src/tool/beerCooler/i18n/tr.ts +1 -12
- package/src/tool/beerCooler/i18n/zh.ts +1 -12
- package/src/tool/carbonationCalculator/beer-carbonation-calculator.css +19 -19
- package/src/tool/carbonationCalculator/bibliography.ts +6 -0
- package/src/tool/carbonationCalculator/entry.ts +1 -0
- package/src/tool/carbonationCalculator/i18n/de.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/en.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/es.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/fr.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/id.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/it.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/ja.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/ko.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/nl.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/pl.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/pt.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/ru.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/sv.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/tr.ts +1 -12
- package/src/tool/carbonationCalculator/i18n/zh.ts +1 -12
- package/src/tool/cocktailBalancer/bibliography.ts +7 -0
- package/src/tool/cocktailBalancer/cocktail-balancer.css +59 -59
- package/src/tool/cocktailBalancer/entry.ts +1 -0
- package/src/tool/cocktailBalancer/i18n/de.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/en.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/es.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/fr.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/id.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/it.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/ja.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/ko.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/nl.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/pl.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/pt.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/ru.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/sv.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/tr.ts +1 -16
- package/src/tool/cocktailBalancer/i18n/zh.ts +1 -16
- package/src/tool/fortifiedWine/bibliography.astro +14 -0
- package/src/tool/fortifiedWine/bibliography.ts +7 -0
- package/src/tool/fortifiedWine/component.astro +331 -0
- package/src/tool/fortifiedWine/entry.ts +62 -0
- package/src/tool/fortifiedWine/fortified-wine-builder.css +534 -0
- package/src/tool/fortifiedWine/i18n/de.ts +66 -0
- package/src/tool/fortifiedWine/i18n/en.ts +140 -0
- package/src/tool/fortifiedWine/i18n/es.ts +140 -0
- package/src/tool/fortifiedWine/i18n/fr.ts +91 -0
- package/src/tool/fortifiedWine/i18n/id.ts +91 -0
- package/src/tool/fortifiedWine/i18n/it.ts +91 -0
- package/src/tool/fortifiedWine/i18n/ja.ts +91 -0
- package/src/tool/fortifiedWine/i18n/ko.ts +91 -0
- package/src/tool/fortifiedWine/i18n/nl.ts +91 -0
- package/src/tool/fortifiedWine/i18n/pl.ts +91 -0
- package/src/tool/fortifiedWine/i18n/pt.ts +91 -0
- package/src/tool/fortifiedWine/i18n/ru.ts +91 -0
- package/src/tool/fortifiedWine/i18n/sv.ts +91 -0
- package/src/tool/fortifiedWine/i18n/tr.ts +91 -0
- package/src/tool/fortifiedWine/i18n/zh.ts +91 -0
- package/src/tool/fortifiedWine/index.ts +8 -0
- package/src/tool/fortifiedWine/logic.ts +46 -0
- package/src/tool/fortifiedWine/seo.astro +41 -0
- package/src/tool/jelloShotLab/bibliography.astro +14 -0
- package/src/tool/jelloShotLab/bibliography.ts +8 -0
- package/src/tool/jelloShotLab/component.astro +183 -0
- package/src/tool/jelloShotLab/entry.ts +62 -0
- package/src/tool/jelloShotLab/i18n/de.ts +156 -0
- package/src/tool/jelloShotLab/i18n/en.ts +156 -0
- package/src/tool/jelloShotLab/i18n/es.ts +156 -0
- package/src/tool/jelloShotLab/i18n/fr.ts +156 -0
- package/src/tool/jelloShotLab/i18n/id.ts +156 -0
- package/src/tool/jelloShotLab/i18n/it.ts +156 -0
- package/src/tool/jelloShotLab/i18n/ja.ts +156 -0
- package/src/tool/jelloShotLab/i18n/ko.ts +156 -0
- package/src/tool/jelloShotLab/i18n/nl.ts +156 -0
- package/src/tool/jelloShotLab/i18n/pl.ts +156 -0
- package/src/tool/jelloShotLab/i18n/pt.ts +156 -0
- package/src/tool/jelloShotLab/i18n/ru.ts +156 -0
- package/src/tool/jelloShotLab/i18n/sv.ts +156 -0
- package/src/tool/jelloShotLab/i18n/tr.ts +156 -0
- package/src/tool/jelloShotLab/i18n/zh.ts +156 -0
- package/src/tool/jelloShotLab/index.ts +11 -0
- package/src/tool/jelloShotLab/jello-shot-lab.css +229 -0
- package/src/tool/jelloShotLab/logic.ts +29 -0
- package/src/tool/jelloShotLab/seo.astro +53 -0
- package/src/tool/partyKeg/bibliography.ts +6 -0
- package/src/tool/partyKeg/entry.ts +1 -0
- package/src/tool/partyKeg/i18n/de.ts +1 -12
- package/src/tool/partyKeg/i18n/en.ts +1 -12
- package/src/tool/partyKeg/i18n/es.ts +1 -12
- package/src/tool/partyKeg/i18n/fr.ts +1 -12
- package/src/tool/partyKeg/i18n/id.ts +1 -12
- package/src/tool/partyKeg/i18n/it.ts +1 -12
- package/src/tool/partyKeg/i18n/ja.ts +1 -12
- package/src/tool/partyKeg/i18n/ko.ts +1 -12
- package/src/tool/partyKeg/i18n/nl.ts +1 -12
- package/src/tool/partyKeg/i18n/pl.ts +1 -12
- package/src/tool/partyKeg/i18n/pt.ts +1 -12
- package/src/tool/partyKeg/i18n/ru.ts +1 -12
- package/src/tool/partyKeg/i18n/sv.ts +1 -12
- package/src/tool/partyKeg/i18n/tr.ts +1 -12
- package/src/tool/partyKeg/i18n/zh.ts +1 -12
- package/src/tool/partyKeg/party-stock-calculator.css +23 -23
- package/src/tools.ts +5 -0
- package/src/types.ts +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from "astro-icon/components";
|
|
3
|
+
import type { FortifiedWineBuilderUI } from "./index";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: FortifiedWineBuilderUI;
|
|
7
|
+
}
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="fw-app" id="fw-calculator">
|
|
12
|
+
<div class="fw-card">
|
|
13
|
+
|
|
14
|
+
<div class="fw-intention">
|
|
15
|
+
<div class="fw-section-label">{ui.intentionTitle}</div>
|
|
16
|
+
<div class="fw-intention-btns">
|
|
17
|
+
<button class="fw-intention-btn active" data-intention="vermouth" data-target="16">{ui.intentionVermouth}</button>
|
|
18
|
+
<button class="fw-intention-btn" data-intention="port" data-target="19">{ui.intentionPort}</button>
|
|
19
|
+
<button class="fw-intention-btn" data-intention="sherry" data-target="16">{ui.intentionSherry}</button>
|
|
20
|
+
<button class="fw-intention-btn" data-intention="custom" data-target="">{ui.intentionCustom}</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="fw-mode">
|
|
25
|
+
<button class="fw-mode-btn active" data-mode="a">{ui.modeALabel}</button>
|
|
26
|
+
<button class="fw-mode-btn" data-mode="b">{ui.modeBLabel}</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="fw-grid">
|
|
30
|
+
|
|
31
|
+
<div class="fw-left">
|
|
32
|
+
|
|
33
|
+
<div class="fw-sec">
|
|
34
|
+
<div class="fw-sec-title">{ui.wineSection}</div>
|
|
35
|
+
<div class="fw-input-row">
|
|
36
|
+
<div class="fw-field">
|
|
37
|
+
<label class="fw-label" for="fw-wine-vol">{ui.wineVolumeLabel}</label>
|
|
38
|
+
<div class="fw-slider-row">
|
|
39
|
+
<input type="range" class="fw-slider" id="fw-wine-vol-slider" min="0.1" max="100" step="0.1" value="5" />
|
|
40
|
+
<input type="number" class="fw-num-input" id="fw-wine-vol" min="0.1" max="9999" step="0.1" value="5" />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="fw-field">
|
|
44
|
+
<label class="fw-label" for="fw-wine-abv">{ui.wineAbvLabel}</label>
|
|
45
|
+
<div class="fw-slider-row">
|
|
46
|
+
<input type="range" class="fw-slider" id="fw-wine-abv-slider" min="0" max="25" step="0.5" value="12" />
|
|
47
|
+
<input type="number" class="fw-num-input" id="fw-wine-abv" min="0" max="25" step="0.1" value="12" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="fw-sec">
|
|
54
|
+
<div class="fw-sec-title">{ui.spiritSection}</div>
|
|
55
|
+
<div class="fw-presets">
|
|
56
|
+
<button class="fw-preset-btn" data-abv="38">{ui.brandyPreset}</button>
|
|
57
|
+
<button class="fw-preset-btn" data-abv="96">{ui.neutralPreset}</button>
|
|
58
|
+
<button class="fw-preset-btn" data-abv="42">{ui.aguardientePreset}</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="fw-field">
|
|
61
|
+
<label class="fw-label" for="fw-spirit-abv">{ui.spiritAbvLabel}</label>
|
|
62
|
+
<div class="fw-slider-row">
|
|
63
|
+
<input type="range" class="fw-slider" id="fw-spirit-abv-slider" min="15" max="96" step="1" value="38" />
|
|
64
|
+
<input type="number" class="fw-num-input" id="fw-spirit-abv" min="15" max="96" step="0.1" value="38" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="fw-sec">
|
|
70
|
+
<div class="fw-field">
|
|
71
|
+
<label class="fw-label" for="fw-target-abv">{ui.targetAbvLabel}</label>
|
|
72
|
+
<div class="fw-slider-row">
|
|
73
|
+
<input type="range" class="fw-slider" id="fw-target-abv-slider" min="1" max="40" step="0.5" value="16" />
|
|
74
|
+
<input type="number" class="fw-num-input" id="fw-target-abv" min="1" max="40" step="0.1" value="16" />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="fw-field" id="fw-target-vol-field" style="display:none; margin-top:0.75rem;">
|
|
78
|
+
<label class="fw-label" for="fw-target-vol">{ui.targetVolumeLabel}</label>
|
|
79
|
+
<div class="fw-slider-row">
|
|
80
|
+
<input type="range" class="fw-slider" id="fw-target-vol-slider" min="0.1" max="100" step="0.1" value="6" />
|
|
81
|
+
<input type="number" class="fw-num-input" id="fw-target-vol" min="0.1" max="9999" step="0.1" value="6" />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="fw-right">
|
|
89
|
+
|
|
90
|
+
<div class="fw-pearson-wrap">
|
|
91
|
+
<div class="fw-pearson-title">{ui.pearsonTitle}</div>
|
|
92
|
+
<div id="fw-pearson-empty" class="fw-empty-state">{ui.emptyState}</div>
|
|
93
|
+
<svg id="fw-pearson-svg" class="fw-pearson-svg" viewBox="0 0 340 220" style="display:none" aria-hidden="true">
|
|
94
|
+
<rect x="60" y="30" width="180" height="160" fill="none" stroke="currentColor" stroke-width="1.5" stroke-opacity="0.2" rx="4" />
|
|
95
|
+
<line id="fw-diag1" x1="60" y1="30" x2="240" y2="190" stroke="#7f1d1d" stroke-width="1.5" stroke-dasharray="4 3" />
|
|
96
|
+
<line id="fw-diag2" x1="60" y1="190" x2="240" y2="30" stroke="#b45309" stroke-width="1.5" stroke-dasharray="4 3" />
|
|
97
|
+
<circle cx="150" cy="110" r="5" fill="#1e293b" id="fw-center-dot" />
|
|
98
|
+
<text id="fw-lbl-wine-abv" x="58" y="26" text-anchor="end" font-size="12" font-weight="700" fill="#7f1d1d">12%</text>
|
|
99
|
+
<text x="58" y="40" text-anchor="end" font-size="10" fill="currentColor" opacity="0.5" id="fw-lbl-wine-name">{ui.wineCornerLabel}</text>
|
|
100
|
+
<text id="fw-lbl-spirit-abv" x="58" y="186" text-anchor="end" font-size="12" font-weight="700" fill="#b45309">38%</text>
|
|
101
|
+
<text x="58" y="200" text-anchor="end" font-size="10" fill="currentColor" opacity="0.5" id="fw-lbl-spirit-name">{ui.spiritCornerLabel}</text>
|
|
102
|
+
<text id="fw-lbl-target" x="150" y="104" text-anchor="middle" font-size="13" font-weight="800" fill="currentColor">16%</text>
|
|
103
|
+
<text id="fw-lbl-parts-wine" x="244" y="26" font-size="11" font-weight="700" fill="#7f1d1d">22 parts</text>
|
|
104
|
+
<text id="fw-lbl-parts-spirit" x="244" y="186" font-size="11" font-weight="700" fill="#b45309">4 parts</text>
|
|
105
|
+
</svg>
|
|
106
|
+
<div id="fw-prop-wrap" style="display:none">
|
|
107
|
+
<div class="fw-prop-bar">
|
|
108
|
+
<div class="fw-prop-wine" id="fw-prop-wine-bar" style="width:85%"></div>
|
|
109
|
+
<div class="fw-prop-spirit" id="fw-prop-spirit-bar" style="width:15%"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="fw-prop-labels">
|
|
112
|
+
<span class="fw-prop-wine-pct" id="fw-prop-wine-pct">85% wine</span>
|
|
113
|
+
<span class="fw-prop-spirit-pct" id="fw-prop-spirit-pct">15% spirit</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div id="fw-error" class="fw-error" style="display:none">{ui.errorAbv}</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="fw-results" id="fw-results" style="display:none">
|
|
120
|
+
<div class="fw-results-title">{ui.resultsTitle}</div>
|
|
121
|
+
<div class="fw-result-grid">
|
|
122
|
+
<div class="fw-result-card accent">
|
|
123
|
+
<div class="fw-result-label">{ui.addLabel}</div>
|
|
124
|
+
<div class="fw-result-value"><span id="fw-res-spirit">—</span><span class="fw-result-unit">L</span></div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="fw-result-card">
|
|
127
|
+
<div class="fw-result-label">{ui.finalVolumeLabel}</div>
|
|
128
|
+
<div class="fw-result-value"><span id="fw-res-total">—</span><span class="fw-result-unit">L</span></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="fw-bottles" id="fw-bottles" style="display:none">
|
|
134
|
+
<span class="fw-bottles-label">{ui.bottlesSection}</span>
|
|
135
|
+
<span class="fw-bottle-badge">
|
|
136
|
+
<Icon name="mdi:bottle-wine-outline" class="fw-bottle-icon" />
|
|
137
|
+
<span id="fw-bottles-75">—</span> × 75cl
|
|
138
|
+
</span>
|
|
139
|
+
<span class="fw-bottle-badge">
|
|
140
|
+
<Icon name="mdi:bottle-wine-outline" class="fw-bottle-icon" />
|
|
141
|
+
<span id="fw-bottles-50">—</span> × 50cl
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="fw-copy-wrap" id="fw-copy-wrap" style="display:none">
|
|
146
|
+
<button class="fw-copy-btn" id="fw-copy-btn" data-copied-text={ui.copiedBtn}>
|
|
147
|
+
<Icon name="mdi:content-copy" />
|
|
148
|
+
<span id="fw-copy-label">{ui.copyBtn}</span>
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<script>
|
|
158
|
+
type Mode = 'a' | 'b';
|
|
159
|
+
let currentMode: Mode = 'a';
|
|
160
|
+
|
|
161
|
+
function getVal(id: string) { return parseFloat((document.getElementById(id) as HTMLInputElement).value); }
|
|
162
|
+
function setVal(id: string, v: number) { (document.getElementById(id) as HTMLInputElement).value = String(v); }
|
|
163
|
+
function el(id: string) { return document.getElementById(id)!; }
|
|
164
|
+
|
|
165
|
+
function setSliderPct(sliderId: string, v: number, min: number, max: number) {
|
|
166
|
+
const pct = ((v - min) / (max - min)) * 100;
|
|
167
|
+
(document.getElementById(sliderId) as HTMLInputElement).style.setProperty('--val', `${pct}%`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function syncSlider(sliderId: string, inputId: string, min: number, max: number) {
|
|
171
|
+
const slider = document.getElementById(sliderId) as HTMLInputElement;
|
|
172
|
+
const input = document.getElementById(inputId) as HTMLInputElement;
|
|
173
|
+
setSliderPct(sliderId, Number(slider.value), min, max);
|
|
174
|
+
input.value = slider.value;
|
|
175
|
+
calculate();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function syncInput(sliderId: string, inputId: string, min: number, max: number) {
|
|
179
|
+
const input = document.getElementById(inputId) as HTMLInputElement;
|
|
180
|
+
let v = parseFloat(input.value);
|
|
181
|
+
if (isNaN(v)) return;
|
|
182
|
+
v = Math.min(max, Math.max(min, v));
|
|
183
|
+
(document.getElementById(sliderId) as HTMLInputElement).value = String(v);
|
|
184
|
+
setSliderPct(sliderId, v, min, max);
|
|
185
|
+
calculate();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function initSlider(sliderId: string, inputId: string, min: number, max: number) {
|
|
189
|
+
const slider = document.getElementById(sliderId) as HTMLInputElement;
|
|
190
|
+
const input = document.getElementById(inputId) as HTMLInputElement;
|
|
191
|
+
slider.addEventListener('input', () => syncSlider(sliderId, inputId, min, max));
|
|
192
|
+
input.addEventListener('input', () => syncInput(sliderId, inputId, min, max));
|
|
193
|
+
setSliderPct(sliderId, parseFloat(slider.value), min, max);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function showError() {
|
|
197
|
+
el('fw-error').style.display = '';
|
|
198
|
+
el('fw-pearson-svg').style.display = 'none';
|
|
199
|
+
el('fw-pearson-empty').style.display = '';
|
|
200
|
+
el('fw-prop-wrap').style.display = 'none';
|
|
201
|
+
el('fw-results').style.display = 'none';
|
|
202
|
+
el('fw-bottles').style.display = 'none';
|
|
203
|
+
el('fw-copy-wrap').style.display = 'none';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function updatePearson(abvs: { wine: number; spirit: number; target: number }, parts: { wine: number; spirit: number }) {
|
|
207
|
+
el('fw-lbl-wine-abv').textContent = `${abvs.wine}%`;
|
|
208
|
+
el('fw-lbl-spirit-abv').textContent = `${abvs.spirit}%`;
|
|
209
|
+
el('fw-lbl-target').textContent = `${abvs.target}%`;
|
|
210
|
+
el('fw-lbl-parts-wine').textContent = `${parts.wine.toFixed(1)} parts`;
|
|
211
|
+
el('fw-lbl-parts-spirit').textContent = `${parts.spirit.toFixed(1)} parts`;
|
|
212
|
+
el('fw-pearson-svg').style.display = '';
|
|
213
|
+
el('fw-pearson-empty').style.display = 'none';
|
|
214
|
+
el('fw-error').style.display = 'none';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function updatePropBar(partsWine: number, totalParts: number) {
|
|
218
|
+
const winePct = Math.round((partsWine / totalParts) * 100);
|
|
219
|
+
const spiritPct = 100 - winePct;
|
|
220
|
+
el('fw-prop-wine-bar').style.width = `${winePct}%`;
|
|
221
|
+
el('fw-prop-spirit-bar').style.width = `${spiritPct}%`;
|
|
222
|
+
el('fw-prop-wine-pct').textContent = `${winePct}% wine`;
|
|
223
|
+
el('fw-prop-spirit-pct').textContent = `${spiritPct}% spirit`;
|
|
224
|
+
el('fw-prop-wrap').style.display = '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function updateResults(vols: { wine: number; spirit: number; total: number }, abvs: { wine: number; spirit: number; target: number }) {
|
|
228
|
+
el('fw-res-spirit').textContent = vols.spirit.toFixed(2);
|
|
229
|
+
el('fw-res-total').textContent = vols.total.toFixed(2);
|
|
230
|
+
el('fw-bottles-75').textContent = String(Math.ceil(vols.total / 0.75));
|
|
231
|
+
el('fw-bottles-50').textContent = String(Math.ceil(vols.total / 0.5));
|
|
232
|
+
el('fw-results').style.display = '';
|
|
233
|
+
el('fw-bottles').style.display = '';
|
|
234
|
+
el('fw-copy-wrap').style.display = '';
|
|
235
|
+
const btn = el('fw-copy-btn') as HTMLElement;
|
|
236
|
+
Object.assign(btn.dataset, { wine: vols.wine.toFixed(2), spirit: vols.spirit.toFixed(2), total: vols.total.toFixed(2), wineAbv: String(abvs.wine), spiritAbv: String(abvs.spirit), targetAbv: String(abvs.target) });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function calculate() {
|
|
240
|
+
const wineAbv = getVal('fw-wine-abv');
|
|
241
|
+
const spiritAbv = getVal('fw-spirit-abv');
|
|
242
|
+
const targetAbv = getVal('fw-target-abv');
|
|
243
|
+
|
|
244
|
+
el('fw-lbl-wine-abv').textContent = `${wineAbv}%`;
|
|
245
|
+
el('fw-lbl-spirit-abv').textContent = `${spiritAbv}%`;
|
|
246
|
+
el('fw-lbl-target').textContent = `${targetAbv}%`;
|
|
247
|
+
|
|
248
|
+
if (spiritAbv <= targetAbv || targetAbv <= wineAbv) { showError(); return; }
|
|
249
|
+
|
|
250
|
+
const partsWine = spiritAbv - targetAbv;
|
|
251
|
+
const partsSpirit = targetAbv - wineAbv;
|
|
252
|
+
const totalParts = partsWine + partsSpirit;
|
|
253
|
+
|
|
254
|
+
updatePearson({ wine: wineAbv, spirit: spiritAbv, target: targetAbv }, { wine: partsWine, spirit: partsSpirit });
|
|
255
|
+
updatePropBar(partsWine, totalParts);
|
|
256
|
+
|
|
257
|
+
let volWine = 0, volSpirit = 0, volTotal = 0;
|
|
258
|
+
if (currentMode === 'a') {
|
|
259
|
+
const wineVol = getVal('fw-wine-vol');
|
|
260
|
+
if (isNaN(wineVol) || wineVol <= 0) { el('fw-results').style.display = 'none'; el('fw-bottles').style.display = 'none'; el('fw-copy-wrap').style.display = 'none'; return; }
|
|
261
|
+
volWine = wineVol;
|
|
262
|
+
volSpirit = wineVol * (partsSpirit / partsWine);
|
|
263
|
+
volTotal = volWine + volSpirit;
|
|
264
|
+
} else {
|
|
265
|
+
const targetVol = getVal('fw-target-vol');
|
|
266
|
+
if (isNaN(targetVol) || targetVol <= 0) { el('fw-results').style.display = 'none'; el('fw-bottles').style.display = 'none'; el('fw-copy-wrap').style.display = 'none'; return; }
|
|
267
|
+
volWine = targetVol * (partsWine / totalParts);
|
|
268
|
+
volSpirit = targetVol * (partsSpirit / totalParts);
|
|
269
|
+
volTotal = targetVol;
|
|
270
|
+
}
|
|
271
|
+
updateResults({ wine: volWine, spirit: volSpirit, total: volTotal }, { wine: wineAbv, spirit: spiritAbv, target: targetAbv });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
initSlider('fw-wine-vol-slider', 'fw-wine-vol', 0.1, 100);
|
|
275
|
+
initSlider('fw-wine-abv-slider', 'fw-wine-abv', 0, 25);
|
|
276
|
+
initSlider('fw-spirit-abv-slider', 'fw-spirit-abv', 15, 96);
|
|
277
|
+
initSlider('fw-target-abv-slider', 'fw-target-abv', 1, 40);
|
|
278
|
+
initSlider('fw-target-vol-slider', 'fw-target-vol', 0.1, 100);
|
|
279
|
+
|
|
280
|
+
document.querySelectorAll<HTMLButtonElement>('.fw-intention-btn').forEach(btn => {
|
|
281
|
+
btn.addEventListener('click', () => {
|
|
282
|
+
document.querySelectorAll('.fw-intention-btn').forEach(b => b.classList.remove('active'));
|
|
283
|
+
btn.classList.add('active');
|
|
284
|
+
const target = btn.dataset.target;
|
|
285
|
+
if (target) {
|
|
286
|
+
setVal('fw-target-abv', Number(target));
|
|
287
|
+
setVal('fw-target-abv-slider', Number(target));
|
|
288
|
+
setSliderPct('fw-target-abv-slider', Number(target), 1, 40);
|
|
289
|
+
}
|
|
290
|
+
calculate();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
document.querySelectorAll<HTMLButtonElement>('.fw-mode-btn').forEach(btn => {
|
|
295
|
+
btn.addEventListener('click', () => {
|
|
296
|
+
document.querySelectorAll('.fw-mode-btn').forEach(b => b.classList.remove('active'));
|
|
297
|
+
btn.classList.add('active');
|
|
298
|
+
currentMode = btn.dataset.mode as Mode;
|
|
299
|
+
const wineVolField = (document.getElementById('fw-wine-vol') as HTMLElement)?.closest('.fw-field') as HTMLElement;
|
|
300
|
+
const targetVolField = el('fw-target-vol-field');
|
|
301
|
+
if (currentMode === 'b') { if (wineVolField) wineVolField.style.display = 'none'; targetVolField.style.display = ''; }
|
|
302
|
+
else { if (wineVolField) wineVolField.style.display = ''; targetVolField.style.display = 'none'; }
|
|
303
|
+
calculate();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
document.querySelectorAll<HTMLButtonElement>('.fw-preset-btn').forEach(btn => {
|
|
308
|
+
btn.addEventListener('click', () => {
|
|
309
|
+
const abv = Number(btn.dataset.abv);
|
|
310
|
+
setVal('fw-spirit-abv', abv);
|
|
311
|
+
setVal('fw-spirit-abv-slider', abv);
|
|
312
|
+
setSliderPct('fw-spirit-abv-slider', abv, 15, 96);
|
|
313
|
+
calculate();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
el('fw-copy-btn').addEventListener('click', function () {
|
|
318
|
+
const btn = this as HTMLElement;
|
|
319
|
+
const label = el('fw-copy-label');
|
|
320
|
+
const copiedText = btn.dataset.copiedText || 'Copied!';
|
|
321
|
+
const copyText = label.textContent || 'Copy Recipe';
|
|
322
|
+
const text = `Recipe: ${btn.dataset.wine}L wine ${btn.dataset.wineAbv}% + ${btn.dataset.spirit}L spirit ${btn.dataset.spiritAbv}% = ${btn.dataset.total}L @ ${btn.dataset.targetAbv}%`;
|
|
323
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
324
|
+
btn.classList.add('copied');
|
|
325
|
+
label.textContent = copiedText;
|
|
326
|
+
setTimeout(() => { btn.classList.remove('copied'); label.textContent = copyText; }, 2000);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
calculate();
|
|
331
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AlcoholToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
export { bibliography } from './bibliography';
|
|
3
|
+
|
|
4
|
+
export interface FortifiedWineBuilderUI {
|
|
5
|
+
[key: string]: string;
|
|
6
|
+
intentionTitle: string;
|
|
7
|
+
intentionVermouth: string;
|
|
8
|
+
intentionPort: string;
|
|
9
|
+
intentionSherry: string;
|
|
10
|
+
intentionCustom: string;
|
|
11
|
+
modeALabel: string;
|
|
12
|
+
modeBLabel: string;
|
|
13
|
+
wineSection: string;
|
|
14
|
+
wineVolumeLabel: string;
|
|
15
|
+
wineAbvLabel: string;
|
|
16
|
+
spiritSection: string;
|
|
17
|
+
spiritAbvLabel: string;
|
|
18
|
+
brandyPreset: string;
|
|
19
|
+
neutralPreset: string;
|
|
20
|
+
aguardientePreset: string;
|
|
21
|
+
targetAbvLabel: string;
|
|
22
|
+
targetVolumeLabel: string;
|
|
23
|
+
resultsTitle: string;
|
|
24
|
+
addLabel: string;
|
|
25
|
+
finalVolumeLabel: string;
|
|
26
|
+
bottlesSection: string;
|
|
27
|
+
copyBtn: string;
|
|
28
|
+
copiedBtn: string;
|
|
29
|
+
pearsonTitle: string;
|
|
30
|
+
wineCornerLabel: string;
|
|
31
|
+
spiritCornerLabel: string;
|
|
32
|
+
emptyState: string;
|
|
33
|
+
errorAbv: string;
|
|
34
|
+
errorMode: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type FortifiedWineBuilderLocaleContent = ToolLocaleContent<FortifiedWineBuilderUI>;
|
|
38
|
+
|
|
39
|
+
export const fortifiedWine: AlcoholToolEntry<FortifiedWineBuilderUI> = {
|
|
40
|
+
id: 'fortified-wine-builder',
|
|
41
|
+
icons: {
|
|
42
|
+
bg: 'mdi:bottle-wine',
|
|
43
|
+
fg: 'mdi:glass-wine',
|
|
44
|
+
},
|
|
45
|
+
i18n: {
|
|
46
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
47
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
48
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
49
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
50
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
51
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
52
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
53
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
54
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
55
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
56
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
57
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
58
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
59
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
60
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
61
|
+
},
|
|
62
|
+
};
|