@jjlmoya/utils-alcohol 1.1.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.
Files changed (62) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +19 -0
  3. package/src/category/i18n/es.ts +28 -0
  4. package/src/category/i18n/fr.ts +19 -0
  5. package/src/category/index.ts +12 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +19 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +155 -0
  14. package/src/pages/[locale].astro +271 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/content_mandatory.test.ts +32 -0
  17. package/src/tests/faq_count.test.ts +17 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/seo_length.test.ts +39 -0
  20. package/src/tests/tool_validation.test.ts +17 -0
  21. package/src/tool/alcoholClearance/component.astro +219 -0
  22. package/src/tool/alcoholClearance/component.css +369 -0
  23. package/src/tool/alcoholClearance/i18n/en.ts +172 -0
  24. package/src/tool/alcoholClearance/i18n/es.ts +181 -0
  25. package/src/tool/alcoholClearance/i18n/fr.ts +163 -0
  26. package/src/tool/alcoholClearance/index.ts +50 -0
  27. package/src/tool/alcoholClearance/logic.ts +59 -0
  28. package/src/tool/beerCooler/component.astro +236 -0
  29. package/src/tool/beerCooler/component.css +381 -0
  30. package/src/tool/beerCooler/i18n/en.ts +168 -0
  31. package/src/tool/beerCooler/i18n/es.ts +181 -0
  32. package/src/tool/beerCooler/i18n/fr.ts +168 -0
  33. package/src/tool/beerCooler/index.ts +49 -0
  34. package/src/tool/beerCooler/logic.ts +34 -0
  35. package/src/tool/carbonationCalculator/component.astro +225 -0
  36. package/src/tool/carbonationCalculator/component.css +483 -0
  37. package/src/tool/carbonationCalculator/i18n/en.ts +175 -0
  38. package/src/tool/carbonationCalculator/i18n/es.ts +179 -0
  39. package/src/tool/carbonationCalculator/i18n/fr.ts +175 -0
  40. package/src/tool/carbonationCalculator/index.ts +48 -0
  41. package/src/tool/carbonationCalculator/logic.ts +40 -0
  42. package/src/tool/cocktailBalancer/bibliography.astro +14 -0
  43. package/src/tool/cocktailBalancer/component.astro +396 -0
  44. package/src/tool/cocktailBalancer/component.css +1218 -0
  45. package/src/tool/cocktailBalancer/data/IngredientRepository.ts +83 -0
  46. package/src/tool/cocktailBalancer/data/Presets.ts +122 -0
  47. package/src/tool/cocktailBalancer/domain/Ingredient.ts +29 -0
  48. package/src/tool/cocktailBalancer/i18n/en.ts +193 -0
  49. package/src/tool/cocktailBalancer/i18n/es.ts +193 -0
  50. package/src/tool/cocktailBalancer/i18n/fr.ts +193 -0
  51. package/src/tool/cocktailBalancer/index.ts +68 -0
  52. package/src/tool/cocktailBalancer/logic.ts +118 -0
  53. package/src/tool/cocktailBalancer/seo.astro +53 -0
  54. package/src/tool/partyKeg/component.astro +269 -0
  55. package/src/tool/partyKeg/component.css +660 -0
  56. package/src/tool/partyKeg/i18n/en.ts +162 -0
  57. package/src/tool/partyKeg/i18n/es.ts +166 -0
  58. package/src/tool/partyKeg/i18n/fr.ts +162 -0
  59. package/src/tool/partyKeg/index.ts +46 -0
  60. package/src/tool/partyKeg/logic.ts +36 -0
  61. package/src/tools.ts +14 -0
  62. package/src/types.ts +72 -0
@@ -0,0 +1,269 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+ import type { PartyKegUI } from "./index";
4
+ import './component.css';
5
+
6
+ interface Props {
7
+ ui: PartyKegUI;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ ---
12
+
13
+ <div class="party-app">
14
+ <div class="party-card">
15
+ <div class="party-deco party-deco-top"></div>
16
+ <div class="party-deco party-deco-bottom"></div>
17
+
18
+ <div class="party-grid">
19
+ <div class="party-inputs">
20
+ <div class="calc-header">
21
+ <div class="calc-header-icon">
22
+ <Icon name="mdi:calculator" class="calc-header-icon-svg" />
23
+ </div>
24
+ <div>
25
+ <h3 class="calc-title">{ui.calcStockTitle}</h3>
26
+ <p class="calc-subtitle">{ui.beerIceSub}</p>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="slider-section">
31
+ <div class="slider-header">
32
+ <label class="slider-lbl">{ui.guestsLabel}</label>
33
+ <span class="slider-val" id="guest-count-display">50</span>
34
+ </div>
35
+ <div class="custom-slider-wrap">
36
+ <input type="range" id="guest-slider" min="5" max="200" step="5" value="50" class="custom-range" />
37
+ <div class="slider-track-fill slider-fill-amber" id="guest-fill" style="width:24%"></div>
38
+ <div class="slider-thumb" id="guest-thumb" style="left:24%"></div>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="slider-section">
43
+ <div class="slider-header">
44
+ <label class="slider-lbl">{ui.durationLabel}</label>
45
+ <div class="slider-val-unit">
46
+ <span class="slider-val" id="duration-display">4</span>
47
+ <span class="slider-unit">{ui.hoursUnit}</span>
48
+ </div>
49
+ </div>
50
+ <div class="custom-slider-wrap">
51
+ <input type="range" id="duration-slider" min="1" max="12" step="1" value="4" class="custom-range" />
52
+ <div class="slider-track-fill slider-fill-blue" id="duration-fill" style="width:33%"></div>
53
+ <div class="slider-thumb" id="duration-thumb" style="left:33%"></div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="vibe-section">
58
+ <label class="slider-lbl">{ui.intensityLabel}</label>
59
+ <div class="vibe-buttons">
60
+ <button class="vibe-btn active" data-mult="1">
61
+ <div class="vibe-bg vibe-bg-green"></div>
62
+ <div class="vibe-content">
63
+ <Icon name="mdi:tea-outline" class="vibe-icon icon-col" />
64
+ <span class="vibe-label text-col">{ui.chillLabel}</span>
65
+ </div>
66
+ <div class="vibe-bar vibe-bar-green active-bar"></div>
67
+ </button>
68
+ <button class="vibe-btn" data-mult="1.5">
69
+ <div class="vibe-bg vibe-bg-amber"></div>
70
+ <div class="vibe-content">
71
+ <Icon name="mdi:glass-mug-variant" class="vibe-icon icon-col" />
72
+ <span class="vibe-label text-col">{ui.standardLabel}</span>
73
+ </div>
74
+ <div class="vibe-bar vibe-bar-amber active-bar"></div>
75
+ </button>
76
+ <button class="vibe-btn" data-mult="2.5">
77
+ <div class="vibe-bg vibe-bg-rose"></div>
78
+ <div class="vibe-content">
79
+ <Icon name="mdi:fire" class="vibe-icon icon-col" />
80
+ <span class="vibe-label text-col">{ui.partyLabel}</span>
81
+ </div>
82
+ <div class="vibe-bar vibe-bar-rose active-bar"></div>
83
+ </button>
84
+ </div>
85
+ </div>
86
+
87
+ <div class="temp-section">
88
+ <div class="slider-header">
89
+ <label class="slider-lbl temp-lbl">
90
+ <Icon name="mdi:sun-thermometer-outline" class="temp-icon" />
91
+ {ui.tempLabel}
92
+ </label>
93
+ <div class="slider-val-unit">
94
+ <span class="slider-val" id="temp-display">25</span>
95
+ <span class="slider-unit">°C</span>
96
+ </div>
97
+ </div>
98
+ <div class="custom-slider-wrap">
99
+ <input type="range" id="temp-slider" min="15" max="40" step="1" value="25" class="custom-range" />
100
+ <div class="slider-track-fill slider-fill-temp" id="temp-fill" style="width:40%"></div>
101
+ <div class="slider-thumb" id="temp-thumb" style="left:40%"></div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="party-results">
107
+ <div class="stats-header">
108
+ <div class="stat-block stat-block-left">
109
+ <div class="stat-deco-icon">
110
+ <Icon name="mdi:beer" class="stat-deco-svg" />
111
+ </div>
112
+ <span class="stat-label">{ui.estimatedVolLabel}</span>
113
+ <div class="stat-value-row">
114
+ <span class="stat-number" id="total-liters">0</span>
115
+ <span class="stat-unit">L</span>
116
+ </div>
117
+ <p class="stat-sub stat-sub-amber" id="keg-text">-- {ui.kegsLabel}</p>
118
+ </div>
119
+
120
+ <div class="stat-block stat-block-right">
121
+ <div class="stat-deco-icon">
122
+ <Icon name="mdi:snowflake" class="stat-deco-svg" />
123
+ </div>
124
+ <span class="stat-label">{ui.iceRequiredLabel}</span>
125
+ <div class="stat-value-row">
126
+ <span class="stat-number" id="total-ice">0</span>
127
+ <span class="stat-unit">kg</span>
128
+ </div>
129
+ <p class="stat-sub stat-sub-cyan" id="bags-text">-- {ui.bagsLabel}</p>
130
+ </div>
131
+ </div>
132
+
133
+ <div class="visual-stage-wrap">
134
+ <div class="visual-stage-header">
135
+ <span class="visual-title">{ui.visualizationTitle}</span>
136
+ <span id="ice-hint" class="ice-hint">-</span>
137
+ </div>
138
+ <div class="visual-stage" id="visual-stage"></div>
139
+ <div class="stage-baseline"></div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <div style="display:none" id="ui-labels" data-ui={JSON.stringify(ui)}></div>
146
+ </div>
147
+
148
+ <script>
149
+ import { calculatePartyStock } from "./logic";
150
+ import type { PartyKegUI } from "./index";
151
+
152
+ class PartyApp {
153
+ state = { guests: 50, duration: 4, temp: 25, drinkRate: 1 };
154
+ ui = {} as PartyKegUI;
155
+
156
+ constructor() {
157
+ const uiEl = document.getElementById("ui-labels");
158
+ if (uiEl) this.ui = JSON.parse(uiEl.dataset.ui || "{}") as PartyKegUI;
159
+ this.init();
160
+ }
161
+
162
+ init() {
163
+ const sliders = [
164
+ { id: "guest-slider", fill: "guest-fill", thumb: "guest-thumb", disp: "guest-count-display", key: "guests" as const, min: 5, max: 200 },
165
+ { id: "duration-slider", fill: "duration-fill", thumb: "duration-thumb", disp: "duration-display", key: "duration" as const, min: 1, max: 12 },
166
+ { id: "temp-slider", fill: "temp-fill", thumb: "temp-thumb", disp: "temp-display", key: "temp" as const, min: 15, max: 40 }
167
+ ];
168
+ sliders.forEach(s => this.bindSlider(s));
169
+ document.querySelectorAll<HTMLButtonElement>(".vibe-btn").forEach(btn => {
170
+ btn.onclick = () => {
171
+ document.querySelectorAll(".vibe-btn").forEach(b => b.classList.remove("active"));
172
+ btn.classList.add("active");
173
+ this.state.drinkRate = parseFloat(btn.dataset.mult ?? '1');
174
+ this.update();
175
+ };
176
+ });
177
+ this.update();
178
+ }
179
+
180
+ bindSlider(s: { id: string; fill: string; thumb: string; disp: string; key: 'guests' | 'duration' | 'temp'; min: number; max: number }) {
181
+ const input = document.getElementById(s.id) as HTMLInputElement | null;
182
+ const fill = document.getElementById(s.fill);
183
+ const thumb = document.getElementById(s.thumb);
184
+ const disp = document.getElementById(s.disp);
185
+ if (!input) return;
186
+ input.oninput = () => {
187
+ const val = parseFloat(input.value);
188
+ const pct = ((val - s.min) / (s.max - s.min)) * 100;
189
+ if (fill) fill.style.width = pct + "%";
190
+ if (thumb) thumb.style.left = pct + "%";
191
+ if (disp) disp.innerText = String(val);
192
+ this.state[s.key] = val;
193
+ this.update();
194
+ };
195
+ input.dispatchEvent(new Event('input'));
196
+ }
197
+
198
+ update() {
199
+ const stats = calculatePartyStock(this.state.guests, this.state.duration, this.state.temp, this.state.drinkRate);
200
+ const litersEl = document.getElementById("total-liters");
201
+ const iceEl = document.getElementById("total-ice");
202
+ const kegEl = document.getElementById("keg-text");
203
+ const bagsEl = document.getElementById("bags-text");
204
+ const hintEl = document.getElementById("ice-hint");
205
+ if (litersEl) litersEl.innerText = String(stats.totalLiters);
206
+ if (iceEl) iceEl.innerText = String(stats.totalIce);
207
+ if (kegEl) kegEl.innerText = `${stats.kegs} ${this.ui.kegsLabel} (50L)`;
208
+ if (bagsEl) bagsEl.innerText = `${stats.bags} ${this.ui.bagsLabel} (2kg)`;
209
+ if (hintEl) hintEl.innerText = this.ui[stats.iceMsg + "Msg"] || stats.iceMsg;
210
+ this.renderVisuals(stats.kegs, stats.bags);
211
+ }
212
+
213
+ buildKegStack(kegs: number): HTMLElement {
214
+ const kegStack = document.createElement("div");
215
+ kegStack.className = "keg-stack";
216
+ const shownKegs = Math.min(kegs, 6);
217
+ for (let i = 0; i < shownKegs; i++) {
218
+ const k = document.createElement("div");
219
+ k.className = "animate-drop keg-item";
220
+ k.style.zIndex = String(i);
221
+ k.style.animationDelay = (i * 0.1) + "s";
222
+ k.innerHTML = '<svg width="70" height="88" viewBox="0 0 40 50" fill="none"><rect x="2" y="5" width="36" height="40" rx="2" fill="#B45309" stroke="#78350F" stroke-width="2"/><line x1="14" y1="5" x2="14" y2="45" stroke="#78350F" stroke-opacity="0.3" stroke-width="1"/><line x1="26" y1="5" x2="26" y2="45" stroke="#78350F" stroke-opacity="0.3" stroke-width="1"/><rect x="0" y="10" width="40" height="4" fill="#64748B" stroke="#334155"/><rect x="0" y="36" width="40" height="4" fill="#64748B" stroke="#334155"/><ellipse cx="20" cy="5" rx="18" ry="3" fill="#D97706" stroke="#78350F" stroke-width="2"/></svg>';
223
+ kegStack.appendChild(k);
224
+ }
225
+ if (kegs > 6) {
226
+ const plus = document.createElement("div");
227
+ plus.innerText = "+" + (kegs - 6);
228
+ plus.className = "keg-plus";
229
+ kegStack.appendChild(plus);
230
+ }
231
+ return kegStack;
232
+ }
233
+
234
+ buildIcePile(bags: number): HTMLElement {
235
+ const icePile = document.createElement("div");
236
+ icePile.className = "ice-pile";
237
+ const shownBags = Math.min(bags, 40);
238
+ for (let i = 0; i < shownBags; i++) {
239
+ const b = document.createElement("div");
240
+ b.className = "animate-drop ice-bag";
241
+ b.style.zIndex = String(Math.floor(i / 3));
242
+ b.style.marginBottom = "-18px";
243
+ b.style.transform = `rotate(${Math.random()*30-15}deg) translateX(${Math.random()*10-5}px)`;
244
+ b.style.animationDelay = (0.05 + Math.random()*0.3) + "s";
245
+ b.innerHTML = '<svg width="45" height="51" viewBox="0 0 30 34" fill="none"><path d="M5 8C5 6.89543 5.89543 6 7 6H23C24.1046 6 25 6.89543 25 8V28C25 30.2091 23.2091 32 21 32H9C6.79086 32 5 30.2091 5 28V8Z" fill="#CFFAFE" stroke="#06B6D4" stroke-width="1.5" stroke-opacity="0.5"/><path d="M10 6L8 2M20 6L22 2" stroke="#06B6D4" stroke-width="1.5" stroke-linecap="round"/><rect x="11" y="4" width="8" height="2" rx="1" fill="#0891B2"/><rect x="8" y="10" width="6" height="6" rx="1" fill="white" fill-opacity="0.6"/><rect x="16" y="14" width="6" height="6" rx="1" fill="white" fill-opacity="0.6"/><rect x="10" y="22" width="6" height="6" rx="1" fill="white" fill-opacity="0.6"/></svg>';
246
+ icePile.appendChild(b);
247
+ }
248
+ if (bags > 40) {
249
+ const plus = document.createElement("div");
250
+ plus.innerText = "+" + (bags - 40);
251
+ plus.className = "bags-plus";
252
+ icePile.appendChild(plus);
253
+ }
254
+ return icePile;
255
+ }
256
+
257
+ renderVisuals(kegs: number, bags: number) {
258
+ const stage = document.getElementById("visual-stage");
259
+ if (!stage) return;
260
+ stage.innerHTML = "";
261
+ const container = document.createElement("div");
262
+ container.className = "visual-container";
263
+ container.appendChild(this.buildKegStack(kegs));
264
+ container.appendChild(this.buildIcePile(bags));
265
+ stage.appendChild(container);
266
+ }
267
+ }
268
+ new PartyApp();
269
+ </script>