Client - init workouts timeline

This commit is contained in:
Sam 2021-09-20 12:18:40 +02:00
parent f3f1142479
commit 45abd58b79
16 changed files with 331 additions and 29 deletions

View File

@ -50,7 +50,7 @@
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
@import '~@/scss/base'; @import '~@/scss/base';
.app-container { .app-container {
height: $app-height; height: $app-height;

View File

@ -0,0 +1,65 @@
<template>
<div class="static-map">
<img
class="map-image"
:src="`${getApiUrl()}workouts/map/${workout.map}?${Date.now()}`"
alt="workout map"
/>
<div class="map-attribution">
<span class="map-attribution-text">©</span>
<a
class="map-attribution-text"
href="http://www.openstreetmap.org/copyright"
target="_blank"
rel="noopener noreferrer"
>
OpenStreetMap
</a>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { IWorkout } from '@/types/workouts'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'StaticMap',
props: {
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
},
setup() {
return { getApiUrl }
},
})
</script>
<style lang="scss">
@import '~@/scss/base';
.static-map {
display: flex;
position: relative;
.map-image {
height: 225px;
width: 400px;
}
.map-attribution {
top: 0;
right: 0;
font-size: 11px;
position: absolute;
}
.map-attribution-text {
background-color: rgba(255, 255, 255, 0.7);
}
}
</style>

View File

@ -1,3 +0,0 @@
<template>
<div>Timeline</div>
</template>

View File

@ -0,0 +1,156 @@
<template>
<div class="timeline-workout">
<Card>
<template #content>
<div class="workout-user-date">
<div class="workout-user">
<img
class="profile-img"
v-if="userPictureUrl !== ''"
alt="User picture"
:src="userPictureUrl"
/>
<div v-else class="no-picture">
<i class="fa fa-user-circle-o" aria-hidden="true" />
</div>
{{ user.username }}
</div>
<div
class="workout-date"
:title="
format(
getDateWithTZ(workout.workout_date, user.timezone),
'dd/MM/yyyy HH:mm'
)
"
>
{{
formatDistance(new Date(workout.workout_date), new Date(), {
addSuffix: true,
locale,
})
}}
</div>
</div>
<div class="workout-map" v-if="workout.with_gpx">
<StaticMap :workout="workout"></StaticMap>
</div>
<div class="workout-data">
<div>
<img class="sport-img" alt="workout sport logo" :src="sport.img" />
</div>
<div>
<i class="fa fa-clock-o" aria-hidden="true" />
{{ workout.moving }}
</div>
<div>
<i class="fa fa-road" aria-hidden="true" />
{{ workout.distance }} km
</div>
</div>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { Locale, format, formatDistance } from 'date-fns'
import { PropType, defineComponent, ComputedRef, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import { ROOT_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user'
import { IWorkout } from '@/types/workouts'
import { useStore } from '@/use/useStore'
import { getApiUrl } from '@/utils'
import { getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'WorkoutCard',
components: {
Card,
StaticMap,
},
props: {
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
user: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
sport: {
type: Object as PropType<ISport>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const userPictureUrl: ComputedRef<string> = computed(() =>
props.user.picture
? `${getApiUrl()}/users/${props.user.username}/picture?${Date.now()}`
: ''
)
const locale: ComputedRef<Locale> = computed(
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
)
return {
format,
formatDistance,
getDateWithTZ,
locale,
t,
userPictureUrl,
}
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
.timeline-workout {
margin-bottom: $default-margin * 2;
::v-deep(.card) {
max-width: 400px;
.card-content {
display: flex;
flex-direction: column;
padding: 0;
.workout-user-date {
display: flex;
justify-content: space-between;
padding: $default-padding * 0.5 $default-padding;
.profile-img {
border-radius: 50%;
height: 25px;
width: 25px;
}
.workout-date {
font-size: 0.75em;
font-style: italic;
}
}
.workout-data {
display: flex;
padding-top: $default-padding * 0.5;
.sport-img {
height: 28px;
width: 28px;
}
div {
width: 33%;
text-align: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div class="timeline">
<WorkoutCard
v-for="workout in workouts"
:workout="workout"
:sport="sports.filter((s) => s.id === workout.sport_id)[0]"
:user="user"
:key="workout.id"
></WorkoutCard>
</div>
</template>
<script lang="ts">
import {
computed,
ComputedRef,
defineComponent,
onBeforeMount,
PropType,
} from 'vue'
import WorkoutCard from '@/components/Dashboard/Timeline/WorkoutCard.vue'
import { SPORTS_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user'
import { IWorkout } from '@/types/workouts'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'Timeline',
components: {
WorkoutCard,
},
props: {
user: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
},
setup() {
const store = useStore()
onBeforeMount(() =>
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_USER_WORKOUTS, { page: 1 })
)
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
)
const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
)
return { workouts, sports }
},
})
</script>

View File

@ -23,6 +23,10 @@ a {
text-decoration: none; text-decoration: none;
} }
img {
max-width: 100%;
}
input { input {
border-radius: $border-radius; border-radius: $border-radius;
border: solid 1px var(--input-border-color); border: solid 1px var(--input-border-color);

View File

@ -7,6 +7,7 @@ export enum RootGetters {
APP_LOADING = 'APP_LOADING', APP_LOADING = 'APP_LOADING',
ERROR_MESSAGES = 'ERROR_MESSAGES', ERROR_MESSAGES = 'ERROR_MESSAGES',
LANGUAGE = 'LANGUAGE', LANGUAGE = 'LANGUAGE',
LOCALE = 'LOCALE', // date-fns
} }
export enum RootMutations { export enum RootMutations {

View File

@ -16,4 +16,7 @@ export const getters: GetterTree<IRootState, IRootState> & IRootGetters = {
[ROOT_STORE.GETTERS.LANGUAGE]: (state: IRootState) => { [ROOT_STORE.GETTERS.LANGUAGE]: (state: IRootState) => {
return state.language return state.language
}, },
[ROOT_STORE.GETTERS.LOCALE]: (state: IRootState) => {
return state.locale
},
} }

View File

@ -3,6 +3,7 @@ import { MutationTree } from 'vuex'
import { ROOT_STORE } from '@/store/constants' import { ROOT_STORE } from '@/store/constants'
import { IRootState, TRootMutations } from '@/store/modules/root/types' import { IRootState, TRootMutations } from '@/store/modules/root/types'
import { IAppConfig } from '@/types/application' import { IAppConfig } from '@/types/application'
import { localeFromLanguage } from '@/utils/locales'
export const mutations: MutationTree<IRootState> & TRootMutations = { export const mutations: MutationTree<IRootState> & TRootMutations = {
[ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES](state: IRootState) { [ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES](state: IRootState) {
@ -28,5 +29,6 @@ export const mutations: MutationTree<IRootState> & TRootMutations = {
}, },
[ROOT_STORE.MUTATIONS.UPDATE_LANG](state: IRootState, language: string) { [ROOT_STORE.MUTATIONS.UPDATE_LANG](state: IRootState, language: string) {
state.language = language state.language = language
state.locale = localeFromLanguage[language]
}, },
} }

View File

@ -1,9 +1,12 @@
import { enUS } from 'date-fns/locale'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { IApplication } from '@/types/application' import { IApplication } from '@/types/application'
export const state: IRootState = { export const state: IRootState = {
root: true, root: true,
language: 'en', language: 'en',
locale: enUS,
errorMessages: null, errorMessages: null,
application: <IApplication>{}, application: <IApplication>{},
appLoading: false, appLoading: false,

View File

@ -1,3 +1,4 @@
import { Locale } from 'date-fns'
import { import {
ActionContext, ActionContext,
CommitOptions, CommitOptions,
@ -11,6 +12,7 @@ import { IAppConfig, IApplication } from '@/types/application'
export interface IRootState { export interface IRootState {
root: boolean root: boolean
language: string language: string
locale: Locale
errorMessages: string | string[] | null errorMessages: string | string[] | null
application: IApplication application: IApplication
appLoading: boolean appLoading: boolean
@ -32,6 +34,8 @@ export interface IRootGetters {
): string | string[] | null ): string | string[] | null
[ROOT_STORE.GETTERS.LANGUAGE](state: IRootState): string [ROOT_STORE.GETTERS.LANGUAGE](state: IRootState): string
[ROOT_STORE.GETTERS.LOCALE](state: IRootState): Locale
} }
export type TRootMutations<S = IRootState> = { export type TRootMutations<S = IRootState> = {

View File

@ -62,8 +62,9 @@ export interface IWorkout {
} }
export interface IWorkoutsPayload { export interface IWorkoutsPayload {
from: string from?: string
to: string to?: string
order: string order?: string
per_page: number per_page?: number
page?: number
} }

View File

@ -0,0 +1,8 @@
/* eslint-disable import/no-duplicates */
import { Locale } from 'date-fns'
import { enUS, fr } from 'date-fns/locale'
export const localeFromLanguage: Record<string, Locale> = {
en: enUS,
fr: fr,
}

View File

@ -0,0 +1,8 @@
export const sportColors: Record<string, string> = {
'Cycling (Sport)': '#55A8A3',
'Cycling (Transport)': '#98C3A9',
Hiking: '#D0838A',
'Mountain Biking': '#ECC77E',
Running: '#926692',
Walking: '#929292',
}

View File

@ -11,6 +11,7 @@ import {
TStatisticsFromApi, TStatisticsFromApi,
} from '@/types/statistics' } from '@/types/statistics'
import { incrementDate, startDate } from '@/utils/dates' import { incrementDate, startDate } from '@/utils/dates'
import { sportColors } from '@/utils/sports'
// date format from api // date format from api
const dateFormats: genericObject = { const dateFormats: genericObject = {
@ -40,15 +41,6 @@ export const getDateKeys = (
return days return days
} }
export const sportColors: Record<string, string> = {
'Cycling (Sport)': '#55A8A3',
'Cycling (Transport)': '#98C3A9',
Hiking: '#D0838A',
'Mountain Biking': '#ECC77E',
Running: '#926692',
Walking: '#929292',
}
const getStatisticsChartDataset = ( const getStatisticsChartDataset = (
sportLabel: string, sportLabel: string,
color: string color: string

View File

@ -5,12 +5,12 @@
</div> </div>
<div class="container dashboard-container"> <div class="container dashboard-container">
<div class="left-container dashboard-sub-container"> <div class="left-container dashboard-sub-container">
<UserMonthStats :user="authUser" /> <UserCalendar :user="authUser" />
<UserRecords /> <!-- <UserMonthStats :user="authUser" />-->
<!-- <UserRecords />-->
</div> </div>
<div class="right-container dashboard-sub-container"> <div class="right-container dashboard-sub-container">
<UserCalendar :user="authUser" /> <Timeline :user="authUser" />
<Timeline />
</div> </div>
</div> </div>
</div> </div>
@ -19,10 +19,10 @@
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent } from 'vue' import { computed, ComputedRef, defineComponent } from 'vue'
import Timeline from '@/components/Dashboard/Timeline.vue' import Timeline from '@/components/Dashboard/Timeline/index.vue'
import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue' import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue'
import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue' // import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue'
import UserRecords from '@/components/Dashboard/UserRecords.vue' // import UserRecords from '@/components/Dashboard/UserRecords.vue'
import UserStatsCards from '@/components/Dashboard/UserStartsCards/index.vue' import UserStatsCards from '@/components/Dashboard/UserStartsCards/index.vue'
import { USER_STORE } from '@/store/constants' import { USER_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
@ -33,8 +33,8 @@
components: { components: {
Timeline, Timeline,
UserCalendar, UserCalendar,
UserMonthStats, // UserMonthStats,
UserRecords, // UserRecords,
UserStatsCards, UserStatsCards,
}, },
setup() { setup() {
@ -47,20 +47,21 @@
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
@import '~@/scss/base'; @import '~@/scss/base';
.dashboard-container { .dashboard-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding-bottom: 30px;
.dashboard-sub-container { .dashboard-sub-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.left-container { .left-container {
width: 35%; width: 65%;
} }
.right-container { .right-container {
width: 65%; width: 35%;
} }
} }
@media screen and (max-width: $small-limit) { @media screen and (max-width: $small-limit) {