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 @@