Client - init workouts timeline
This commit is contained in:
parent
f3f1142479
commit
45abd58b79
@ -50,7 +50,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base';
|
||||
.app-container {
|
||||
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;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: $border-radius;
|
||||
border: solid 1px var(--input-border-color);
|
||||
|
@ -7,6 +7,7 @@ export enum RootGetters {
|
||||
APP_LOADING = 'APP_LOADING',
|
||||
ERROR_MESSAGES = 'ERROR_MESSAGES',
|
||||
LANGUAGE = 'LANGUAGE',
|
||||
LOCALE = 'LOCALE', // date-fns
|
||||
}
|
||||
|
||||
export enum RootMutations {
|
||||
|
@ -16,4 +16,7 @@ export const getters: GetterTree<IRootState, IRootState> & IRootGetters = {
|
||||
[ROOT_STORE.GETTERS.LANGUAGE]: (state: IRootState) => {
|
||||
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 { IRootState, TRootMutations } from '@/store/modules/root/types'
|
||||
import { IAppConfig } from '@/types/application'
|
||||
import { localeFromLanguage } from '@/utils/locales'
|
||||
|
||||
export const mutations: MutationTree<IRootState> & TRootMutations = {
|
||||
[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) {
|
||||
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 { IApplication } from '@/types/application'
|
||||
|
||||
export const state: IRootState = {
|
||||
root: true,
|
||||
language: 'en',
|
||||
locale: enUS,
|
||||
errorMessages: null,
|
||||
application: <IApplication>{},
|
||||
appLoading: false,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { Locale } from 'date-fns'
|
||||
import {
|
||||
ActionContext,
|
||||
CommitOptions,
|
||||
@ -11,6 +12,7 @@ import { IAppConfig, IApplication } from '@/types/application'
|
||||
export interface IRootState {
|
||||
root: boolean
|
||||
language: string
|
||||
locale: Locale
|
||||
errorMessages: string | string[] | null
|
||||
application: IApplication
|
||||
appLoading: boolean
|
||||
@ -32,6 +34,8 @@ export interface IRootGetters {
|
||||
): string | string[] | null
|
||||
|
||||
[ROOT_STORE.GETTERS.LANGUAGE](state: IRootState): string
|
||||
|
||||
[ROOT_STORE.GETTERS.LOCALE](state: IRootState): Locale
|
||||
}
|
||||
|
||||
export type TRootMutations<S = IRootState> = {
|
||||
|
@ -62,8 +62,9 @@ export interface IWorkout {
|
||||
}
|
||||
|
||||
export interface IWorkoutsPayload {
|
||||
from: string
|
||||
to: string
|
||||
order: string
|
||||
per_page: number
|
||||
from?: string
|
||||
to?: string
|
||||
order?: string
|
||||
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,
|
||||
} from '@/types/statistics'
|
||||
import { incrementDate, startDate } from '@/utils/dates'
|
||||
import { sportColors } from '@/utils/sports'
|
||||
|
||||
// date format from api
|
||||
const dateFormats: genericObject = {
|
||||
@ -40,15 +41,6 @@ export const getDateKeys = (
|
||||
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 = (
|
||||
sportLabel: string,
|
||||
color: string
|
||||
|
@ -5,12 +5,12 @@
|
||||
</div>
|
||||
<div class="container dashboard-container">
|
||||
<div class="left-container dashboard-sub-container">
|
||||
<UserMonthStats :user="authUser" />
|
||||
<UserRecords />
|
||||
<UserCalendar :user="authUser" />
|
||||
<!-- <UserMonthStats :user="authUser" />-->
|
||||
<!-- <UserRecords />-->
|
||||
</div>
|
||||
<div class="right-container dashboard-sub-container">
|
||||
<UserCalendar :user="authUser" />
|
||||
<Timeline />
|
||||
<Timeline :user="authUser" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -19,10 +19,10 @@
|
||||
<script lang="ts">
|
||||
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 UserMonthStats from '@/components/Dashboard/UserMonthStats.vue'
|
||||
import UserRecords from '@/components/Dashboard/UserRecords.vue'
|
||||
// import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue'
|
||||
// import UserRecords from '@/components/Dashboard/UserRecords.vue'
|
||||
import UserStatsCards from '@/components/Dashboard/UserStartsCards/index.vue'
|
||||
import { USER_STORE } from '@/store/constants'
|
||||
import { IAuthUserProfile } from '@/types/user'
|
||||
@ -33,8 +33,8 @@
|
||||
components: {
|
||||
Timeline,
|
||||
UserCalendar,
|
||||
UserMonthStats,
|
||||
UserRecords,
|
||||
// UserMonthStats,
|
||||
// UserRecords,
|
||||
UserStatsCards,
|
||||
},
|
||||
setup() {
|
||||
@ -47,20 +47,21 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base';
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 30px;
|
||||
.dashboard-sub-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-container {
|
||||
width: 35%;
|
||||
width: 65%;
|
||||
}
|
||||
.right-container {
|
||||
width: 65%;
|
||||
width: 35%;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $small-limit) {
|
||||
|
Loading…
Reference in New Issue
Block a user