diff --git a/backend/src/API.php b/backend/src/API.php
index e60d2ea..50de303 100644
--- a/backend/src/API.php
+++ b/backend/src/API.php
@@ -46,7 +46,7 @@ class API extends APIlite {
if (is_array($existing)) {
throw new \Exception('User with this email already exists');
}
- $userId = $this->users()->userSave(array(
+ $userId = $this->users()->user(null, array(
'email' => $email,
'password_hash' => $this->users()->hashString($password),
'token' => null,
@@ -192,7 +192,7 @@ class API extends APIlite {
if ($kcal_100 == 0) {
$kcal_100 = $this->computeKcal100($protein_g_100, $carbs_g_100, $fat_g_100);
}
- $ingredientId = $this->ingredients()->ingredientSave(array(
+ $ingredientId = $this->ingredients()->ingredient(null, array(
'user_id' => $user_id,
'name' => $name,
'protein_g_100' => $this->round2($protein_g_100),
@@ -326,7 +326,7 @@ class API extends APIlite {
$user_id = $this->requireUserIDbyToken($token);
$name = $this->normalizeName($name);
$this->assertMealType($meal_type);
- $mealId = $this->meals()->mealSave(array(
+ $mealId = $this->meals()->meal(null, array(
'user_id' => $user_id,
'name' => $name,
'meal_type' => $meal_type,
@@ -387,7 +387,7 @@ class API extends APIlite {
throw new \Exception('position must be >= 1');
}
$this->getIngredientAccessible($user_id, $ingredient_id);
- $mealItemId = $this->mealItems()->mealItemSave(array(
+ $mealItemId = $this->mealItems()->mealItem(null, array(
'meal_id' => $meal_id,
'ingredient_id' => $ingredient_id,
'grams' => $this->round2($grams),
@@ -508,7 +508,7 @@ class API extends APIlite {
throw new \Exception('Failed to update diary entry');
}
} else {
- $inserted = $this->diaryEntries()->diaryEntrySave(array(
+ $inserted = $this->diaryEntries()->diaryEntry(null, array(
'diary_day_id' => (int) $day['diary_day_id'],
'meal_type' => $meal_type,
'meal_id' => $meal_id,
@@ -890,7 +890,7 @@ class API extends APIlite {
if (is_array($day)) {
return $this->mapDiaryDay($day);
}
- $dayId = $this->diaryDays()->diaryDaySave(array(
+ $dayId = $this->diaryDays()->diaryDay(null, array(
'user_id' => $user_id,
'day_date' => $day_date,
'created_at' => '`NOW`'
diff --git a/frontend/src/assets/css/style.css b/frontend/src/assets/css/style.css
index 42d15e0..6570916 100644
--- a/frontend/src/assets/css/style.css
+++ b/frontend/src/assets/css/style.css
@@ -405,6 +405,12 @@ select {
color: var(--color-muted);
}
+.app-topbar__actions {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
.app-content {
padding: var(--space-lg) var(--space-xl);
}
@@ -609,6 +615,24 @@ select {
margin-bottom: var(--space-md);
}
+.settings-card {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.settings-card__theme {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-sm);
+}
+
+.settings-card__theme span {
+ color: var(--color-muted);
+ font-size: var(--fs-sm);
+}
+
.app-bottom-tabs {
display: none;
}
@@ -671,4 +695,8 @@ select {
color: var(--color-green);
font-weight: 700;
}
+
+ .app-topbar__actions {
+ gap: var(--space-xs);
+ }
}
diff --git a/frontend/src/components/common/ThemeToggle.vue b/frontend/src/components/common/ThemeToggle.vue
new file mode 100644
index 0000000..c947f43
--- /dev/null
+++ b/frontend/src/components/common/ThemeToggle.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
diff --git a/frontend/src/components/navigation/AppTopbar.vue b/frontend/src/components/navigation/AppTopbar.vue
index 6ebe3d4..cf681e5 100644
--- a/frontend/src/components/navigation/AppTopbar.vue
+++ b/frontend/src/components/navigation/AppTopbar.vue
@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
+import ThemeToggle from '@/components/common/ThemeToggle.vue'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
@@ -33,8 +34,11 @@ const title = computed(() => {
{{ title }}
-
- {{ auth.userEmail }}
+
+
+
+ {{ auth.userEmail }}
+
diff --git a/frontend/src/locales/cs.json b/frontend/src/locales/cs.json
index 369e896..c21fd7d 100644
--- a/frontend/src/locales/cs.json
+++ b/frontend/src/locales/cs.json
@@ -29,7 +29,8 @@
},
"theme": {
"light": "Světlý režim",
- "dark": "Tmavý režim"
+ "dark": "Tmavý režim",
+ "toggle": "Přepnout režim"
},
"locale": {
"sk": "Slovenština",
@@ -133,6 +134,7 @@
"settings": {
"accountTitle": "Nastavení účtu",
"loggedInAs": "Přihlášený uživatel",
+ "themeTitle": "Vzhled",
"logout": "Odhlásit se"
}
}
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json
index b134841..e74a2a8 100644
--- a/frontend/src/locales/de.json
+++ b/frontend/src/locales/de.json
@@ -29,7 +29,8 @@
},
"theme": {
"light": "Heller Modus",
- "dark": "Dunkler Modus"
+ "dark": "Dunkler Modus",
+ "toggle": "Modus wechseln"
},
"locale": {
"sk": "Slowakisch",
@@ -133,6 +134,7 @@
"settings": {
"accountTitle": "Kontoeinstellungen",
"loggedInAs": "Angemeldet als",
+ "themeTitle": "Darstellung",
"logout": "Abmelden"
}
}
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 6a05dc5..6240fc8 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -29,7 +29,8 @@
},
"theme": {
"light": "Light mode",
- "dark": "Dark mode"
+ "dark": "Dark mode",
+ "toggle": "Toggle mode"
},
"locale": {
"sk": "Slovak",
@@ -133,6 +134,7 @@
"settings": {
"accountTitle": "Account settings",
"loggedInAs": "Logged in as",
+ "themeTitle": "Appearance",
"logout": "Log out"
}
}
diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json
index 9c9f9de..6116bf0 100644
--- a/frontend/src/locales/es.json
+++ b/frontend/src/locales/es.json
@@ -29,7 +29,8 @@
},
"theme": {
"light": "Modo claro",
- "dark": "Modo oscuro"
+ "dark": "Modo oscuro",
+ "toggle": "Cambiar modo"
},
"locale": {
"sk": "Eslovaco",
@@ -133,6 +134,7 @@
"settings": {
"accountTitle": "Ajustes de cuenta",
"loggedInAs": "Sesión iniciada como",
+ "themeTitle": "Apariencia",
"logout": "Cerrar sesión"
}
}
diff --git a/frontend/src/locales/sk.json b/frontend/src/locales/sk.json
index c481254..a5f9353 100644
--- a/frontend/src/locales/sk.json
+++ b/frontend/src/locales/sk.json
@@ -29,7 +29,8 @@
},
"theme": {
"light": "Svetlý režim",
- "dark": "Tmavý režim"
+ "dark": "Tmavý režim",
+ "toggle": "Prepnúť režim"
},
"locale": {
"sk": "Slovenčina",
@@ -133,6 +134,7 @@
"settings": {
"accountTitle": "Nastavenia účtu",
"loggedInAs": "Prihlásený používateľ",
+ "themeTitle": "Vzhľad",
"logout": "Odhlásiť sa"
}
}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 44f9442..77ecfb3 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -5,8 +5,11 @@ import App from './App.vue'
import i18n from './i18n'
import router from './router'
import { pinia } from './stores'
+import { useThemeStore } from './stores/theme'
const app = createApp(App)
+const themeStore = useThemeStore(pinia)
+themeStore.initialize()
app.component('font-awesome-icon', FontAwesomeIcon)
app.use(pinia)
diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts
new file mode 100644
index 0000000..f6d04a4
--- /dev/null
+++ b/frontend/src/stores/theme.ts
@@ -0,0 +1,44 @@
+import { computed, ref } from 'vue'
+import { defineStore } from 'pinia'
+
+export type ThemeMode = 'light' | 'dark'
+
+const THEME_KEY = 'theme'
+
+const isThemeMode = (value: unknown): value is ThemeMode => value === 'light' || value === 'dark'
+
+export const useThemeStore = defineStore('theme', () => {
+ const mode = ref
('light')
+ const initialized = ref(false)
+
+ const applyTheme = (nextMode: ThemeMode) => {
+ mode.value = nextMode
+ document.documentElement.setAttribute('data-theme', nextMode)
+ localStorage.setItem(THEME_KEY, nextMode)
+ }
+
+ const initialize = () => {
+ if (initialized.value) {
+ return
+ }
+ const storedTheme = localStorage.getItem(THEME_KEY)
+ applyTheme(isThemeMode(storedTheme) ? storedTheme : 'light')
+ initialized.value = true
+ }
+
+ const toggle = () => {
+ applyTheme(mode.value === 'dark' ? 'light' : 'dark')
+ }
+
+ const isDarkMode = computed(() => mode.value === 'dark')
+ const currentThemeLabelKey = computed(() => (isDarkMode.value ? 'theme.dark' : 'theme.light'))
+
+ return {
+ mode,
+ isDarkMode,
+ currentThemeLabelKey,
+ initialize,
+ applyTheme,
+ toggle,
+ }
+})
diff --git a/frontend/src/views/AuthView.vue b/frontend/src/views/AuthView.vue
index 958b5e1..aea26da 100644
--- a/frontend/src/views/AuthView.vue
+++ b/frontend/src/views/AuthView.vue
@@ -8,17 +8,14 @@ import {
faEyeSlash,
faGlobe,
faLock,
- faMoon,
faRightToBracket,
- faSun,
} from '@fortawesome/free-solid-svg-icons'
+import ThemeToggle from '@/components/common/ThemeToggle.vue'
import i18n from '@/i18n'
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
import { useAuthStore } from '@/stores/auth'
-type ThemeMode = 'light' | 'dark'
-
const { t } = useI18n({ useScope: 'global' })
const router = useRouter()
const route = useRoute()
@@ -57,31 +54,6 @@ watch(
{ immediate: true },
)
-const getInitialTheme = (): ThemeMode => {
- const storedTheme = localStorage.getItem('theme')
- return storedTheme === 'dark' ? 'dark' : 'light'
-}
-
-const theme = ref(getInitialTheme())
-
-const applyTheme = (nextTheme: ThemeMode) => {
- theme.value = nextTheme
- document.documentElement.setAttribute('data-theme', nextTheme)
- localStorage.setItem('theme', nextTheme)
-}
-
-applyTheme(theme.value)
-
-const toggleTheme = () => {
- applyTheme(theme.value === 'dark' ? 'light' : 'dark')
-}
-
-const isDarkMode = computed(() => theme.value === 'dark')
-
-const themeLabel = computed(() => {
- return isDarkMode.value ? t('theme.dark') : t('theme.light')
-})
-
const submitLabel = computed(() => {
return isLoading.value ? t('auth.submitting') : t('auth.submit')
})
@@ -166,10 +138,7 @@ const submitForm = async () => {
-
+
{{ t('auth.title') }}
diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue
index 5909f89..8899c9a 100644
--- a/frontend/src/views/SettingsView.vue
+++ b/frontend/src/views/SettingsView.vue
@@ -1,6 +1,7 @@