Compare commits
6 Commits
2468b31462
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e9aa1ac4 | |||
| b3be74e652 | |||
| af437963bb | |||
| 6ff28dae13 | |||
| 69e5f4e320 | |||
| b0b5d0972a |
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
/backend/config/Config.cust.php
|
/backend/config/Config.cust.php
|
||||||
/frontend/node_modules
|
/frontend/node_modules
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
|
/dist
|
||||||
|
|||||||
24
AGENTS.md
@ -39,16 +39,35 @@ It describes what the project is, what is already implemented, and what still ne
|
|||||||
- state management: `frontend/src/stores/ui.ts`
|
- state management: `frontend/src/stores/ui.ts`
|
||||||
- mounted globally in `frontend/src/App.vue`
|
- mounted globally in `frontend/src/App.vue`
|
||||||
- card-level loading/error states are wired in `TodayView`, `MealsView`, `MealDetailView`, `IngredientsView`
|
- card-level loading/error states are wired in `TodayView`, `MealsView`, `MealDetailView`, `IngredientsView`
|
||||||
|
- expired-session handling is global:
|
||||||
|
- token error detector in `frontend/src/utils/error.ts` emits `auth:session-expired`
|
||||||
|
- listener in `frontend/src/main.ts` clears session, shows info toast, and redirects to auth route
|
||||||
- Frontend i18n is wired and used across new UI:
|
- Frontend i18n is wired and used across new UI:
|
||||||
- setup in `frontend/src/i18n/index.ts`
|
- setup in `frontend/src/i18n/index.ts`
|
||||||
- locale files in `frontend/src/locales/{sk,cs,en,es,de}.json`
|
- locale files in `frontend/src/locales/{sk,cs,en,es,de}.json`
|
||||||
- language switcher updates locale dynamically.
|
- language switcher updates locale dynamically.
|
||||||
- app UI keys include (`nav`, `pageTitles`, `mealTypes`, `common`, `nutrition`, `today`, `meals`, `ingredients`, `stats`, `settings`, `ux`)
|
- app UI keys include (`nav`, `pageTitles`, `mealTypes`, `common`, `nutrition`, `today`, `meals`, `ingredients`, `stats`, `settings`, `ux`)
|
||||||
|
- `nutrition.short.fiber` exists in all locales and is used in macro badges
|
||||||
|
- `ux.toast.sessionExpired` exists in all locales for auto-logout flow
|
||||||
- Frontend theme system is implemented:
|
- Frontend theme system is implemented:
|
||||||
- centralized theme store: `frontend/src/stores/theme.ts`
|
- centralized theme store: `frontend/src/stores/theme.ts`
|
||||||
- shared toggle component: `frontend/src/components/common/ThemeToggle.vue`
|
- shared toggle component: `frontend/src/components/common/ThemeToggle.vue`
|
||||||
- toggle available in auth, app topbar, and settings
|
- toggle available in auth, app topbar, and settings
|
||||||
- design tokens in `frontend/src/assets/css/style.css` (`:root` variables).
|
- design tokens in `frontend/src/assets/css/style.css` (`:root` variables).
|
||||||
|
- Frontend macro-badge visual system is implemented:
|
||||||
|
- shared component: `frontend/src/components/common/MacroBadge.vue`
|
||||||
|
- shared colors/tokens/styles in `frontend/src/assets/css/style.css`
|
||||||
|
- used in `IngredientsView`, `MealsView`, `DayTotalsCard`, and `DayMealCard`
|
||||||
|
- `Today` day totals card layout: kcal block on the left, macros on the right in one row
|
||||||
|
- macro colors:
|
||||||
|
- protein `#3B82F6`
|
||||||
|
- carbs `#F59E0B`
|
||||||
|
- fat `#EF4444`
|
||||||
|
- fiber `#10B981`
|
||||||
|
- style uses subtle tinted background + full-color text, pill shape (no saturated full backgrounds)
|
||||||
|
- Frontend typography is local (no remote font CDN):
|
||||||
|
- `frontend/src/assets/css/style.css` uses local `@font-face` for `DM Sans` and `Space Grotesk`
|
||||||
|
- font files are stored in `frontend/src/assets/fonts/*.woff2`
|
||||||
- App logo is served from `frontend/public/Nutrio.png` (copied from `doc/Nutrio.png`).
|
- 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`.
|
- 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`.
|
- Pinia is installed and configured in `frontend/src/main.ts` via `frontend/src/stores/index.ts`.
|
||||||
@ -58,7 +77,7 @@ It describes what the project is, what is already implemented, and what still ne
|
|||||||
- `frontend/src/utils/{nutrition,api,date,error}.ts`
|
- `frontend/src/utils/{nutrition,api,date,error}.ts`
|
||||||
- Ingredients form UX detail:
|
- Ingredients form UX detail:
|
||||||
- create/edit form in `IngredientsView` is hidden by default
|
- create/edit form in `IngredientsView` is hidden by default
|
||||||
- shows only after `Nová surovina` or `Upraviť`
|
- shows only after `Nova surovina` or `Upravit`
|
||||||
- hides again after successful save (form remount resets fields)
|
- hides again after successful save (form remount resets fields)
|
||||||
- `frontend/src/BackendAPI.ts` is generated via `backend/scripts/buildTypeScript.php` and should not be edited manually.
|
- `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).
|
- `backend/data.json` contains sample meal data (not currently wired into DB/API flow).
|
||||||
@ -168,6 +187,8 @@ All actions are invoked through `backend/public/API.php` with `?action=<method_n
|
|||||||
- raw API response is wrapped as `{ status, data }`
|
- raw API response is wrapped as `{ status, data }`
|
||||||
- generated `BackendAPI.ts` currently resolves `response.data` in `callPromise` for non-`__HELP__` actions
|
- generated `BackendAPI.ts` currently resolves `response.data` in `callPromise` for non-`__HELP__` actions
|
||||||
- frontend stores use `frontend/src/utils/api.ts` (`unwrapApiData`) to normalize both envelope and unwrapped runtime shapes
|
- frontend stores use `frontend/src/utils/api.ts` (`unwrapApiData`) to normalize both envelope and unwrapped runtime shapes
|
||||||
|
- Auto-logout on expired token currently depends on the shared error mapper:
|
||||||
|
- if a view/store handles API errors without `toErrorMessage(...)`, global `auth:session-expired` event is not emitted there
|
||||||
- `frontend/src/BackendAPI.ts` is generated output; regenerate when backend API changes, do not patch manually.
|
- `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.
|
- In vue-i18n locale strings, `@` must be escaped as `{'@'}` to avoid "Invalid linked format" errors.
|
||||||
|
|
||||||
@ -206,3 +227,4 @@ Frontend:
|
|||||||
- When changing schema, always bump DB version in `Maintenance.php` with forward-only migration steps.
|
- When changing schema, always bump DB version in `Maintenance.php` with forward-only migration steps.
|
||||||
- Keep API action names stable unless frontend is updated at the same time.
|
- Keep API action names stable unless frontend is updated at the same time.
|
||||||
- In source files, use tab characters for indentation (do not add space-based indentation).
|
- In source files, use tab characters for indentation (do not add space-based indentation).
|
||||||
|
|
||||||
|
|||||||
@ -105,7 +105,7 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
|||||||
return password_verify($password, $password_hash);
|
return password_verify($password, $password_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generateToken(int $user_id, int $ttl_seconds = 3600): string {
|
public function generateToken(int $user_id, int $ttl_seconds = 604800): string {
|
||||||
if ($user_id <= 0) {
|
if ($user_id <= 0) {
|
||||||
throw new \Exception('Invalid user_id');
|
throw new \Exception('Invalid user_id');
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ class Users extends \TPsoft\DBmodel\DBmodel {
|
|||||||
if (!hash_equals($stored_token, $token)) {
|
if (!hash_equals($stored_token, $token)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$refresh_expires = date('Y-m-d H:i:s', time() + 3600);
|
$refresh_expires = date('Y-m-d H:i:s', time() + 604800); // 7 days
|
||||||
$updated = $this->user($user_id, array(
|
$updated = $this->user($user_id, array(
|
||||||
'token_expires' => $refresh_expires
|
'token_expires' => $refresh_expires
|
||||||
));
|
));
|
||||||
|
|||||||
94
build.bat
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions
|
||||||
|
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
|
|
||||||
|
set "DIST_DIR=%ROOT%\dist"
|
||||||
|
set "DIST_PUBLIC=%DIST_DIR%\public"
|
||||||
|
set "DIST_APP=%DIST_PUBLIC%"
|
||||||
|
set "FRONTEND_DIR=%ROOT%\frontend"
|
||||||
|
set "BACKEND_DIR=%ROOT%\backend"
|
||||||
|
|
||||||
|
echo [1/6] Cleaning dist...
|
||||||
|
if exist "%DIST_DIR%" (
|
||||||
|
rmdir /S /Q "%DIST_DIR%"
|
||||||
|
if exist "%DIST_DIR%" (
|
||||||
|
echo ERROR: Failed to remove "%DIST_DIR%".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir "%DIST_DIR%" >nul 2>&1 || (
|
||||||
|
echo ERROR: Failed to create "%DIST_DIR%".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/6] Building frontend...
|
||||||
|
pushd "%FRONTEND_DIR%" >nul 2>&1 || (
|
||||||
|
echo ERROR: Frontend directory not found: "%FRONTEND_DIR%".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo - npm install
|
||||||
|
call npm install
|
||||||
|
if errorlevel 1 (
|
||||||
|
popd >nul
|
||||||
|
echo ERROR: npm install failed.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo - npm run build
|
||||||
|
call npm run build
|
||||||
|
if errorlevel 1 (
|
||||||
|
popd >nul
|
||||||
|
echo ERROR: npm run build failed.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%FRONTEND_DIR%\dist" (
|
||||||
|
popd >nul
|
||||||
|
echo ERROR: Frontend build output not found at "%FRONTEND_DIR%\dist".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
popd >nul
|
||||||
|
|
||||||
|
echo [3/6] Copy backend root to dist/...
|
||||||
|
call :RunRobocopy "%BACKEND_DIR%" "%DIST_DIR%" /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP ^
|
||||||
|
/XD "%BACKEND_DIR%\.git" "%BACKEND_DIR%\.vscode" "%BACKEND_DIR%\tests" "%BACKEND_DIR%\node_modules" "%BACKEND_DIR%\frontend" "%BACKEND_DIR%\dist" ^
|
||||||
|
/XF ".env" ".env.*"
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
|
echo [4/6] Copy frontend/dist to dist/public...
|
||||||
|
call :RunRobocopy "%FRONTEND_DIR%\dist" "%DIST_APP%" /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
|
echo [5/6] Applying .env.production (if present)...
|
||||||
|
set "ENV_NOTE=No backend/.env.production found"
|
||||||
|
if exist "%BACKEND_DIR%\.env.production" (
|
||||||
|
copy /Y "%BACKEND_DIR%\.env.production" "%DIST_DIR%\.env" >nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: Failed to copy backend/.env.production to dist/.env.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
set "ENV_NOTE=Copied backend/.env.production to dist/.env"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [6/6] Summary
|
||||||
|
echo - Frontend build: OK
|
||||||
|
echo - Backend root copied to: "%DIST_DIR%" (excluding excluded dirs)
|
||||||
|
echo - Frontend assets copied to: "%DIST_APP%"
|
||||||
|
echo - Env: %ENV_NOTE%
|
||||||
|
echo - DocumentRoot should be: "%DIST_PUBLIC%"
|
||||||
|
echo - Frontend app is served from: "%DIST_APP%"
|
||||||
|
echo DONE "%DIST_DIR%"
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:RunRobocopy
|
||||||
|
robocopy %*
|
||||||
|
if errorlevel 8 (
|
||||||
|
echo ERROR: robocopy failed with exit code %ERRORLEVEL%.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
exit /b 0
|
||||||
9
deploy.bat
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
call php d:\www\sftpsync\src\sftpsync.php --host nutrio.tpsoft.org --user igor ^
|
||||||
|
--delete-dir /storage/tpsoft.org/nutrio/public ^
|
||||||
|
--sync d:/www/Nutrio/dist /storage/tpsoft.org/nutrio ^
|
||||||
|
--skip .git ^
|
||||||
|
--print-relative
|
||||||
|
|
||||||
|
echo ✔️ Done.
|
||||||
136
doc/prompt.txt
@ -100,3 +100,139 @@ Layout:
|
|||||||
|
|
||||||
Výsledok:
|
Výsledok:
|
||||||
Čistý, konzistentný, moderný vzhľad bez prehnaných farieb.
|
Čistý, konzistentný, moderný vzhľad bez prehnaných farieb.
|
||||||
|
|
||||||
|
|
||||||
|
----- 2026-02-14 13:35:31 -----------------------------------------------------
|
||||||
|
V koreňovom adresári projektu vytvor Windows batch skript build.bat.
|
||||||
|
|
||||||
|
Štruktúra projektu:
|
||||||
|
- frontend/ (Vue 3 + Vite) -> build do frontend/dist
|
||||||
|
- backend/ (PHP) -> web root je backend/public a PHP súbory v public používajú relatívne cesty typu ../src/Init.php
|
||||||
|
=> Musíme zachovať štruktúru, aby v dist platilo: dist/public/.. = dist/
|
||||||
|
|
||||||
|
Cieľ:
|
||||||
|
- v koreňi vytvoriť root/dist tak, aby som na serveri nastavil DocumentRoot na dist/public
|
||||||
|
- relatívne cesty v PHP z dist/public na dist/src musia fungovať (napr. dist/public/index.php -> ../src/Init.php)
|
||||||
|
- frontend (Vite) nasadiť ako statické súbory do dist/public/app (aby nekolidoval s backend/public/index.php)
|
||||||
|
|
||||||
|
Výsledná štruktúra dist:
|
||||||
|
- dist/public/ (WEB ROOT) = kópia backend/public
|
||||||
|
- dist/src/ = kópia backend/src
|
||||||
|
- dist/vendor/ = kópia backend/vendor (ak existuje)
|
||||||
|
- dist/config/ = kópia backend/config (ak existuje)
|
||||||
|
- dist/… = ostatné potrebné backend súbory/adresáre z backend/ (mimo public)
|
||||||
|
- dist/public/app/ = Vite build (obsah frontend/dist)
|
||||||
|
|
||||||
|
Požiadavky na build.bat:
|
||||||
|
|
||||||
|
1) Vyčistenie:
|
||||||
|
- ak existuje root/dist, zmaž ho celý
|
||||||
|
- vytvor root/dist
|
||||||
|
|
||||||
|
2) Frontend build (nepoužívaj npm ci):
|
||||||
|
- cd frontend
|
||||||
|
- spusti "npm install"
|
||||||
|
- spusti "npm run build"
|
||||||
|
- ak build zlyhá, ukonči skript s exit code 1
|
||||||
|
- vráť sa do root
|
||||||
|
|
||||||
|
3) Kopírovanie backend do dist:
|
||||||
|
- skopíruj backend/public -> dist/public
|
||||||
|
- skopíruj všetko potrebné z backend/ do dist/ TAK, aby platili relatívne cesty z dist/public na dist/src atď.
|
||||||
|
Konkrétne:
|
||||||
|
- kopíruj backend/src -> dist/src (ak existuje)
|
||||||
|
- kopíruj backend/vendor -> dist/vendor (ak existuje)
|
||||||
|
- kopíruj ďalšie bežné backend adresáre (napr. config, templates, storage…) do dist/ na rovnakú úroveň ako public
|
||||||
|
- NEkopíruj backend/public druhýkrát mimo dist/public
|
||||||
|
- NEkopíruj: .git, .vscode, tests, node_modules, frontend, dist
|
||||||
|
|
||||||
|
4) Skopírovanie Vite buildu do dist/public:
|
||||||
|
- vytvor dist/public
|
||||||
|
- skopíruj obsah frontend/dist -> dist/public
|
||||||
|
|
||||||
|
5) .env:
|
||||||
|
- ak existuje backend/.env.production, skopíruj ho do dist/.env
|
||||||
|
- inak nič
|
||||||
|
|
||||||
|
6) Robustnosť:
|
||||||
|
- použi robocopy na kopírovanie (rýchle a s exclude)
|
||||||
|
- kontroluj ERRORLEVEL po každom kritickom kroku a pri chybe ukonči skript s exit code 1
|
||||||
|
- používaj echo logy (kroky + DONE)
|
||||||
|
|
||||||
|
7) Výstup:
|
||||||
|
- na konci vypíš, že DocumentRoot má byť nastavený na dist/public a frontend je v dist/public
|
||||||
|
|
||||||
|
Dodaj kompletný obsah súboru build.bat.
|
||||||
|
|
||||||
|
----- 2026-02-15 17:05:23 -----------------------------------------------------
|
||||||
|
Úloha:
|
||||||
|
V projekte vytvor plne funkčnú konfiguráciu PWA.
|
||||||
|
|
||||||
|
1️⃣ Manifest
|
||||||
|
Vytvor súbor: frontend/public/manifest.json
|
||||||
|
Obsah musí:
|
||||||
|
- byť validný JSON
|
||||||
|
obsahovať:
|
||||||
|
- name
|
||||||
|
- short_name
|
||||||
|
- start_url
|
||||||
|
- display = "standalone"
|
||||||
|
- background_color
|
||||||
|
- theme_color
|
||||||
|
- icons pole
|
||||||
|
Použi tieto hodnoty:
|
||||||
|
{
|
||||||
|
"name": "Nutrio",
|
||||||
|
"short_name": "Nutrio",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#22c55e"
|
||||||
|
}
|
||||||
|
2️⃣ Ikony
|
||||||
|
Zdrojový obrázok:
|
||||||
|
frontend/public/Nutrio.600.png
|
||||||
|
Z neho vytvor PNG ikony do adresára:
|
||||||
|
frontend/public/icons/
|
||||||
|
Vygeneruj tieto veľkosti:
|
||||||
|
16x16
|
||||||
|
32x32
|
||||||
|
48x48
|
||||||
|
180x180 (apple touch icon)
|
||||||
|
192x192 (PWA required)
|
||||||
|
512x512 (PWA required)
|
||||||
|
Požiadavky:
|
||||||
|
zachovať pomer strán
|
||||||
|
zachovať priehľadnosť
|
||||||
|
nepoužívať žiadne orezávanie
|
||||||
|
výstupné názvy:
|
||||||
|
icon-16.png
|
||||||
|
icon-32.png
|
||||||
|
icon-48.png
|
||||||
|
icon-180.png
|
||||||
|
icon-192.png
|
||||||
|
icon-512.png
|
||||||
|
3️⃣ Manifest icons sekcia
|
||||||
|
Pole icons musí obsahovať iba:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
4️⃣ Úprava index.html
|
||||||
|
Do frontend/index.html pridaj do <head>:
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16.png">
|
||||||
|
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
||||||
|
<meta name="theme-color" content="#22c55e">
|
||||||
|
5️⃣ Výstup
|
||||||
|
nevysvetľuj
|
||||||
|
nevypisuj komentáre
|
||||||
|
vytvor alebo uprav iba potrebné súbory
|
||||||
|
ak adresár icons neexistuje, vytvor ho
|
||||||
@ -2,7 +2,11 @@
|
|||||||
<html lang="">
|
<html lang="">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16.png">
|
||||||
|
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
||||||
|
<meta name="theme-color" content="#22c55e">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Nutrio</title>
|
<title>Nutrio</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 66 KiB |
BIN
frontend/public/icons/icon-16.png
Normal file
|
After Width: | Height: | Size: 874 B |
BIN
frontend/public/icons/icon-180.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
frontend/public/icons/icon-192.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/icons/icon-32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/public/icons/icon-48.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
frontend/public/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
20
frontend/public/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Nutrio",
|
||||||
|
"short_name": "Nutrio",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#22c55e",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -167,6 +167,7 @@
|
|||||||
"accountTitle": "Nastavení účtu",
|
"accountTitle": "Nastavení účtu",
|
||||||
"loggedInAs": "Přihlášený uživatel",
|
"loggedInAs": "Přihlášený uživatel",
|
||||||
"themeTitle": "Vzhled",
|
"themeTitle": "Vzhled",
|
||||||
|
"languageTitle": "Jazyk",
|
||||||
"logout": "Odhlásit se"
|
"logout": "Odhlásit se"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,7 @@
|
|||||||
"accountTitle": "Kontoeinstellungen",
|
"accountTitle": "Kontoeinstellungen",
|
||||||
"loggedInAs": "Angemeldet als",
|
"loggedInAs": "Angemeldet als",
|
||||||
"themeTitle": "Darstellung",
|
"themeTitle": "Darstellung",
|
||||||
|
"languageTitle": "Sprache",
|
||||||
"logout": "Abmelden"
|
"logout": "Abmelden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,7 @@
|
|||||||
"accountTitle": "Account settings",
|
"accountTitle": "Account settings",
|
||||||
"loggedInAs": "Logged in as",
|
"loggedInAs": "Logged in as",
|
||||||
"themeTitle": "Appearance",
|
"themeTitle": "Appearance",
|
||||||
|
"languageTitle": "Language",
|
||||||
"logout": "Log out"
|
"logout": "Log out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,7 @@
|
|||||||
"accountTitle": "Ajustes de cuenta",
|
"accountTitle": "Ajustes de cuenta",
|
||||||
"loggedInAs": "Sesión iniciada como",
|
"loggedInAs": "Sesión iniciada como",
|
||||||
"themeTitle": "Apariencia",
|
"themeTitle": "Apariencia",
|
||||||
|
"languageTitle": "Idioma",
|
||||||
"logout": "Cerrar sesión"
|
"logout": "Cerrar sesión"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,7 @@
|
|||||||
"accountTitle": "Nastavenia účtu",
|
"accountTitle": "Nastavenia účtu",
|
||||||
"loggedInAs": "Prihlásený používateľ",
|
"loggedInAs": "Prihlásený používateľ",
|
||||||
"themeTitle": "Vzhľad",
|
"themeTitle": "Vzhľad",
|
||||||
|
"languageTitle": "Jazyk",
|
||||||
"logout": "Odhlásiť sa"
|
"logout": "Odhlásiť sa"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n({ useScope: 'global' })
|
||||||
|
|
||||||
|
const isSupportedLocale = (value: string): value is AppLocale => {
|
||||||
|
return SUPPORTED_LOCALES.includes(value as AppLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLocale = (value: string) => {
|
||||||
|
if (!isSupportedLocale(value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i18n.global.locale.value = value
|
||||||
|
document.documentElement.setAttribute('lang', value)
|
||||||
|
localStorage.setItem('locale', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const localeValue = computed({
|
||||||
|
get: () => i18n.global.locale.value as AppLocale,
|
||||||
|
set: (value: string) => setLocale(value),
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => i18n.global.locale.value,
|
||||||
|
(nextLocale) => {
|
||||||
|
document.documentElement.setAttribute('lang', nextLocale)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
const onLogout = async () => {
|
const onLogout = async () => {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
@ -25,6 +54,16 @@ const onLogout = async () => {
|
|||||||
<span>{{ t('settings.themeTitle') }}</span>
|
<span>{{ t('settings.themeTitle') }}</span>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-card__theme">
|
||||||
|
<span>{{ t('settings.languageTitle') }}</span>
|
||||||
|
<label class="locale-control" :aria-label="t('settings.languageTitle')">
|
||||||
|
<select v-model="localeValue">
|
||||||
|
<option v-for="lang in SUPPORTED_LOCALES" :key="lang" :value="lang">
|
||||||
|
{{ t(`locale.${lang}`) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<button class="btn btn-danger" type="button" @click="onLogout">{{ t('settings.logout') }}</button>
|
<button class="btn btn-danger" type="button" @click="onLogout">{{ t('settings.logout') }}</button>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||