@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,193 @@
1
+ import type { WithContext, SoftwareApplication } from 'schema-dts';
2
+ import type { CocktailBalancerUI, CocktailBalancerLocaleContent } from '../index';
3
+
4
+ const slug = 'equilibre-cocktail';
5
+ const title = 'Équilibreur de Cocktails - La Loi du Sour';
6
+ const description = 'Calculez l\'équilibre parfait entre le sucré et l\'acide pour vos cocktails. Maîtrisez le nombre d\'or de la mixologie.';
7
+
8
+ const ui: CocktailBalancerUI = {
9
+ title: 'Cocktail Balancer',
10
+ presetsBtn: 'Recettes',
11
+ saveBtn: 'Sauvegarder',
12
+ resetBtn: 'Reset',
13
+ emptyStateTitle: 'Votre Plan de Travail est Vide',
14
+ emptyStateDescription: 'Ajoutez des ingrédients pour analyser l\'équilibre de votre cocktail en temps réel.',
15
+ addBtn: 'Ajouter Ingrédient',
16
+ addMoreBtn: 'Ajouter un Autre Ingrédient',
17
+ flavorProfileTitle: 'Profil de Saveur',
18
+ volLabel: 'Volume',
19
+ sugarLabel: 'Sucre',
20
+ colorLabel: 'Couleur',
21
+ sourLawTitle: 'Loi du Sour',
22
+ acidDryLabel: 'Acide (Dry)',
23
+ balanceLabel: 'Équilibre',
24
+ sweetLabel: 'Sucré (Sweet)',
25
+ aiSuggestionTitle: 'Suggestion de l\'IA',
26
+ addIngredientTitle: 'Ajouter Ingrédient',
27
+ searchPlaceholder: 'Chercher rhum, citron, sirop...',
28
+ presetsTitle: 'Recettes & Presets',
29
+ savedSectionTitle: 'Mes Sauvegardes',
30
+ classicsSectionTitle: 'Classiques',
31
+ confirmDeleteTitle: 'Tout effacer ?',
32
+ confirmDeleteText: 'Cette action supprimera tous les ingrédients de votre plan de travail. Elle est irréversible.',
33
+ cancelBtn: 'Annuler',
34
+ deleteBtn: 'Effacer',
35
+ verdictSpiritSeco: 'Spiritueux / Sec',
36
+ verdictSoloDulce: 'Seulement Sucré (Old Fashioned)',
37
+ verdictMuyAcido: 'Trop Acide / Bone Dry',
38
+ verdictAcido: 'Acide / Tart',
39
+ verdictEquilibrado: 'Équilibré (Sour)',
40
+ verdictDulce: 'Sucré / Commercial',
41
+ verdictEmpalagoso: 'Écoeurant',
42
+ fixAddBitters: 'Manque d\'Amertume',
43
+ fixAddSugar: 'Trop Acide',
44
+ fixAddAcid: 'Trop Sucré'
45
+ };
46
+
47
+ const faqTitle = 'Foire Aux Questions';
48
+ const bibliographyTitle = 'Bibliographie & Sources';
49
+
50
+ const faq: CocktailBalancerLocaleContent['faq'] = [
51
+ {
52
+ question: "Qu'est-ce que la 'Loi du Sour' ?",
53
+ answer: "C'est le nombre d'or de la mixologie qui équilibre trois éléments : la base forte (spiritueux), l'acide (agrumes) et le sucré (sirops). Une recette classique suit généralement le ratio 2:1:1 (Fort:Acide:Sucré), bien que cela varie selon la force et la densité.",
54
+ },
55
+ {
56
+ question: "Comment la dilution affecte-t-elle l'équilibre du cocktail ?",
57
+ answer: "La glace ne fait pas que refroidir ; elle ajoute de l'eau (dilution) qui ouvre les arômes du spiritueux et adoucit les pics d'acidité et de douceur. Un cocktail équilibré dans le shaker peut se déséquilibrer s'il reste trop longtemps avec de la glace dans le verre.",
58
+ },
59
+ {
60
+ question: "Pourquoi mes cocktails maison n'ont-ils pas le même goût qu'au bar ?",
61
+ answer: "C'est généralement dû au manque d'équilibre entre le sucre et le pH de l'agrume. Les citrons varient en acidité selon la saison. Notre calculateur vous aide à ajuster la quantité exacte de sirop en fonction du volume de jus utilisé.",
62
+ },
63
+ ];
64
+
65
+ const howTo: CocktailBalancerLocaleContent['howTo'] = [
66
+ {
67
+ name: "Sélectionner la base alcoolisée",
68
+ text: "Choisissez le spiritueux principal (Gin, Rhum, Whisky) dans notre base de données pour connaître son apport en corps et en alcool.",
69
+ },
70
+ {
71
+ name: "Entrer l'agent acide",
72
+ text: "Ajoutez le volume de jus de citron, citron vert ou pamplemousse. Le calculateur analysera l'impact du pH sur le mélange.",
73
+ },
74
+ {
75
+ name: "Ajuster le composant sucré",
76
+ text: "Entrez le type de sirop (simple, 2:1, agave) et observez l'indicateur d'équilibre bouger en temps réel.",
77
+ },
78
+ ];
79
+
80
+ const bibliography: CocktailBalancerLocaleContent['bibliography'] = [
81
+ {
82
+ name: "Liquid Intelligence: The Art and Science of the Perfect Cocktail",
83
+ url: "http://www.cookingissues.com/index.html%3Fp=4587.html",
84
+ },
85
+ {
86
+ name: "The Bar Book: Elements of Cocktail Technique - Jeffrey Morgenthaler",
87
+ url: "https://jeffreymorgenthaler.com/the-bar-book/",
88
+ },
89
+ {
90
+ name: "Cocktail Balance - Difford's Guide",
91
+ url: "https://www.diffordsguide.com/encyclopedia/1066/cocktails/cocktail-balance",
92
+ },
93
+ ];
94
+
95
+ const seo: CocktailBalancerLocaleContent['seo'] = [
96
+ {
97
+ type: 'title',
98
+ text: 'Ingénierie Moléculaire & Équilibre Liquide',
99
+ level: 2
100
+ },
101
+ {
102
+ type: 'paragraph',
103
+ html: 'Bienvenue dans le laboratoire numérique où l\'intuition rencontre les mathématiques. Cet outil n\'est pas un simple livre de recettes ; c\'est un <strong>simulateur physico-chimique avancé</strong> conçu pour déconstruire et analyser la structure moléculaire de vos cocktails en temps réel. Chaque goutte de citrus, chaque mesure de spiritueux, chaque gramme de sirop interagit selon des lois chimiques immuables qui déterminent si votre boisson sera un chef-d\'œuvre ou un échec décevant.'
104
+ },
105
+ {
106
+ type: 'stats',
107
+ items: [
108
+ { label: 'Citron Vert', value: '~6% Acide', icon: 'mdi:fruit-citrus' },
109
+ { label: 'Citron', value: '~6% Acide', icon: 'mdi:fruit-citrus' },
110
+ { label: 'Pamplemousse', value: '~1-2% Acide', icon: 'mdi:fruit-citrus' }
111
+ ],
112
+ columns: 3
113
+ },
114
+ {
115
+ type: 'card',
116
+ title: 'La Science de l\'Acide',
117
+ icon: 'mdi:fruit-citrus',
118
+ html: 'L\'acidité n\'est pas seulement une saveur ; c\'est la colonne vertébrale structurelle de tout cocktail équilibré. Sans la bonne acidité, une boisson devient plate, unidimensionnelle et oubliable. Notre algorithme distingue l\'acidité titrable d\'un citron vert persan par rapport à un citron Eureka, tenant compte des variations saisonnières du pH des agrumes qui peuvent décaler votre recette d\'un point entier d\'équilibre sur le palais.'
119
+ },
120
+ {
121
+ type: 'card',
122
+ title: 'Contrôle du Brix (Douceur)',
123
+ icon: 'mdi:spoon-sugar',
124
+ html: 'Le corps et la texture de votre cocktail dépendent entièrement du sucre dissous. Un Sirop Simple (1:1) se comporte très différemment d\'un Riche Sirop (2:1), du miel ou du nectar d\'agave. Chaque édulcorant a un degré Brix et une viscosité différents qui affectent la façon dont la boisson enrobe la langue. Notre calculateur calcule les grammes exacts de sucre dissous pour prédire la sensation en bouche finale.'
125
+ },
126
+ {
127
+ type: 'card',
128
+ title: 'Thermodynamique et Dilution',
129
+ icon: 'mdi:water-percent',
130
+ html: 'Un cocktail agité se dilue de 25 à 40 % selon la température de la glace, la technique et la durée. Cet ajout d\'eau n\'est pas un défaut ; c\'est un ingrédient essentiel qui ouvre les arômes et adoucit la sensation alcoolisée. Notre calculateur estime l\'ABV Final après dilution, permettant de concevoir des boissons avec la force et l\'équilibre voulus.'
131
+ },
132
+ {
133
+ type: 'title',
134
+ text: 'Au-delà du Ratio de Base',
135
+ level: 2
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: 'De nombreux bartenders apprennent la règle classique 2:1:1 (2 parts de spiritueux, 1 acide, 1 sucré) et la traitent comme une vérité universelle. Cependant, <strong>la chimie est bien plus nuancée</strong>. Un citron de Sicile contient une acidité différente d\'un citron vert du Mexique. Un triple sec comme le Cointreau se comporte radicalement différemment d\'un Curaçao bleu. La même recette peut être parfaitement équilibrée une semaine et brutalement acide la suivante, simplement en raison des variations saisonnières des fruits.'
140
+ },
141
+ {
142
+ type: 'paragraph',
143
+ html: 'Cet équilibreur brise ces barrières simplistes. En entrant vos ingrédients spécifiques, vous consultez une base de données vivante qui ajuste dynamiquement les vecteurs d\'acidité et de douceur pour offrir une carte sensorielle précise de votre création. Arrêtez de deviner et commencez à élaborer vos cocktails avec la même rigueur scientifique que les bartenders des établissements de classe mondiale.'
144
+ },
145
+ {
146
+ type: 'summary',
147
+ title: 'À qui s\'adresse cet outil ?',
148
+ items: [
149
+ 'Bartenders Professionnels : Standardisez les recettes et créez des menus signature avec une cohérence reproductible.',
150
+ 'Amateurs à la Maison : Arrêtez de deviner et comprenez pourquoi vos cocktails réussissent ou échouent.',
151
+ 'Développeurs de Boissons : Prototypez rapidement de nouveaux concepts de saveurs avant les productions coûteuses.'
152
+ ]
153
+ },
154
+ {
155
+ type: 'diagnostic',
156
+ title: 'La Zone Dorée',
157
+ icon: 'mdi:star',
158
+ variant: 'success',
159
+ badge: 'Objectif',
160
+ html: 'C\'est l\'objectif ultime : un pH contrôlé où le sucre neutralise l\'agression de l\'acide sans masquer les huiles essentielles et les composés aromatiques du spiritueux de base. C\'est là que vivent les classiques immortels — le Daiquiri, la Margarita, le Sidecar — des boissons qui ont survécu des décennies parce qu\'elles obéissent aux lois fondamentales de la chimie des saveurs.'
161
+ },
162
+ {
163
+ type: 'tip',
164
+ title: 'Conseil Expert : Toujours Utiliser des Agrumes Frais',
165
+ html: 'Pressez toujours les agrumes au dernier moment. Le jus de citron et de citron vert s\'oxyde rapidement, perdant son acidité vive en 20 à 30 minutes après l\'extraction. Un cocktail réalisé avec un jus vraiment frais aura toujours une brillance et une vivacité en bouche qu\'aucun produit en bouteille ne peut reproduire.'
166
+ }
167
+ ];
168
+
169
+ const schemas: CocktailBalancerLocaleContent['schemas'] = [
170
+ {
171
+ '@context': 'https://schema.org',
172
+ '@type': 'SoftwareApplication',
173
+ name: title,
174
+ description: description,
175
+ applicationCategory: 'UtilityApplication',
176
+ operatingSystem: 'Web',
177
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
178
+ } as WithContext<SoftwareApplication>,
179
+ ];
180
+
181
+ export const content: CocktailBalancerLocaleContent = {
182
+ slug,
183
+ title,
184
+ description,
185
+ ui,
186
+ seo,
187
+ faqTitle,
188
+ faq,
189
+ bibliographyTitle,
190
+ bibliography,
191
+ howTo,
192
+ schemas,
193
+ };
@@ -0,0 +1,68 @@
1
+ import type { AlcoholToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import CocktailBalancerCalculator from './component.astro';
3
+ import CocktailBalancerSEO from './seo.astro';
4
+ import CocktailBalancerBibliography from './bibliography.astro';
5
+
6
+ export interface CocktailBalancerUI {
7
+ [key: string]: string;
8
+ title: string;
9
+ presetsBtn: string;
10
+ saveBtn: string;
11
+ resetBtn: string;
12
+ emptyStateTitle: string;
13
+ emptyStateDescription: string;
14
+ addBtn: string;
15
+ addMoreBtn: string;
16
+ flavorProfileTitle: string;
17
+ volLabel: string;
18
+ sugarLabel: string;
19
+ colorLabel: string;
20
+ sourLawTitle: string;
21
+ acidDryLabel: string;
22
+ balanceLabel: string;
23
+ sweetLabel: string;
24
+ aiSuggestionTitle: string;
25
+ addIngredientTitle: string;
26
+ searchPlaceholder: string;
27
+ presetsTitle: string;
28
+ savedSectionTitle: string;
29
+ classicsSectionTitle: string;
30
+ confirmDeleteTitle: string;
31
+ confirmDeleteText: string;
32
+ cancelBtn: string;
33
+ deleteBtn: string;
34
+ verdictSpiritSeco: string;
35
+ verdictSoloDulce: string;
36
+ verdictMuyAcido: string;
37
+ verdictAcido: string;
38
+ verdictEquilibrado: string;
39
+ verdictDulce: string;
40
+ verdictEmpalagoso: string;
41
+ fixAddBitters: string;
42
+ fixAddSugar: string;
43
+ fixAddAcid: string;
44
+ }
45
+
46
+ export type CocktailBalancerLocaleContent = ToolLocaleContent<CocktailBalancerUI>;
47
+
48
+ export const cocktailBalancer: AlcoholToolEntry<CocktailBalancerUI> = {
49
+ id: 'cocktail-balancer',
50
+ icons: {
51
+ bg: 'mdi:glass-cocktail',
52
+ fg: 'mdi:fruit-citrus',
53
+ },
54
+ i18n: {
55
+ es: () => import('./i18n/es').then((m) => m.content),
56
+ en: () => import('./i18n/en').then((m) => m.content),
57
+ fr: () => import('./i18n/fr').then((m) => m.content),
58
+ },
59
+ };
60
+
61
+ export { CocktailBalancerCalculator, CocktailBalancerSEO, CocktailBalancerBibliography };
62
+
63
+ export const COCKTAIL_BALANCER_TOOL: ToolDefinition = {
64
+ entry: cocktailBalancer as AlcoholToolEntry<Record<string, string>>,
65
+ Component: CocktailBalancerCalculator,
66
+ SEOComponent: CocktailBalancerSEO,
67
+ BibliographyComponent: CocktailBalancerBibliography,
68
+ };
@@ -0,0 +1,118 @@
1
+ import type { CocktailComponent, CocktailStats } from './domain/Ingredient';
2
+
3
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
4
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
5
+ const fullHex = hex.replace(shorthandRegex, (_m, r: string, g: string, b: string) => r + r + g + g + b + b);
6
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
7
+
8
+ return result
9
+ ? {
10
+ r: parseInt(result[1] ?? '', 16),
11
+ g: parseInt(result[2] ?? '', 16),
12
+ b: parseInt(result[3] ?? '', 16),
13
+ }
14
+ : { r: 255, g: 255, b: 255 };
15
+ }
16
+
17
+ function rgbToHex(r: number, g: number, b: number): string {
18
+ return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
19
+ }
20
+
21
+ function computeBalanceRatio(sugarGrams: number, acidGrams: number): number {
22
+ if (acidGrams > 0) return sugarGrams / acidGrams;
23
+ if (sugarGrams > 0) return 999;
24
+ return 0;
25
+ }
26
+
27
+ function accumulateComponents(components: CocktailComponent[]) {
28
+ let totalVol = 0, totalAlcoholVol = 0, totalSugarGrams = 0, totalAcidGrams = 0;
29
+ let weightedBitterness = 0, weightedComplexity = 0;
30
+ let weightedR = 0, weightedG = 0, weightedB = 0;
31
+
32
+ components.forEach((comp) => {
33
+ if (comp.volumeMl <= 0) return;
34
+ const vol = comp.volumeMl;
35
+ totalVol += vol;
36
+ totalAlcoholVol += vol * (comp.ingredient.abv / 100);
37
+ totalSugarGrams += vol * (comp.ingredient.sugar / 100);
38
+ totalAcidGrams += vol * (comp.ingredient.acid / 100);
39
+ weightedBitterness += vol * (comp.ingredient.bitterness || 0);
40
+ weightedComplexity += vol * (comp.ingredient.complexity || 0);
41
+ const rgb = hexToRgb(comp.ingredient.color || '#ffffff');
42
+ weightedR += vol * rgb.r;
43
+ weightedG += vol * rgb.g;
44
+ weightedB += vol * rgb.b;
45
+ });
46
+
47
+ return { totalVol, totalAlcoholVol, totalSugarGrams, totalAcidGrams, weightedBitterness, weightedComplexity, weightedR, weightedG, weightedB };
48
+ }
49
+
50
+ function emptyStats(): CocktailStats {
51
+ return {
52
+ totalVolumeMl: 0, finalAbv: 0, totalSugarGrams: 0, totalAcidGrams: 0,
53
+ sugarConcentration: 0, acidConcentration: 0, balanceRatio: 0,
54
+ bitternessIndex: 0, complexityIndex: 0, finalColor: '#ffffff',
55
+ };
56
+ }
57
+
58
+ export function calculateCocktail(components: CocktailComponent[]): CocktailStats {
59
+ const acc = accumulateComponents(components);
60
+ if (acc.totalVol === 0) return emptyStats();
61
+
62
+ const v = acc.totalVol;
63
+ return {
64
+ totalVolumeMl: v,
65
+ finalAbv: (acc.totalAlcoholVol / v) * 100,
66
+ totalSugarGrams: acc.totalSugarGrams,
67
+ totalAcidGrams: acc.totalAcidGrams,
68
+ sugarConcentration: (acc.totalSugarGrams / v) * 100,
69
+ acidConcentration: (acc.totalAcidGrams / v) * 100,
70
+ balanceRatio: computeBalanceRatio(acc.totalSugarGrams, acc.totalAcidGrams),
71
+ bitternessIndex: acc.weightedBitterness / v,
72
+ complexityIndex: acc.weightedComplexity / v,
73
+ finalColor: rgbToHex(Math.round(acc.weightedR / v), Math.round(acc.weightedG / v), Math.round(acc.weightedB / v)),
74
+ };
75
+ }
76
+
77
+ export function getBalanceVerdict(stats: CocktailStats): { labelKey: string; colorClass: string } {
78
+ const r = stats.balanceRatio;
79
+
80
+ if (stats.totalAcidGrams === 0 && stats.totalSugarGrams < 2) {
81
+ return { labelKey: 'verdictSpiritSeco', colorClass: 'text-indigo-400' };
82
+ }
83
+ if (stats.totalAcidGrams === 0) {
84
+ return { labelKey: 'verdictSoloDulce', colorClass: 'text-amber-400' };
85
+ }
86
+ if (r < 4) return { labelKey: 'verdictMuyAcido', colorClass: 'text-red-500' };
87
+ if (r < 6) return { labelKey: 'verdictAcido', colorClass: 'text-lime-400' };
88
+ if (r < 9) return { labelKey: 'verdictEquilibrado', colorClass: 'text-emerald-400' };
89
+ if (r < 12) return { labelKey: 'verdictDulce', colorClass: 'text-yellow-400' };
90
+ return { labelKey: 'verdictEmpalagoso', colorClass: 'text-red-500' };
91
+ }
92
+
93
+ function getSugarFix(stats: CocktailStats): { actionKey: string; amountValue: string } | null {
94
+ const TARGET_RATIO = 7.5;
95
+ const targetSugar = TARGET_RATIO * stats.totalAcidGrams;
96
+ const vol = ((targetSugar - stats.totalSugarGrams) * 100) / 61.5;
97
+ if (vol < 2.5) return null;
98
+ return { actionKey: 'fixAddSugar', amountValue: `+${Math.ceil(vol / 2.5) * 2.5}ml Jarabe` };
99
+ }
100
+
101
+ function getAcidFix(stats: CocktailStats): { actionKey: string; amountValue: string } | null {
102
+ const TARGET_RATIO = 7.5;
103
+ const targetAcid = stats.totalSugarGrams / TARGET_RATIO;
104
+ const vol = ((targetAcid - stats.totalAcidGrams) * 100) / 6.0;
105
+ if (vol < 2.5) return null;
106
+ return { actionKey: 'fixAddAcid', amountValue: `+${Math.ceil(vol / 2.5) * 2.5}ml Lima` };
107
+ }
108
+
109
+ export function getFixSuggestion(stats: CocktailStats): { actionKey: string; amountValue: string } | null {
110
+ if (stats.totalVolumeMl === 0) return null;
111
+ if (stats.totalAcidGrams === 0 && stats.totalSugarGrams < 2) return null;
112
+ if (stats.totalAcidGrams === 0) return { actionKey: 'fixAddBitters', amountValue: '+2 dashes Bitters' };
113
+
114
+ const currentRatio = stats.balanceRatio;
115
+ if (currentRatio >= 6.0 && currentRatio <= 9.0) return null;
116
+ if (currentRatio < 6.0) return getSugarFix(stats);
117
+ return getAcidFix(stats);
118
+ }
@@ -0,0 +1,53 @@
1
+ ---
2
+ import {
3
+ SEOTitle, SEOTable, SEOTip, SEOCard, SEOStats,
4
+ SEOGlossary, SEOProsCons, SEOSummary, SEODiagnostic, SEOArticle
5
+ } from '@jjlmoya/utils-shared';
6
+ import { cocktailBalancer } from './index';
7
+ import type { KnownLocale } from '../../types';
8
+
9
+ interface Props {
10
+ locale?: KnownLocale;
11
+ }
12
+
13
+ const { locale = 'es' } = Astro.props;
14
+ const content = await cocktailBalancer.i18n[locale]?.();
15
+ if (!content) return null;
16
+
17
+ const { seo } = content;
18
+ ---
19
+
20
+ <SEOArticle>
21
+ {seo.map((section: any) => {
22
+ switch (section.type) {
23
+ case 'summary':
24
+ return <SEOSummary title={section.title} items={section.items} />;
25
+ case 'title':
26
+ return <SEOTitle title={section.text} level={section.level || 2} />;
27
+ case 'paragraph':
28
+ return <p set:html={section.html} />;
29
+ case 'stats':
30
+ return <SEOStats stats={section.items} columns={section.columns} />;
31
+ case 'card':
32
+ return <SEOCard title={section.title} icon={section.icon}><Fragment set:html={section.html} /></SEOCard>;
33
+ case 'tip':
34
+ return <SEOTip title={section.title}><Fragment set:html={section.html} /></SEOTip>;
35
+ case 'table':
36
+ return (
37
+ <SEOTable headers={section.headers}>
38
+ {section.rows.map((row: string[]) => (
39
+ <tr>{row.map((cell: string) => <td set:html={cell} />)}</tr>
40
+ ))}
41
+ </SEOTable>
42
+ );
43
+ case 'proscons':
44
+ return <SEOProsCons title={section.title} items={section.items} />;
45
+ case 'diagnostic':
46
+ return <SEODiagnostic title={section.title} icon={section.icon} type={section.variant} badge={section.badge}><Fragment set:html={section.html} /></SEODiagnostic>;
47
+ case 'glossary':
48
+ return <SEOGlossary items={section.items} />;
49
+ default:
50
+ return null;
51
+ }
52
+ })}
53
+ </SEOArticle>