added theme toggle,
fixed create entityies in API
This commit is contained in:
@ -46,7 +46,7 @@ class API extends APIlite {
|
|||||||
if (is_array($existing)) {
|
if (is_array($existing)) {
|
||||||
throw new \Exception('User with this email already exists');
|
throw new \Exception('User with this email already exists');
|
||||||
}
|
}
|
||||||
$userId = $this->users()->userSave(array(
|
$userId = $this->users()->user(null, array(
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'password_hash' => $this->users()->hashString($password),
|
'password_hash' => $this->users()->hashString($password),
|
||||||
'token' => null,
|
'token' => null,
|
||||||
@ -192,7 +192,7 @@ class API extends APIlite {
|
|||||||
if ($kcal_100 == 0) {
|
if ($kcal_100 == 0) {
|
||||||
$kcal_100 = $this->computeKcal100($protein_g_100, $carbs_g_100, $fat_g_100);
|
$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,
|
'user_id' => $user_id,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'protein_g_100' => $this->round2($protein_g_100),
|
'protein_g_100' => $this->round2($protein_g_100),
|
||||||
@ -326,7 +326,7 @@ class API extends APIlite {
|
|||||||
$user_id = $this->requireUserIDbyToken($token);
|
$user_id = $this->requireUserIDbyToken($token);
|
||||||
$name = $this->normalizeName($name);
|
$name = $this->normalizeName($name);
|
||||||
$this->assertMealType($meal_type);
|
$this->assertMealType($meal_type);
|
||||||
$mealId = $this->meals()->mealSave(array(
|
$mealId = $this->meals()->meal(null, array(
|
||||||
'user_id' => $user_id,
|
'user_id' => $user_id,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'meal_type' => $meal_type,
|
'meal_type' => $meal_type,
|
||||||
@ -387,7 +387,7 @@ class API extends APIlite {
|
|||||||
throw new \Exception('position must be >= 1');
|
throw new \Exception('position must be >= 1');
|
||||||
}
|
}
|
||||||
$this->getIngredientAccessible($user_id, $ingredient_id);
|
$this->getIngredientAccessible($user_id, $ingredient_id);
|
||||||
$mealItemId = $this->mealItems()->mealItemSave(array(
|
$mealItemId = $this->mealItems()->mealItem(null, array(
|
||||||
'meal_id' => $meal_id,
|
'meal_id' => $meal_id,
|
||||||
'ingredient_id' => $ingredient_id,
|
'ingredient_id' => $ingredient_id,
|
||||||
'grams' => $this->round2($grams),
|
'grams' => $this->round2($grams),
|
||||||
@ -508,7 +508,7 @@ class API extends APIlite {
|
|||||||
throw new \Exception('Failed to update diary entry');
|
throw new \Exception('Failed to update diary entry');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$inserted = $this->diaryEntries()->diaryEntrySave(array(
|
$inserted = $this->diaryEntries()->diaryEntry(null, array(
|
||||||
'diary_day_id' => (int) $day['diary_day_id'],
|
'diary_day_id' => (int) $day['diary_day_id'],
|
||||||
'meal_type' => $meal_type,
|
'meal_type' => $meal_type,
|
||||||
'meal_id' => $meal_id,
|
'meal_id' => $meal_id,
|
||||||
@ -890,7 +890,7 @@ class API extends APIlite {
|
|||||||
if (is_array($day)) {
|
if (is_array($day)) {
|
||||||
return $this->mapDiaryDay($day);
|
return $this->mapDiaryDay($day);
|
||||||
}
|
}
|
||||||
$dayId = $this->diaryDays()->diaryDaySave(array(
|
$dayId = $this->diaryDays()->diaryDay(null, array(
|
||||||
'user_id' => $user_id,
|
'user_id' => $user_id,
|
||||||
'day_date' => $day_date,
|
'day_date' => $day_date,
|
||||||
'created_at' => '`NOW`'
|
'created_at' => '`NOW`'
|
||||||
|
|||||||
@ -405,6 +405,12 @@ select {
|
|||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-topbar__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
padding: var(--space-lg) var(--space-xl);
|
padding: var(--space-lg) var(--space-xl);
|
||||||
}
|
}
|
||||||
@ -609,6 +615,24 @@ select {
|
|||||||
margin-bottom: var(--space-md);
|
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 {
|
.app-bottom-tabs {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -671,4 +695,8 @@ select {
|
|||||||
color: var(--color-green);
|
color: var(--color-green);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-topbar__actions {
|
||||||
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
frontend/src/components/common/ThemeToggle.vue
Normal file
32
frontend/src/components/common/ThemeToggle.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
showLabel?: boolean
|
||||||
|
}>(), {
|
||||||
|
showLabel: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
themeStore.initialize()
|
||||||
|
|
||||||
|
const icon = computed(() => (themeStore.isDarkMode ? faMoon : faSun))
|
||||||
|
const themeLabel = computed(() => t(themeStore.currentThemeLabelKey))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="theme-btn"
|
||||||
|
:aria-label="t('theme.toggle')"
|
||||||
|
@click="themeStore.toggle"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
<span v-if="props.showLabel">{{ themeLabel }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@ -2,6 +2,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -33,8 +34,11 @@ const title = computed(() => {
|
|||||||
<div>
|
<div>
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="app-topbar__actions">
|
||||||
|
<ThemeToggle :show-label="false" />
|
||||||
<div class="app-topbar__user" v-if="auth.userEmail">
|
<div class="app-topbar__user" v-if="auth.userEmail">
|
||||||
{{ auth.userEmail }}
|
{{ auth.userEmail }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"light": "Světlý režim",
|
"light": "Světlý režim",
|
||||||
"dark": "Tmavý režim"
|
"dark": "Tmavý režim",
|
||||||
|
"toggle": "Přepnout režim"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"sk": "Slovenština",
|
"sk": "Slovenština",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accountTitle": "Nastavení účtu",
|
"accountTitle": "Nastavení účtu",
|
||||||
"loggedInAs": "Přihlášený uživatel",
|
"loggedInAs": "Přihlášený uživatel",
|
||||||
|
"themeTitle": "Vzhled",
|
||||||
"logout": "Odhlásit se"
|
"logout": "Odhlásit se"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"light": "Heller Modus",
|
"light": "Heller Modus",
|
||||||
"dark": "Dunkler Modus"
|
"dark": "Dunkler Modus",
|
||||||
|
"toggle": "Modus wechseln"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"sk": "Slowakisch",
|
"sk": "Slowakisch",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accountTitle": "Kontoeinstellungen",
|
"accountTitle": "Kontoeinstellungen",
|
||||||
"loggedInAs": "Angemeldet als",
|
"loggedInAs": "Angemeldet als",
|
||||||
|
"themeTitle": "Darstellung",
|
||||||
"logout": "Abmelden"
|
"logout": "Abmelden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"light": "Light mode",
|
"light": "Light mode",
|
||||||
"dark": "Dark mode"
|
"dark": "Dark mode",
|
||||||
|
"toggle": "Toggle mode"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"sk": "Slovak",
|
"sk": "Slovak",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accountTitle": "Account settings",
|
"accountTitle": "Account settings",
|
||||||
"loggedInAs": "Logged in as",
|
"loggedInAs": "Logged in as",
|
||||||
|
"themeTitle": "Appearance",
|
||||||
"logout": "Log out"
|
"logout": "Log out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"light": "Modo claro",
|
"light": "Modo claro",
|
||||||
"dark": "Modo oscuro"
|
"dark": "Modo oscuro",
|
||||||
|
"toggle": "Cambiar modo"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"sk": "Eslovaco",
|
"sk": "Eslovaco",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accountTitle": "Ajustes de cuenta",
|
"accountTitle": "Ajustes de cuenta",
|
||||||
"loggedInAs": "Sesión iniciada como",
|
"loggedInAs": "Sesión iniciada como",
|
||||||
|
"themeTitle": "Apariencia",
|
||||||
"logout": "Cerrar sesión"
|
"logout": "Cerrar sesión"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"light": "Svetlý režim",
|
"light": "Svetlý režim",
|
||||||
"dark": "Tmavý režim"
|
"dark": "Tmavý režim",
|
||||||
|
"toggle": "Prepnúť režim"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"sk": "Slovenčina",
|
"sk": "Slovenčina",
|
||||||
@ -133,6 +134,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"accountTitle": "Nastavenia účtu",
|
"accountTitle": "Nastavenia účtu",
|
||||||
"loggedInAs": "Prihlásený používateľ",
|
"loggedInAs": "Prihlásený používateľ",
|
||||||
|
"themeTitle": "Vzhľad",
|
||||||
"logout": "Odhlásiť sa"
|
"logout": "Odhlásiť sa"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,11 @@ import App from './App.vue'
|
|||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { pinia } from './stores'
|
import { pinia } from './stores'
|
||||||
|
import { useThemeStore } from './stores/theme'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const themeStore = useThemeStore(pinia)
|
||||||
|
themeStore.initialize()
|
||||||
|
|
||||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|||||||
44
frontend/src/stores/theme.ts
Normal file
44
frontend/src/stores/theme.ts
Normal file
@ -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<ThemeMode>('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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -8,17 +8,14 @@ import {
|
|||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
faLock,
|
faLock,
|
||||||
faMoon,
|
|
||||||
faRightToBracket,
|
faRightToBracket,
|
||||||
faSun,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
import ThemeToggle from '@/components/common/ThemeToggle.vue'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
|
import { SUPPORTED_LOCALES, type AppLocale } from '@/i18n'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
type ThemeMode = 'light' | 'dark'
|
|
||||||
|
|
||||||
const { t } = useI18n({ useScope: 'global' })
|
const { t } = useI18n({ useScope: 'global' })
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -57,31 +54,6 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const getInitialTheme = (): ThemeMode => {
|
|
||||||
const storedTheme = localStorage.getItem('theme')
|
|
||||||
return storedTheme === 'dark' ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = ref<ThemeMode>(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(() => {
|
const submitLabel = computed(() => {
|
||||||
return isLoading.value ? t('auth.submitting') : t('auth.submit')
|
return isLoading.value ? t('auth.submitting') : t('auth.submit')
|
||||||
})
|
})
|
||||||
@ -166,10 +138,7 @@ const submitForm = async () => {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="button" class="theme-btn" :aria-label="t('auth.themeToggle')" @click="toggleTheme">
|
<ThemeToggle />
|
||||||
<font-awesome-icon :icon="isDarkMode ? faMoon : faSun" />
|
|
||||||
<span>{{ themeLabel }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{{ t('auth.title') }}</h2>
|
<h2>{{ t('auth.title') }}</h2>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
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 { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -20,6 +21,10 @@ const onLogout = async () => {
|
|||||||
<p v-if="auth.userEmail">
|
<p v-if="auth.userEmail">
|
||||||
{{ t('settings.loggedInAs') }}: <strong>{{ auth.userEmail }}</strong>
|
{{ t('settings.loggedInAs') }}: <strong>{{ auth.userEmail }}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="settings-card__theme">
|
||||||
|
<span>{{ t('settings.themeTitle') }}</span>
|
||||||
|
<ThemeToggle />
|
||||||
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user