Client - init workouts timeline
This commit is contained in:
parent
f3f1142479
commit
45abd58b79
@ -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;
|
||||||
|
65
fittrackee_client/src/components/Common/StaticMap.vue
Normal file
65
fittrackee_client/src/components/Common/StaticMap.vue
Normal 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>
|
@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>Timeline</div>
|
|
||||||
</template>
|
|
@ -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>
|
@ -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>
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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> = {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
8
fittrackee_client/src/utils/locales.ts
Normal file
8
fittrackee_client/src/utils/locales.ts
Normal 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,
|
||||||
|
}
|
8
fittrackee_client/src/utils/sports.ts
Normal file
8
fittrackee_client/src/utils/sports.ts
Normal 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',
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user