@jjlmoya/utils-cooking 1.38.0 → 1.40.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/scripts/fix-translations.js +51 -0
- package/src/category/index.ts +5 -0
- package/src/entries.ts +6 -1
- package/src/index.ts +3 -0
- package/src/tests/i18n-titles.test.ts +2 -2
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/sous-vide-pasteurization-curves.test.ts +66 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/pectin-jam-setting-calculator/bibliography.astro +6 -0
- package/src/tool/pectin-jam-setting-calculator/bibliography.ts +10 -0
- package/src/tool/pectin-jam-setting-calculator/component.astro +170 -0
- package/src/tool/pectin-jam-setting-calculator/components/CalculatorInputs.astro +44 -0
- package/src/tool/pectin-jam-setting-calculator/components/DropTestVisualizer.astro +40 -0
- package/src/tool/pectin-jam-setting-calculator/components/FruitSelector.astro +38 -0
- package/src/tool/pectin-jam-setting-calculator/components/RecipeResults.astro +72 -0
- package/src/tool/pectin-jam-setting-calculator/entry.ts +26 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/de.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/en.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/es.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/fr.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/id.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/it.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ja.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ko.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/nl.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/pl.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/pt.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/ru.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/sv.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/tr.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/i18n/zh.ts +248 -0
- package/src/tool/pectin-jam-setting-calculator/index.ts +11 -0
- package/src/tool/pectin-jam-setting-calculator/logic.ts +96 -0
- package/src/tool/pectin-jam-setting-calculator/pectin-jam-setting-calculator.css +730 -0
- package/src/tool/pectin-jam-setting-calculator/seo.astro +15 -0
- package/src/tool/sous-vide-pasteurization-curves/bibliography.astro +6 -0
- package/src/tool/sous-vide-pasteurization-curves/bibliography.ts +10 -0
- package/src/tool/sous-vide-pasteurization-curves/component.astro +275 -0
- package/src/tool/sous-vide-pasteurization-curves/components/Controls.astro +90 -0
- package/src/tool/sous-vide-pasteurization-curves/components/LethalityChart.astro +28 -0
- package/src/tool/sous-vide-pasteurization-curves/components/ResultsDisplay.astro +36 -0
- package/src/tool/sous-vide-pasteurization-curves/entry.ts +26 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/de.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/en.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/es.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/fr.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/id.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/it.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/ja.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/ko.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/nl.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/pl.ts +154 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/pt.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/ru.ts +154 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/sv.ts +154 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/tr.ts +154 -0
- package/src/tool/sous-vide-pasteurization-curves/i18n/zh.ts +323 -0
- package/src/tool/sous-vide-pasteurization-curves/index.ts +11 -0
- package/src/tool/sous-vide-pasteurization-curves/logic.ts +89 -0
- package/src/tool/sous-vide-pasteurization-curves/seo.astro +15 -0
- package/src/tool/sous-vide-pasteurization-curves/sous-vide-pasteurization-curves.css +456 -0
- package/src/tools.ts +5 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const bibliography = [
|
|
2
|
+
{
|
|
3
|
+
name: 'A Practical Guide to Sous Vide Cooking (Douglas Baldwin)',
|
|
4
|
+
url: 'https://douglasbaldwin.com/sous-vide.html',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'USDA FSIS Compliance Guidelines for Meeting Lethality Performance Standards',
|
|
8
|
+
url: 'https://www.canr.msu.edu/smprv/uploads/files/Appendix_A_and_Compliance_Guidelines.pdf'
|
|
9
|
+
},
|
|
10
|
+
];
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Controls from './components/Controls.astro';
|
|
3
|
+
import ResultsDisplay from './components/ResultsDisplay.astro';
|
|
4
|
+
import LethalityChart from './components/LethalityChart.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { ui } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="sv-container">
|
|
14
|
+
<div class="sv-dashboard">
|
|
15
|
+
<div class="sv-bg-particles" id="sv-particles-container"></div>
|
|
16
|
+
|
|
17
|
+
<div class="sv-row-top">
|
|
18
|
+
<LethalityChart ui={ui} />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="sv-row-bottom">
|
|
22
|
+
<Controls ui={ui} />
|
|
23
|
+
<ResultsDisplay ui={ui} />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<p class="sv-disclaimer">{ui.disclaimer}</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import { SousVideLogic } from './logic';
|
|
32
|
+
|
|
33
|
+
const $ = (id: string) => document.getElementById(id);
|
|
34
|
+
const $$ = (sel: string) => document.querySelectorAll(sel);
|
|
35
|
+
|
|
36
|
+
const STORAGE_KEY = 'sous_vide_pasteurization_state';
|
|
37
|
+
|
|
38
|
+
let system: 'metric' | 'imperial' = 'metric';
|
|
39
|
+
let bathTemp = 56.0;
|
|
40
|
+
let thickness = 25;
|
|
41
|
+
let shape: 'slab' | 'cylinder' | 'sphere' = 'slab';
|
|
42
|
+
let pathogen: 'salmonella' | 'listeria' = 'salmonella';
|
|
43
|
+
|
|
44
|
+
const tempSlider = $('sv-temp-slider') as HTMLInputElement;
|
|
45
|
+
const tempBadge = $('sv-temp-badge');
|
|
46
|
+
const thicknessSlider = $('sv-thickness-slider') as HTMLInputElement;
|
|
47
|
+
const thicknessBadge = $('sv-thickness-badge');
|
|
48
|
+
const systemButtons = $$('#sv-system-toggle .sv-toggle-btn');
|
|
49
|
+
const shapeButtons = $$('#sv-shape-toggle .sv-toggle-btn');
|
|
50
|
+
const pathogenButtons = $$('#sv-pathogen-toggle .sv-toggle-btn');
|
|
51
|
+
const heatingVal = $('sv-heating-val');
|
|
52
|
+
const lethalityVal = $('sv-lethality-val');
|
|
53
|
+
const totalVal = $('sv-total-val');
|
|
54
|
+
const dangerAlert = $('sv-danger-alert');
|
|
55
|
+
const curveLine = $('sv-curve-line');
|
|
56
|
+
const yGrid = $('sv-y-grid');
|
|
57
|
+
const xGrid = $('sv-x-grid');
|
|
58
|
+
const particlesContainer = $('sv-particles-container');
|
|
59
|
+
|
|
60
|
+
function initParticles() {
|
|
61
|
+
if (!particlesContainer) return;
|
|
62
|
+
particlesContainer.innerHTML = '';
|
|
63
|
+
const numParticles = 15;
|
|
64
|
+
for (let i = 0; i < numParticles; i++) {
|
|
65
|
+
const bubble = document.createElement('div');
|
|
66
|
+
bubble.className = 'sv-bg-bubble';
|
|
67
|
+
const size = Math.random() * 8 + 4;
|
|
68
|
+
bubble.style.width = `${size}px`;
|
|
69
|
+
bubble.style.height = `${size}px`;
|
|
70
|
+
bubble.style.left = `${Math.random() * 100}%`;
|
|
71
|
+
bubble.style.animationDelay = `${Math.random() * 8}s`;
|
|
72
|
+
particlesContainer.appendChild(bubble);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function updateParticlesSpeed() {
|
|
77
|
+
const bubbles = $$('.sv-bg-bubble') as NodeListOf<HTMLElement>;
|
|
78
|
+
const speedFactor = Math.max(0.5, (bathTemp - 50) / 10);
|
|
79
|
+
bubbles.forEach(b => {
|
|
80
|
+
const duration = 8 / speedFactor;
|
|
81
|
+
b.style.animationDuration = `${duration}s`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function drawGrid(totalMinutes: number) {
|
|
86
|
+
if (!yGrid || !xGrid) return;
|
|
87
|
+
|
|
88
|
+
let yHTML = '';
|
|
89
|
+
for (let i = 0; i <= 7; i++) {
|
|
90
|
+
const y = 170 - (i * (140 / 7));
|
|
91
|
+
yHTML += `<line x1="50" y1="${y}" x2="360" y2="${y}" class="sv-grid-line" />`;
|
|
92
|
+
yHTML += `<text x="40" y="${y + 4}" class="sv-axis-text" text-anchor="end">${i}D</text>`;
|
|
93
|
+
}
|
|
94
|
+
yGrid.innerHTML = yHTML;
|
|
95
|
+
|
|
96
|
+
let xHTML = '';
|
|
97
|
+
const intervals = 5;
|
|
98
|
+
const intervalMinutes = totalMinutes / intervals;
|
|
99
|
+
for (let i = 0; i <= intervals; i++) {
|
|
100
|
+
const x = 50 + (i * (310 / intervals));
|
|
101
|
+
const label = Math.round(i * intervalMinutes);
|
|
102
|
+
xHTML += `<line x1="${x}" y1="30" x2="${x}" y2="170" class="sv-grid-line" />`;
|
|
103
|
+
xHTML += `<text x="${x}" y="188" class="sv-axis-text" text-anchor="middle">${label}</text>`;
|
|
104
|
+
}
|
|
105
|
+
xGrid.innerHTML = xHTML;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateChart(heatingMinutes: number, totalMinutes: number) {
|
|
109
|
+
if (!curveLine) return;
|
|
110
|
+
|
|
111
|
+
const xStart = 50;
|
|
112
|
+
const xEnd = 360;
|
|
113
|
+
const yStart = 170;
|
|
114
|
+
const yEnd = 30;
|
|
115
|
+
|
|
116
|
+
const heatRatio = totalMinutes > 0 ? (heatingMinutes / totalMinutes) : 0;
|
|
117
|
+
const xMid = xStart + (xEnd - xStart) * heatRatio;
|
|
118
|
+
const yMid = yStart - 5;
|
|
119
|
+
|
|
120
|
+
let d = `M ${xStart} ${yStart}`;
|
|
121
|
+
d += ` Q ${(xStart + xMid) / 2} ${yStart}, ${xMid} ${yMid}`;
|
|
122
|
+
d += ` Q ${(xMid + xEnd) / 2} ${yMid}, ${xEnd} ${yEnd}`;
|
|
123
|
+
|
|
124
|
+
curveLine.setAttribute('d', d);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function save() {
|
|
128
|
+
try {
|
|
129
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
130
|
+
system,
|
|
131
|
+
sliderTemp: tempSlider.value,
|
|
132
|
+
sliderThick: thicknessSlider.value,
|
|
133
|
+
shape,
|
|
134
|
+
pathogen
|
|
135
|
+
}));
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function restoreSystem(sys: string) {
|
|
140
|
+
system = sys;
|
|
141
|
+
systemButtons.forEach(b => {
|
|
142
|
+
b.classList.remove('active');
|
|
143
|
+
if (b.getAttribute('data-value') === system) b.classList.add('active');
|
|
144
|
+
});
|
|
145
|
+
if (system === 'metric') {
|
|
146
|
+
tempSlider.min = '50'; tempSlider.max = '70'; tempSlider.step = '0.5';
|
|
147
|
+
thicknessSlider.min = '5'; thicknessSlider.max = '100'; thicknessSlider.step = '1';
|
|
148
|
+
} else {
|
|
149
|
+
tempSlider.min = '122'; tempSlider.max = '158'; tempSlider.step = '1';
|
|
150
|
+
thicknessSlider.min = '0.2'; thicknessSlider.max = '4.0'; thicknessSlider.step = '0.1';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function restore() {
|
|
155
|
+
try {
|
|
156
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
157
|
+
if (!raw) return;
|
|
158
|
+
const data = JSON.parse(raw);
|
|
159
|
+
if (data.system) restoreSystem(data.system);
|
|
160
|
+
if (data.sliderTemp) tempSlider.value = data.sliderTemp;
|
|
161
|
+
if (data.sliderThick) thicknessSlider.value = data.sliderThick;
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readSliders() {
|
|
166
|
+
if (system === 'metric') {
|
|
167
|
+
bathTemp = parseFloat(tempSlider.value);
|
|
168
|
+
thickness = parseFloat(thicknessSlider.value);
|
|
169
|
+
if (tempBadge) tempBadge.textContent = bathTemp.toFixed(1) + uniTemp;
|
|
170
|
+
if (thicknessBadge) thicknessBadge.textContent = thickness + uniThick;
|
|
171
|
+
} else {
|
|
172
|
+
const f = parseFloat(tempSlider.value);
|
|
173
|
+
const inch = parseFloat(thicknessSlider.value);
|
|
174
|
+
bathTemp = (f - 32) / 1.8;
|
|
175
|
+
thickness = inch * 25.4;
|
|
176
|
+
if (tempBadge) tempBadge.textContent = f.toFixed(0) + uniTemp;
|
|
177
|
+
if (thicknessBadge) thicknessBadge.textContent = inch.toFixed(1) + uniThick;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function showResults(result: ReturnType<typeof SousVideLogic.calculate>) {
|
|
182
|
+
if (heatingVal) heatingVal.textContent = result.heatingMinutes + ' min';
|
|
183
|
+
if (lethalityVal) lethalityVal.textContent = result.lethalityMinutes + ' min';
|
|
184
|
+
if (totalVal) totalVal.textContent = String(result.totalMinutes);
|
|
185
|
+
if (dangerAlert) dangerAlert.style.display = result.isDangerZone ? 'flex' : 'none';
|
|
186
|
+
drawGrid(result.totalMinutes);
|
|
187
|
+
updateChart(result.heatingMinutes, result.totalMinutes);
|
|
188
|
+
updateParticlesSpeed();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function update() {
|
|
192
|
+
readSliders();
|
|
193
|
+
const result = SousVideLogic.calculate({ bathTemp, thickness, shape, pathogen });
|
|
194
|
+
showResults(result);
|
|
195
|
+
save();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function handleSystemChange(newSystem: 'metric' | 'imperial') {
|
|
199
|
+
if (system === newSystem) return;
|
|
200
|
+
|
|
201
|
+
const oldSliderTemp = parseFloat(tempSlider.value);
|
|
202
|
+
const oldSliderThick = parseFloat(thicknessSlider.value);
|
|
203
|
+
|
|
204
|
+
system = newSystem;
|
|
205
|
+
|
|
206
|
+
if (system === 'metric') {
|
|
207
|
+
const tempC = (oldSliderTemp - 32) / 1.8;
|
|
208
|
+
tempSlider.min = '50';
|
|
209
|
+
tempSlider.max = '70';
|
|
210
|
+
tempSlider.step = '0.5';
|
|
211
|
+
tempSlider.value = Math.max(50, Math.min(70, tempC)).toFixed(1);
|
|
212
|
+
|
|
213
|
+
const thickMm = oldSliderThick * 25.4;
|
|
214
|
+
thicknessSlider.min = '5';
|
|
215
|
+
thicknessSlider.max = '100';
|
|
216
|
+
thicknessSlider.step = '1';
|
|
217
|
+
thicknessSlider.value = Math.max(5, Math.min(100, thickMm)).toFixed(0);
|
|
218
|
+
} else {
|
|
219
|
+
const tempF = oldSliderTemp * 1.8 + 32;
|
|
220
|
+
tempSlider.min = '122';
|
|
221
|
+
tempSlider.max = '158';
|
|
222
|
+
tempSlider.step = '1';
|
|
223
|
+
tempSlider.value = Math.max(122, Math.min(158, tempF)).toFixed(0);
|
|
224
|
+
|
|
225
|
+
const thickIn = oldSliderThick / 25.4;
|
|
226
|
+
thicknessSlider.min = '0.2';
|
|
227
|
+
thicknessSlider.max = '4.0';
|
|
228
|
+
thicknessSlider.step = '0.1';
|
|
229
|
+
thicknessSlider.value = Math.max(0.2, Math.min(4.0, thickIn)).toFixed(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
update();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (tempSlider) {
|
|
236
|
+
tempSlider.addEventListener('input', update);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (thicknessSlider) {
|
|
240
|
+
thicknessSlider.addEventListener('input', update);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
systemButtons.forEach(btn => {
|
|
244
|
+
btn.addEventListener('click', (e) => {
|
|
245
|
+
systemButtons.forEach(b => b.classList.remove('active'));
|
|
246
|
+
const target = e.currentTarget as HTMLButtonElement;
|
|
247
|
+
target.classList.add('active');
|
|
248
|
+
handleSystemChange(target.getAttribute('data-value') as 'metric' | 'imperial');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
shapeButtons.forEach(btn => {
|
|
253
|
+
btn.addEventListener('click', (e) => {
|
|
254
|
+
shapeButtons.forEach(b => b.classList.remove('active'));
|
|
255
|
+
const target = e.currentTarget as HTMLButtonElement;
|
|
256
|
+
target.classList.add('active');
|
|
257
|
+
shape = target.getAttribute('data-value') as 'slab' | 'cylinder' | 'sphere';
|
|
258
|
+
update();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pathogenButtons.forEach(btn => {
|
|
263
|
+
btn.addEventListener('click', (e) => {
|
|
264
|
+
pathogenButtons.forEach(b => b.classList.remove('active'));
|
|
265
|
+
const target = e.currentTarget as HTMLButtonElement;
|
|
266
|
+
target.classList.add('active');
|
|
267
|
+
pathogen = target.getAttribute('data-value') as 'salmonella' | 'listeria';
|
|
268
|
+
update();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
restore();
|
|
273
|
+
initParticles();
|
|
274
|
+
update();
|
|
275
|
+
</script>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="sv-controls-panel">
|
|
10
|
+
<div class="sv-glass-card">
|
|
11
|
+
<div class="sv-input-group">
|
|
12
|
+
<span class="sv-label">{ui.systemLabel}</span>
|
|
13
|
+
<div id="sv-system-toggle" class="sv-toggle-grid">
|
|
14
|
+
<button type="button" class="sv-toggle-btn active" data-value="metric">
|
|
15
|
+
{ui.systemMetric}
|
|
16
|
+
</button>
|
|
17
|
+
<button type="button" class="sv-toggle-btn" data-value="imperial">
|
|
18
|
+
{ui.systemImperial}
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="sv-glass-card">
|
|
25
|
+
<div class="sv-input-group">
|
|
26
|
+
<div class="sv-label-container">
|
|
27
|
+
<label for="sv-temp-slider" class="sv-label">{ui.bathTempLabel}</label>
|
|
28
|
+
<span id="sv-temp-badge" class="sv-value-badge">56.0{ui.tempUnitC}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<input
|
|
31
|
+
type="range"
|
|
32
|
+
id="sv-temp-slider"
|
|
33
|
+
class="sv-slider"
|
|
34
|
+
min="50"
|
|
35
|
+
max="70"
|
|
36
|
+
step="0.5"
|
|
37
|
+
value="56.0"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="sv-glass-card">
|
|
43
|
+
<div class="sv-input-group">
|
|
44
|
+
<div class="sv-label-container">
|
|
45
|
+
<label for="sv-thickness-slider" class="sv-label">{ui.thicknessLabel}</label>
|
|
46
|
+
<span id="sv-thickness-badge" class="sv-value-badge">25{ui.mmUnit}</span>
|
|
47
|
+
</div>
|
|
48
|
+
<input
|
|
49
|
+
type="range"
|
|
50
|
+
id="sv-thickness-slider"
|
|
51
|
+
class="sv-slider"
|
|
52
|
+
min="5"
|
|
53
|
+
max="100"
|
|
54
|
+
step="1"
|
|
55
|
+
value="25"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="sv-glass-card">
|
|
61
|
+
<div class="sv-input-group">
|
|
62
|
+
<span class="sv-label">{ui.shapeLabel}</span>
|
|
63
|
+
<div id="sv-shape-toggle" class="sv-toggle-grid">
|
|
64
|
+
<button type="button" class="sv-toggle-btn active" data-value="slab">
|
|
65
|
+
{ui.shapeSlab.split(' ')[0]}
|
|
66
|
+
</button>
|
|
67
|
+
<button type="button" class="sv-toggle-btn" data-value="cylinder">
|
|
68
|
+
{ui.shapeCylinder.split(' ')[0]}
|
|
69
|
+
</button>
|
|
70
|
+
<button type="button" class="sv-toggle-btn" data-value="sphere">
|
|
71
|
+
{ui.shapeSphere.split(' ')[0]}
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="sv-glass-card">
|
|
78
|
+
<div class="sv-input-group">
|
|
79
|
+
<span class="sv-label">{ui.pathogenLabel}</span>
|
|
80
|
+
<div id="sv-pathogen-toggle" class="sv-pathogen-grid sv-toggle-grid">
|
|
81
|
+
<button type="button" class="sv-toggle-btn active" data-value="salmonella">
|
|
82
|
+
{ui.pathogenSalmonella.split(' ')[0]}
|
|
83
|
+
</button>
|
|
84
|
+
<button type="button" class="sv-toggle-btn" data-value="listeria">
|
|
85
|
+
Listeria
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="sv-chart-panel">
|
|
10
|
+
<h3 class="sv-chart-title">{ui.chartTitle}</h3>
|
|
11
|
+
|
|
12
|
+
<svg class="sv-svg-chart" viewBox="0 0 400 220" id="sv-chart-svg">
|
|
13
|
+
<line x1="40" y1="20" x2="40" y2="180" stroke="rgba(226,232,240,0.2)" stroke-width="1.5" />
|
|
14
|
+
<line x1="40" y1="180" x2="380" y2="180" stroke="rgba(226,232,240,0.2)" stroke-width="1.5" />
|
|
15
|
+
|
|
16
|
+
<g id="sv-y-grid"></g>
|
|
17
|
+
<g id="sv-x-grid"></g>
|
|
18
|
+
|
|
19
|
+
<text x="15" y="100" class="sv-axis-text" transform="rotate(-90 15 100)" text-anchor="middle">
|
|
20
|
+
{ui.chartYLabel}
|
|
21
|
+
</text>
|
|
22
|
+
<text x="210" y="210" class="sv-axis-text" text-anchor="middle">
|
|
23
|
+
{ui.chartXLabel}
|
|
24
|
+
</text>
|
|
25
|
+
|
|
26
|
+
<path id="sv-curve-line" class="sv-curve-path" d="" />
|
|
27
|
+
</svg>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
ui: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { ui } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="sv-glass-card sv-results-panel">
|
|
10
|
+
<h3 class="sv-chart-title">{ui.resultsTitle}</h3>
|
|
11
|
+
|
|
12
|
+
<div class="sv-results-hero">
|
|
13
|
+
<div class="sv-hero-ring">
|
|
14
|
+
<span id="sv-total-val" class="sv-hero-value">--</span>
|
|
15
|
+
<span class="sv-hero-unit">{ui.minutesUnit}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<span class="sv-hero-label">{ui.totalTime}</span>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="sv-results-grid">
|
|
21
|
+
<div class="sv-result-card">
|
|
22
|
+
<span class="sv-card-metric-label">{ui.heatingTime}</span>
|
|
23
|
+
<span id="sv-heating-val" class="sv-card-metric-value">--</span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="sv-result-card">
|
|
27
|
+
<span class="sv-card-metric-label">{ui.lethalityTime}</span>
|
|
28
|
+
<span id="sv-lethality-val" class="sv-card-metric-value">--</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div id="sv-danger-alert" class="sv-danger-alert" style="display: none;">
|
|
33
|
+
<h4 class="sv-danger-title">{ui.dangerZoneTitle}</h4>
|
|
34
|
+
<p class="sv-danger-desc">{ui.dangerZoneDesc}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CookingToolEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const sousVidePasteurization: CookingToolEntry = {
|
|
4
|
+
id: 'sous-vide-pasteurization-curves',
|
|
5
|
+
icons: {
|
|
6
|
+
bg: 'mdi:water',
|
|
7
|
+
fg: 'mdi:thermometer-alert',
|
|
8
|
+
},
|
|
9
|
+
i18n: {
|
|
10
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
11
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
12
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
15
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
16
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
17
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
18
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
19
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
20
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
21
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
22
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
23
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
24
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
25
|
+
},
|
|
26
|
+
};
|