added transalations for UI texts

This commit is contained in:
2026-02-14 07:34:04 +01:00
parent 3010a66d59
commit 9b2f2c4e91
24 changed files with 681 additions and 118 deletions

View File

@ -15,27 +15,39 @@ It describes what the project is, what is already implemented, and what still ne
- `backend/`
- `frontend/`
## Current State (as of 2026-02-13)
## Current State (as of 2026-02-14)
- `README.md` already contains product specification in Slovak and English.
- Backend DB migrations exist in `backend/src/Maintenance.php` up to version `7`.
- Backend API methods are implemented in `backend/src/API.php`.
- Frontend auth page is now implemented:
- Frontend auth page is implemented:
- `frontend/src/App.vue` renders router view.
- `frontend/src/router/index.ts` maps `/` to `frontend/src/views/AuthView.vue`.
- `frontend/src/router/index.ts` maps `/` to `frontend/src/views/AuthView.vue` for guests.
- `AuthView` serves as login + registration entry (single form, email + password).
- Successful login stores `token` and `user_email` in `localStorage`.
- Login now uses `frontend/src/stores/auth.ts` (`Pinia`) and redirects to authenticated app routes.
- `frontend/src/views/AuthView.vue` formatting now uses tab-based indentation.
- Login response parsing in `AuthView` now reads user fields from `response.data.user`.
- Frontend i18n is wired:
- Frontend authenticated area is implemented:
- `frontend/src/views/AppLayout.vue` is shell layout.
- Desktop navigation: `frontend/src/components/navigation/AppSidebar.vue`.
- Mobile navigation: `frontend/src/components/navigation/AppBottomTabs.vue`.
- Top bar/title: `frontend/src/components/navigation/AppTopbar.vue`.
- Pages: `TodayView`, `MealsView`, `MealDetailView`, `IngredientsView`, `StatsView`, `SettingsView`.
- Route guard is active for `/app/*` (requires token), guest-only for `/`.
- Frontend i18n is wired and used across new UI:
- setup in `frontend/src/i18n/index.ts`
- locale files in `frontend/src/locales/{sk,cs,en,es,de}.ts`
- locale files in `frontend/src/locales/{sk,cs,en,es,de}.json`
- language switcher updates locale dynamically.
- new app UI keys are added (`nav`, `pageTitles`, `mealTypes`, `common`, `nutrition`, `today`, `meals`, `ingredients`, `stats`, `settings`).
- Frontend theme system is implemented:
- light/dark mode toggle in auth page
- design tokens in `frontend/src/assets/css/style.css` (`:root` variables).
- App logo is served from `frontend/public/Nutrio.png` (copied from `doc/Nutrio.png`).
- Font Awesome is installed and registered globally in `frontend/src/main.ts`.
- Pinia is installed and configured in `frontend/src/main.ts` via `frontend/src/stores/index.ts`.
- Frontend domain/store structure exists:
- `frontend/src/types/domain.ts`
- `frontend/src/stores/{auth,ingredients,meals,diary}.ts`
- `frontend/src/utils/{nutrition,api,date}.ts`
- `frontend/src/BackendAPI.ts` is generated via `backend/scripts/buildTypeScript.php` and should not be edited manually.
- `backend/data.json` contains sample meal data (not currently wired into DB/API flow).
@ -143,7 +155,7 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
- APIlite response handling detail:
- raw API response is wrapped as `{ status, data }`
- generated `BackendAPI.ts` currently resolves `response.data` in `callPromise` for non-`__HELP__` actions
- frontend parsing must match the actual returned runtime shape
- frontend stores use `frontend/src/utils/api.ts` (`unwrapApiData`) to normalize both envelope and unwrapped runtime shapes
- `frontend/src/BackendAPI.ts` is generated output; regenerate when backend API changes, do not patch manually.
- In vue-i18n locale strings, `@` must be escaped as `{'@'}` to avoid "Invalid linked format" errors.
@ -169,8 +181,9 @@ Frontend:
## Product Behavior Target (what to build next)
- Harden auth (token transport/header strategy, token revoke strategy, brute-force/rate-limits).
- Build remaining frontend screens for ingredients, meals, meal item editor, diary day, diary range.
- Connect frontend to implemented backend actions.
- Polish authenticated frontend UX (validation messages, delete confirmations, optimistic updates, better loading/error states).
- Add diary range screen/workflow on frontend (backend endpoint already exists).
- Add i18n coverage for any future UI additions and keep keys stable.
- Add API tests for validation, ownership checks, and totals calculation consistency.
- Add pagination/filter strategy where list endpoints grow.

View File

@ -37,3 +37,9 @@ Preferencie:
- UI môže byť čisté bez knižnice, alebo minimalisticky (napr. jednoduché CSS rozhodni a drž konzistentne a pokracuj v pouzivani suboru frontend/src/assets/css/style.css).
- Použi slovenské názvy v UI (Raňajky, Obed, Večera), ale kľúče v kóde nech sú anglické (breakfast/lunch/dinner).
Výstup: konkrétny návrh + ukážky kódu, nie všeobecné rady.
----- 2026-02-14 07:16:25 -----------------------------------------------------
dopln kluce pre UI texty pre preklad, pre kluce pouzivaj anglictinu, dopln potom aj vsetky preklady vo frontend/src/locales/*.json
----- 2026-02-14 07:33:30 -----------------------------------------------------
doplniť jemnejší UX flow (toasty, confirm modaly pri delete, loading/error states per card)

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import { computed, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Ingredient } from '@/types/domain'
const props = defineProps<{
@ -20,6 +21,12 @@ const emit = defineEmits<{
(event: 'cancel'): void
}>()
const { t } = useI18n()
const formTitle = computed(() => {
return props.initial ? t('ingredients.editTitle') : t('ingredients.newTitle')
})
const form = reactive({
name: '',
protein_g_100: 0,
@ -57,40 +64,40 @@ const onSubmit = () => {
<template>
<form class="card ingredient-form" @submit.prevent="onSubmit">
<h3>{{ props.initial ? 'Upraviť surovinu' : 'Nová surovina' }}</h3>
<h3>{{ formTitle }}</h3>
<div class="grid-two">
<label>
<span>Názov</span>
<span>{{ t('ingredients.name') }}</span>
<input v-model="form.name" class="input-text" type="text" required />
</label>
<label>
<span>Protein / 100 g</span>
<span>{{ t('ingredients.protein100') }}</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>
<span>{{ t('ingredients.carbs100') }}</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>
<span>{{ t('ingredients.sugar100') }}</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>
<span>{{ t('ingredients.fat100') }}</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>
<span>{{ t('ingredients.fiber100') }}</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>
<span>{{ t('ingredients.kcal100Auto') }}</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>
<button class="btn btn-primary" type="submit">{{ props.submitLabel ?? t('ingredients.submitDefault') }}</button>
<button class="btn" type="button" @click="emit('cancel')">{{ t('common.cancel') }}</button>
</div>
</form>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Ingredient, MealItem } from '@/types/domain'
const props = defineProps<{
@ -13,6 +14,8 @@ const emit = defineEmits<{
(event: 'remove-item', mealItemId: number): void
}>()
const { t } = useI18n()
const addIngredientId = ref<number | null>(null)
const addGrams = ref<number>(100)
@ -48,25 +51,25 @@ const addItem = () => {
<template>
<section class="card meal-items-editor">
<h3>Položky jedálnička</h3>
<h3>{{ t('meals.itemsTitle') }}</h3>
<div class="meal-items-editor__add-row">
<select v-model.number="addIngredientId" class="input-select">
<option :value="null">Vyber surovinu</option>
<option :value="null">{{ t('meals.selectIngredient') }}</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>
<button class="btn btn-primary" type="button" @click="addItem">{{ t('meals.addItem') }}</button>
</div>
<table class="table" v-if="props.items.length > 0">
<thead>
<tr>
<th>Surovina</th>
<th>Gramáž</th>
<th>Akcia</th>
<th>{{ t('ingredients.name') }}</th>
<th>{{ t('meals.grams') }}</th>
<th>{{ t('common.actions') }}</th>
</tr>
</thead>
<tbody>
@ -97,12 +100,12 @@ const addItem = () => {
</td>
<td>
<button class="btn btn-danger" type="button" @click="emit('remove-item', item.meal_item_id)">
Zmazať
{{ t('common.delete') }}
</button>
</td>
</tr>
</tbody>
</table>
<p v-else class="empty-state">Tento jedálniček zatiaľ nemá položky.</p>
<p v-else class="empty-state">{{ t('meals.noItems') }}</p>
</section>
</template>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { MealType } from '@/types/domain'
const props = defineProps<{
@ -8,20 +9,22 @@ const props = defineProps<{
const emit = defineEmits<{
(event: 'update:modelValue', value: MealType | ''): void
}>()
const { t } = useI18n()
</script>
<template>
<label class="filter-control">
<span>Typ jedla</span>
<span>{{ t('meals.mealTypeFilter') }}</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>
<option value="">{{ t('common.all') }}</option>
<option value="breakfast">{{ t('mealTypes.breakfast') }}</option>
<option value="lunch">{{ t('mealTypes.lunch') }}</option>
<option value="dinner">{{ t('mealTypes.dinner') }}</option>
</select>
</label>
</template>

View File

@ -1,11 +1,16 @@
<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' },
]
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const tabs = computed(() => [
{ to: { name: 'today' }, label: t('nav.today') },
{ to: { name: 'meals' }, label: t('nav.meals') },
{ to: { name: 'ingredients' }, label: t('nav.ingredients') },
{ to: { name: 'stats' }, label: t('nav.stats') },
{ to: { name: 'settings' }, label: t('nav.more') },
])
</script>
<template>

View File

@ -1,18 +1,23 @@
<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' },
]
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const navItems = computed(() => [
{ to: { name: 'today' }, label: t('nav.today') },
{ to: { name: 'meals' }, label: t('nav.meals') },
{ to: { name: 'ingredients' }, label: t('nav.ingredients') },
{ to: { name: 'stats' }, label: t('nav.stats') },
{ to: { name: 'settings' }, label: t('nav.settings') },
])
</script>
<template>
<aside class="app-sidebar">
<div class="app-sidebar__brand">
<img src="/Nutrio.png" alt="Nutrio" class="app-sidebar__logo" />
<strong>Nutrio</strong>
<img src="/Nutrio.png" :alt="t('app.name')" class="app-sidebar__logo" />
<strong>{{ t('app.name') }}</strong>
</div>
<nav class="app-sidebar__nav">
<RouterLink

View File

@ -1,27 +1,29 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const auth = useAuthStore()
const { t } = useI18n()
const title = computed(() => {
switch (route.name) {
case 'today':
return 'Denný prehľad'
return t('pageTitles.today')
case 'meals':
return 'Jedálničky'
return t('pageTitles.meals')
case 'meal-detail':
return 'Detail jedálnička'
return t('pageTitles.mealDetail')
case 'ingredients':
return 'Suroviny'
return t('pageTitles.ingredients')
case 'stats':
return 'Štatistiky'
return t('pageTitles.stats')
case 'settings':
return 'Nastavenia'
return t('pageTitles.settings')
default:
return 'Nutrio'
return t('pageTitles.default')
}
})
</script>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Meal, MealType } from '@/types/domain'
const props = defineProps<{
@ -14,6 +15,8 @@ const emit = defineEmits<{
(event: 'select-meal', mealType: MealType, mealId: number | null): void
}>()
const { t } = useI18n()
const selectedValue = computed({
get: () => (props.selectedMealId === null ? '' : String(props.selectedMealId)),
set: (value: string) => {
@ -32,16 +35,19 @@ const selectedValue = computed({
<h3>{{ label }}</h3>
</div>
<select v-model="selectedValue" class="input-select">
<option value="">Bez jedálnička</option>
<option value="">{{ t('today.noMealPlan') }}</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
{{ 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>
<p class="day-meal-card__summary" v-else>
Zatiaľ bez položiek.
{{ t('today.noItems') }}
</p>
</section>
</template>

View File

@ -1,29 +1,32 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { NutritionTotals } from '@/types/domain'
defineProps<{
totals: NutritionTotals
}>()
const { t } = useI18n()
</script>
<template>
<section class="card totals-card">
<h3>Súčty dňa</h3>
<h3>{{ t('today.dayTotals') }}</h3>
<div class="totals-grid">
<div>
<span>Kcal</span>
<span>{{ t('common.kcalUnit') }}</span>
<strong>{{ totals.kcal }}</strong>
</div>
<div>
<span>Bielkoviny</span>
<span>{{ t('nutrition.labels.protein') }}</span>
<strong>{{ totals.protein_g }} g</strong>
</div>
<div>
<span>Sacharidy</span>
<span>{{ t('nutrition.labels.carbs') }}</span>
<strong>{{ totals.carbs_g }} g</strong>
</div>
<div>
<span>Tuky</span>
<span>{{ t('nutrition.labels.fat') }}</span>
<strong>{{ totals.fat_g }} g</strong>
</div>
</div>

View File

@ -1,5 +1,11 @@
<template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<div class="card" aria-hidden="true">
<p>MealPickerModal placeholder</p>
<p>{{ t('today.mealPickerPlaceholder') }}</p>
</div>
</template>

View File

@ -37,5 +37,102 @@
"en": "Angličtina",
"es": "Španělština",
"de": "Němčina"
},
"nav": {
"today": "Dnes",
"meals": "Jídelníčky",
"ingredients": "Suroviny",
"stats": "Statistiky",
"settings": "Nastavení",
"more": "Více"
},
"pageTitles": {
"today": "Denní přehled",
"meals": "Jídelníčky",
"mealDetail": "Detail jídelníčku",
"ingredients": "Suroviny",
"stats": "Statistiky",
"settings": "Nastavení",
"default": "Nutrio"
},
"mealTypes": {
"breakfast": "Snídaně",
"lunch": "Oběd",
"dinner": "Večeře"
},
"common": {
"date": "Datum",
"all": "Všechny",
"none": "Žádné",
"save": "Uložit",
"saving": "Ukládám...",
"cancel": "Zrušit",
"create": "Vytvořit",
"edit": "Upravit",
"delete": "Smazat",
"actions": "Akce",
"loading": "Načítám...",
"kcalUnit": "kcal"
},
"nutrition": {
"short": {
"protein": "B",
"carbs": "S",
"fat": "T"
},
"labels": {
"protein": "Bílkoviny",
"carbs": "Sacharidy",
"fat": "Tuky"
}
},
"today": {
"loadingDay": "Načítám den...",
"noMealPlan": "Bez jídelníčku",
"noItems": "Zatím bez položek.",
"dayTotals": "Součty dne",
"mealPickerPlaceholder": "Výběr jídelníčku bude doplněn."
},
"meals": {
"mealTypeFilter": "Typ jídla",
"namePlaceholder": "Název jídelníčku",
"createButton": "Vytvořit",
"libraryTitle": "Knihovna jídelníčků",
"empty": "Zatím nemáš žádné jídelníčky.",
"loadingMeal": "Načítám jídelníček...",
"nameLabel": "Název",
"mealTypeLabel": "Typ jídla",
"saveChanges": "Uložit změny",
"deleteMeal": "Smazat jídelníček",
"notFound": "Jídelníček neexistuje.",
"itemsTitle": "Položky jídelníčku",
"selectIngredient": "Vyber surovinu",
"addItem": "Přidat",
"noItems": "Tento jídelníček zatím nemá položky.",
"grams": "Gramáž"
},
"ingredients": {
"newTitle": "Nová surovina",
"editTitle": "Upravit surovinu",
"databaseTitle": "Databáze surovin",
"newButton": "Nová surovina",
"empty": "Zatím nemáš uložené suroviny.",
"name": "Název",
"protein100": "Bílkoviny / 100 g",
"carbs100": "Sacharidy / 100 g",
"sugar100": "Cukr / 100 g",
"fat100": "Tuky / 100 g",
"fiber100": "Vláknina / 100 g",
"kcal100Auto": "Kcal / 100 g (0 = auto)",
"submitDefault": "Uložit"
},
"stats": {
"title": "Statistiky",
"placeholder": "Základní přehled bude doplněn v další iteraci."
},
"settings": {
"accountTitle": "Nastavení účtu",
"loggedInAs": "Přihlášený uživatel",
"logout": "Odhlásit se"
}
}

View File

@ -37,5 +37,102 @@
"en": "Englisch",
"es": "Spanisch",
"de": "Deutsch"
},
"nav": {
"today": "Heute",
"meals": "Mahlzeiten",
"ingredients": "Zutaten",
"stats": "Statistik",
"settings": "Einstellungen",
"more": "Mehr"
},
"pageTitles": {
"today": "Tagesübersicht",
"meals": "Mahlzeiten",
"mealDetail": "Mahlzeit-Detail",
"ingredients": "Zutaten",
"stats": "Statistik",
"settings": "Einstellungen",
"default": "Nutrio"
},
"mealTypes": {
"breakfast": "Frühstück",
"lunch": "Mittagessen",
"dinner": "Abendessen"
},
"common": {
"date": "Datum",
"all": "Alle",
"none": "Keine",
"save": "Speichern",
"saving": "Speichert...",
"cancel": "Abbrechen",
"create": "Erstellen",
"edit": "Bearbeiten",
"delete": "Löschen",
"actions": "Aktion",
"loading": "Lädt...",
"kcalUnit": "kcal"
},
"nutrition": {
"short": {
"protein": "E",
"carbs": "K",
"fat": "F"
},
"labels": {
"protein": "Eiweiß",
"carbs": "Kohlenhydrate",
"fat": "Fett"
}
},
"today": {
"loadingDay": "Tag wird geladen...",
"noMealPlan": "Kein Mahlzeitenplan",
"noItems": "Noch keine Einträge.",
"dayTotals": "Tagessummen",
"mealPickerPlaceholder": "Mahlzeiten-Auswahl wird später ergänzt."
},
"meals": {
"mealTypeFilter": "Mahlzeittyp",
"namePlaceholder": "Name des Mahlzeitenplans",
"createButton": "Erstellen",
"libraryTitle": "Mahlzeiten-Bibliothek",
"empty": "Du hast noch keine Mahlzeitenpläne.",
"loadingMeal": "Mahlzeitenplan wird geladen...",
"nameLabel": "Name",
"mealTypeLabel": "Mahlzeittyp",
"saveChanges": "Änderungen speichern",
"deleteMeal": "Mahlzeitenplan löschen",
"notFound": "Mahlzeitenplan existiert nicht.",
"itemsTitle": "Mahlzeiten-Elemente",
"selectIngredient": "Zutat auswählen",
"addItem": "Hinzufügen",
"noItems": "Dieser Mahlzeitenplan hat noch keine Einträge.",
"grams": "Gramm"
},
"ingredients": {
"newTitle": "Neue Zutat",
"editTitle": "Zutat bearbeiten",
"databaseTitle": "Zutaten-Datenbank",
"newButton": "Neue Zutat",
"empty": "Du hast noch keine gespeicherten Zutaten.",
"name": "Name",
"protein100": "Eiweiß / 100 g",
"carbs100": "Kohlenhydrate / 100 g",
"sugar100": "Zucker / 100 g",
"fat100": "Fett / 100 g",
"fiber100": "Ballaststoffe / 100 g",
"kcal100Auto": "Kcal / 100 g (0 = auto)",
"submitDefault": "Speichern"
},
"stats": {
"title": "Statistik",
"placeholder": "Grundübersicht wird in der nächsten Iteration ergänzt."
},
"settings": {
"accountTitle": "Kontoeinstellungen",
"loggedInAs": "Angemeldet als",
"logout": "Abmelden"
}
}

View File

@ -37,5 +37,102 @@
"en": "English",
"es": "Spanish",
"de": "German"
},
"nav": {
"today": "Today",
"meals": "Meals",
"ingredients": "Ingredients",
"stats": "Stats",
"settings": "Settings",
"more": "More"
},
"pageTitles": {
"today": "Daily Overview",
"meals": "Meals",
"mealDetail": "Meal Detail",
"ingredients": "Ingredients",
"stats": "Stats",
"settings": "Settings",
"default": "Nutrio"
},
"mealTypes": {
"breakfast": "Breakfast",
"lunch": "Lunch",
"dinner": "Dinner"
},
"common": {
"date": "Date",
"all": "All",
"none": "None",
"save": "Save",
"saving": "Saving...",
"cancel": "Cancel",
"create": "Create",
"edit": "Edit",
"delete": "Delete",
"actions": "Action",
"loading": "Loading...",
"kcalUnit": "kcal"
},
"nutrition": {
"short": {
"protein": "P",
"carbs": "C",
"fat": "F"
},
"labels": {
"protein": "Protein",
"carbs": "Carbs",
"fat": "Fat"
}
},
"today": {
"loadingDay": "Loading day...",
"noMealPlan": "No meal plan",
"noItems": "No items yet.",
"dayTotals": "Day totals",
"mealPickerPlaceholder": "Meal picker will be added later."
},
"meals": {
"mealTypeFilter": "Meal type",
"namePlaceholder": "Meal plan name",
"createButton": "Create",
"libraryTitle": "Meal library",
"empty": "You do not have any meal plans yet.",
"loadingMeal": "Loading meal plan...",
"nameLabel": "Name",
"mealTypeLabel": "Meal type",
"saveChanges": "Save changes",
"deleteMeal": "Delete meal plan",
"notFound": "Meal plan does not exist.",
"itemsTitle": "Meal items",
"selectIngredient": "Select ingredient",
"addItem": "Add",
"noItems": "This meal plan has no items yet.",
"grams": "Grams"
},
"ingredients": {
"newTitle": "New ingredient",
"editTitle": "Edit ingredient",
"databaseTitle": "Ingredient database",
"newButton": "New ingredient",
"empty": "You do not have any saved ingredients yet.",
"name": "Name",
"protein100": "Protein / 100 g",
"carbs100": "Carbs / 100 g",
"sugar100": "Sugar / 100 g",
"fat100": "Fat / 100 g",
"fiber100": "Fiber / 100 g",
"kcal100Auto": "Kcal / 100 g (0 = auto)",
"submitDefault": "Save"
},
"stats": {
"title": "Stats",
"placeholder": "Basic overview will be added in the next iteration."
},
"settings": {
"accountTitle": "Account settings",
"loggedInAs": "Logged in as",
"logout": "Log out"
}
}

View File

@ -37,5 +37,102 @@
"en": "Inglés",
"es": "Español",
"de": "Alemán"
},
"nav": {
"today": "Hoy",
"meals": "Menús",
"ingredients": "Ingredientes",
"stats": "Estadísticas",
"settings": "Ajustes",
"more": "Más"
},
"pageTitles": {
"today": "Resumen diario",
"meals": "Menús",
"mealDetail": "Detalle del menú",
"ingredients": "Ingredientes",
"stats": "Estadísticas",
"settings": "Ajustes",
"default": "Nutrio"
},
"mealTypes": {
"breakfast": "Desayuno",
"lunch": "Comida",
"dinner": "Cena"
},
"common": {
"date": "Fecha",
"all": "Todos",
"none": "Ninguno",
"save": "Guardar",
"saving": "Guardando...",
"cancel": "Cancelar",
"create": "Crear",
"edit": "Editar",
"delete": "Eliminar",
"actions": "Acción",
"loading": "Cargando...",
"kcalUnit": "kcal"
},
"nutrition": {
"short": {
"protein": "P",
"carbs": "C",
"fat": "G"
},
"labels": {
"protein": "Proteínas",
"carbs": "Carbohidratos",
"fat": "Grasas"
}
},
"today": {
"loadingDay": "Cargando día...",
"noMealPlan": "Sin menú",
"noItems": "Todavía sin elementos.",
"dayTotals": "Totales del día",
"mealPickerPlaceholder": "El selector de menú se añadirá más tarde."
},
"meals": {
"mealTypeFilter": "Tipo de comida",
"namePlaceholder": "Nombre del menú",
"createButton": "Crear",
"libraryTitle": "Biblioteca de menús",
"empty": "Aún no tienes menús.",
"loadingMeal": "Cargando menú...",
"nameLabel": "Nombre",
"mealTypeLabel": "Tipo de comida",
"saveChanges": "Guardar cambios",
"deleteMeal": "Eliminar menú",
"notFound": "El menú no existe.",
"itemsTitle": "Elementos del menú",
"selectIngredient": "Selecciona ingrediente",
"addItem": "Añadir",
"noItems": "Este menú aún no tiene elementos.",
"grams": "Gramos"
},
"ingredients": {
"newTitle": "Nuevo ingrediente",
"editTitle": "Editar ingrediente",
"databaseTitle": "Base de datos de ingredientes",
"newButton": "Nuevo ingrediente",
"empty": "Aún no tienes ingredientes guardados.",
"name": "Nombre",
"protein100": "Proteína / 100 g",
"carbs100": "Carbohidratos / 100 g",
"sugar100": "Azúcar / 100 g",
"fat100": "Grasas / 100 g",
"fiber100": "Fibra / 100 g",
"kcal100Auto": "Kcal / 100 g (0 = auto)",
"submitDefault": "Guardar"
},
"stats": {
"title": "Estadísticas",
"placeholder": "El resumen básico se añadirá en la próxima iteración."
},
"settings": {
"accountTitle": "Ajustes de cuenta",
"loggedInAs": "Sesión iniciada como",
"logout": "Cerrar sesión"
}
}

View File

@ -37,5 +37,102 @@
"en": "Angličtina",
"es": "Španielčina",
"de": "Nemčina"
},
"nav": {
"today": "Dnes",
"meals": "Jedálničky",
"ingredients": "Suroviny",
"stats": "Štatistiky",
"settings": "Nastavenia",
"more": "Viac"
},
"pageTitles": {
"today": "Denný prehľad",
"meals": "Jedálničky",
"mealDetail": "Detail jedálnička",
"ingredients": "Suroviny",
"stats": "Štatistiky",
"settings": "Nastavenia",
"default": "Nutrio"
},
"mealTypes": {
"breakfast": "Raňajky",
"lunch": "Obed",
"dinner": "Večera"
},
"common": {
"date": "Dátum",
"all": "Všetky",
"none": "Žiadne",
"save": "Uložiť",
"saving": "Ukladám...",
"cancel": "Zrušiť",
"create": "Vytvoriť",
"edit": "Upraviť",
"delete": "Zmazať",
"actions": "Akcia",
"loading": "Načítavam...",
"kcalUnit": "kcal"
},
"nutrition": {
"short": {
"protein": "B",
"carbs": "S",
"fat": "T"
},
"labels": {
"protein": "Bielkoviny",
"carbs": "Sacharidy",
"fat": "Tuky"
}
},
"today": {
"loadingDay": "Načítavam deň...",
"noMealPlan": "Bez jedálnička",
"noItems": "Zatiaľ bez položiek.",
"dayTotals": "Súčty dňa",
"mealPickerPlaceholder": "Výber jedálnička bude doplnený."
},
"meals": {
"mealTypeFilter": "Typ jedla",
"namePlaceholder": "Názov jedálnička",
"createButton": "Vytvoriť",
"libraryTitle": "Knižnica jedálničkov",
"empty": "Zatiaľ nemáš žiadne jedálničky.",
"loadingMeal": "Načítavam jedálniček...",
"nameLabel": "Názov",
"mealTypeLabel": "Typ jedla",
"saveChanges": "Uložiť zmeny",
"deleteMeal": "Zmazať jedálniček",
"notFound": "Jedálniček neexistuje.",
"itemsTitle": "Položky jedálnička",
"selectIngredient": "Vyber surovinu",
"addItem": "Pridať",
"noItems": "Tento jedálniček zatiaľ nemá položky.",
"grams": "Gramáž"
},
"ingredients": {
"newTitle": "Nová surovina",
"editTitle": "Upraviť surovinu",
"databaseTitle": "Databáza surovín",
"newButton": "Nová surovina",
"empty": "Zatiaľ nemáš uložené suroviny.",
"name": "Názov",
"protein100": "Bielkoviny / 100 g",
"carbs100": "Sacharidy / 100 g",
"sugar100": "Cukor / 100 g",
"fat100": "Tuky / 100 g",
"fiber100": "Vláknina / 100 g",
"kcal100Auto": "Kcal / 100 g (0 = auto)",
"submitDefault": "Uložiť"
},
"stats": {
"title": "Štatistiky",
"placeholder": "Základný prehľad bude doplnený v ďalšej iterácii."
},
"settings": {
"accountTitle": "Nastavenia účtu",
"loggedInAs": "Prihlásený používateľ",
"logout": "Odhlásiť sa"
}
}

View File

@ -69,9 +69,3 @@ export interface DiaryDay {
export type DayMealsByType = Record<MealType, Meal | null>
export const MEAL_TYPES: MealType[] = ['breakfast', 'lunch', 'dinner']
export const MEAL_TYPE_LABELS_SK: Record<MealType, string> = {
breakfast: 'Raňajky',
lunch: 'Obed',
dinner: 'Večera',
}

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IngredientForm from '@/components/ingredients/IngredientForm.vue'
import { useIngredientsStore } from '@/stores/ingredients'
import type { Ingredient } from '@/types/domain'
@ -7,6 +8,7 @@ import type { Ingredient } from '@/types/domain'
const ingredientsStore = useIngredientsStore()
const editingId = ref<number | null>(null)
const saving = ref(false)
const { t } = useI18n()
onMounted(async () => {
if (ingredientsStore.items.length <= 0) {
@ -67,37 +69,39 @@ const removeIngredient = async (ingredientId: number) => {
<section class="page">
<IngredientForm
:initial="editingIngredient"
:submit-label="saving ? 'Ukladám...' : 'Uložiť'"
:submit-label="saving ? t('common.saving') : t('common.save')"
@save="saveIngredient"
@cancel="cancelEdit"
/>
<section class="card">
<div class="section-header">
<h3>Databáza surovín</h3>
<button class="btn" type="button" @click="startCreate">Nová surovina</button>
<h3>{{ t('ingredients.databaseTitle') }}</h3>
<button class="btn" type="button" @click="startCreate">{{ t('ingredients.newButton') }}</button>
</div>
<div class="list" v-if="ingredientsStore.sortedItems.length > 0">
<div class="list-row" v-for="ingredient in ingredientsStore.sortedItems" :key="ingredient.ingredient_id">
<div>
<strong>{{ ingredient.name }}</strong>
<p>
B {{ ingredient.protein_g_100 }} · S {{ ingredient.carbs_g_100 }} · T {{ ingredient.fat_g_100 }}
{{ 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>
<div class="list-row__actions">
<button class="btn" type="button" @click="startEdit(ingredient.ingredient_id)">Upraviť</button>
<button class="btn" type="button" @click="startEdit(ingredient.ingredient_id)">{{ t('common.edit') }}</button>
<button
class="btn btn-danger"
type="button"
@click="removeIngredient(ingredient.ingredient_id)"
>
Zmazať
{{ t('common.delete') }}
</button>
</div>
</div>
</div>
<p v-else class="empty-state">Zatiaľ nemáš uložené suroviny.</p>
<p v-else class="empty-state">{{ t('ingredients.empty') }}</p>
</section>
</section>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import MealItemsEditor from '@/components/meals/MealItemsEditor.vue'
import { useIngredientsStore } from '@/stores/ingredients'
import { useMealsStore } from '@/stores/meals'
@ -10,6 +11,7 @@ const route = useRoute()
const router = useRouter()
const mealsStore = useMealsStore()
const ingredientsStore = useIngredientsStore()
const { t } = useI18n()
const loading = ref(false)
const saving = ref(false)
@ -117,29 +119,29 @@ const removeItem = async (mealItemId: number) => {
<template>
<section class="page">
<div v-if="loading" class="card">Načítavam jedálniček...</div>
<div v-if="loading" class="card">{{ t('meals.loadingMeal') }}</div>
<template v-else-if="meal">
<section class="card">
<div class="grid-two">
<label>
<span>Názov</span>
<span>{{ t('meals.nameLabel') }}</span>
<input v-model="mealName" class="input-text" type="text" />
</label>
<label>
<span>Typ jedla</span>
<span>{{ t('meals.mealTypeLabel') }}</span>
<select v-model="mealType" class="input-select">
<option value="breakfast">Raňajky</option>
<option value="lunch">Obed</option>
<option value="dinner">Večera</option>
<option value="breakfast">{{ t('mealTypes.breakfast') }}</option>
<option value="lunch">{{ t('mealTypes.lunch') }}</option>
<option value="dinner">{{ t('mealTypes.dinner') }}</option>
</select>
</label>
</div>
<div class="form-actions">
<button class="btn btn-primary" type="button" :disabled="saving" @click="saveMeal">
{{ saving ? 'Ukladám...' : 'Uložiť zmeny' }}
{{ saving ? t('common.saving') : t('meals.saveChanges') }}
</button>
<button class="btn btn-danger" type="button" @click="removeMeal">Zmazať jedálniček</button>
<button class="btn btn-danger" type="button" @click="removeMeal">{{ t('meals.deleteMeal') }}</button>
</div>
</section>
@ -152,6 +154,6 @@ const removeItem = async (mealItemId: number) => {
/>
</template>
<div v-else class="card">Jedálniček neexistuje.</div>
<div v-else class="card">{{ t('meals.notFound') }}</div>
</section>
</template>

View File

@ -1,13 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import MealTypeFilter from '@/components/meals/MealTypeFilter.vue'
import { useMealsStore } from '@/stores/meals'
import type { MealType } from '@/types/domain'
import { MEAL_TYPE_LABELS_SK } from '@/types/domain'
const router = useRouter()
const mealsStore = useMealsStore()
const { t } = useI18n()
const filterType = ref<MealType | ''>('')
const newMealName = ref('')
@ -53,19 +54,19 @@ const createMeal = async () => {
</div>
<section class="card form-inline">
<input v-model="newMealName" class="input-text" type="text" placeholder="Názov jedálnička" />
<input v-model="newMealName" class="input-text" type="text" :placeholder="t('meals.namePlaceholder')" />
<select v-model="newMealType" class="input-select">
<option value="breakfast">Raňajky</option>
<option value="lunch">Obed</option>
<option value="dinner">Večera</option>
<option value="breakfast">{{ t('mealTypes.breakfast') }}</option>
<option value="lunch">{{ t('mealTypes.lunch') }}</option>
<option value="dinner">{{ t('mealTypes.dinner') }}</option>
</select>
<button class="btn btn-primary" type="button" :disabled="creating" @click="createMeal">
{{ creating ? 'Ukladám...' : 'Vytvoriť' }}
{{ creating ? t('common.saving') : t('meals.createButton') }}
</button>
</section>
<section class="card">
<h3>Knižnica jedálničkov</h3>
<h3>{{ t('meals.libraryTitle') }}</h3>
<div class="list" v-if="filteredMeals.length > 0">
<RouterLink
v-for="meal in filteredMeals"
@ -75,12 +76,12 @@ const createMeal = async () => {
>
<div>
<strong>{{ meal.name }}</strong>
<p>{{ MEAL_TYPE_LABELS_SK[meal.meal_type] }}</p>
<p>{{ t(`mealTypes.${meal.meal_type}`) }}</p>
</div>
<div class="list-row__meta">{{ meal.totals?.kcal ?? 0 }} kcal</div>
<div class="list-row__meta">{{ meal.totals?.kcal ?? 0 }} {{ t('common.kcalUnit') }}</div>
</RouterLink>
</div>
<p v-else class="empty-state">Zatiaľ nemáš žiadne jedálničky.</p>
<p v-else class="empty-state">{{ t('meals.empty') }}</p>
</section>
</section>
</template>

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const auth = useAuthStore()
const { t } = useI18n()
const onLogout = async () => {
await auth.logout()
@ -14,9 +16,11 @@ const onLogout = async () => {
<template>
<section class="page">
<section class="card settings-card">
<h3>Nastavenia účtu</h3>
<p v-if="auth.userEmail">Prihlásený používateľ: <strong>{{ auth.userEmail }}</strong></p>
<button class="btn btn-danger" type="button" @click="onLogout">Odhlásiť sa</button>
<h3>{{ t('settings.accountTitle') }}</h3>
<p v-if="auth.userEmail">
{{ t('settings.loggedInAs') }}: <strong>{{ auth.userEmail }}</strong>
</p>
<button class="btn btn-danger" type="button" @click="onLogout">{{ t('settings.logout') }}</button>
</section>
</section>
</template>

View File

@ -1,8 +1,14 @@
<template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<section class="page">
<section class="card">
<h3>Štatistiky</h3>
<p>Základný prehľad bude doplnený v ďalšej iterácii.</p>
<h3>{{ t('stats.title') }}</h3>
<p>{{ t('stats.placeholder') }}</p>
</section>
</section>
</template>

View File

@ -1,17 +1,19 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import DayMealCard from '@/components/today/DayMealCard.vue'
import DayTotalsCard from '@/components/today/DayTotalsCard.vue'
import { useDiaryStore } from '@/stores/diary'
import { useMealsStore } from '@/stores/meals'
import { MEAL_TYPES, MEAL_TYPE_LABELS_SK, type MealType } from '@/types/domain'
import { MEAL_TYPES, type MealType } from '@/types/domain'
import { todayISO } from '@/utils/date'
const route = useRoute()
const router = useRouter()
const diaryStore = useDiaryStore()
const mealsStore = useMealsStore()
const { t } = useI18n()
const isWorking = ref(false)
const selectedDate = ref(todayISO())
@ -21,6 +23,8 @@ const resolveDate = (): string => {
return dateFromRoute.length > 0 ? dateFromRoute : todayISO()
}
const mealTypeLabel = (mealType: MealType): string => t(`mealTypes.${mealType}`)
const reloadDay = async (date: string) => {
selectedDate.value = date
diaryStore.ensureCurrentDay(date)
@ -66,12 +70,12 @@ const dayTotals = computed(() => diaryStore.computedTotals)
<section class="page">
<div class="page-header">
<label class="filter-control">
<span>Dátum</span>
<span>{{ t('common.date') }}</span>
<input v-model="selectedDate" class="input-date" type="date" @change="onDateChange" />
</label>
</div>
<div v-if="isWorking || diaryStore.loading" class="card">Načítavam deň...</div>
<div v-if="isWorking || diaryStore.loading" class="card">{{ t('today.loadingDay') }}</div>
<template v-else>
<div class="grid-three">
@ -79,7 +83,7 @@ const dayTotals = computed(() => diaryStore.computedTotals)
v-for="mealType in MEAL_TYPES"
:key="mealType"
:meal-type="mealType"
:label="MEAL_TYPE_LABELS_SK[mealType]"
:label="mealTypeLabel(mealType)"
:meal-options="diaryStore.selectedMealOptions[mealType]"
:selected-meal-id="diaryStore.selectedMealId(mealType)"
:meal="dayMeals[mealType]"

View File

@ -9,13 +9,17 @@ import pkg from './package.json';
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: "0.0.0.0", // sprístupní server na všetkých interfejsoch
port: 5173, // môžeš zmeniť port, ak potrebuješ
},
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})