Client - use nested routes for user profile

This commit is contained in:
Sam 2021-10-23 20:04:38 +02:00
parent d072189936
commit 3bcc93bb1a
8 changed files with 215 additions and 173 deletions

View File

@ -0,0 +1,121 @@
<template>
<div class="box user-header">
<UserPicture :user="user" />
<div class="user-details">
<div class="user-name">{{ user.username }}</div>
<div class="user-stats">
<div class="user-stat">
<span class="stat-number">{{ user.nb_workouts }}</span>
<span class="stat-label">
{{ $t('workouts.WORKOUT', user.nb_workouts) }}
</span>
</div>
<div class="user-stat">
<span class="stat-number">{{
Number(user.total_distance).toFixed(0)
}}</span>
<span class="stat-label">km</span>
</div>
<div class="user-stat hide-small">
<span class="stat-number">{{ user.nb_sports }}</span>
<span class="stat-label">
{{ $t('workouts.SPORT', user.nb_sports) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue'
import { IAuthUserProfile } from '@/types/user'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'ProfileDisplay',
components: {
UserPicture,
},
props: {
user: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
},
setup(props) {
return {
authUserPictureUrl: computed(() =>
props.user.picture
? `${getApiUrl()}/users/${
props.user.username
}/picture?${Date.now()}`
: ''
),
}
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
.user-header {
display: flex;
align-items: stretch;
.user-details {
flex-grow: 1;
padding: $default-padding;
display: flex;
flex-direction: column;
align-items: center;
.user-name {
font-size: 2em;
height: 60%;
}
.user-stats {
display: flex;
gap: $default-padding * 4;
.user-stat {
display: flex;
flex-direction: column;
align-items: center;
padding-top: $default-padding;
.stat-number,
.stat-label {
padding: 0 $default-padding * 0.5;
}
.stat-number {
font-weight: bold;
font-size: 1.5em;
}
}
}
@media screen and (max-width: $x-small-limit) {
.user-name {
font-size: 1.5em;
}
.user-stats {
gap: $default-padding * 2;
.user-stat {
.stat-number {
font-weight: bold;
font-size: 1.2em;
}
&.hide-small {
display: none;
}
}
}
}
}
}
</style>

View File

@ -1,55 +1,24 @@
<template> <template>
<div id="user-profile"> <div id="user-profile">
<div class="box user-header"> <UserHeader :user="user" />
<UserPicture :user="user" />
<div class="user-details">
<div class="user-name">{{ user.username }}</div>
<div class="user-stats">
<div class="user-stat">
<span class="stat-number">{{ user.nb_workouts }}</span>
<span class="stat-label">
{{ $t('workouts.WORKOUT', user.nb_workouts) }}
</span>
</div>
<div class="user-stat">
<span class="stat-number">{{
Number(user.total_distance).toFixed(0)
}}</span>
<span class="stat-label">km</span>
</div>
<div class="user-stat hide-small">
<span class="stat-number">{{ user.nb_sports }}</span>
<span class="stat-label">
{{ $t('workouts.SPORT', user.nb_sports) }}
</span>
</div>
</div>
</div>
</div>
<div class="box"> <div class="box">
<UserProfileTabs :tabs="tabs" :selectedTab="tab" :edition="false" /> <UserProfileTabs :tabs="tabs" :selectedTab="tab" :edition="false" />
<UserInfos :user="user" v-if="tab === 'PROFILE'" /> <router-view :user="user"></router-view>
<UserPreferences :user="user" v-if="tab === 'PREFERENCES'" />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ComputedRef, PropType, computed, defineComponent } from 'vue' import { PropType, defineComponent } from 'vue'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue' import UserHeader from '@/components/User/ProfileDisplay/UserHeader.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
import UserPicture from '@/components/User/UserPicture.vue'
import UserProfileTabs from '@/components/User/UserProfileTabs.vue' import UserProfileTabs from '@/components/User/UserProfileTabs.vue'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { getApiUrl } from '@/utils'
export default defineComponent({ export default defineComponent({
name: 'ProfileDisplay', name: 'ProfileDisplay',
components: { components: {
UserInfos, UserHeader,
UserPicture,
UserPreferences,
UserProfileTabs, UserProfileTabs,
}, },
props: { props: {
@ -62,14 +31,10 @@
required: true, required: true,
}, },
}, },
setup(props) { setup() {
const tabs = ['PROFILE', 'PREFERENCES'] return {
const authUserPictureUrl: ComputedRef<string> = computed(() => tabs: ['PROFILE', 'PREFERENCES'],
props.user.picture }
? `${getApiUrl()}/users/${props.user.username}/picture?${Date.now()}`
: ''
)
return { authUserPictureUrl, tabs }
}, },
}) })
</script> </script>
@ -84,62 +49,5 @@
width: 100%; width: 100%;
margin: 0 auto 50px auto; margin: 0 auto 50px auto;
} }
.user-header {
display: flex;
align-items: stretch;
.user-details {
flex-grow: 1;
padding: $default-padding;
display: flex;
flex-direction: column;
align-items: center;
.user-name {
font-size: 2em;
height: 60%;
}
.user-stats {
display: flex;
gap: $default-padding * 4;
.user-stat {
display: flex;
flex-direction: column;
align-items: center;
padding-top: $default-padding;
.stat-number,
.stat-label {
padding: 0 $default-padding * 0.5;
}
.stat-number {
font-weight: bold;
font-size: 1.5em;
}
}
}
@media screen and (max-width: $x-small-limit) {
.user-name {
font-size: 1.5em;
}
.user-stats {
gap: $default-padding * 2;
.user-stat {
.stat-number {
font-weight: bold;
font-size: 1.2em;
}
&.hide-small {
display: none;
}
}
}
}
}
}
} }
</style> </style>

View File

@ -81,7 +81,7 @@
<button class="confirm" type="submit"> <button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }} {{ $t('buttons.SUBMIT') }}
</button> </button>
<button class="cancel" @click.prevent="$router.go(-1)"> <button class="cancel" @click.prevent="$router.push('/profile')">
{{ $t('buttons.CANCEL') }} {{ $t('buttons.CANCEL') }}
</button> </button>
<button class="danger" @click.prevent="updateDisplayModal(true)"> <button class="danger" @click.prevent="updateDisplayModal(true)">

View File

@ -39,7 +39,10 @@
<button class="confirm" type="submit"> <button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }} {{ $t('buttons.SUBMIT') }}
</button> </button>
<button class="cancel" @click.prevent="$router.go(-1)"> <button
class="cancel"
@click.prevent="$router.push('/profile/preferences')"
>
{{ $t('buttons.CANCEL') }} {{ $t('buttons.CANCEL') }}
</button> </button>
</div> </div>

View File

@ -1,7 +1,9 @@
<template> <template>
<div id="user-profile-edition"> <div id="user-profile-edition">
<Card> <Card>
<template #title>{{ $t(`user.PROFILE.${tab}_EDITION`) }}</template> <template #title>
{{ $t(`user.PROFILE.${tab}_EDITION`) }}
</template>
<template #content> <template #content>
<UserProfileTabs <UserProfileTabs
:tabs="tabs" :tabs="tabs"
@ -9,20 +11,15 @@
:edition="true" :edition="true"
:disabled="loading" :disabled="loading"
/> />
<UserInfosEdition v-if="tab === 'PROFILE'" :user="user" /> <router-view :user="user"></router-view>
<UserPreferencesEdition v-if="tab === 'PREFERENCES'" :user="user" />
<UserPictureEdition v-if="tab === 'PICTURE'" :user="user" />
</template> </template>
</Card> </Card>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType, computed, defineComponent, ref } from 'vue' import { computed, defineComponent, PropType } from 'vue'
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
import UserProfileTabs from '@/components/User/UserProfileTabs.vue' import UserProfileTabs from '@/components/User/UserProfileTabs.vue'
import { USER_STORE } from '@/store/constants' import { USER_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
@ -31,9 +28,6 @@
export default defineComponent({ export default defineComponent({
name: 'ProfileEdition', name: 'ProfileEdition',
components: { components: {
UserInfosEdition,
UserPictureEdition,
UserPreferencesEdition,
UserProfileTabs, UserProfileTabs,
}, },
props: { props: {
@ -46,14 +40,12 @@
required: true, required: true,
}, },
}, },
setup(props) { setup() {
const store = useStore() const store = useStore()
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES'] return {
const selectedTab = ref(props.tab) loading: computed(() => store.getters[USER_STORE.GETTERS.USER_LOADING]),
const loading = computed( tabs: ['PROFILE', 'PICTURE', 'PREFERENCES'],
() => store.getters[USER_STORE.GETTERS.USER_LOADING] }
)
return { loading, selectedTab, tabs }
}, },
}) })
</script> </script>

View File

@ -1,18 +1,31 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Profile from '@/components/User/ProfileDisplay/index.vue'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
import ProfileEdition from '@/components/User/ProfileEdition/index.vue'
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
import store from '@/store' import store from '@/store'
import { USER_STORE } from '@/store/constants' import { USER_STORE } from '@/store/constants'
import AddWorkout from '@/views/AddWorkout.vue'
import Dashboard from '@/views/DashBoard.vue' import Dashboard from '@/views/DashBoard.vue'
import LoginOrRegister from '@/views/LoginOrRegister.vue' import LoginOrRegister from '@/views/LoginOrRegister.vue'
import NotFoundView from '@/views/NotFoundView.vue' import NotFoundView from '@/views/NotFoundView.vue'
import PasswordResetView from '@/views/PasswordResetView.vue' import PasswordResetView from '@/views/PasswordResetView.vue'
import ProfileView from '@/views/ProfileView.vue' import ProfileView from '@/views/ProfileView.vue'
import StatisticsView from '@/views/StatisticsView.vue' import StatisticsView from '@/views/StatisticsView.vue'
import AddWorkout from '@/views/workouts/AddWorkout.vue'
import EditWorkout from '@/views/workouts/EditWorkout.vue' import EditWorkout from '@/views/workouts/EditWorkout.vue'
import Workout from '@/views/workouts/Workout.vue' import Workout from '@/views/workouts/Workout.vue'
import Workouts from '@/views/workouts/WorkoutsView.vue' import Workouts from '@/views/workouts/WorkoutsView.vue'
const getTabFromPath = (path: string): string => {
const regex = /(\/profile)(\/edit)*(\/*)/
const tag = path.replace(regex, '').toUpperCase()
return tag === '' ? 'PROFILE' : tag.toUpperCase()
}
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: '/',
@ -31,12 +44,6 @@ const routes: Array<RouteRecordRaw> = [
component: LoginOrRegister, component: LoginOrRegister,
props: { action: 'register' }, props: { action: 'register' },
}, },
{
path: '/profile',
name: 'Profile',
component: ProfileView,
props: { edition: false, tab: 'PROFILE' },
},
{ {
path: '/password-reset/sent', path: '/password-reset/sent',
name: 'PasswordEmailSent', name: 'PasswordEmailSent',
@ -62,28 +69,56 @@ const routes: Array<RouteRecordRaw> = [
props: { action: 'reset' }, props: { action: 'reset' },
}, },
{ {
path: '/profile/edit/picture', path: '/profile',
name: 'UserPictureEdition', name: 'Profile',
component: ProfileView, component: ProfileView,
props: { edition: true, tab: 'PICTURE' }, children: [
{
path: '',
name: 'UserProfile',
component: Profile,
props: (route) => ({
tab: getTabFromPath(route.path),
}),
children: [
{
path: '',
name: 'UserInfos',
component: UserInfos,
}, },
{ {
path: '/profile/preferences', path: 'preferences',
name: 'UserPreferences', name: 'UserPreferences',
component: ProfileView, component: UserPreferences,
props: { edition: false, tab: 'PREFERENCES' }, },
],
}, },
{ {
path: '/profile/edit/preferences', path: 'edit',
name: 'UserProfileEdition',
component: ProfileEdition,
props: (route) => ({
tab: getTabFromPath(route.path),
}),
children: [
{
path: '',
name: 'UserInfosEdition',
component: UserInfosEdition,
},
{
path: 'picture',
name: 'UserPictureEdition',
component: UserPictureEdition,
},
{
path: 'preferences',
name: 'UserPreferencesEdition', name: 'UserPreferencesEdition',
component: ProfileView, component: UserPreferencesEdition,
props: { edition: true, tab: 'PREFERENCES' },
}, },
{ ],
path: '/profile/edit', },
name: 'ProfileEdition', ],
component: ProfileView,
props: { edition: true, tab: 'PROFILE' },
}, },
{ {
path: '/statistics', path: '/statistics',

View File

@ -1,41 +1,24 @@
<template> <template>
<div id="profile" class="container" v-if="authUser.username"> <div id="profile" class="container" v-if="authUser.username">
<ProfileEdition :user="authUser" :tab="tab" v-if="edition" /> <router-view :user="authUser"></router-view>
<Profile :user="authUser" :tab="tab" v-else />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from 'vue' import { computed, ComputedRef, defineComponent } from 'vue'
import Profile from '@/components/User/ProfileDisplay/index.vue'
import ProfileEdition from '@/components/User/ProfileEdition/index.vue'
import { USER_STORE } from '@/store/constants' import { USER_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
export default defineComponent({ export default defineComponent({
name: 'ProfileView', name: 'ProfileView',
components: {
Profile,
ProfileEdition,
},
props: {
edition: {
type: Boolean,
required: true,
},
tab: {
type: String,
required: true,
},
},
setup() { setup() {
const store = useStore() const store = useStore()
return { const authUser: ComputedRef<IAuthUserProfile> = computed(
authUser: computed(
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE] () => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
), )
} return { authUser }
}, },
}) })
</script> </script>