@jjlmoya/utils-cooking 1.2.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 (130) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +24 -0
  3. package/src/category/i18n/es.ts +208 -0
  4. package/src/category/i18n/fr.ts +24 -0
  5. package/src/category/index.ts +37 -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 +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/i18n-titles.test.ts +66 -0
  18. package/src/tests/locale_completeness.test.ts +42 -0
  19. package/src/tests/mocks/astro_mock.js +2 -0
  20. package/src/tests/no_h1_in_components.test.ts +48 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/american-kitchen-converter/AmericanKitchenEngine.ts +259 -0
  24. package/src/tool/american-kitchen-converter/bibliography.astro +6 -0
  25. package/src/tool/american-kitchen-converter/component.astro +838 -0
  26. package/src/tool/american-kitchen-converter/i18n/en.ts +282 -0
  27. package/src/tool/american-kitchen-converter/i18n/es.ts +281 -0
  28. package/src/tool/american-kitchen-converter/i18n/fr.ts +292 -0
  29. package/src/tool/american-kitchen-converter/index.ts +24 -0
  30. package/src/tool/american-kitchen-converter/seo.astro +8 -0
  31. package/src/tool/banana-ripeness/BananaCare.css +587 -0
  32. package/src/tool/banana-ripeness/BananaEngine.ts +79 -0
  33. package/src/tool/banana-ripeness/bibliography.astro +6 -0
  34. package/src/tool/banana-ripeness/component.astro +285 -0
  35. package/src/tool/banana-ripeness/i18n/en.ts +177 -0
  36. package/src/tool/banana-ripeness/i18n/es.ts +177 -0
  37. package/src/tool/banana-ripeness/i18n/fr.ts +177 -0
  38. package/src/tool/banana-ripeness/index.ts +24 -0
  39. package/src/tool/banana-ripeness/seo.astro +8 -0
  40. package/src/tool/brine/bibliography.astro +6 -0
  41. package/src/tool/brine/component.astro +884 -0
  42. package/src/tool/brine/i18n/en.ts +221 -0
  43. package/src/tool/brine/i18n/es.ts +222 -0
  44. package/src/tool/brine/i18n/fr.ts +221 -0
  45. package/src/tool/brine/index.ts +26 -0
  46. package/src/tool/brine/seo.astro +8 -0
  47. package/src/tool/cookware-guide/CookwareGuide.css +487 -0
  48. package/src/tool/cookware-guide/bibliography.astro +6 -0
  49. package/src/tool/cookware-guide/component.astro +164 -0
  50. package/src/tool/cookware-guide/i18n/en.ts +163 -0
  51. package/src/tool/cookware-guide/i18n/es.ts +163 -0
  52. package/src/tool/cookware-guide/i18n/fr.ts +164 -0
  53. package/src/tool/cookware-guide/index.ts +24 -0
  54. package/src/tool/cookware-guide/init.ts +174 -0
  55. package/src/tool/cookware-guide/seo.astro +8 -0
  56. package/src/tool/egg-timer/EggTimer.css +503 -0
  57. package/src/tool/egg-timer/bibliography.astro +14 -0
  58. package/src/tool/egg-timer/component.astro +281 -0
  59. package/src/tool/egg-timer/i18n/en.ts +230 -0
  60. package/src/tool/egg-timer/i18n/es.ts +222 -0
  61. package/src/tool/egg-timer/i18n/fr.ts +121 -0
  62. package/src/tool/egg-timer/index.ts +27 -0
  63. package/src/tool/egg-timer/seo.astro +39 -0
  64. package/src/tool/ingredient-rescaler/IngredientRescaler.css +308 -0
  65. package/src/tool/ingredient-rescaler/bibliography.astro +6 -0
  66. package/src/tool/ingredient-rescaler/component.astro +107 -0
  67. package/src/tool/ingredient-rescaler/i18n/en.ts +265 -0
  68. package/src/tool/ingredient-rescaler/i18n/es.ts +268 -0
  69. package/src/tool/ingredient-rescaler/i18n/fr.ts +207 -0
  70. package/src/tool/ingredient-rescaler/index.ts +24 -0
  71. package/src/tool/ingredient-rescaler/init.ts +200 -0
  72. package/src/tool/ingredient-rescaler/seo.astro +8 -0
  73. package/src/tool/kitchen-timer/KitchenTimer.css +325 -0
  74. package/src/tool/kitchen-timer/bibliography.astro +6 -0
  75. package/src/tool/kitchen-timer/component.astro +341 -0
  76. package/src/tool/kitchen-timer/i18n/en.ts +154 -0
  77. package/src/tool/kitchen-timer/i18n/es.ts +154 -0
  78. package/src/tool/kitchen-timer/i18n/fr.ts +154 -0
  79. package/src/tool/kitchen-timer/index.ts +26 -0
  80. package/src/tool/kitchen-timer/init.ts +55 -0
  81. package/src/tool/kitchen-timer/lib/AudioHelper.ts +27 -0
  82. package/src/tool/kitchen-timer/lib/DockManager.ts +97 -0
  83. package/src/tool/kitchen-timer/lib/KitchenTimer.ts +264 -0
  84. package/src/tool/kitchen-timer/seo.astro +8 -0
  85. package/src/tool/meringue-peak/MeringueCalculator.css +298 -0
  86. package/src/tool/meringue-peak/bibliography.astro +6 -0
  87. package/src/tool/meringue-peak/component.astro +169 -0
  88. package/src/tool/meringue-peak/i18n/en.ts +257 -0
  89. package/src/tool/meringue-peak/i18n/es.ts +234 -0
  90. package/src/tool/meringue-peak/i18n/fr.ts +234 -0
  91. package/src/tool/meringue-peak/index.ts +24 -0
  92. package/src/tool/meringue-peak/seo.astro +8 -0
  93. package/src/tool/mold-scaler/MoldScaler.css +406 -0
  94. package/src/tool/mold-scaler/bibliography.astro +6 -0
  95. package/src/tool/mold-scaler/component.astro +126 -0
  96. package/src/tool/mold-scaler/i18n/en.ts +268 -0
  97. package/src/tool/mold-scaler/i18n/es.ts +269 -0
  98. package/src/tool/mold-scaler/i18n/fr.ts +276 -0
  99. package/src/tool/mold-scaler/index.ts +26 -0
  100. package/src/tool/mold-scaler/init.ts +264 -0
  101. package/src/tool/mold-scaler/seo.astro +8 -0
  102. package/src/tool/pizza/Pizza.css +569 -0
  103. package/src/tool/pizza/bibliography.astro +6 -0
  104. package/src/tool/pizza/calculator.ts +143 -0
  105. package/src/tool/pizza/component.astro +237 -0
  106. package/src/tool/pizza/i18n/en.ts +288 -0
  107. package/src/tool/pizza/i18n/es.ts +289 -0
  108. package/src/tool/pizza/i18n/fr.ts +288 -0
  109. package/src/tool/pizza/index.ts +27 -0
  110. package/src/tool/pizza/seo.astro +8 -0
  111. package/src/tool/roux-guide/RouxGuide.css +483 -0
  112. package/src/tool/roux-guide/bibliography.astro +6 -0
  113. package/src/tool/roux-guide/component.astro +194 -0
  114. package/src/tool/roux-guide/i18n/en.ts +233 -0
  115. package/src/tool/roux-guide/i18n/es.ts +225 -0
  116. package/src/tool/roux-guide/i18n/fr.ts +225 -0
  117. package/src/tool/roux-guide/index.ts +24 -0
  118. package/src/tool/roux-guide/init.ts +187 -0
  119. package/src/tool/roux-guide/seo.astro +8 -0
  120. package/src/tool/sourdough-calculator/SourdoughCalculator.css +369 -0
  121. package/src/tool/sourdough-calculator/bibliography.astro +6 -0
  122. package/src/tool/sourdough-calculator/component.astro +198 -0
  123. package/src/tool/sourdough-calculator/i18n/en.ts +242 -0
  124. package/src/tool/sourdough-calculator/i18n/es.ts +243 -0
  125. package/src/tool/sourdough-calculator/i18n/fr.ts +248 -0
  126. package/src/tool/sourdough-calculator/index.ts +24 -0
  127. package/src/tool/sourdough-calculator/init.ts +131 -0
  128. package/src/tool/sourdough-calculator/seo.astro +8 -0
  129. package/src/tools.ts +29 -0
  130. package/src/types.ts +73 -0
@@ -0,0 +1,276 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+
3
+ export const content: ToolLocaleContent = {
4
+ slug: 'adaptateur-taille-moule-patisserie',
5
+ title: 'Adaptateur de Taille de Moule et Calculateur de Cuisson',
6
+ description:
7
+ 'Adaptez n’importe quelle recette de pâtisserie à vos moules instantanément. Calculez le facteur de conversion pour les moules ronds, carrés et rectangulaires avec une précision professionnelle.',
8
+ faqTitle: 'Questions Fréquentes sur la Conversion',
9
+ bibliographyTitle: 'Ressources Scientifiques et Références',
10
+
11
+ ui: {
12
+ originalRecipe: 'Recette Originale',
13
+ yourMold: 'Votre Moule',
14
+ round: 'Rond',
15
+ square: 'Carré',
16
+ rectangular: 'Rectangulaire',
17
+ diameter: 'Diamètre (cm)',
18
+ side: 'Côté (cm)',
19
+ width: 'Largeur (cm)',
20
+ length: 'Longueur (cm)',
21
+ multiplyingFactor: 'Facteur de Conversion',
22
+ equivalentMolds: 'Les moules sont équivalents. Utilisez les mêmes quantités.',
23
+ smallerMold: 'Votre moule est plus petit. Réduisez les ingrédients en les multipliant par',
24
+ largerMold: 'Votre moule est plus grand. Augmentez les ingrédients en les multipliant par',
25
+ ingredientCalculator: 'Calculateur de Poids d\'Ingrédients',
26
+ addIngredient: 'Ajouter un Ingrédient',
27
+ ingredient: 'Ingrédient',
28
+ original: 'Original',
29
+ final: 'Final',
30
+ exampleIngredient: 'Ex: Farine',
31
+ delete: 'Supprimer',
32
+ originalVisualization: 'Original',
33
+ yourVisualization: 'Le vôtre',
34
+ defaultIngredient1: 'Farine',
35
+ defaultIngredient2: 'Sucre',
36
+ reduce: 'Réduire',
37
+ increase: 'Augmenter',
38
+ },
39
+
40
+ faq: [
41
+ {
42
+ question: 'Comment est calculé exactement le facteur multiplicateur ?',
43
+ answer:
44
+ 'Le facteur est obtenu en divisant la surface de votre moule cible par la surface du moule d\'origine mentionné dans la recette. Si le résultat est 1,5, vous multipliez chaque ingrédient par 1,5.',
45
+ },
46
+ {
47
+ question: 'Puis-je convertir un moule rond en un moule carré ?',
48
+ answer:
49
+ 'Oui, notre outil utilise des formules géométriques pour comparer les surfaces quelle que soit la forme. Entrez simplement les dimensions et le système gère l\'équivalence automatiquement.',
50
+ },
51
+ {
52
+ question: 'La hauteur du moule affecte-t-elle le calcul ?',
53
+ answer:
54
+ 'Cet outil se concentre sur la surface de base, qui est le facteur le plus critique. Si votre moule est beaucoup plus haut ou plus bas que l\'original, vous devrez peut-être ajuster le temps de cuisson.',
55
+ },
56
+ {
57
+ question: 'Comment ajouter plusieurs ingrédients à ma liste ?',
58
+ answer:
59
+ 'Cliquez sur le bouton "Ajouter un Ingrédient" pour créer une nouvelle ligne. Tapez le nom et le poids original ; la colonne finale se met à jour instantanément avec le montant proportionnel.',
60
+ },
61
+ {
62
+ question: 'Le calcul est-il fiable pour les très grandes plaques ?',
63
+ answer:
64
+ 'Mathématiquement oui, mais gardez à l\'esprit que les très gros gâteaux mettent plus de temps à cuire au centre. Vous devrez peut-être baisser la température du four légèrement.',
65
+ },
66
+ ],
67
+
68
+ bibliography: [
69
+ {
70
+ name: 'Science et Technologie de la Boulangerie - E.J. Pyler',
71
+ url: 'https://www.bakingbusiness.com/',
72
+ },
73
+ {
74
+ name: 'Le Chef Pâtissier Professionnel - Bo Friberg',
75
+ url: 'https://www.wiley.com/',
76
+ },
77
+ ],
78
+
79
+ howTo: [
80
+ {
81
+ name: 'Définissez le moule de la recette originale',
82
+ text: 'Sélectionnez la forme et les dimensions du moule pour lequel la recette a été initialement conçue.',
83
+ },
84
+ {
85
+ name: 'Saisissez les mesures de votre moule cible',
86
+ text: 'Sélectionnez la forme de votre moule et entrez ses dimensions. Le système calculera le facteur immédiatement.',
87
+ },
88
+ {
89
+ name: 'Utilisez le convertisseur de poids d\'ingrédients',
90
+ text: 'Ajoutez les ingrédients de votre recette pour voir exactement les quantités dont vous avez besoin pour la nouvelle taille.',
91
+ },
92
+ {
93
+ name: 'Surveillez les temps de cuisson',
94
+ text: 'N\'oubliez pas que changer la taille du moule nécessite généralement des ajustements de temps, même si la température reste la même.',
95
+ },
96
+ ],
97
+
98
+ seo: [
99
+ {
100
+ type: 'title',
101
+ text: 'Guide Avancé pour Adapter vos Recettes selon la Taille du Moule',
102
+ level: 2,
103
+ },
104
+ {
105
+ type: 'paragraph',
106
+ html: 'Adapter les quantités d’une recette pour une taille de moule différente est une compétence fondamentale en pâtisserie professionnelle. Il ne s’agit pas seulement d’ajouter quelques ingrédients à l’œil ; cela nécessite de respecter les <strong>proportions géométriques</strong> pour maintenir la même hauteur et texture.',
107
+ },
108
+ {
109
+ type: 'diagnostic',
110
+ variant: 'info',
111
+ title: 'Le Secret de la Surface',
112
+ html: 'Le secret d’une bonne adaptation réside dans la surface, pas dans le diamètre. Une augmentation de 25 % du diamètre d’un moule rond double presque sa surface totale.',
113
+ },
114
+ {
115
+ type: 'stats',
116
+ columns: 4,
117
+ items: [
118
+ {
119
+ value: 'x1.56',
120
+ label: 'Facteur 20cm à 25cm',
121
+ icon: 'mdi:resize',
122
+ },
123
+ {
124
+ value: 'x2.25',
125
+ label: 'Facteur 15cm à 22.5cm',
126
+ icon: 'mdi:arrow-up-bold-outline',
127
+ },
128
+ {
129
+ value: '0.64',
130
+ label: 'Facteur 25cm à 20cm',
131
+ icon: 'mdi:arrow-down-bold-outline',
132
+ },
133
+ {
134
+ value: 'πr²',
135
+ label: 'Formule d’aire circulaire',
136
+ icon: 'mdi:math-compass',
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ type: 'title',
142
+ text: 'Comparaison des Formes et Efficacité Thermique',
143
+ level: 3,
144
+ },
145
+ {
146
+ type: 'comparative',
147
+ columns: 3,
148
+ items: [
149
+ {
150
+ title: 'Moules Ronds',
151
+ icon: 'mdi:circle-outline',
152
+ description: 'Le standard de la pâtisserie. Ils offrent une cuisson très uniforme de la périphérie vers le centre.',
153
+ points: [
154
+ 'Distribution de chaleur optimale',
155
+ 'Idéal pour gâteaux hauts',
156
+ 'Calcul basé sur le rayon',
157
+ ],
158
+ },
159
+ {
160
+ title: 'Moules Carrés',
161
+ icon: 'mdi:square-outline',
162
+ description: 'Permettent une utilisation maximale de l’espace. Idéal pour les brownies et les coupes individuelles nettes.',
163
+ highlight: true,
164
+ points: [
165
+ 'Cuisson rapide des coins',
166
+ 'Facile à portionner',
167
+ 'Calcul Côté x Côté',
168
+ ],
169
+ },
170
+ {
171
+ title: 'Moules Rectangulaires',
172
+ icon: 'mdi:rectangle-outline',
173
+ description: 'Parfaits pour les grands plateaux et les génoises. Nécessitent une surveillance au centre.',
174
+ points: [
175
+ 'Capacité totale élevée',
176
+ 'Polyvalence d’usage',
177
+ 'Calcul Largeur x Longueur',
178
+ ],
179
+ },
180
+ ],
181
+ },
182
+ {
183
+ type: 'title',
184
+ text: 'Mathématiques de la Conversion de Surface',
185
+ level: 3,
186
+ },
187
+ {
188
+ type: 'paragraph',
189
+ html: 'Pour calculer le facteur de conversion correct, nous devons comparer les surfaces de base en utilisant ces formules géométriques standard :',
190
+ },
191
+ {
192
+ type: 'table',
193
+ headers: ['Forme du Moule', 'Formule de Surface', 'Considération Clé'],
194
+ rows: [
195
+ ['Circulaire', 'π × Rayon²', 'Le rayon est la moitié du diamètre'],
196
+ ['Carré', 'Côté × Côté', 'Mesures internes uniquement'],
197
+ ['Rectangulaire', 'Largeur × Longueur', 'Standard pour les brownies et plaques'],
198
+ ],
199
+ },
200
+ {
201
+ type: 'title',
202
+ text: 'Erreurs Courantes lors du Changement de Taille de Moule',
203
+ level: 3,
204
+ },
205
+ {
206
+ type: 'paragraph',
207
+ html: 'De nombreux pâtissiers amateurs font l’erreur d’adapter les ingrédients de manière linéaire en se basant sur le diamètre. Évitez ces pièges courants :',
208
+ },
209
+ {
210
+ type: 'list',
211
+ items: [
212
+ '<strong>Adaptation linéaire :</strong> Doubler le diamètre ne double pas les ingrédients ; cela les quadruple.',
213
+ '<strong>Ignorer la profondeur :</strong> Des moules profonds nécessitent une cuisson plus longue à basse température.',
214
+ '<strong>Agents levants :</strong> La levure chimique ne nécessite pas toujours une adaptation parfaitement linéaire.',
215
+ '<strong>Évaporation :</strong> Les petites quantités peuvent sécher plus vite à cause du ratio surface-volume.',
216
+ ],
217
+ },
218
+ {
219
+ type: 'diagnostic',
220
+ variant: 'warning',
221
+ title: 'Limite de Capacité du Moule',
222
+ html: 'Ne remplissez jamais un moule à plus des 2/3 de sa capacité totale, quel que soit le facteur calculé, pour permettre une expansion correcte.',
223
+ },
224
+ {
225
+ type: 'title',
226
+ text: 'Glossaire des Termes de Conversion Géométrique',
227
+ level: 3,
228
+ },
229
+ {
230
+ type: 'glossary',
231
+ items: [
232
+ {
233
+ term: 'Facteur de Conversion',
234
+ definition: 'Le nombre par lequel multiplier tous les ingrédients pour adapter la recette au nouveau moule.',
235
+ },
236
+ {
237
+ term: 'Surface de Base',
238
+ definition: 'La mesure de l’aire du fond du moule. En pâtisserie, c’est la donnée la plus pertinente pour le volume.',
239
+ },
240
+ {
241
+ term: 'Rayon',
242
+ definition: 'Distance du centre vers le bord d’un cercle. Essentiel pour la formule πr².',
243
+ },
244
+ {
245
+ term: 'Transfert Thermique',
246
+ definition: 'La manière dont l’énergie voyage dans le moule. Change drastiquement selon la forme et le matériau.',
247
+ },
248
+ ],
249
+ },
250
+ {
251
+ type: 'title',
252
+ text: 'Conseils de Pro pour la Cuisson des Recettes Adaptées',
253
+ level: 3,
254
+ },
255
+ {
256
+ type: 'tip',
257
+ html: 'Si vous passez à un moule beaucoup plus grand, utilisez des bandes de cuisson ou un noyau thermique au centre pour assurer une distribution uniforme de la chaleur sans dessécher les bords.',
258
+ },
259
+ {
260
+ type: 'paragraph',
261
+ html: 'Maîtriser l’adaptation des moules vous donne une liberté créative totale. Utilisez cette calculatrice pour éliminer les approximations et obtenir des résultats professionnels constants.',
262
+ },
263
+ ],
264
+
265
+ schemas: [
266
+ {
267
+ '@context': 'https://schema.org',
268
+ '@type': 'WebApplication',
269
+ 'name': 'Calculateur de Moule JJLMoya',
270
+ 'url': 'https://utils.jjlmoya.com/fr/adaptateur-taille-moule-patisserie',
271
+ 'description': 'Outil professionnel pour convertir le poids des ingrédients entre différentes tailles et formes de moules à pâtisserie.',
272
+ 'applicationCategory': 'UtilitiesApplication',
273
+ 'operatingSystem': 'All',
274
+ },
275
+ ],
276
+ };
@@ -0,0 +1,26 @@
1
+ import type { CookingToolEntry, ToolDefinition } from '../../types';
2
+ import MoldScalerComponent from './component.astro';
3
+ import MoldScalerSEO from './seo.astro';
4
+ import MoldScalerBibliography from './bibliography.astro';
5
+
6
+ export const moldScaler: CookingToolEntry = {
7
+ id: 'mold-scaler',
8
+ icons: {
9
+ bg: 'mdi:cake',
10
+ fg: 'mdi:resize',
11
+ },
12
+ i18n: {
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ en: () => import('./i18n/en').then((m) => m.content),
15
+ fr: () => import('./i18n/fr').then((m) => m.content),
16
+ },
17
+ };
18
+
19
+ export { MoldScalerComponent, MoldScalerSEO, MoldScalerBibliography };
20
+
21
+ export const MOLD_SCALER_TOOL: ToolDefinition = {
22
+ entry: moldScaler,
23
+ Component: MoldScalerComponent,
24
+ SEOComponent: MoldScalerSEO,
25
+ BibliographyComponent: MoldScalerBibliography,
26
+ };
@@ -0,0 +1,264 @@
1
+ type Shape = 'round' | 'square' | 'rectangular';
2
+
3
+ interface Mold {
4
+ shape: Shape;
5
+ dim1: number;
6
+ dim2: number;
7
+ }
8
+
9
+ interface Ingredient {
10
+ id: string;
11
+ name: string;
12
+ weight: number;
13
+ }
14
+
15
+ interface MoldScalerState {
16
+ original: Mold;
17
+ target: Mold;
18
+ ingredients: Ingredient[];
19
+ factor: number;
20
+ }
21
+
22
+ interface MoldScalerElements {
23
+ originalInputs: HTMLElement | null;
24
+ targetInputs: HTMLElement | null;
25
+ resultFactor: HTMLElement | null;
26
+ resultText: HTMLElement | null;
27
+ shapeOriginal: SVGPathElement | null;
28
+ shapeTarget: SVGPathElement | null;
29
+ ingredientsList: HTMLElement | null;
30
+ addBtn: HTMLElement | null;
31
+ }
32
+
33
+ interface MoldScalerContext {
34
+ state: MoldScalerState;
35
+ els: MoldScalerElements;
36
+ ui: Record<string, string>;
37
+ }
38
+
39
+ class MoldLogic {
40
+ static getArea(mold: Mold): number {
41
+ if (mold.shape === 'round') {
42
+ return Math.PI * Math.pow(mold.dim1 / 2, 2);
43
+ }
44
+ if (mold.shape === 'square') {
45
+ return mold.dim1 * mold.dim1;
46
+ }
47
+ return mold.dim1 * mold.dim2;
48
+ }
49
+
50
+ static getPath(mold: Mold, scale = 6): string {
51
+ if (mold.shape === 'round') {
52
+ const r = (mold.dim1 / 2) * scale;
53
+ return `M 0,${-r} A ${r},${r} 0 1,1 0,${r} A ${r},${r} 0 1,1 0,${-r}`;
54
+ }
55
+ const w = mold.dim1 * scale;
56
+ const h = (mold.shape === 'square' ? mold.dim1 : mold.dim2) * scale;
57
+ const x = -w / 2;
58
+ const y = -h / 2;
59
+ return `M ${x},${y} h ${w} v ${h} h ${-w} Z`;
60
+ }
61
+
62
+ static calculateFactor(original: Mold, target: Mold): number {
63
+ const area1 = this.getArea(original);
64
+ const area2 = this.getArea(target);
65
+ return Math.round((area2 / area1) * 100) / 100;
66
+ }
67
+ }
68
+
69
+ function getInputHtml(shape: Shape, mold: Mold, type: string, ui: Record<string, string>): string {
70
+ if (shape === 'round') {
71
+ return `<div class="ms-input-group"><label class="ms-label">${ui.diameter}</label><input type="number" class="ms-input" value="${mold.dim1}" min="1" step="0.5" data-type="${type}" data-key="dim1"></div>`;
72
+ }
73
+ if (shape === 'square') {
74
+ return `<div class="ms-input-group"><label class="ms-label">${ui.side}</label><input type="number" class="ms-input" value="${mold.dim1}" min="1" step="0.5" data-type="${type}" data-key="dim1"></div>`;
75
+ }
76
+ return `<div class="ms-inputs-grid"><div class="ms-input-group"><label class="ms-label">${ui.width}</label><input type="number" class="ms-input" value="${mold.dim1}" min="1" step="0.5" data-type="${type}" data-key="dim1"></div><div class="ms-input-group"><label class="ms-label">${ui.length}</label><input type="number" class="ms-input" value="${mold.dim2}" min="1" step="0.5" data-type="${type}" data-key="dim2"></div></div>`;
77
+ }
78
+
79
+ function renderInput(ctx: MoldScalerContext, type: 'original' | 'target'): void {
80
+ const container = type === 'original' ? ctx.els.originalInputs : ctx.els.targetInputs;
81
+ if (!container) return;
82
+ const html = getInputHtml(ctx.state[type].shape, ctx.state[type], type, ctx.ui);
83
+ container.innerHTML = html;
84
+ }
85
+
86
+ function buildIngredientRow(ing: Ingredient, factor: number, ui: Record<string, string>): string {
87
+ const finalValue = Math.round(ing.weight * factor);
88
+ return `<div class="ms-ingredient-row" data-id="${ing.id}"><div class="ms-input-group"><label class="ms-label">${ui.ingredient}</label><input type="text" class="ms-input ms-ing-name" value="${ing.name}" placeholder="${ui.exampleIngredient}"></div><div class="ms-input-group"><label class="ms-label">${ui.original}</label><input type="number" class="ms-input ms-ing-weight" value="${ing.weight || ''}" placeholder="0"></div><div class="ms-input-group"><label class="ms-label">${ui.final}</label><div class="ms-ingredient-final">${ing.weight > 0 ? `${finalValue}g` : '-'}</div></div><button class="ms-del-btn" data-id="${ing.id}" title="${ui.delete}"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></div>`;
89
+ }
90
+
91
+ function bindNameInputs(ctx: MoldScalerContext): void {
92
+ ctx.els.ingredientsList?.querySelectorAll('.ms-ing-name').forEach((input) => {
93
+ const inp = input as HTMLInputElement;
94
+ inp.addEventListener('change', (e: Event) => {
95
+ const target = e.target as HTMLInputElement;
96
+ const id = target.closest('.ms-ingredient-row')?.getAttribute('data-id') ?? '';
97
+ const ing = ctx.state.ingredients.find((i: Ingredient) => i.id === id);
98
+ if (ing) ing.name = target.value;
99
+ });
100
+ });
101
+ }
102
+
103
+ function bindWeightInputs(ctx: MoldScalerContext, updateUI: () => void): void {
104
+ ctx.els.ingredientsList?.querySelectorAll('.ms-ing-weight').forEach((input) => {
105
+ const inp = input as HTMLInputElement;
106
+ inp.addEventListener('input', (e: Event) => {
107
+ const target = e.target as HTMLInputElement;
108
+ const id = target.closest('.ms-ingredient-row')?.getAttribute('data-id') ?? '';
109
+ const ing = ctx.state.ingredients.find((i: Ingredient) => i.id === id);
110
+ if (ing) {
111
+ ing.weight = parseFloat(target.value) || 0;
112
+ updateUI();
113
+ }
114
+ });
115
+ });
116
+ }
117
+
118
+ function bindDeleteButtons(ctx: MoldScalerContext, updateUI: () => void): void {
119
+ ctx.els.ingredientsList?.querySelectorAll('.ms-del-btn').forEach((btn) => {
120
+ const button = btn as HTMLButtonElement;
121
+ button.addEventListener('click', (e: Event) => {
122
+ const id = (e.currentTarget as HTMLElement).getAttribute('data-id') ?? '';
123
+ ctx.state.ingredients = ctx.state.ingredients.filter((i: Ingredient) => i.id !== id);
124
+ renderIngredients(ctx, updateUI);
125
+ });
126
+ });
127
+ }
128
+
129
+ function bindIngredientEvents(ctx: MoldScalerContext, updateUI: () => void): void {
130
+ bindNameInputs(ctx);
131
+ bindWeightInputs(ctx, updateUI);
132
+ bindDeleteButtons(ctx, updateUI);
133
+ }
134
+
135
+ function renderIngredients(ctx: MoldScalerContext, updateUI: () => void): void {
136
+ if (!ctx.els.ingredientsList) return;
137
+
138
+ if (ctx.state.ingredients.length === 0) {
139
+ ctx.els.ingredientsList.innerHTML = `<div class="ms-empty-state">${ctx.ui.addIngredient}</div>`;
140
+ return;
141
+ }
142
+
143
+ ctx.els.ingredientsList.innerHTML = ctx.state.ingredients.map((ing: Ingredient) => buildIngredientRow(ing, ctx.state.factor, ctx.ui)).join('');
144
+ bindIngredientEvents(ctx, updateUI);
145
+ }
146
+
147
+ function updateResultText(ctx: MoldScalerContext): void {
148
+ if (!ctx.els.resultText) return;
149
+ if (ctx.state.factor === 1) {
150
+ ctx.els.resultText.innerHTML = ctx.ui.equivalentMolds;
151
+ } else if (ctx.state.factor < 1) {
152
+ ctx.els.resultText.innerHTML = `${ctx.ui.smallerMold} <strong>${ctx.state.factor}</strong>.`;
153
+ } else {
154
+ ctx.els.resultText.innerHTML = `${ctx.ui.largerMold} <strong>${ctx.state.factor}</strong>.`;
155
+ }
156
+ }
157
+
158
+ function updateUI(ctx: MoldScalerContext): void {
159
+ ctx.state.factor = MoldLogic.calculateFactor(ctx.state.original, ctx.state.target);
160
+
161
+ if (ctx.els.resultFactor) {
162
+ ctx.els.resultFactor.textContent = `x${ctx.state.factor.toFixed(2)}`;
163
+ }
164
+
165
+ updateResultText(ctx);
166
+
167
+ if (ctx.els.shapeOriginal) {
168
+ ctx.els.shapeOriginal.setAttribute('d', MoldLogic.getPath(ctx.state.original));
169
+ }
170
+ if (ctx.els.shapeTarget) {
171
+ ctx.els.shapeTarget.setAttribute('d', MoldLogic.getPath(ctx.state.target));
172
+ }
173
+
174
+ const finalElements = document.querySelectorAll('.ms-ingredient-final');
175
+ ctx.state.ingredients.forEach((ing: Ingredient, index: number) => {
176
+ const el = finalElements[index];
177
+ if (el) {
178
+ const final = Math.round(ing.weight * ctx.state.factor);
179
+ el.textContent = ing.weight > 0 ? `${final}g` : '-';
180
+ }
181
+ });
182
+ }
183
+
184
+ function bindShapeButtons(ctx: MoldScalerContext, updateFn: () => void): void {
185
+ document.querySelectorAll('.ms-shape-btn').forEach((btn) => {
186
+ btn.addEventListener('click', (e) => {
187
+ const b = e.currentTarget as HTMLElement;
188
+ const target = b.dataset.target as 'original' | 'target' | undefined;
189
+ const shape = b.dataset.shape as Shape | undefined;
190
+
191
+ if ((target === 'original' || target === 'target') && shape) {
192
+ ctx.state[target].shape = shape;
193
+ document.querySelectorAll(`.ms-shape-btn[data-target="${target}"]`).forEach((btn) => {
194
+ btn.classList.toggle('active', btn === b);
195
+ });
196
+ renderInput(ctx, target);
197
+ updateFn();
198
+ }
199
+ });
200
+ });
201
+ }
202
+
203
+ function bindDimensionInputs(ctx: MoldScalerContext, updateFn: () => void): void {
204
+ [ctx.els.originalInputs, ctx.els.targetInputs].forEach((container) => {
205
+ container?.addEventListener('input', (e: Event) => {
206
+ const input = e.target as HTMLInputElement;
207
+ const type = input.dataset.type as 'original' | 'target' | undefined;
208
+ const key = input.dataset.key as 'dim1' | 'dim2' | undefined;
209
+
210
+ if ((type === 'original' || type === 'target') && (key === 'dim1' || key === 'dim2')) {
211
+ ctx.state[type][key] = parseFloat(input.value) || 0;
212
+ updateFn();
213
+ }
214
+ });
215
+ });
216
+ }
217
+
218
+ function bindAddButton(ctx: MoldScalerContext): void {
219
+ ctx.els.addBtn?.addEventListener('click', () => {
220
+ ctx.state.ingredients.push({
221
+ id: Math.random().toString(36).slice(2, 11),
222
+ name: '',
223
+ weight: 0,
224
+ });
225
+ renderIngredients(ctx, () => updateUI(ctx));
226
+ });
227
+ }
228
+
229
+ export function initMoldScaler(ui: Record<string, string>): void {
230
+ const state = {
231
+ original: { shape: 'round' as Shape, dim1: 20, dim2: 20 },
232
+ target: { shape: 'round' as Shape, dim1: 20, dim2: 20 },
233
+ ingredients: [
234
+ { id: '1', name: ui.defaultIngredient1, weight: 0 },
235
+ { id: '2', name: ui.defaultIngredient2, weight: 0 },
236
+ ] as Ingredient[],
237
+ factor: 1,
238
+ };
239
+
240
+ const els = {
241
+ originalInputs: document.getElementById('original-inputs') as HTMLElement | null,
242
+ targetInputs: document.getElementById('target-inputs') as HTMLElement | null,
243
+ resultFactor: document.getElementById('result-factor') as HTMLElement | null,
244
+ resultText: document.getElementById('result-text') as HTMLElement | null,
245
+ shapeOriginal: document.getElementById('shape-original') as SVGPathElement | null,
246
+ shapeTarget: document.getElementById('shape-target') as SVGPathElement | null,
247
+ ingredientsList: document.getElementById('ingredients-list') as HTMLElement | null,
248
+ addBtn: document.getElementById('add-ingredient-btn') as HTMLElement | null,
249
+ };
250
+
251
+ const ctx: MoldScalerContext = { state, els, ui };
252
+ const updateFn = () => updateUI(ctx);
253
+
254
+ renderInput(ctx, 'original');
255
+ renderInput(ctx, 'target');
256
+ renderIngredients(ctx, updateFn);
257
+ bindShapeButtons(ctx, updateFn);
258
+ bindDimensionInputs(ctx, updateFn);
259
+ bindAddButton(ctx);
260
+ updateFn();
261
+ }
262
+
263
+
264
+
@@ -0,0 +1,8 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { content } from './i18n/es';
4
+
5
+ const locale = 'es';
6
+ ---
7
+
8
+ <SEORenderer content={{ sections: content.seo, locale }} />