added component for MacroBadge,

used MacroBadge.vue in Day Meal, Meal View and Ingrediens View
This commit is contained in:
2026-02-14 13:04:37 +01:00
parent 2b237d3d71
commit ee144847bd
12 changed files with 256 additions and 46 deletions

View File

@ -46,3 +46,57 @@ na zobrazeni app/ingredients sprav aby formular pre pridanie novej suroviny sa z
----- 2026-02-14 08:08:33 -----------------------------------------------------
dopln jemnejší UX flow (toasty, confirm modaly pri delete, loading/error states per card)
----- 2026-02-14 12:44:33 -----------------------------------------------------
v zozname surovin app/ingredients sa zobrazuju len bielkoviny, sacharidy a tuky, pridaj tam aj vlakninu,
zaroven to chcem farebne rozlisit, urob to ako farebny stitok, ze bielkoviny budu modre, sacharidy oranzove, tuky cervene (mierne tlmena) a vlaknina tmavsia zelena
podobne farebne to rozlis aj na denndom prehlade app/today v sucte dna a tiez tam pridaj vlakninu
v zozname jedalnickov app/meals je pri kazdom len pocet kalorii, pridaj tam tiez sumar makrozivin (bielkoviny, sacharidy, tuky a vlaknina)
GPT upravil takto:
Rozšír UI aplikácie Nutrio nasledovne:
1) Ingredients list (route: /app/ingredients)
- Aktuálne sa zobrazujú len bielkoviny, sacharidy a tuky.
- Pridaj zobrazenie vlákniny (fiber_g_100).
- Makrá zobraz ako malé farebné štítky (badge), nie ako obyčajný text.
Farby štítkov:
- Bielkoviny (protein) → modrá #3B82F6
- Sacharidy (carbs) → oranžová #F59E0B
- Tuky (fat) → tlmená červená #EF4444
- Vláknina (fiber) → tmavšia zelená #10B981
Štýl badge:
- malé zaoblené pill tvary
- jemné svetlé pozadie (napr. 1015% opacity farby)
- text farba plná farba
- formát: B 12g / S 24g / T 5g / V 6g
2) Today page (route: /app/today)
- V dennom súčte pridaj vlákninu.
- Makrá zobraz rovnakým farebným štýlom ako v ingredients.
- Kalórie nech ostanú neutrálne (tmavá šedá), vizuálne dominantné veľkosťou písma, nie farbou.
Layout:
- hore veľké číslo kcal
- pod tým horizontálne makrá ako farebné štítky
3) Meals list (route: /app/meals)
- Aktuálne sa zobrazuje len počet kalórií.
- Pridaj aj súhrn makier (protein, carbs, fat, fiber).
- Zobraz ich rovnakým badge systémom pre konzistentnosť.
- Kcal nech je oddelené (napr. nad makrami alebo výraznejšie písmo).
4) Konzistentnosť:
- Použi jeden spoločný komponent napr. <MacroBadge />
- Nepoužívaj sýte plné farebné pozadia.
- Použi minimalistický SaaS štýl.
- Farby makier musia byť identické naprieč celou aplikáciou.
- Nepridávaj nové knižnice.
Výsledok:
Čistý, konzistentný, moderný vzhľad bez prehnaných farieb.

View File

@ -13,6 +13,10 @@
--color-success-bg: #e7f4d8;
--color-error-bg: #f6dde0;
--color-error: #7d2430;
--macro-protein: #3B82F6;
--macro-carbs: #F59E0B;
--macro-fat: #EF4444;
--macro-fiber: #10B981;
--radius-md: 0.875rem;
--radius-lg: 1.25rem;
--space-xs: 0.5rem;
@ -535,6 +539,20 @@ select {
font-size: var(--fs-sm);
}
.list-row__meta--nutrition {
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.42rem;
}
.list-row__kcal {
font-size: 1rem;
font-weight: 700;
color: color-mix(in srgb, var(--color-text) 88%, var(--color-muted));
}
.list-row__actions {
display: flex;
gap: var(--space-xs);
@ -556,20 +574,89 @@ select {
color: var(--color-muted);
}
.totals-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--space-sm);
.day-meal-card__summary--nutrition {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.totals-grid span {
.day-meal-card__kcal {
font-size: var(--fs-sm);
font-weight: 700;
color: color-mix(in srgb, var(--color-text) 88%, var(--color-muted));
}
.totals-card__kcal {
display: flex;
flex-direction: column;
margin-bottom: var(--space-sm);
}
.totals-card__kcal-label {
display: block;
color: var(--color-muted);
font-size: var(--fs-sm);
}
.totals-grid strong {
font-size: 1.1rem;
.totals-card__kcal-value {
font-size: clamp(2rem, 5vw, 2.75rem);
line-height: 1.05;
letter-spacing: -0.015em;
color: color-mix(in srgb, var(--color-text) 90%, var(--color-muted));
}
.macro-badge-group {
display: flex;
flex-wrap: wrap;
gap: 0.42rem;
}
.macro-badge-group--with-top-gap {
margin-top: 0.45rem;
}
.macro-badge-group--compact {
gap: 0.32rem;
}
.macro-badge-group--right {
justify-content: flex-end;
}
.macro-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.24rem 0.56rem;
border: 1px solid transparent;
font-size: 0.76rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
}
.macro-badge--protein {
color: var(--macro-protein);
border-color: color-mix(in srgb, var(--macro-protein) 30%, var(--color-border));
background: color-mix(in srgb, var(--macro-protein) 13%, var(--color-surface));
}
.macro-badge--carbs {
color: var(--macro-carbs);
border-color: color-mix(in srgb, var(--macro-carbs) 30%, var(--color-border));
background: color-mix(in srgb, var(--macro-carbs) 13%, var(--color-surface));
}
.macro-badge--fat {
color: var(--macro-fat);
border-color: color-mix(in srgb, var(--macro-fat) 30%, var(--color-border));
background: color-mix(in srgb, var(--macro-fat) 13%, var(--color-surface));
}
.macro-badge--fiber {
color: var(--macro-fiber);
border-color: color-mix(in srgb, var(--macro-fiber) 30%, var(--color-border));
background: color-mix(in srgb, var(--macro-fiber) 13%, var(--color-surface));
}
.meal-items-editor h3,
@ -747,10 +834,6 @@ select {
grid-template-columns: 1fr;
}
.totals-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-two {
grid-template-columns: 1fr;
}

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
export type MacroType = 'protein' | 'carbs' | 'fat' | 'fiber'
const props = defineProps<{
macro: MacroType
label: string
grams: number
}>()
const formattedGrams = computed(() => {
if (!Number.isFinite(props.grams)) {
return '0'
}
return props.grams.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})
})
</script>
<template>
<span :class="['macro-badge', `macro-badge--${macro}`]">
{{ label }} {{ formattedGrams }}g
</span>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import MacroBadge from '@/components/common/MacroBadge.vue'
import type { Meal, MealType } from '@/types/domain'
const props = defineProps<{
@ -40,12 +41,15 @@ const selectedValue = computed({
{{ option.label }}
</option>
</select>
<p class="day-meal-card__summary" v-if="meal?.totals">
{{ meal.totals.kcal }} {{ t('common.kcalUnit') }}
· {{ t('nutrition.short.protein') }} {{ meal.totals.protein_g }} g
· {{ t('nutrition.short.carbs') }} {{ meal.totals.carbs_g }} g
· {{ t('nutrition.short.fat') }} {{ meal.totals.fat_g }} g
</p>
<div class="day-meal-card__summary day-meal-card__summary--nutrition" v-if="meal?.totals">
<span class="day-meal-card__kcal">{{ meal.totals.kcal }} {{ t('common.kcalUnit') }}</span>
<div class="macro-badge-group macro-badge-group--compact">
<MacroBadge macro="protein" :label="t('nutrition.short.protein')" :grams="meal.totals.protein_g" />
<MacroBadge macro="carbs" :label="t('nutrition.short.carbs')" :grams="meal.totals.carbs_g" />
<MacroBadge macro="fat" :label="t('nutrition.short.fat')" :grams="meal.totals.fat_g" />
<MacroBadge macro="fiber" :label="t('nutrition.short.fiber')" :grams="meal.totals.fiber_g" />
</div>
</div>
<p class="day-meal-card__summary" v-else>
{{ t('today.noItems') }}
</p>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import MacroBadge from '@/components/common/MacroBadge.vue'
import type { NutritionTotals } from '@/types/domain'
defineProps<{
@ -12,23 +13,15 @@ const { t } = useI18n()
<template>
<section class="card totals-card">
<h3>{{ t('today.dayTotals') }}</h3>
<div class="totals-grid">
<div>
<span>{{ t('common.kcalUnit') }}</span>
<strong>{{ totals.kcal }}</strong>
</div>
<div>
<span>{{ t('nutrition.labels.protein') }}</span>
<strong>{{ totals.protein_g }} g</strong>
</div>
<div>
<span>{{ t('nutrition.labels.carbs') }}</span>
<strong>{{ totals.carbs_g }} g</strong>
</div>
<div>
<span>{{ t('nutrition.labels.fat') }}</span>
<strong>{{ totals.fat_g }} g</strong>
</div>
<div class="totals-card__kcal">
<span class="totals-card__kcal-label">{{ t('common.kcalUnit') }}</span>
<strong class="totals-card__kcal-value">{{ totals.kcal }}</strong>
</div>
<div class="macro-badge-group">
<MacroBadge macro="protein" :label="t('nutrition.short.protein')" :grams="totals.protein_g" />
<MacroBadge macro="carbs" :label="t('nutrition.short.carbs')" :grams="totals.carbs_g" />
<MacroBadge macro="fat" :label="t('nutrition.short.fat')" :grams="totals.fat_g" />
<MacroBadge macro="fiber" :label="t('nutrition.short.fiber')" :grams="totals.fiber_g" />
</div>
</section>
</template>

View File

@ -110,7 +110,8 @@
"short": {
"protein": "B",
"carbs": "S",
"fat": "T"
"fat": "T",
"fiber": "V"
},
"labels": {
"protein": "Bílkoviny",

View File

@ -110,7 +110,8 @@
"short": {
"protein": "E",
"carbs": "K",
"fat": "F"
"fat": "F",
"fiber": "Ba"
},
"labels": {
"protein": "Eiweiß",

View File

@ -110,7 +110,8 @@
"short": {
"protein": "P",
"carbs": "C",
"fat": "F"
"fat": "F",
"fiber": "Fi"
},
"labels": {
"protein": "Protein",

View File

@ -110,7 +110,8 @@
"short": {
"protein": "P",
"carbs": "C",
"fat": "G"
"fat": "G",
"fiber": "Fi"
},
"labels": {
"protein": "Proteínas",

View File

@ -110,7 +110,8 @@
"short": {
"protein": "B",
"carbs": "S",
"fat": "T"
"fat": "T",
"fiber": "V"
},
"labels": {
"protein": "Bielkoviny",

View File

@ -2,6 +2,7 @@
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IngredientForm from '@/components/ingredients/IngredientForm.vue'
import MacroBadge from '@/components/common/MacroBadge.vue'
import { useIngredientsStore } from '@/stores/ingredients'
import { useUIStore } from '@/stores/ui'
import type { Ingredient } from '@/types/domain'
@ -132,11 +133,28 @@ const removeIngredient = async (ingredientId: number) => {
<div class="list-row" v-for="ingredient in ingredientsStore.sortedItems" :key="ingredient.ingredient_id">
<div>
<strong>{{ ingredient.name }}</strong>
<p>
{{ t('nutrition.short.protein') }} {{ ingredient.protein_g_100 }}
· {{ t('nutrition.short.carbs') }} {{ ingredient.carbs_g_100 }}
· {{ t('nutrition.short.fat') }} {{ ingredient.fat_g_100 }}
</p>
<div class="macro-badge-group macro-badge-group--with-top-gap">
<MacroBadge
macro="protein"
:label="t('nutrition.short.protein')"
:grams="ingredient.protein_g_100"
/>
<MacroBadge
macro="carbs"
:label="t('nutrition.short.carbs')"
:grams="ingredient.carbs_g_100"
/>
<MacroBadge
macro="fat"
:label="t('nutrition.short.fat')"
:grams="ingredient.fat_g_100"
/>
<MacroBadge
macro="fiber"
:label="t('nutrition.short.fiber')"
:grams="ingredient.fiber_g_100"
/>
</div>
</div>
<div class="list-row__actions">
<button class="btn" type="button" @click="startEdit(ingredient.ingredient_id)">{{ t('common.edit') }}</button>

View File

@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import MealTypeFilter from '@/components/meals/MealTypeFilter.vue'
import MacroBadge from '@/components/common/MacroBadge.vue'
import { useMealsStore } from '@/stores/meals'
import { useUIStore } from '@/stores/ui'
import type { MealType } from '@/types/domain'
@ -96,7 +97,31 @@ const createMeal = async () => {
<strong>{{ meal.name }}</strong>
<p>{{ t(`mealTypes.${meal.meal_type}`) }}</p>
</div>
<div class="list-row__meta">{{ meal.totals?.kcal ?? 0 }} {{ t('common.kcalUnit') }}</div>
<div class="list-row__meta list-row__meta--nutrition">
<div class="list-row__kcal">{{ meal.totals?.kcal ?? 0 }} {{ t('common.kcalUnit') }}</div>
<div class="macro-badge-group macro-badge-group--right macro-badge-group--compact">
<MacroBadge
macro="protein"
:label="t('nutrition.short.protein')"
:grams="meal.totals?.protein_g ?? 0"
/>
<MacroBadge
macro="carbs"
:label="t('nutrition.short.carbs')"
:grams="meal.totals?.carbs_g ?? 0"
/>
<MacroBadge
macro="fat"
:label="t('nutrition.short.fat')"
:grams="meal.totals?.fat_g ?? 0"
/>
<MacroBadge
macro="fiber"
:label="t('nutrition.short.fiber')"
:grams="meal.totals?.fiber_g ?? 0"
/>
</div>
</div>
</RouterLink>
</div>
<p v-else class="empty-state">{{ t('meals.empty') }}</p>