added implementation frontend by prompt @ 2026-02-14 05:35:18 #CODEX

This commit is contained in:
2026-02-14 07:13:06 +01:00
parent 92086055dc
commit 3010a66d59
32 changed files with 2024 additions and 39 deletions

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import type { Ingredient } from '@/types/domain'
const props = defineProps<{
initial?: Ingredient | null
submitLabel?: string
}>()
const emit = defineEmits<{
(event: 'save', payload: {
name: string
protein_g_100: number
carbs_g_100: number
sugar_g_100: number
fat_g_100: number
fiber_g_100: number
kcal_100: number
}): void
(event: 'cancel'): void
}>()
const form = reactive({
name: '',
protein_g_100: 0,
carbs_g_100: 0,
sugar_g_100: 0,
fat_g_100: 0,
fiber_g_100: 0,
kcal_100: 0,
})
const fillFromInitial = () => {
form.name = props.initial?.name ?? ''
form.protein_g_100 = props.initial?.protein_g_100 ?? 0
form.carbs_g_100 = props.initial?.carbs_g_100 ?? 0
form.sugar_g_100 = props.initial?.sugar_g_100 ?? 0
form.fat_g_100 = props.initial?.fat_g_100 ?? 0
form.fiber_g_100 = props.initial?.fiber_g_100 ?? 0
form.kcal_100 = props.initial?.kcal_100 ?? 0
}
watch(() => props.initial, fillFromInitial, { immediate: true })
const onSubmit = () => {
emit('save', {
name: form.name.trim(),
protein_g_100: Number(form.protein_g_100),
carbs_g_100: Number(form.carbs_g_100),
sugar_g_100: Number(form.sugar_g_100),
fat_g_100: Number(form.fat_g_100),
fiber_g_100: Number(form.fiber_g_100),
kcal_100: Number(form.kcal_100),
})
}
</script>
<template>
<form class="card ingredient-form" @submit.prevent="onSubmit">
<h3>{{ props.initial ? 'Upraviť surovinu' : 'Nová surovina' }}</h3>
<div class="grid-two">
<label>
<span>Názov</span>
<input v-model="form.name" class="input-text" type="text" required />
</label>
<label>
<span>Protein / 100 g</span>
<input v-model.number="form.protein_g_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
<label>
<span>Carbs / 100 g</span>
<input v-model.number="form.carbs_g_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
<label>
<span>Sugar / 100 g</span>
<input v-model.number="form.sugar_g_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
<label>
<span>Fat / 100 g</span>
<input v-model.number="form.fat_g_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
<label>
<span>Fiber / 100 g</span>
<input v-model.number="form.fiber_g_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
<label>
<span>Kcal / 100 g (0 = auto)</span>
<input v-model.number="form.kcal_100" class="input-number" type="number" min="0" step="0.01" required />
</label>
</div>
<div class="form-actions">
<button class="btn btn-primary" type="submit">{{ props.submitLabel ?? 'Uložiť' }}</button>
<button class="btn" type="button" @click="emit('cancel')">Zrušiť</button>
</div>
</form>
</template>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { Ingredient, MealItem } from '@/types/domain'
const props = defineProps<{
items: MealItem[]
ingredients: Ingredient[]
}>()
const emit = defineEmits<{
(event: 'add-item', payload: { ingredient_id: number; grams: number }): void
(event: 'update-item', payload: { meal_item_id: number; ingredient_id: number; grams: number; position: number }): void
(event: 'remove-item', mealItemId: number): void
}>()
const addIngredientId = ref<number | null>(null)
const addGrams = ref<number>(100)
const updateIngredient = (item: MealItem, ingredientId: number) => {
emit('update-item', {
meal_item_id: item.meal_item_id,
ingredient_id: ingredientId,
grams: item.grams,
position: item.position,
})
}
const updateGrams = (item: MealItem, grams: number) => {
emit('update-item', {
meal_item_id: item.meal_item_id,
ingredient_id: item.ingredient_id,
grams,
position: item.position,
})
}
const addItem = () => {
if (!addIngredientId.value || addGrams.value <= 0) {
return
}
emit('add-item', {
ingredient_id: addIngredientId.value,
grams: addGrams.value,
})
addGrams.value = 100
}
</script>
<template>
<section class="card meal-items-editor">
<h3>Položky jedálnička</h3>
<div class="meal-items-editor__add-row">
<select v-model.number="addIngredientId" class="input-select">
<option :value="null">Vyber surovinu</option>
<option v-for="ingredient in props.ingredients" :key="ingredient.ingredient_id" :value="ingredient.ingredient_id">
{{ ingredient.name }}
</option>
</select>
<input v-model.number="addGrams" type="number" min="1" class="input-number" />
<button class="btn btn-primary" type="button" @click="addItem">Pridať</button>
</div>
<table class="table" v-if="props.items.length > 0">
<thead>
<tr>
<th>Surovina</th>
<th>Gramáž</th>
<th>Akcia</th>
</tr>
</thead>
<tbody>
<tr v-for="item in props.items" :key="item.meal_item_id">
<td>
<select
class="input-select"
:value="item.ingredient_id"
@change="updateIngredient(item, Number(($event.target as HTMLSelectElement).value))"
>
<option
v-for="ingredient in props.ingredients"
:key="ingredient.ingredient_id"
:value="ingredient.ingredient_id"
>
{{ ingredient.name }}
</option>
</select>
</td>
<td>
<input
class="input-number"
type="number"
min="1"
:value="item.grams"
@change="updateGrams(item, Number(($event.target as HTMLInputElement).value))"
/>
</td>
<td>
<button class="btn btn-danger" type="button" @click="emit('remove-item', item.meal_item_id)">
Zmazať
</button>
</td>
</tr>
</tbody>
</table>
<p v-else class="empty-state">Tento jedálniček zatiaľ nemá položky.</p>
</section>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { MealType } from '@/types/domain'
const props = defineProps<{
modelValue: MealType | ''
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: MealType | ''): void
}>()
</script>
<template>
<label class="filter-control">
<span>Typ jedla</span>
<select
class="input-select"
:value="props.modelValue"
@change="emit('update:modelValue', ($event.target as HTMLSelectElement).value as MealType | '')"
>
<option value="">Všetky</option>
<option value="breakfast">Raňajky</option>
<option value="lunch">Obed</option>
<option value="dinner">Večera</option>
</select>
</label>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
const tabs = [
{ to: { name: 'today' }, label: 'Dnes' },
{ to: { name: 'meals' }, label: 'Jedlá' },
{ to: { name: 'ingredients' }, label: 'Suroviny' },
{ to: { name: 'stats' }, label: 'Stats' },
{ to: { name: 'settings' }, label: 'Viac' },
]
</script>
<template>
<nav class="app-bottom-tabs">
<RouterLink
v-for="tab in tabs"
:key="tab.label"
:to="tab.to"
class="app-bottom-tabs__link"
>
{{ tab.label }}
</RouterLink>
</nav>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
const navItems = [
{ to: { name: 'today' }, label: 'Dnes' },
{ to: { name: 'meals' }, label: 'Jedálničky' },
{ to: { name: 'ingredients' }, label: 'Suroviny' },
{ to: { name: 'stats' }, label: 'Štatistiky' },
{ to: { name: 'settings' }, label: 'Nastavenia' },
]
</script>
<template>
<aside class="app-sidebar">
<div class="app-sidebar__brand">
<img src="/Nutrio.png" alt="Nutrio" class="app-sidebar__logo" />
<strong>Nutrio</strong>
</div>
<nav class="app-sidebar__nav">
<RouterLink
v-for="item in navItems"
:key="item.label"
:to="item.to"
class="app-nav-link"
>
{{ item.label }}
</RouterLink>
</nav>
</aside>
</template>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const auth = useAuthStore()
const title = computed(() => {
switch (route.name) {
case 'today':
return 'Denný prehľad'
case 'meals':
return 'Jedálničky'
case 'meal-detail':
return 'Detail jedálnička'
case 'ingredients':
return 'Suroviny'
case 'stats':
return 'Štatistiky'
case 'settings':
return 'Nastavenia'
default:
return 'Nutrio'
}
})
</script>
<template>
<header class="app-topbar">
<div>
<h1>{{ title }}</h1>
</div>
<div class="app-topbar__user" v-if="auth.userEmail">
{{ auth.userEmail }}
</div>
</header>
</template>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { Meal, MealType } from '@/types/domain'
const props = defineProps<{
mealType: MealType
label: string
mealOptions: Array<{ value: number; label: string }>
selectedMealId: number | null
meal: Meal | null
}>()
const emit = defineEmits<{
(event: 'select-meal', mealType: MealType, mealId: number | null): void
}>()
const selectedValue = computed({
get: () => (props.selectedMealId === null ? '' : String(props.selectedMealId)),
set: (value: string) => {
if (value.length <= 0) {
emit('select-meal', props.mealType, null)
return
}
emit('select-meal', props.mealType, Number(value))
},
})
</script>
<template>
<section class="card day-meal-card">
<div class="day-meal-card__header">
<h3>{{ label }}</h3>
</div>
<select v-model="selectedValue" class="input-select">
<option value="">Bez jedálnička</option>
<option v-for="option in mealOptions" :key="option.value" :value="String(option.value)">
{{ option.label }}
</option>
</select>
<p class="day-meal-card__summary" v-if="meal?.totals">
{{ meal.totals.kcal }} kcal · B {{ meal.totals.protein_g }} g · S {{ meal.totals.carbs_g }} g · T {{ meal.totals.fat_g }} g
</p>
<p class="day-meal-card__summary" v-else>
Zatiaľ bez položiek.
</p>
</section>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { NutritionTotals } from '@/types/domain'
defineProps<{
totals: NutritionTotals
}>()
</script>
<template>
<section class="card totals-card">
<h3>Súčty dňa</h3>
<div class="totals-grid">
<div>
<span>Kcal</span>
<strong>{{ totals.kcal }}</strong>
</div>
<div>
<span>Bielkoviny</span>
<strong>{{ totals.protein_g }} g</strong>
</div>
<div>
<span>Sacharidy</span>
<strong>{{ totals.carbs_g }} g</strong>
</div>
<div>
<span>Tuky</span>
<strong>{{ totals.fat_g }} g</strong>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,5 @@
<template>
<div class="card" aria-hidden="true">
<p>MealPickerModal placeholder</p>
</div>
</template>