Client - use <script setup> in components

This commit is contained in:
Sam
2021-11-10 21:19:27 +01:00
parent 857c0ecd2d
commit 1bede62d80
126 changed files with 2133 additions and 3207 deletions

View File

@ -17,7 +17,7 @@
</div>
<router-link
class="workout-title"
v-if="workout"
v-if="workout.id"
:to="{
name: 'Workout',
params: { workoutId: workout.id },
@ -27,7 +27,7 @@
</router-link>
<div
class="workout-date"
v-if="workout && user"
v-if="workout.workout_date && user"
:title="
format(
getDateWithTZ(workout.workout_date, user.timezone),
@ -47,7 +47,7 @@
class="workout-map"
:class="{ 'no-cursor': !workout }"
@click="
workout
workout.id
? $router.push({
name: 'Workout',
params: { workoutId: workout.id },
@ -66,11 +66,16 @@
class="workout-data"
:class="{ 'without-gpx': workout && !workout.with_gpx }"
@click="
$router.push({ name: 'Workout', params: { workoutId: workout.id } })
workout.id
? $router.push({
name: 'Workout',
params: { workoutId: workout.id },
})
: null
"
>
<div class="img">
<SportImage v-if="sport" :sport-label="sport.label" />
<SportImage v-if="sport.label" :sport-label="sport.label" />
</div>
<div class="data">
<i class="fa fa-clock-o" aria-hidden="true" />
@ -103,9 +108,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { Locale, format, formatDistance } from 'date-fns'
import { PropType, defineComponent, ComputedRef, computed } from 'vue'
import { ComputedRef, computed, toRefs, withDefaults } from 'vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import UserPicture from '@/components/User/UserPicture.vue'
@ -116,39 +121,22 @@
import { useStore } from '@/use/useStore'
import { getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'WorkoutCard',
components: {
StaticMap,
UserPicture,
},
props: {
workout: {
type: Object as PropType<IWorkout>,
required: false,
},
user: {
type: Object as PropType<IUserProfile>,
required: true,
},
sport: {
type: Object as PropType<ISport>,
required: false,
},
},
setup() {
const store = useStore()
const locale: ComputedRef<Locale> = computed(
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
)
return {
format,
formatDistance,
getDateWithTZ,
locale,
}
},
interface Props {
user: IUserProfile
workout?: IWorkout
sport?: ISport
}
const props = withDefaults(defineProps<Props>(), {
workout: () => ({} as IWorkout),
sport: () => ({} as ISport),
})
const store = useStore()
const { user, workout, sport } = toRefs(props)
const locale: ComputedRef<Locale> = computed(
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
)
</script>
<style lang="scss" scoped>

View File

@ -36,9 +36,9 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { ChartData, ChartOptions } from 'chart.js'
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
import { ComputedRef, computed, ref } from 'vue'
import { LineChart, useLineChart } from 'vue-chart-3'
import { useI18n } from 'vue-i18n'
@ -50,160 +50,142 @@
} from '@/types/workouts'
import { getDatasets } from '@/utils/workouts'
export default defineComponent({
name: 'WorkoutChart',
components: {
LineChart,
},
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
workoutData: {
type: Object as PropType<IWorkoutData>,
required: true,
interface Props {
authUser: IUserProfile
workoutData: IWorkoutData
}
const props = defineProps<Props>()
const emit = defineEmits(['getCoordinates'])
const { t } = useI18n()
let displayDistance = ref(true)
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(props.workoutData.chartData, t)
)
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
labels: displayDistance.value
? datasets.value.distance_labels
: datasets.value.duration_labels,
datasets: JSON.parse(
JSON.stringify([
datasets.value.datasets.speed,
datasets.value.datasets.elevation,
])
),
}))
const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates
)
const options = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: true,
animation: false,
layout: {
padding: {
top: 22,
},
},
emits: ['getCoordinates'],
setup(props, { emit }) {
const { t } = useI18n()
let displayDistance = ref(true)
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(props.workoutData.chartData, t)
)
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
labels: displayDistance.value
? datasets.value.distance_labels
: datasets.value.duration_labels,
datasets: JSON.parse(
JSON.stringify([
datasets.value.datasets.speed,
datasets.value.datasets.elevation,
])
),
}))
const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates
)
const options = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: true,
animation: false,
layout: {
padding: {
top: 22,
scales: {
[displayDistance.value ? 'xDistance' : 'xDuration']: {
grid: {
drawOnChartArea: false,
},
ticks: {
count: 10,
callback: function (value) {
return displayDistance.value
? Number(value).toFixed(2)
: formatDuration(value)
},
},
scales: {
[displayDistance.value ? 'xDistance' : 'xDuration']: {
grid: {
drawOnChartArea: false,
},
ticks: {
count: 10,
callback: function (value) {
return displayDistance.value
? Number(value).toFixed(2)
: formatDuration(value)
},
},
type: 'linear',
bounds: 'data',
title: {
display: true,
text: displayDistance.value
? t('workouts.DISTANCE') + ' (km)'
: t('workouts.DURATION'),
},
},
ySpeed: {
grid: {
drawOnChartArea: false,
},
position: 'left',
title: {
display: true,
text: t('workouts.SPEED') + ' (km/h)',
},
},
yElevation: {
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
position: 'right',
title: {
display: true,
text: t('workouts.ELEVATION') + ' (m)',
},
},
type: 'linear',
bounds: 'data',
title: {
display: true,
text: displayDistance.value
? t('workouts.DISTANCE') + ' (km)'
: t('workouts.DURATION'),
},
elements: {
point: {
pointStyle: 'circle',
pointRadius: 0,
},
},
ySpeed: {
grid: {
drawOnChartArea: false,
},
plugins: {
datalabels: {
display: false,
},
tooltip: {
interaction: {
intersect: false,
mode: 'index',
},
callbacks: {
label: function (context) {
const label = ` ${context.dataset.label}: ${context.formattedValue}`
return context.dataset.yAxisID === 'yElevation'
? label + ' m'
: label + ' km/h'
},
title: function (tooltipItems) {
if (tooltipItems.length > 0) {
emitCoordinates(coordinates.value[tooltipItems[0].dataIndex])
}
return tooltipItems.length === 0
? ''
: displayDistance.value
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
: `${t('workouts.DURATION')}: ${formatDuration(
tooltipItems[0].label.replace(',', '')
)}`
},
},
},
position: 'left',
title: {
display: true,
text: t('workouts.SPEED') + ' (km/h)',
},
}))
function updateDisplayDistance() {
displayDistance.value = !displayDistance.value
}
function formatDuration(duration: string | number): string {
return new Date(+duration * 1000).toISOString().substr(11, 8)
}
function emitCoordinates(coordinates: TCoordinates) {
emit('getCoordinates', coordinates)
}
function emitEmptyCoordinates() {
emitCoordinates({ latitude: null, longitude: null })
}
const { lineChartProps } = useLineChart({
chartData,
options,
})
return {
displayDistance,
lineChartProps,
emitEmptyCoordinates,
updateDisplayDistance,
}
},
yElevation: {
beginAtZero: true,
grid: {
drawOnChartArea: false,
},
position: 'right',
title: {
display: true,
text: t('workouts.ELEVATION') + ' (m)',
},
},
},
elements: {
point: {
pointStyle: 'circle',
pointRadius: 0,
},
},
plugins: {
datalabels: {
display: false,
},
tooltip: {
interaction: {
intersect: false,
mode: 'index',
},
callbacks: {
label: function (context) {
const label = ` ${context.dataset.label}: ${context.formattedValue}`
return context.dataset.yAxisID === 'yElevation'
? label + ' m'
: label + ' km/h'
},
title: function (tooltipItems) {
if (tooltipItems.length > 0) {
emitCoordinates(coordinates.value[tooltipItems[0].dataIndex])
}
return tooltipItems.length === 0
? ''
: displayDistance.value
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
: `${t('workouts.DURATION')}: ${formatDuration(
tooltipItems[0].label.replace(',', '')
)}`
},
},
},
},
}))
const { lineChartProps } = useLineChart({
chartData,
options,
})
function updateDisplayDistance() {
displayDistance.value = !displayDistance.value
}
function formatDuration(duration: string | number): string {
return new Date(+duration * 1000).toISOString().substr(11, 8)
}
function emitCoordinates(coordinates: TCoordinates) {
emit('getCoordinates', coordinates)
}
function emitEmptyCoordinates() {
emitCoordinates({ latitude: null, longitude: null })
}
</script>
<style lang="scss" scoped>

View File

@ -80,29 +80,21 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { ISport } from '@/types/sports'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutCardTitle',
props: {
sport: {
type: Object as PropType<ISport>,
required: true,
},
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
emits: ['displayModal'],
setup(props, { emit }) {
return { emit }
},
})
interface Props {
sport: ISport
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const emit = defineEmits(['displayModal'])
const { sport, workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -3,7 +3,7 @@
<div class="workout-data">
<i class="fa fa-clock-o" aria-hidden="true" />
{{ $t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="LD" />
<WorkoutRecord :workoutObject="workoutObject" recordType="LD" />
<div v-if="withPause">
({{ $t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
{{ $t('workouts.TOTAL_DURATION') }}:
@ -14,16 +14,16 @@
<i class="fa fa-road" aria-hidden="true" />
{{ $t('workouts.DISTANCE') }}:
<span>{{ workoutObject.distance }} km</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="FD" />
<WorkoutRecord :workoutObject="workoutObject" recordType="FD" />
</div>
<div class="workout-data">
<i class="fa fa-tachometer" aria-hidden="true" />
{{ $t('workouts.AVERAGE_SPEED') }}:
<span>{{ workoutObject.aveSpeed }} km/h</span
><WorkoutRecord :workoutObject="workoutObject" record_type="AS" /><br />
><WorkoutRecord :workoutObject="workoutObject" recordType="AS" /><br />
{{ $t('workouts.MAX_SPEED') }}:
<span>{{ workoutObject.maxSpeed }} km/h</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="MS" />
<WorkoutRecord :workoutObject="workoutObject" recordType="MS" />
</div>
<div
class="workout-data"
@ -52,35 +52,24 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import WorkoutRecord from '@/components/Workout/WorkoutDetail/WorkoutRecord.vue'
import WorkoutWeather from '@/components/Workout/WorkoutDetail/WorkoutWeather.vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutData',
components: {
WorkoutRecord,
WorkoutWeather,
},
props: {
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
setup(props) {
return {
withPause: computed(
() =>
props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
),
}
},
})
interface Props {
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { workoutObject } = toRefs(props)
const withPause = computed(
() =>
props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
)
</script>
<style lang="scss" scoped>

View File

@ -28,10 +28,10 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { gpx } from '@tmcw/togeojson'
import { LGeoJson, LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet'
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
import { ComputedRef, computed, ref, toRefs, withDefaults } from 'vue'
import { ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
@ -40,90 +40,69 @@
import { useStore } from '@/use/useStore'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'WorkoutMap',
components: {
LGeoJson,
LMap,
LMarker,
LTileLayer,
},
props: {
workoutData: {
type: Object as PropType<IWorkoutData>,
},
markerCoordinates: {
type: Object as PropType<TCoordinates>,
required: false,
},
},
setup(props) {
const store = useStore()
interface Props {
workoutData: IWorkoutData
markerCoordinates?: TCoordinates
}
const props = withDefaults(defineProps<Props>(), {
markerCoordinates: () => ({} as TCoordinates),
})
function getGeoJson(gpxContent: string): GeoJSONData {
if (!gpxContent || gpxContent !== '') {
try {
const jsonData = gpx(
new DOMParser().parseFromString(gpxContent, 'text/xml')
)
return { jsonData }
} catch (e) {
console.error('Invalid gpx content')
return {}
}
}
const store = useStore()
const { workoutData, markerCoordinates } = toRefs(props)
const workoutMap = ref<null | {
leafletObject: { fitBounds: (bounds: number[][]) => null }
}>(null)
const bounds = computed(() =>
props.workoutData
? [
[
props.workoutData.workout.bounds[0],
props.workoutData.workout.bounds[1],
],
[
props.workoutData.workout.bounds[2],
props.workoutData.workout.bounds[3],
],
]
: []
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const center = computed(() => getCenter(bounds))
const geoJson = computed(() =>
props.workoutData && props.workoutData.gpx
? getGeoJson(props.workoutData.gpx)
: {}
)
function getGeoJson(gpxContent: string): GeoJSONData {
if (!gpxContent || gpxContent !== '') {
try {
const jsonData = gpx(
new DOMParser().parseFromString(gpxContent, 'text/xml')
)
return { jsonData }
} catch (e) {
console.error('Invalid gpx content')
return {}
}
function getCenter(bounds: ComputedRef<number[][]>): number[] {
return [
(bounds.value[0][0] + bounds.value[1][0]) / 2,
(bounds.value[0][1] + bounds.value[1][1]) / 2,
]
}
function fitBounds(bounds: number[][]) {
if (workoutMap.value?.leafletObject) {
workoutMap.value?.leafletObject.fitBounds(bounds)
}
}
const workoutMap = ref<null | {
leafletObject: { fitBounds: (bounds: number[][]) => null }
}>(null)
const bounds = computed(() =>
props.workoutData
? [
[
props.workoutData.workout.bounds[0],
props.workoutData.workout.bounds[1],
],
[
props.workoutData.workout.bounds[2],
props.workoutData.workout.bounds[3],
],
]
: []
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const center = computed(() => getCenter(bounds))
const geoJson = computed(() =>
props.workoutData && props.workoutData.gpx
? getGeoJson(props.workoutData.gpx)
: {}
)
return {
appConfig,
bounds,
center,
geoJson,
workoutMap,
fitBounds,
getApiUrl,
}
},
})
}
return {}
}
function getCenter(bounds: ComputedRef<number[][]>): number[] {
return [
(bounds.value[0][0] + bounds.value[1][0]) / 2,
(bounds.value[0][1] + bounds.value[1][1]) / 2,
]
}
function fitBounds(bounds: number[][]) {
if (workoutMap.value?.leafletObject) {
workoutMap.value?.leafletObject.fitBounds(bounds)
}
}
</script>
<style lang="scss" scoped>

View File

@ -3,7 +3,7 @@
class="workout-record"
v-if="
workoutObject.records &&
workoutObject.records.find((record) => record.record_type === record_type)
workoutObject.records.find((record) => record.record_type === recordType)
"
>
<sup>
@ -12,24 +12,18 @@
</span>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutRecord',
props: {
record_type: {
type: String,
required: true,
},
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
})
interface Props {
recordType: string
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { recordType, workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -97,20 +97,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutWeather',
props: {
workoutObject: {
type: Object as PropType<IWorkoutObject>,
required: true,
},
},
})
interface Props {
workoutObject: IWorkoutObject
}
const props = defineProps<Props>()
const { workoutObject } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -26,15 +26,15 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {
ComputedRef,
PropType,
Ref,
computed,
defineComponent,
ref,
toRefs,
watch,
withDefaults,
} from 'vue'
import { useRoute } from 'vue-router'
@ -54,147 +54,122 @@
import { useStore } from '@/use/useStore'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
export default defineComponent({
name: 'WorkoutDetail',
components: {
WorkoutCardTitle,
WorkoutData,
WorkoutMap,
},
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
displaySegment: {
type: Boolean,
required: true,
},
markerCoordinates: {
type: Object as PropType<TCoordinates>,
required: false,
},
sports: {
type: Object as PropType<ISport[]>,
},
workoutData: {
type: Object as PropType<IWorkoutData>,
required: true,
},
},
setup(props) {
const route = useRoute()
const store = useStore()
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,
}
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteWorkout(workoutId: string) {
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId,
})
}
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
)
let displayModal: Ref<boolean> = ref(false)
watch(
() => route.params.segmentId,
async (newSegmentId) => {
if (newSegmentId) {
segmentId.value = +newSegmentId
}
}
)
return {
sport: computed(() =>
props.sports
? props.sports.find(
(sport) => sport.id === props.workoutData.workout.sport_id
)
: {}
),
workoutObject: computed(() =>
getWorkoutObject(workout.value, segment.value)
),
displayModal,
deleteWorkout,
updateDisplayModal,
}
},
interface Props {
authUser: IUserProfile
displaySegment: boolean
sports: ISport[]
workoutData: IWorkoutData
markerCoordinates?: TCoordinates
}
const props = withDefaults(defineProps<Props>(), {
markerCoordinates: () => ({} as TCoordinates),
})
const route = useRoute()
const store = useStore()
const { markerCoordinates, workoutData } = toRefs(props)
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
)
let displayModal: Ref<boolean> = ref(false)
const sport = computed(() =>
props.sports
? props.sports.find(
(sport) => sport.id === props.workoutData.workout.sport_id
)
: {}
)
const workoutObject = computed(() =>
getWorkoutObject(workout.value, segment.value)
)
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,
}
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteWorkout(workoutId: string) {
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId,
})
}
watch(
() => route.params.segmentId,
async (newSegmentId) => {
if (newSegmentId) {
segmentId.value = +newSegmentId
}
}
)
</script>
<style lang="scss" scoped>

View File

@ -31,9 +31,9 @@
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withoutGpx">{{
$t('workouts.WITHOUT_GPX')
}}</label>
<label for="withoutGpx">
{{ $t('workouts.WITHOUT_GPX') }}
</label>
</div>
</div>
<div class="form-item">
@ -42,7 +42,7 @@
id="sport"
required
:disabled="loading"
v-model="workoutDataObject.sport_id"
v-model="workoutForm.sport_id"
>
<option
v-for="sport in translatedSports.filter((s) => s.is_active)"
@ -95,7 +95,7 @@
type="text"
:required="!isCreation"
:disabled="loading"
v-model="workoutDataObject.title"
v-model="workoutForm.title"
/>
</div>
<div v-if="!withGpx">
@ -109,7 +109,7 @@
type="date"
required
:disabled="loading"
v-model="workoutDataObject.workoutDate"
v-model="workoutForm.workoutDate"
/>
<input
id="workout-time"
@ -118,7 +118,7 @@
type="time"
required
:disabled="loading"
v-model="workoutDataObject.workoutTime"
v-model="workoutForm.workoutTime"
/>
</div>
</div>
@ -134,7 +134,7 @@
pattern="^([0-9]*[0-9])$"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationHour"
v-model="workoutForm.workoutDurationHour"
/>
:
<input
@ -146,7 +146,7 @@
placeholder="MM"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationMinutes"
v-model="workoutForm.workoutDurationMinutes"
/>
:
<input
@ -158,7 +158,7 @@
placeholder="SS"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationSeconds"
v-model="workoutForm.workoutDurationSeconds"
/>
</div>
</div>
@ -172,7 +172,7 @@
step="0.1"
required
:disabled="loading"
v-model="workoutDataObject.workoutDistance"
v-model="workoutForm.workoutDistance"
/>
</div>
</div>
@ -180,7 +180,7 @@
<label> {{ $t('workouts.NOTES') }}: </label>
<CustomTextArea
name="notes"
:input="workoutDataObject.notes"
:input="workoutForm.notes"
:disabled="loading"
@updateValue="updateNotes"
/>
@ -205,17 +205,17 @@
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import {
ComputedRef,
PropType,
defineComponent,
computed,
reactive,
ref,
toRefs,
watch,
onMounted,
onUnmounted,
withDefaults,
} from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@ -230,189 +230,154 @@
import { getReadableFileSize } from '@/utils/files'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'WorkoutEdition',
props: {
authUser: {
type: Object as PropType<IUserProfile>,
required: true,
},
isCreation: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
workout: {
type: Object as PropType<IWorkout>,
required: false,
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const router = useRouter()
onMounted(() => {
if (props.workout && props.workout.id) {
formatWorkoutForm(props.workout)
}
})
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
const gpx_limit_import = appConfig.value.gpx_limit_import
const zipSizeLimit = appConfig.value.max_zip_file_size
? getReadableFileSize(appConfig.value.max_zip_file_size)
: ''
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const workoutForm = reactive({
sport_id: '',
title: '',
notes: '',
workoutDate: '',
workoutTime: '',
workoutDurationHour: '',
workoutDurationMinutes: '',
workoutDurationSeconds: '',
workoutDistance: '',
})
let withGpx = ref(
props.workout ? props.workout.with_gpx : props.isCreation
)
let gpxFile: File | null = null
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWithGpx() {
withGpx.value = !withGpx.value
}
function updateFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
gpxFile = event.target.files[0]
}
}
function formatWorkoutForm(workout: IWorkout) {
workoutForm.sport_id = `${workout.sport_id}`
workoutForm.title = workout.title
workoutForm.notes = workout.notes
if (!workout.with_gpx) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, props.authUser.timezone),
'yyyy-MM-dd'
)
const duration = workout.duration.split(':')
workoutForm.workoutDistance = `${workout.distance}`
workoutForm.workoutDate = workoutDateTime.workout_date
workoutForm.workoutTime = workoutDateTime.workout_time
workoutForm.workoutDurationHour = duration[0]
workoutForm.workoutDurationMinutes = duration[1]
workoutForm.workoutDurationSeconds = duration[2]
}
}
function formatPayload(payload: IWorkoutForm) {
payload.title = workoutForm.title
payload.distance = +workoutForm.workoutDistance
payload.duration =
+workoutForm.workoutDurationHour * 3600 +
+workoutForm.workoutDurationMinutes * 60 +
+workoutForm.workoutDurationSeconds
payload.workout_date = `${workoutForm.workoutDate} ${workoutForm.workoutTime}`
}
function updateWorkout() {
const payload: IWorkoutForm = {
sport_id: +workoutForm.sport_id,
notes: workoutForm.notes,
}
if (props.workout) {
if (props.workout.with_gpx) {
payload.title = workoutForm.title
} else {
formatPayload(payload)
}
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id,
data: payload,
})
} else {
if (withGpx.value) {
if (!gpxFile) {
const errorMessage = 'workouts.NO_FILE_PROVIDED'
store.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
errorMessage
)
return
}
payload.file = gpxFile
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
} else {
formatPayload(payload)
store.dispatch(
WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX,
payload
)
}
}
}
function onCancel() {
if (props.workout) {
router.push({
name: 'Workout',
params: { workoutId: props.workout.id },
})
} else {
router.go(-1)
}
}
watch(
() => props.workout,
async (
newWorkout: IWorkout | undefined,
previousWorkout: IWorkout | undefined
) => {
if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
}
}
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return {
appConfig,
errorMessages,
fileSizeLimit,
gpx_limit_import,
translatedSports,
withGpx,
zipSizeLimit,
workoutDataObject: workoutForm,
onCancel,
updateFile,
updateNotes,
updateWithGpx,
updateWorkout,
}
},
interface Props {
authUser: IUserProfile
sports: ISport[]
isCreation?: boolean
loading?: boolean
workout?: IWorkout
}
const props = withDefaults(defineProps<Props>(), {
isCreation: false,
loading: false,
workout: () => ({} as IWorkout),
})
const { t } = useI18n()
const store = useStore()
const router = useRouter()
const { workout, isCreation, loading } = toRefs(props)
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
const gpx_limit_import = appConfig.value.gpx_limit_import
const zipSizeLimit = appConfig.value.max_zip_file_size
? getReadableFileSize(appConfig.value.max_zip_file_size)
: ''
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const workoutForm = reactive({
sport_id: '',
title: '',
notes: '',
workoutDate: '',
workoutTime: '',
workoutDurationHour: '',
workoutDurationMinutes: '',
workoutDurationSeconds: '',
workoutDistance: '',
})
let withGpx = ref(
props.workout.id ? props.workout.with_gpx : props.isCreation
)
let gpxFile: File | null = null
onMounted(() => {
if (props.workout.id) {
formatWorkoutForm(props.workout)
}
})
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWithGpx() {
withGpx.value = !withGpx.value
}
function updateFile(event: Event & { target: HTMLInputElement }) {
if (event.target.files) {
gpxFile = event.target.files[0]
}
}
function formatWorkoutForm(workout: IWorkout) {
workoutForm.sport_id = `${workout.sport_id}`
workoutForm.title = workout.title
workoutForm.notes = workout.notes
if (!workout.with_gpx) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, props.authUser.timezone),
'yyyy-MM-dd'
)
const duration = workout.duration.split(':')
workoutForm.workoutDistance = `${workout.distance}`
workoutForm.workoutDate = workoutDateTime.workout_date
workoutForm.workoutTime = workoutDateTime.workout_time
workoutForm.workoutDurationHour = duration[0]
workoutForm.workoutDurationMinutes = duration[1]
workoutForm.workoutDurationSeconds = duration[2]
}
}
function formatPayload(payload: IWorkoutForm) {
payload.title = workoutForm.title
payload.distance = +workoutForm.workoutDistance
payload.duration =
+workoutForm.workoutDurationHour * 3600 +
+workoutForm.workoutDurationMinutes * 60 +
+workoutForm.workoutDurationSeconds
payload.workout_date = `${workoutForm.workoutDate} ${workoutForm.workoutTime}`
}
function updateWorkout() {
const payload: IWorkoutForm = {
sport_id: +workoutForm.sport_id,
notes: workoutForm.notes,
}
if (props.workout.id) {
if (props.workout.with_gpx) {
payload.title = workoutForm.title
} else {
formatPayload(payload)
}
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id,
data: payload,
})
} else {
if (withGpx.value) {
if (!gpxFile) {
const errorMessage = 'workouts.NO_FILE_PROVIDED'
store.commit(ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES, errorMessage)
return
}
payload.file = gpxFile
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
} else {
formatPayload(payload)
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX, payload)
}
}
}
function onCancel() {
if (props.workout.id) {
router.push({
name: 'Workout',
params: { workoutId: props.workout.id },
})
} else {
router.go(-1)
}
}
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
watch(
() => props.workout,
async (
newWorkout: IWorkout | undefined,
previousWorkout: IWorkout | undefined
) => {
if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
}
}
)
</script>
<style lang="scss" scoped>

View File

@ -9,18 +9,17 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
export default defineComponent({
name: 'WorkoutNotes',
props: {
notes: {
type: String,
required: false,
},
},
interface Props {
notes?: string | null
}
const props = withDefaults(defineProps<Props>(), {
notes: () => null,
})
const { notes } = toRefs(props)
</script>
<style lang="scss" scoped>

View File

@ -24,20 +24,17 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
<script setup lang="ts">
import { toRefs } from 'vue'
import { IWorkoutSegment } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutSegments',
props: {
segments: {
type: Object as PropType<IWorkoutSegment[]>,
required: true,
},
},
})
interface Props {
segments: IWorkoutSegment[]
}
const props = defineProps<Props>()
const { segments } = toRefs(props)
</script>
<style lang="scss" scoped>