Client - display segment

This commit is contained in:
Sam 2021-09-27 10:01:17 +02:00
parent de32c136b6
commit b7b2eb0daf
12 changed files with 413 additions and 110 deletions

View File

@ -2,44 +2,48 @@
<div id="workout-info"> <div id="workout-info">
<div class="workout-data"> <div class="workout-data">
<i class="fa fa-clock-o" aria-hidden="true" /> <i class="fa fa-clock-o" aria-hidden="true" />
{{ t('workouts.DURATION') }}: <span>{{ workout.moving }}</span> {{ t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
<WorkoutRecord :workout="workout" record_type="LD" /> <WorkoutRecord :workoutObject="workoutObject" record_type="LD" />
<div v-if="withPause"> <div v-if="withPause">
({{ t('workouts.PAUSES') }}: <span>{{ workout.pauses }}</span> - ({{ t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
{{ t('workouts.TOTAL_DURATION') }}: <span>{{ workout.duration }})</span> {{ t('workouts.TOTAL_DURATION') }}:
<span>{{ workoutObject.duration }})</span>
</div> </div>
</div> </div>
<div class="workout-data"> <div class="workout-data">
<i class="fa fa-road" aria-hidden="true" /> <i class="fa fa-road" aria-hidden="true" />
{{ t('workouts.DISTANCE') }}: <span>{{ workout.distance }} km</span> {{ t('workouts.DISTANCE') }}: <span>{{ workoutObject.distance }} km</span>
<WorkoutRecord :workout="workout" record_type="FD" /> <WorkoutRecord :workoutObject="workoutObject" record_type="FD" />
</div> </div>
<div class="workout-data"> <div class="workout-data">
<i class="fa fa-tachometer" aria-hidden="true" /> <i class="fa fa-tachometer" aria-hidden="true" />
{{ t('workouts.AVERAGE_SPEED') }}: {{ t('workouts.AVERAGE_SPEED') }}:
<span>{{ workout.ave_speed }} km/h</span <span>{{ workoutObject.aveSpeed }} km/h</span
><WorkoutRecord :workout="workout" record_type="AS" /><br /> ><WorkoutRecord :workoutObject="workoutObject" record_type="AS" /><br />
{{ t('workouts.MAX_SPEED') }}: <span>{{ workout.max_speed }} km/h</span> {{ t('workouts.MAX_SPEED') }}:
<WorkoutRecord :workout="workout" record_type="MS" /> <span>{{ workoutObject.maxSpeed }} km/h</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="MS" />
</div> </div>
<div <div
class="workout-data" class="workout-data"
v-if="workout.max_alt !== null && workout.min_alt !== null" v-if="workoutObject.maxAlt !== null && workoutObject.minAlt !== null"
> >
<img class="mountains" src="/img/misc/mountains.svg" /> <img class="mountains" src="/img/misc/mountains.svg" />
{{ t('workouts.MIN_ALTITUDE') }}: <span>{{ workout.min_alt }} m</span {{ t('workouts.MIN_ALTITUDE') }}: <span>{{ workoutObject.minAlt }} m</span
><br /> ><br />
{{ t('workouts.MAX_ALTITUDE') }}: <span>{{ workout.max_alt }} m</span> {{ t('workouts.MAX_ALTITUDE') }}:
<span>{{ workoutObject.maxAlt }} m</span>
</div> </div>
<div <div
class="workout-data" class="workout-data"
v-if="workout.ascent !== null && workout.descent !== null" v-if="workoutObject.ascent !== null && workoutObject.descent !== null"
> >
<i class="fa fa-location-arrow" aria-hidden="true" /> <i class="fa fa-location-arrow" aria-hidden="true" />
{{ t('workouts.ASCENT') }}: <span>{{ workout.ascent }} m</span><br /> {{ t('workouts.ASCENT') }}: <span>{{ workoutObject.ascent }} m</span
{{ t('workouts.DESCENT') }}: <span>{{ workout.descent }} m</span> ><br />
{{ t('workouts.DESCENT') }}: <span>{{ workoutObject.descent }} m</span>
</div> </div>
<WorkoutWeather :workout="workout" /> <WorkoutWeather :workoutObject="workoutObject" />
</div> </div>
</template> </template>
@ -49,7 +53,7 @@
import WorkoutRecord from '@/components/Workout/WorkoutDetail/WorkoutRecord.vue' import WorkoutRecord from '@/components/Workout/WorkoutDetail/WorkoutRecord.vue'
import WorkoutWeather from '@/components/Workout/WorkoutDetail/WorkoutWeather.vue' import WorkoutWeather from '@/components/Workout/WorkoutDetail/WorkoutWeather.vue'
import { IWorkout } from '@/types/workouts' import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({ export default defineComponent({
name: 'WorkoutData', name: 'WorkoutData',
components: { components: {
@ -57,8 +61,8 @@
WorkoutWeather, WorkoutWeather,
}, },
props: { props: {
workout: { workoutObject: {
type: Object as PropType<IWorkout>, type: Object as PropType<IWorkoutObject>,
required: true, required: true,
}, },
}, },
@ -67,7 +71,8 @@
return { return {
withPause: computed( withPause: computed(
() => () =>
props.workout.pauses !== '0:00:00' && props.workout.pauses !== null props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
), ),
t, t,
} }

View File

@ -2,8 +2,8 @@
<span <span
class="workout-record" class="workout-record"
v-if=" v-if="
workout.records && workoutObject.records &&
workout.records.find((record) => record.record_type === record_type) workoutObject.records.find((record) => record.record_type === record_type)
" "
> >
<sup> <sup>
@ -15,12 +15,12 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
import { IWorkout } from '@/types/workouts' import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({ export default defineComponent({
name: 'WorkoutRecord', name: 'WorkoutRecord',
props: { props: {
workout: { workoutObject: {
type: Object as PropType<IWorkout>, type: Object as PropType<IWorkoutObject>,
required: true, required: true,
}, },
record_type: { record_type: {

View File

@ -1,5 +1,8 @@
<template> <template>
<div id="workout-weather" v-if="workout.weather_start && workout.weather_end"> <div
id="workout-weather"
v-if="workoutObject.weatherStart && workoutObject.weatherEnd"
>
<table class="weather-table"> <table class="weather-table">
<thead> <thead>
<tr> <tr>
@ -9,12 +12,16 @@
{{ t('workouts.START') }} {{ t('workouts.START') }}
<img <img
class="weather-img" class="weather-img"
:src="`/img/weather/${workout.weather_start.icon}.svg`" :src="`/img/weather/${workoutObject.weatherStart.icon}.svg`"
:alt=" :alt="
t(`workouts.WEATHER.DARK_SKY.${workout.weather_start.icon}`) t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherStart.icon}`
)
" "
:title=" :title="
t(`workouts.WEATHER.DARK_SKY.${workout.weather_start.icon}`) t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherStart.icon}`
)
" "
/> />
</div> </div>
@ -24,12 +31,16 @@
{{ t('workouts.END') }} {{ t('workouts.END') }}
<img <img
class="weather-img" class="weather-img"
:src="`/img/weather/${workout.weather_end.icon}.svg`" :src="`/img/weather/${workoutObject.weatherEnd.icon}.svg`"
:alt=" :alt="
t(`workouts.WEATHER.DARK_SKY.${workout.weather_end.icon}`) t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherEnd.icon}`
)
" "
:title=" :title="
t(`workouts.WEATHER.DARK_SKY.${workout.weather_end.icon}`) t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherEnd.icon}`
)
" "
/> />
</div> </div>
@ -46,8 +57,12 @@
:title="t(`workouts.WEATHER.TEMPERATURE`)" :title="t(`workouts.WEATHER.TEMPERATURE`)"
/> />
</td> </td>
<td>{{ Number(workout.weather_start.temperature).toFixed(1) }}°C</td> <td>
<td>{{ Number(workout.weather_end.temperature).toFixed(1) }}°C</td> {{ Number(workoutObject.weatherStart.temperature).toFixed(1) }}°C
</td>
<td>
{{ Number(workoutObject.weatherEnd.temperature).toFixed(1) }}°C
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@ -59,9 +74,11 @@
/> />
</td> </td>
<td> <td>
{{ Number(workout.weather_start.humidity * 100).toFixed(1) }}% {{ Number(workoutObject.weatherStart.humidity * 100).toFixed(1) }}%
</td>
<td>
{{ Number(workoutObject.weatherEnd.humidity * 100).toFixed(1) }}%
</td> </td>
<td>{{ Number(workout.weather_end.humidity * 100).toFixed(1) }}%</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@ -72,8 +89,8 @@
:title="t(`workouts.WEATHER.WIND`)" :title="t(`workouts.WEATHER.WIND`)"
/> />
</td> </td>
<td>{{ Number(workout.weather_start.wind).toFixed(1) }}m/s</td> <td>{{ Number(workoutObject.weatherStart.wind).toFixed(1) }}m/s</td>
<td>{{ Number(workout.weather_end.wind).toFixed(1) }}m/s</td> <td>{{ Number(workoutObject.weatherEnd.wind).toFixed(1) }}m/s</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -84,12 +101,12 @@
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { IWorkout } from '@/types/workouts' import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({ export default defineComponent({
name: 'WorkoutWeather', name: 'WorkoutWeather',
props: { props: {
workout: { workoutObject: {
type: Object as PropType<IWorkout>, type: Object as PropType<IWorkoutObject>,
required: true, required: true,
}, },
}, },

View File

@ -4,18 +4,15 @@
<template #title> <template #title>
<div <div
class="workout-previous workout-arrow" class="workout-previous workout-arrow"
:class="{ inactive: !workoutData.workout.previous_workout }" :class="{ inactive: !workoutObject.previousUrl }"
:title=" :title="
workoutData.workout.previous_workout workoutObject.previousUrl
? t('workouts.PREVIOUS_WORKOUT') ? t(`workouts.PREVIOUS_${workoutObject.type}`)
: t('workouts.NO_PREVIOUS_WORKOUT') : t(`workouts.NO_PREVIOUS_${workoutObject.type}`)
" "
@click=" @click="
workoutData.workout.previous_workout workoutObject.previousUrl
? $router.push({ ? $router.push(workoutObject.previousUrl)
name: 'Workout',
params: { workoutId: workoutData.workout.previous_workout },
})
: null : null
" "
> >
@ -26,27 +23,41 @@
<img alt="workout sport logo" :src="sport.img" /> <img alt="workout sport logo" :src="sport.img" />
</div> </div>
<div class="workout-title-date"> <div class="workout-title-date">
<div class="workout-title">{{ workoutData.workout.title }}</div> <div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
{{ workoutObject.title }}
</div>
<div class="workout-title" v-else>
<router-link
:to="{
name: 'Workout',
params: { workoutId: workoutObject.workoutId },
}"
>
{{ workoutObject.title }}
</router-link>
<span class="workout-segment">
<i class="fa fa-map-marker" aria-hidden="true" />
{{ t('workouts.SEGMENT') }}
{{ workoutObject.segmentId + 1 }}
</span>
</div>
<div class="workout-date"> <div class="workout-date">
{{ workoutDate.workout_date }} - {{ workoutDate.workout_time }} {{ workoutObject.workoutDate }} -
{{ workoutObject.workoutTime }}
</div> </div>
</div> </div>
</div> </div>
<div <div
class="workout-next workout-arrow" class="workout-next workout-arrow"
:class="{ inactive: !workoutData.workout.next_workout }" :class="{ inactive: !workoutObject.nextUrl }"
:title=" :title="
workoutData.workout.next_workout workoutObject.nextUrl
? t('workouts.NEXT_WORKOUT') ? t(`workouts.NEXT_${workoutObject.type}`)
: t('workouts.NO_NEXT_WORKOUT') : t(`workouts.NO_NEXT_${workoutObject.type}`)
" "
@click=" @click="
workoutData.workout.next_workout workoutObject.nextUrl ? $router.push(workoutObject.nextUrl) : null
? $router.push({
name: 'Workout',
params: { workoutId: workoutData.workout.next_workout },
})
: null
" "
> >
<i class="fa fa-chevron-right" aria-hidden="true" /> <i class="fa fa-chevron-right" aria-hidden="true" />
@ -57,25 +68,37 @@
:workoutData="workoutData" :workoutData="workoutData"
:markerCoordinates="markerCoordinates" :markerCoordinates="markerCoordinates"
/> />
<WorkoutData :workout="workoutData.workout" /> <WorkoutData :workoutObject="workoutObject" />
</template> </template>
</Card> </Card>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent, computed, watch } from 'vue' import {
ComputedRef,
PropType,
Ref,
defineComponent,
computed,
ref,
watch,
} from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import Card from '@/components/Common/Card.vue' import Card from '@/components/Common/Card.vue'
import WorkoutData from '@/components/Workout/WorkoutDetail/WorkoutData.vue' import WorkoutData from '@/components/Workout/WorkoutDetail/WorkoutData.vue'
import WorkoutMap from '@/components/Workout/WorkoutDetail/WorkoutMap.vue' import WorkoutMap from '@/components/Workout/WorkoutDetail/WorkoutMap.vue'
import { WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports' import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { IWorkoutData, TCoordinates } from '@/types/workouts' import {
import { useStore } from '@/use/useStore' IWorkout,
IWorkoutData,
IWorkoutObject,
IWorkoutSegment,
TCoordinates,
} from '@/types/workouts'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates' import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
export default defineComponent({ export default defineComponent({
@ -101,17 +124,98 @@
type: Object as PropType<IWorkoutData>, type: Object as PropType<IWorkoutData>,
required: true, required: true,
}, },
displaySegment: {
type: Boolean,
required: true,
},
}, },
setup(props) { setup(props) {
const route = useRoute() const route = useRoute()
const store = useStore()
const { t } = useI18n() const { t } = useI18n()
function getWorkoutObjectUrl(
workout: IWorkout,
displaySegment: boolean,
segmentId: number | null
): Record<string, string | null> {
const previousUrl =
displaySegment && segmentId && segmentId !== 1
? `/workouts/${workout.id}/segment/${segmentId - 1}`
: !displaySegment && workout.previous_workout
? `/workouts/${workout.previous_workout}`
: null
const nextUrl =
displaySegment && segmentId && segmentId < workout.segments.length
? `/workouts/${workout.id}/segment/${segmentId + 1}`
: !displaySegment && workout.next_workout
? `/workouts/${workout.next_workout}`
: null
return {
previousUrl,
nextUrl,
}
}
function getWorkoutObject(
workout: IWorkout,
segment: IWorkoutSegment | null
): IWorkoutObject {
const urls = getWorkoutObjectUrl(
workout,
props.displaySegment,
segmentId.value ? +segmentId.value : null
)
const workoutDate = formatWorkoutDate(
getDateWithTZ(
props.workoutData.workout.workout_date,
props.authUser.timezone
)
)
return {
ascent: segment ? segment.ascent : workout.ascent,
aveSpeed: segment ? segment.ave_speed : workout.ave_speed,
distance: segment ? segment.distance : workout.distance,
descent: segment ? segment.descent : workout.descent,
duration: segment ? segment.duration : workout.duration,
maxAlt: segment ? segment.max_alt : workout.max_alt,
maxSpeed: segment ? segment.max_speed : workout.max_speed,
minAlt: segment ? segment.min_alt : workout.min_alt,
moving: segment ? segment.moving : workout.moving,
nextUrl: urls.nextUrl,
pauses: segment ? segment.pauses : workout.pauses,
previousUrl: urls.previousUrl,
records: segment ? [] : workout.records,
segmentId: segment ? segment.segment_id : null,
title: workout.title,
type: props.displaySegment ? 'SEGMENT' : 'WORKOUT',
workoutDate: workoutDate.workout_date,
weatherEnd: segment ? null : workout.weather_end,
workoutId: workout.id,
weatherStart: segment ? null : workout.weather_start,
workoutTime: workoutDate.workout_time,
}
}
const workout: ComputedRef<IWorkout> = computed(
() => props.workoutData.workout
)
let segmentId: Ref<number | null> = ref(
route.params.workoutId ? +route.params.segmentId : null
)
const segment: ComputedRef<IWorkoutSegment | null> = computed(() =>
workout.value.segments.length > 0 && segmentId.value
? workout.value.segments[+segmentId.value - 1]
: null
)
watch( watch(
() => route.params.workoutId, () => route.params.segmentId,
async (newWorkoutId) => { async (newSegmentId) => {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, newWorkoutId) if (newSegmentId) {
segmentId.value = +newSegmentId
}
} }
) )
return { return {
sport: computed(() => sport: computed(() =>
props.sports props.sports
@ -120,13 +224,8 @@
) )
: {} : {}
), ),
workoutDate: computed(() => workoutObject: computed(() =>
formatWorkoutDate( getWorkoutObject(workout.value, segment.value)
getDateWithTZ(
props.workoutData.workout.workout_date,
props.authUser.timezone
)
)
), ),
t, t,
} }
@ -166,6 +265,10 @@
font-size: 0.8em; font-size: 0.8em;
font-weight: normal; font-weight: normal;
} }
.workout-segment {
font-style: italic;
font-weight: normal;
}
} }
} }
.card-content { .card-content {

View File

@ -0,0 +1,72 @@
<template>
<div id="workout-segments">
<Card :without-title="false">
<template #title>{{ t('workouts.SEGMENT', 2) }}</template>
<template #content>
<ul>
<li v-for="(segment, index) in segments" :key="segment.segment_id">
<router-link
:to="{
name: 'WorkoutSegment',
params: {
workoutId: segment.workout_id,
segmentId: index + 1,
},
}"
>{{ t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
>
({{ t('workouts.DISTANCE') }}: {{ segment.distance }} km,
{{ t('workouts.DURATION') }}: {{ segment.duration }})
</li>
</ul>
</template>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import { IWorkoutSegment } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutSegments',
components: {
Card,
},
props: {
segments: {
type: Object as PropType<IWorkoutSegment[]>,
required: true,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
#workout-segments {
::v-deep(.card) {
.card-title {
text-transform: capitalize;
}
.card-content {
padding-bottom: 0;
padding-top: 0;
a {
font-weight: bold;
}
ul {
padding: 0 $default-padding;
list-style: square;
}
}
}
}
</style>

View File

@ -13,22 +13,27 @@
"MAX_ALTITUDE": "max. altitude", "MAX_ALTITUDE": "max. altitude",
"MAX_SPEED": "max. speed", "MAX_SPEED": "max. speed",
"MIN_ALTITUDE": "min. altitude", "MIN_ALTITUDE": "min. altitude",
"NEXT_SEGMENT": "No next segment",
"NEXT_WORKOUT": "Next workout", "NEXT_WORKOUT": "Next workout",
"NO_DATA_CLEANING": "data from gpx, without any cleaning", "NO_DATA_CLEANING": "data from gpx, without any cleaning",
"NO_MAP": "No map", "NO_MAP": "No map",
"NO_NEXT_SEGMENT": "No next segment",
"NO_NEXT_WORKOUT": "No next workout", "NO_NEXT_WORKOUT": "No next workout",
"NO_NOTES": "No notes", "NO_NOTES": "No notes",
"NO_PREVIOUS_SEGMENT": "No previous segment",
"NO_PREVIOUS_WORKOUT": "No previous workout", "NO_PREVIOUS_WORKOUT": "No previous workout",
"NO_RECORDS": "No records.", "NO_RECORDS": "No records.",
"NO_WORKOUTS": "No workouts.", "NO_WORKOUTS": "No workouts.",
"NOTES": "Notes", "NOTES": "Notes",
"PAUSES": "pauses", "PAUSES": "pauses",
"PREVIOUS_SEGMENT": "Previous segment",
"PREVIOUS_WORKOUT": "Previous workout", "PREVIOUS_WORKOUT": "Previous workout",
"RECORD": "record | records", "RECORD": "record | records",
"RECORD_AS": "Ave. speed", "RECORD_AS": "Ave. speed",
"RECORD_FD": "Farest distance", "RECORD_FD": "Farest distance",
"RECORD_LD": "Longest duration", "RECORD_LD": "Longest duration",
"RECORD_MS": "Max. speed", "RECORD_MS": "Max. speed",
"SEGMENT": "segment | segments",
"SPEED": "speed", "SPEED": "speed",
"SPORT": "sport | sports", "SPORT": "sport | sports",
"START": "start", "START": "start",

View File

@ -13,22 +13,27 @@
"MAX_ALTITUDE": "altitude max", "MAX_ALTITUDE": "altitude max",
"MAX_SPEED": "vitesse max", "MAX_SPEED": "vitesse max",
"MIN_ALTITUDE": "altitude min", "MIN_ALTITUDE": "altitude min",
"NEXT_SEGMENT": "Segment suivant",
"NEXT_WORKOUT": "Séance suivante", "NEXT_WORKOUT": "Séance suivante",
"NO_DATA_CLEANING": "données issues du fichier gpx, sans correction", "NO_DATA_CLEANING": "données issues du fichier gpx, sans correction",
"NO_MAP": "Pas de carte", "NO_MAP": "Pas de carte",
"NO_NEXT_SEGMENT": "Pas de segment suivant",
"NO_NEXT_WORKOUT": "Pas de séance suivante", "NO_NEXT_WORKOUT": "Pas de séance suivante",
"NO_NOTES": "Pas de notes", "NO_NOTES": "Pas de notes",
"NO_PREVIOUS_WORKOUT": "Pas de séances précédente", "NO_PREVIOUS_SEGMENT": "Pas de segment précédent",
"NO_PREVIOUS_WORKOUT": "Pas de séance précédente",
"NO_RECORDS": "Pas de records.", "NO_RECORDS": "Pas de records.",
"NO_WORKOUTS": "Pas de séances.", "NO_WORKOUTS": "Pas de séances.",
"NOTES": "Notes", "NOTES": "Notes",
"PAUSES": "pauses", "PAUSES": "pauses",
"PREVIOUS_SEGMENT": "Segment précédent",
"PREVIOUS_WORKOUT": "Séance précédente", "PREVIOUS_WORKOUT": "Séance précédente",
"RECORD": "record | records", "RECORD": "record | records",
"RECORD_AS": "Vitesse moy.", "RECORD_AS": "Vitesse moy.",
"RECORD_FD": "Distance la + longue", "RECORD_FD": "Distance la + longue",
"RECORD_LD": "Durée la + longue", "RECORD_LD": "Durée la + longue",
"RECORD_MS": "Vitesse max.", "RECORD_MS": "Vitesse max.",
"SEGMENT": "segment | segments",
"SPEED": "vitesse", "SPEED": "vitesse",
"SPORT": "sport | sports", "SPORT": "sport | sports",
"START": "début", "START": "début",

View File

@ -29,6 +29,13 @@ const routes: Array<RouteRecordRaw> = [
path: '/workouts/:workoutId', path: '/workouts/:workoutId',
name: 'Workout', name: 'Workout',
component: Workout, component: Workout,
props: { displaySegment: false },
},
{
path: '/workouts/:workoutId/segment/:segmentId',
name: 'WorkoutSegment',
component: Workout,
props: { displaySegment: true },
}, },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView }, { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView },
] ]

View File

@ -7,7 +7,7 @@ import {
IWorkoutsActions, IWorkoutsActions,
IWorkoutsState, IWorkoutsState,
} from '@/store/modules/workouts/types' } from '@/store/modules/workouts/types'
import { IWorkoutsPayload } from '@/types/workouts' import { IWorkout, IWorkoutPayload, IWorkoutsPayload } from '@/types/workouts'
import { handleError } from '@/utils' import { handleError } from '@/utils'
const getWorkouts = ( const getWorkouts = (
@ -51,20 +51,31 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
}, },
[WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA]( [WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA](
context: ActionContext<IWorkoutsState, IRootState>, context: ActionContext<IWorkoutsState, IRootState>,
workoutId: string payload: IWorkoutPayload
): void { ): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES) context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true) context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true)
const segmentUrl = payload.segmentId ? `/segment/${payload.segmentId}` : ''
authApi authApi
.get(`workouts/${workoutId}`) .get(`workouts/${payload.workoutId}`)
.then((res) => { .then((res) => {
const workout: IWorkout = res.data.data.workouts[0]
if (res.data.status === 'success') { if (res.data.status === 'success') {
if (
payload.segmentId &&
(workout.segments.length === 0 ||
!workout.segments[+payload.segmentId - 1])
) {
throw new Error('WORKOUT_NOT_FOUND')
}
context.commit( context.commit(
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT, WORKOUTS_STORE.MUTATIONS.SET_WORKOUT,
res.data.data.workouts[0] res.data.data.workouts[0]
) )
if (res.data.data.workouts[0].with_gpx) { if (res.data.data.workouts[0].with_gpx) {
authApi.get(`workouts/${workoutId}/chart_data`).then((res) => { authApi
.get(`workouts/${payload.workoutId}/chart_data${segmentUrl}`)
.then((res) => {
if (res.data.status === 'success') { if (res.data.status === 'success') {
context.commit( context.commit(
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_CHART_DATA, WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_CHART_DATA,
@ -72,7 +83,9 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
) )
} }
}) })
authApi.get(`workouts/${workoutId}/gpx`).then((res) => { authApi
.get(`workouts/${payload.workoutId}/gpx${segmentUrl}`)
.then((res) => {
if (res.data.status === 'success') { if (res.data.status === 'success') {
context.commit( context.commit(
WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX, WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_GPX,
@ -82,10 +95,14 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
}) })
} }
} else { } else {
context.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
handleError(context, null) handleError(context, null)
} }
}) })
.catch((error) => handleError(context, error)) .catch((error) => {
context.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
handleError(context, error)
})
.finally(() => .finally(() =>
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false) context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
) )

View File

@ -12,6 +12,7 @@ import {
IWorkoutApiChartData, IWorkoutApiChartData,
IWorkoutsPayload, IWorkoutsPayload,
IWorkoutData, IWorkoutData,
IWorkoutPayload,
} from '@/types/workouts' } from '@/types/workouts'
export interface IWorkoutsState { export interface IWorkoutsState {
@ -31,7 +32,7 @@ export interface IWorkoutsActions {
): void ): void
[WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA]( [WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA](
context: ActionContext<IWorkoutsState, IRootState>, context: ActionContext<IWorkoutsState, IRootState>,
workoutId: string | string[] payload: IWorkoutPayload
): void ): void
} }

View File

@ -73,6 +73,35 @@ export interface IWorkout {
workout_date: string workout_date: string
} }
export interface IWorkoutObject {
ascent: number | null
aveSpeed: number
descent: number | null
distance: number
duration: string
maxAlt: number | null
maxSpeed: number
minAlt: number | null
moving: string
nextUrl: string | null
pauses: string | null
previousUrl: string | null
records: IRecord[]
segmentId: number | null
title: string
type: string
workoutDate: string
weatherEnd: IWeather | null
workoutId: string
weatherStart: IWeather | null
workoutTime: string
}
export interface IWorkoutPayload {
workoutId: string | string[]
segmentId?: string | string[]
}
export interface IWorkoutsPayload { export interface IWorkoutsPayload {
from?: string from?: string
to?: string to?: string

View File

@ -14,6 +14,7 @@
:sports="sports" :sports="sports"
:authUser="authUser" :authUser="authUser"
:markerCoordinates="markerCoordinates" :markerCoordinates="markerCoordinates"
:displaySegment="displaySegment"
/> />
<WorkoutChart <WorkoutChart
v-if=" v-if="
@ -21,9 +22,17 @@
" "
:workoutData="workoutData" :workoutData="workoutData"
:authUser="authUser" :authUser="authUser"
:displaySegment="displaySegment"
@getCoordinates="updateCoordinates" @getCoordinates="updateCoordinates"
/> />
<WorkoutNotes :notes="workoutData.workout.notes" /> <WorkoutSegments
v-if="!displaySegment && workoutData.workout.segments.length > 1"
:segments="workoutData.workout.segments"
></WorkoutSegments>
<WorkoutNotes
v-if="!displaySegment"
:notes="workoutData.workout.notes"
/>
</div> </div>
<div v-else> <div v-else>
<NotFound target="WORKOUT" /> <NotFound target="WORKOUT" />
@ -40,6 +49,7 @@
computed, computed,
defineComponent, defineComponent,
ref, ref,
watch,
onBeforeMount, onBeforeMount,
onUnmounted, onUnmounted,
} from 'vue' } from 'vue'
@ -50,9 +60,10 @@
import WorkoutChart from '@/components/Workout/WorkoutChart/index.vue' import WorkoutChart from '@/components/Workout/WorkoutChart/index.vue'
import WorkoutDetail from '@/components/Workout/WorkoutDetail/index.vue' import WorkoutDetail from '@/components/Workout/WorkoutDetail/index.vue'
import WorkoutNotes from '@/components/Workout/WorkoutNotes.vue' import WorkoutNotes from '@/components/Workout/WorkoutNotes.vue'
import WorkoutSegments from '@/components/Workout/WorkoutSegments.vue'
import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants' import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { IWorkoutData, TCoordinates } from '@/types/workouts' import { IWorkoutData, IWorkoutPayload, TCoordinates } from '@/types/workouts'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
export default defineComponent({ export default defineComponent({
@ -63,16 +74,25 @@
WorkoutChart, WorkoutChart,
WorkoutDetail, WorkoutDetail,
WorkoutNotes, WorkoutNotes,
WorkoutSegments,
}, },
setup() { props: {
displaySegment: {
type: Boolean,
required: true,
},
},
setup(props) {
const route = useRoute() const route = useRoute()
const store = useStore() const store = useStore()
onBeforeMount(() =>
store.dispatch( onBeforeMount(() => {
WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, const payload: IWorkoutPayload = { workoutId: route.params.workoutId }
route.params.workoutId if (props.displaySegment) {
) payload.segmentId = route.params.segmentId
) }
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, payload)
})
const workoutData: ComputedRef<IWorkoutData> = computed( const workoutData: ComputedRef<IWorkoutData> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUT_DATA] () => store.getters[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]
@ -93,9 +113,31 @@
} }
} }
watch(
() => route.params.workoutId,
async (newWorkoutId) => {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, {
workoutId: newWorkoutId,
})
}
)
watch(
() => route.params.segmentId,
async (newSegmentId) => {
const payload: IWorkoutPayload = {
workoutId: route.params.workoutId,
}
if (newSegmentId) {
payload.segmentId = newSegmentId
}
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, payload)
}
)
onUnmounted(() => { onUnmounted(() => {
store.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT) store.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
}) })
return { return {
authUser, authUser,
markerCoordinates, markerCoordinates,