Added env files, completed user settings page.
This commit is contained in:
parent
88089f2e11
commit
0663296052
|
@ -0,0 +1,3 @@
|
|||
API_URL=http://localhost:8080
|
||||
CDN_URL=http://localhost:8082
|
||||
SEARCH_URL=http://localhost:8081
|
|
@ -0,0 +1,3 @@
|
|||
API_URL=https://api.gymboard.com
|
||||
CDN_URL=https://cdn.gymboard.com
|
||||
SEARCH_URL=https://search.gymboard.com
|
|
@ -11,6 +11,7 @@
|
|||
"@quasar/cli": "^2.0.0",
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"luxon": "^3.2.1",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.0.11",
|
||||
|
@ -2166,6 +2167,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "http://192.168.88.248:8081/repository/npm-public/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
|
@ -7937,6 +7947,11 @@
|
|||
"is-obj": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "http://192.168.88.248:8081/repository/npm-public/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
|
||||
},
|
||||
"eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@quasar/cli": "^2.0.0",
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"luxon": "^3.2.1",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.0.11",
|
||||
|
|
|
@ -12,6 +12,8 @@ const { configure } = require('quasar/wrappers');
|
|||
const path = require('path');
|
||||
const { withCtx } = require('vue');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = configure(function (ctx) {
|
||||
return {
|
||||
eslint: {
|
||||
|
@ -65,7 +67,9 @@ module.exports = configure(function (ctx) {
|
|||
// publicPath: '/',
|
||||
// analyze: true,
|
||||
env: {
|
||||
API: ctx.dev ? 'http://localhost:8080' : 'https://api.gymboard.com',
|
||||
API_URL: process.env.API_URL,
|
||||
CDN_URL: process.env.CDN_URL,
|
||||
SEARCH_URL: process.env.SEARCH_URL,
|
||||
},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {api} from 'src/api/main/index';
|
||||
import {AuthStoreType} from 'stores/auth-store';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import {WeightUnit} from 'src/api/main/submission';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
|
@ -21,7 +22,7 @@ export interface UserPersonalDetails {
|
|||
userId: string;
|
||||
birthDate?: string;
|
||||
currentWeight?: number;
|
||||
currentWeightUnit?: number;
|
||||
currentWeightUnit?: WeightUnit;
|
||||
currentMetricWeight?: number;
|
||||
sex: PersonSex;
|
||||
}
|
||||
|
|
|
@ -3,12 +3,9 @@ import GymsModule from 'src/api/main/gyms';
|
|||
import ExercisesModule from 'src/api/main/exercises';
|
||||
import LeaderboardsModule from 'src/api/main/leaderboards';
|
||||
import AuthModule from 'src/api/main/auth';
|
||||
|
||||
export const BASE_URL = 'http://localhost:8080';
|
||||
|
||||
// TODO: Figure out how to get the base URL from environment.
|
||||
console.log(process.env);
|
||||
export const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
baseURL: process.env.API_URL,
|
||||
});
|
||||
|
||||
class GymboardApi {
|
||||
|
|
|
@ -37,7 +37,7 @@ export default boot(({ app }) => {
|
|||
}
|
||||
|
||||
// Temporary override if you want to test a particular locale.
|
||||
i18n.global.locale.value = 'nl-NL';
|
||||
// i18n.global.locale.value = 'nl-NL';
|
||||
|
||||
// Set i18n instance on app
|
||||
app.use(i18n);
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
<div class="row justify-between">
|
||||
<span class="property-label">{{ label }}</span>
|
||||
|
||||
<div v-if="typeof modelValue === 'string'">
|
||||
<div v-if="(typeof modelValue === 'string' || typeof modelValue === 'number') && inputType !== 'select'">
|
||||
<q-input
|
||||
:model-value="modelValue"
|
||||
@update:modelValue="onValueUpdated"
|
||||
:type="inputType"
|
||||
:step="numberInputStep"
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
|
@ -14,6 +15,16 @@
|
|||
<div v-if="typeof modelValue === 'boolean'">
|
||||
<q-toggle :model-value="modelValue" @update:modelValue="onValueUpdated"/>
|
||||
</div>
|
||||
|
||||
<div v-if="inputType === 'select'">
|
||||
<q-select
|
||||
:model-value="modelValue"
|
||||
:options="selectOptions"
|
||||
emit-value
|
||||
map-options
|
||||
@update:modelValue="onValueUpdated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -24,6 +35,8 @@ interface Props {
|
|||
label: string;
|
||||
inputType?: any;
|
||||
modelValue: string | number | boolean | DateTime;
|
||||
selectOptions?: Array<object>;
|
||||
numberInputStep?: number;
|
||||
}
|
||||
defineProps<Props>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
|
|
@ -48,11 +48,25 @@ export default {
|
|||
},
|
||||
userSettingsPage: {
|
||||
title: 'Account Settings',
|
||||
password: 'Password',
|
||||
passwordHint: 'Set a new password for your account.',
|
||||
updatePassword: 'Update Password',
|
||||
passwordUpdated: 'Password updated.',
|
||||
passwordInvalid: 'Invalid password.',
|
||||
personalDetails: {
|
||||
birthDate: 'Date of Birth'
|
||||
title: 'Personal Details',
|
||||
birthDate: 'Date of Birth',
|
||||
sex: 'Sex',
|
||||
sexMale: 'Male',
|
||||
sexFemale: 'Female',
|
||||
sexUnknown: 'Prefer not to say',
|
||||
currentWeight: 'Current Weight',
|
||||
currentWeightUnit: 'Current Weight Unit'
|
||||
},
|
||||
preferences: {
|
||||
accountPrivate: 'Private Account'
|
||||
title: 'Preferences',
|
||||
accountPrivate: 'Private',
|
||||
language: 'Language'
|
||||
},
|
||||
save: 'Save',
|
||||
undo: 'Undo'
|
||||
|
@ -65,5 +79,9 @@ export default {
|
|||
},
|
||||
generalErrors: {
|
||||
apiError: 'An API error occurred. Please try again later.'
|
||||
},
|
||||
weightUnit: {
|
||||
kilograms: 'Kilograms',
|
||||
pounds: 'Pounds'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ export default {
|
|||
password: 'Wachtwoord',
|
||||
logIn: 'Inloggen',
|
||||
createAccount: 'Account aanmaken',
|
||||
authFailed: 'Ongeldige inloggegevens.',
|
||||
},
|
||||
indexPage: {
|
||||
searchHint: 'Zoek een sportschool',
|
||||
|
@ -39,8 +40,48 @@ export default {
|
|||
submit: 'Sturen',
|
||||
},
|
||||
},
|
||||
userPage: {
|
||||
notFound: {
|
||||
title: 'Gebruiker niet gevonden',
|
||||
description: 'Wij konden de gebruiker voor wie jij zoekt niet vinden, helaas.'
|
||||
}
|
||||
},
|
||||
userSettingsPage: {
|
||||
title: 'Account instellingen',
|
||||
password: 'Wachtwoord',
|
||||
passwordHint: 'Stel een nieuw wachtwoord voor je account in.',
|
||||
updatePassword: 'Wachtwoord bijwerken',
|
||||
passwordUpdated: 'Wachtwoord succesvol bijgewerkt.',
|
||||
passwordInvalid: 'Ongeldig wachtwoord.',
|
||||
personalDetails: {
|
||||
title: 'Persoonlijke gegevens',
|
||||
birthDate: 'Geboortedatum',
|
||||
sex: 'Geslacht',
|
||||
sexMale: 'Mannelijk',
|
||||
sexFemale: 'Vrouwelijk',
|
||||
sexUnknown: 'Liever niet zeggen',
|
||||
currentWeight: 'Huidige gewicht',
|
||||
currentWeightUnit: 'Eenheden van huidige gewicht'
|
||||
},
|
||||
preferences: {
|
||||
title: 'Voorkeuren',
|
||||
accountPrivate: 'Privaat',
|
||||
language: 'Taal'
|
||||
},
|
||||
save: 'Opslaan',
|
||||
undo: 'Terugzetten'
|
||||
},
|
||||
accountMenuItem: {
|
||||
logIn: 'Inloggen',
|
||||
profile: 'Profile',
|
||||
settings: 'Instellingen',
|
||||
logOut: 'Uitloggen',
|
||||
},
|
||||
generalErrors: {
|
||||
apiError: 'Er is een API fout opgetreden. Probeer het nogmals later.'
|
||||
},
|
||||
weightUnit: {
|
||||
kilograms: 'Kilogram',
|
||||
pounds: 'Ponden'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<router-link
|
||||
:to="{
|
||||
path: '/register',
|
||||
query: route.query.next ? { next: route.query.next } : {},
|
||||
query: route.query.next ? { next: route.query.next } : {}
|
||||
}"
|
||||
class="q-mt-md text-primary text-center col-12"
|
||||
>
|
||||
|
@ -78,7 +78,7 @@ const passwordVisible = ref(false);
|
|||
/**
|
||||
* The main login function. It attempts to log in the user, and gracefully
|
||||
* handles failures.
|
||||
*
|
||||
*
|
||||
* Upon successful login, we set the app's locale to the user's preferred
|
||||
* locale, and then redirect to the pre-configured next URL (if there is one).
|
||||
*/
|
||||
|
@ -87,8 +87,8 @@ async function tryLogin() {
|
|||
await api.auth.login(authStore, loginModel.value);
|
||||
|
||||
// Set the locale to the user's preferred locale.
|
||||
i18n.locale.value = resolveLocale(authStore.user?.preferences?.locale);
|
||||
|
||||
i18n.locale.value = resolveLocale(authStore.user?.preferences?.locale).value;
|
||||
|
||||
// Redirect back to whatever was set as the next URL.
|
||||
const dest = route.query.next
|
||||
? decodeURIComponent(route.query.next as string)
|
||||
|
|
|
@ -1,17 +1,58 @@
|
|||
<!--
|
||||
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>Personal Information</h4>
|
||||
<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">
|
||||
<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>
|
||||
|
@ -19,14 +60,21 @@
|
|||
</div>
|
||||
|
||||
<div v-if="preferences">
|
||||
<h4>Preferences</h4>
|
||||
<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">
|
||||
<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>
|
||||
|
@ -44,10 +92,16 @@ 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();
|
||||
|
@ -55,6 +109,8 @@ 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;
|
||||
|
@ -67,6 +123,8 @@ onMounted(async () => {
|
|||
|
||||
preferences.value = await api.auth.getMyPreferences(authStore);
|
||||
initialPreferences = structuredClone(toRaw(preferences.value));
|
||||
|
||||
newPassword.value = '';
|
||||
});
|
||||
|
||||
const personalDetailsChanged = computed(() => {
|
||||
|
@ -79,10 +137,23 @@ const preferencesChanged = computed(() => {
|
|||
JSON.stringify(initialPreferences) !== JSON.stringify(preferences.value);
|
||||
});
|
||||
|
||||
const canUpdatePassword = computed(() => {
|
||||
const p = newPassword.value;
|
||||
return p.length >= 8;
|
||||
});
|
||||
|
||||
async function savePersonalDetails() {
|
||||
if (personalDetails.value) {
|
||||
personalDetails.value = await api.auth.updateMyPersonalDetails(authStore, personalDetails.value);
|
||||
initialPersonalDetails = structuredClone(toRaw(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,14 +165,61 @@ 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>
|
||||
|
|
Loading…
Reference in New Issue