Gymboard/gymboard-app/src/pages/auth/UserSettingsPage.vue

226 lines
7.1 KiB
Vue

<!--
The page where users can edit their personal information and preferences.
-->
<template>
<q-page>
<StandardCenteredPage>
<h3>{{ $t('userSettingsPage.title') }}</h3>
<hr>
<div class="row justify-between">
<span class="property-label">{{ $t('userSettingsPage.password') }}</span>
<q-input
type="password"
v-model="newPassword"
:hint="$t('userSettingsPage.passwordHint')"
/>
</div>
<div v-if="canUpdatePassword">
<q-btn :label="$t('userSettingsPage.updatePassword')" color="positive" @click="updatePassword"/>
</div>
<div v-if="personalDetails">
<h4>{{ $t('userSettingsPage.personalDetails.title') }}</h4>
<EditablePropertyRow
v-model="personalDetails.birthDate"
:label="$t('userSettingsPage.personalDetails.birthDate')"
input-type="date"
/>
<EditablePropertyRow
v-model="personalDetails.sex"
:label="$t('userSettingsPage.personalDetails.sex')"
input-type="select"
:select-options="[
{ label: $t('userSettingsPage.personalDetails.sexMale'), value: 'MALE' },
{ label: $t('userSettingsPage.personalDetails.sexFemale'), value: 'FEMALE' },
{ label: $t('userSettingsPage.personalDetails.sexUnknown'), value: 'UNKNOWN' },
]"
/>
<EditablePropertyRow
v-model="personalDetails.currentWeight"
:label="$t('userSettingsPage.personalDetails.currentWeight')"
input-type="number"
:number-input-step="0.1"
/>
<EditablePropertyRow
v-model="personalDetails.currentWeightUnit"
:label="$t('userSettingsPage.personalDetails.currentWeightUnit')"
input-type="select"
:select-options="[
{ label: $t('weightUnit.kilograms'), value: WeightUnit.KILOGRAMS },
{ label: $t('weightUnit.pounds'), value: WeightUnit.POUNDS },
]"
/>
<div v-if="personalDetailsChanged" class="save-button-row">
<q-btn :label="$t('userSettingsPage.save')" color="positive" @click="savePersonalDetails"/>
<q-btn :label="$t('userSettingsPage.undo')" color="secondary" @click="undoPersonalDetailsChanges"/>
</div>
</div>
<div v-if="preferences">
<h4>{{ $t('userSettingsPage.preferences.title') }}</h4>
<EditablePropertyRow
v-model="preferences.accountPrivate"
:label="$t('userSettingsPage.preferences.accountPrivate')"
/>
<EditablePropertyRow
v-model="preferences.locale"
:label="$t('userSettingsPage.preferences.language')"
input-type="select"
:select-options="supportedLocales"
@update:modelValue="updateLocale"
/>
<div v-if="preferencesChanged" class="save-button-row">
<q-btn :label="$t('userSettingsPage.save')" color="positive" @click="savePreferences"/>
<q-btn :label="$t('userSettingsPage.undo')" color="secondary" @click="undoPreferencesChanges"/>
</div>
</div>
</StandardCenteredPage>
</q-page>
</template>
<script setup lang="ts">
import StandardCenteredPage from 'components/StandardCenteredPage.vue';
import {useRoute, useRouter} from 'vue-router';
import {useAuthStore} from 'stores/auth-store';
import {computed, onMounted, ref, Ref, toRaw} from 'vue';
import {UserPersonalDetails, UserPreferences} from 'src/api/main/auth';
import api from 'src/api/main';
import EditablePropertyRow from 'components/EditablePropertyRow.vue';
import {WeightUnit} from 'src/api/main/submission';
import {resolveLocale, supportedLocales} from 'src/i18n';
import {useI18n} from 'vue-i18n';
import {useQuasar} from 'quasar';
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const authStore = useAuthStore();
const i18n = useI18n({useScope: 'global'});
const personalDetails: Ref<UserPersonalDetails | undefined> = ref();
const preferences: Ref<UserPreferences | undefined> = ref();
let initialPersonalDetails: UserPersonalDetails | null = null;
let initialPreferences: UserPreferences | null = null;
const newPassword = ref('');
onMounted(async () => {
// Redirect away from the page if the user isn't viewing their own settings.
const userId = route.params.userId as string;
if (!authStore.user || authStore.user.id !== userId) {
await router.push(`/users/${userId}`);
}
personalDetails.value = await api.auth.getMyPersonalDetails(authStore);
initialPersonalDetails = structuredClone(toRaw(personalDetails.value));
preferences.value = await api.auth.getMyPreferences(authStore);
initialPreferences = structuredClone(toRaw(preferences.value));
newPassword.value = '';
});
const personalDetailsChanged = computed(() => {
return initialPersonalDetails !== null &&
JSON.stringify(initialPersonalDetails) !== JSON.stringify(personalDetails.value);
});
const preferencesChanged = computed(() => {
return initialPreferences !== null &&
JSON.stringify(initialPreferences) !== JSON.stringify(preferences.value);
});
const canUpdatePassword = computed(() => {
const p = newPassword.value;
return p.length >= 8;
});
async function savePersonalDetails() {
if (personalDetails.value) {
try {
personalDetails.value = await api.auth.updateMyPersonalDetails(authStore, personalDetails.value);
initialPersonalDetails = structuredClone(toRaw(personalDetails.value));
} catch (error: any) {
if (error.response && error.response.status === 400) {
console.warn('bad request');
} else {
console.error(error);
}
}
}
}
function undoPersonalDetailsChanges() {
personalDetails.value = structuredClone(initialPersonalDetails);
}
async function savePreferences() {
if (preferences.value) {
preferences.value = await api.auth.updateMyPreferences(authStore, preferences.value);
initialPreferences = structuredClone(toRaw(preferences.value));
updateLocale();
}
}
function updateLocale() {
const chosenLocale = resolveLocale(preferences.value?.locale);
i18n.locale.value = chosenLocale.value;
}
function undoPreferencesChanges() {
preferences.value = structuredClone(initialPreferences);
updateLocale();
}
async function updatePassword() {
try {
await api.auth.updatePassword(newPassword.value, authStore);
newPassword.value = '';
quasar.notify({
message: i18n.t('userSettingsPage.passwordUpdated'),
type: 'positive',
position: 'top'
});
} catch (error: any) {
if (error.response && error.response.status === 400) {
newPassword.value = '';
quasar.notify({
message: i18n.t('userSettingsPage.passwordInvalid'),
type: 'warning',
position: 'top'
});
} else {
quasar.notify({
message: i18n.t('generalErrors.apiError'),
type: 'danger',
position: 'top'
});
}
}
}
</script>
<style scoped>
.property-label {
font-weight: bold;
margin-top: auto;
margin-bottom: auto;
}
.save-button-row {
display: flex;
flex-direction: row;
column-gap: 10px;
justify-content: flex-end;
margin-top: 10px;
margin-bottom: 10px;
}
</style>