Client - display user stats (wip)

This commit is contained in:
Sam 2021-08-18 17:10:16 +02:00
parent cd20e3c167
commit 8247751281
14 changed files with 257 additions and 8 deletions

1
fittrackee_client/.env Normal file
View File

@ -0,0 +1 @@
VUE_APP_API_URL=http://localhost:5000

View File

@ -0,0 +1,28 @@
<template>
<div class="card">
<div class="card-title">
<slot name="title"></slot>
</div>
<div class="card-content">
<slot name="content"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Card',
})
</script>
<style lang="scss">
@import '~@/scss/base';
.card {
border: solid 1px var(--card-border-color);
border-radius: 4px;
margin: $default-margin;
padding: $default-padding $default-padding * 2;
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="user-stat-card">
<Card>
<template #content>
<div class="stat-content">
<div class="stat-icon">
<i class="fa" :class="`fa-${icon}`" />
</div>
<div class="stat-details">
<div class="stat-huge">{{ value }}</div>
<div>{{ text }}</div>
</div>
</div>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Card from '@/components/Common/Card.vue'
export default defineComponent({
name: 'UserStatCard',
components: {
Card,
},
props: {
icon: {
type: String,
required: true,
},
value: {
type: [String, Number],
required: true,
},
text: {
type: String,
required: true,
},
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
.user-stat-card {
flex: 1;
max-width: 25%;
@media screen and (max-width: $small-limit) {
flex: 1 0 50%;
max-width: 49%;
}
.stat-content {
display: flex;
flex-direction: row;
justify-content: space-between;
.stat-icon {
width: 30%;
text-align: center;
vertical-align: center;
@media screen and (max-width: $medium-limit) {
width: 50%;
text-align: left;
}
.fa {
font-size: 3em;
@media screen and (max-width: $medium-limit) {
font-size: 2em;
}
@media screen and (max-width: $x-small-limit) {
font-size: 1.5em;
}
}
}
.stat-details {
width: 70%;
text-align: right;
@media screen and (max-width: $medium-limit) {
width: 100%;
}
.stat-huge {
font-size: 1.7em;
font-weight: bold;
@media screen and (max-width: $medium-limit) {
font-size: 1.3em;
}
@media screen and (max-width: $x-small-limit) {
font-size: 1em;
}
}
}
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div id="user-stats">
<UserStatCard
icon="calendar"
:value="user.nb_workouts"
:text="t('workouts.WORKOUT', user.nb_workouts)"
/>
<UserStatCard
icon="road"
:value="Number(user.total_distance).toFixed(2)"
:text="t('workouts.KM')"
/>
<UserStatCard
icon="clock-o"
:value="total_duration.days"
:text="total_duration.duration"
/>
<UserStatCard
icon="tags"
:value="user.nb_sports"
:text="t('workouts.SPORT', user.nb_sports)"
/>
</div>
</template>
<script lang="ts">
import { ComputedRef, PropType, defineComponent, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { IAuthUserProfile } from '@/store/modules/user/interfaces'
import UserStatCard from '@/components/Dashboard/UserStatCard.vue'
export default defineComponent({
name: 'UserStats',
components: {
UserStatCard,
},
props: {
user: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
const total_duration: ComputedRef<string> = computed(
() => props.user.total_duration
)
function get_duration(total_duration: ComputedRef<string>) {
const duration = total_duration.value.match(/day/g)
? total_duration.value.split(', ')[1]
: total_duration.value
return {
days: total_duration.value.match(/day/g)
? `${total_duration.value.split(' ')[0]} ${
total_duration.value.match(/days/g)
? t('common.DAY', 2)
: t('common.DAY', 1)
}`
: `0 ${t('common.DAY', 2)},`,
duration: `${duration.split(':')[0]}h ${duration.split(':')[1]}min`,
}
}
return {
t,
total_duration: computed(() => get_duration(total_duration)),
}
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
#user-stats {
display: flex;
flex: 1 0 25%;
justify-content: space-around;
flex-wrap: wrap;
}
</style>

View File

@ -67,7 +67,7 @@
} }
} }
@media screen and (max-width: $small-limit) { @media screen and (max-width: $x-small-limit) {
.footer-items { .footer-items {
font-size: 0.85em; font-size: 0.85em;

View File

@ -23,7 +23,9 @@
<router-link class="nav-item" to="/">{{ <router-link class="nav-item" to="/">{{
t('dashboard.DASHBOARD') t('dashboard.DASHBOARD')
}}</router-link> }}</router-link>
<div class="nav-item">{{ t('workouts.WORKOUTS') }}</div> <div class="nav-item">
{{ capitalize(t('workouts.WORKOUT', 1)) }}
</div>
<div class="nav-item">{{ t('statistics.STATISTICS') }}</div> <div class="nav-item">{{ t('statistics.STATISTICS') }}</div>
<div class="nav-item">{{ t('administration.ADMIN') }}</div> <div class="nav-item">{{ t('administration.ADMIN') }}</div>
<div class="nav-item">{{ t('workouts.ADD_WORKOUT') }}</div> <div class="nav-item">{{ t('workouts.ADD_WORKOUT') }}</div>
@ -79,7 +81,7 @@
import { ROOT_STORE, USER_STORE } from '@/store/constants' import { ROOT_STORE, USER_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/store/modules/user/interfaces' import { IAuthUserProfile } from '@/store/modules/user/interfaces'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
import { getApiUrl } from '@/utils' import { capitalize, getApiUrl } from '@/utils'
import Dropdown from '@/components/Common/Dropdown.vue' import Dropdown from '@/components/Common/Dropdown.vue'
export default defineComponent({ export default defineComponent({
@ -134,6 +136,7 @@
isMenuOpen, isMenuOpen,
language, language,
t, t,
capitalize,
openMenu, openMenu,
closeMenu, closeMenu,
updateLanguage, updateLanguage,

View File

@ -1,3 +1,4 @@
{ {
"DAY": "day | days",
"HOME": "Home" "HOME": "Home"
} }

View File

@ -1,4 +1,6 @@
{ {
"ADD_WORKOUT": "Add workout", "ADD_WORKOUT": "Add workout",
"WORKOUTS": "Workouts" "KM": "km",
"SPORT": "sport | sports",
"WORKOUT": "workout | workouts"
} }

View File

@ -1,3 +1,4 @@
{ {
"DAY": "jour | jours",
"HOME": "Accueil" "HOME": "Accueil"
} }

View File

@ -1,4 +1,6 @@
{ {
"ADD_WORKOUT": "Ajouter une séance", "ADD_WORKOUT": "Ajouter une séance",
"WORKOUTS": "Séances" "KM": "km",
"SPORT": "sport | sports",
"WORKOUT": "séance | séances"
} }

View File

@ -6,6 +6,7 @@
--app-loading-color: #f3f3f3; --app-loading-color: #f3f3f3;
--app-loading-top-color: var(--app-color); --app-loading-top-color: var(--app-color);
--card-border-color: #c4c7cf;
--input-border-color: #9da3af; --input-border-color: #9da3af;
--nav-bar-background-color: #FFFFFF; --nav-bar-background-color: #FFFFFF;

View File

@ -3,7 +3,8 @@
*/ */
$container-width: 1140px; $container-width: 1140px;
$medium-limit: 1000px; $medium-limit: 1000px;
$small-limit: 500px; $small-limit: 700px;
$x-small-limit: 500px;
/* /*
* === HEIGHT === * === HEIGHT ===

View File

@ -7,7 +7,7 @@ import { IRootState } from '@/store/modules/root/interfaces'
export const getApiUrl = (): string => { export const getApiUrl = (): string => {
return process.env.NODE_ENV === 'production' return process.env.NODE_ENV === 'production'
? '/api' ? '/api'
: 'http://localhost:5000/api' : `${process.env.VUE_APP_API_URL}/api/`
} }
// TODO: update api error messages to remove these workarounds // TODO: update api error messages to remove these workarounds
@ -39,3 +39,6 @@ export const handleError = (
: `api.ERROR.${removeLastDot(errorMessages)}` : `api.ERROR.${removeLastDot(errorMessages)}`
) )
} }
export const capitalize = (text: string): string =>
text.charAt(0).toUpperCase() + text.slice(1)

View File

@ -1,5 +1,30 @@
<template> <template>
<div id="dashboard"> <div id="dashboard">
<p>TODO Dashboard</p> <div class="container">
<UserStats :user="authUser" v-if="authUser.username" />
</div>
</div> </div>
</template> </template>
<script lang="ts">
import { computed, ComputedRef, defineComponent } from 'vue'
import { USER_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/store/modules/user/interfaces'
import { useStore } from '@/use/useStore'
import UserStats from '@/components/Dashboard/UserStats.vue'
export default defineComponent({
name: 'Dashboard',
components: {
UserStats,
},
setup() {
const store = useStore()
const authUser: ComputedRef<IAuthUserProfile> = computed(
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
return { authUser }
},
})
</script>