499 lines
16 KiB
Vue
Raw Normal View History

2021-09-28 12:39:12 +02:00
<template>
<div
id="workout-edition"
2021-10-23 21:34:02 +02:00
class="center-card center-card with-margin"
:class="{ 'center-form': workout && workout.with_gpx }"
>
<Card>
2021-09-29 11:32:05 +02:00
<template #title>{{
2021-10-20 17:38:25 +02:00
$t(`workouts.${isCreation ? 'ADD' : 'EDIT'}_WORKOUT`)
2021-09-29 11:32:05 +02:00
}}</template>
2021-09-28 12:39:12 +02:00
<template #content>
<div id="workout-form">
<form :class="{ errors: formErrors }" @submit.prevent="updateWorkout">
2021-09-28 12:39:12 +02:00
<div class="form-items">
2021-09-29 11:32:05 +02:00
<div class="form-item-radio" v-if="isCreation">
<div>
2021-09-29 11:32:05 +02:00
<input
id="withGpx"
type="radio"
:checked="withGpx"
:disabled="loading"
@click="updateWithGpx"
/>
2021-10-20 17:38:25 +02:00
<label for="withGpx">{{ $t('workouts.WITH_GPX') }}</label>
2021-09-29 11:32:05 +02:00
</div>
<div>
2021-09-29 11:32:05 +02:00
<input
id="withoutGpx"
type="radio"
:checked="!withGpx"
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withoutGpx">
{{ $t('workouts.WITHOUT_GPX') }}
</label>
2021-09-29 11:32:05 +02:00
</div>
</div>
2021-09-28 12:39:12 +02:00
<div class="form-item">
2021-10-20 17:38:25 +02:00
<label> {{ $t('workouts.SPORT', 1) }}: </label>
2021-09-29 11:32:05 +02:00
<select
id="sport"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.sport_id"
2021-09-29 11:32:05 +02:00
>
2021-09-28 12:39:12 +02:00
<option
2021-10-27 18:54:41 +02:00
v-for="sport in translatedSports.filter((s) => s.is_active)"
2021-09-28 12:39:12 +02:00
:value="sport.id"
:key="sport.id"
>
2021-11-11 09:41:23 +01:00
{{ sport.translatedLabel }}
2021-09-28 12:39:12 +02:00
</option>
</select>
</div>
2021-09-29 11:32:05 +02:00
<div class="form-item" v-if="isCreation && withGpx">
<label for="gpxFile">
2021-10-20 17:38:25 +02:00
{{ $t('workouts.GPX_FILE') }}
{{ $t('workouts.ZIP_ARCHIVE_DESCRIPTION') }}:
2021-09-29 11:32:05 +02:00
</label>
<input
id="gpxFile"
name="gpxFile"
type="file"
accept=".gpx, .zip"
:disabled="loading"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
@input="updateFile"
/>
<div class="files-help info-box">
<div>
2021-10-20 17:38:25 +02:00
<strong>{{ $t('workouts.GPX_FILE') }}:</strong>
<ul>
2021-10-20 17:38:25 +02:00
<li>
{{ $t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}
</li>
</ul>
</div>
<div>
2021-10-20 17:38:25 +02:00
<strong>{{ $t('workouts.ZIP_ARCHIVE') }}:</strong>
<ul>
2021-10-20 17:38:25 +02:00
<li>{{ $t('workouts.NO_FOLDER') }}</li>
<li>
2021-10-20 17:38:25 +02:00
{{ $t('workouts.MAX_FILES') }}: {{ gpx_limit_import }}
</li>
2021-10-20 17:38:25 +02:00
<li>{{ $t('workouts.MAX_SIZE') }}: {{ zipSizeLimit }}</li>
</ul>
</div>
</div>
2021-09-29 11:32:05 +02:00
</div>
<div class="form-item" v-else>
2021-10-20 17:38:25 +02:00
<label for="title"> {{ $t('workouts.TITLE') }}: </label>
2021-09-28 12:39:12 +02:00
<input
id="title"
name="title"
type="text"
2021-09-29 11:32:05 +02:00
:required="!isCreation"
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.title"
2021-09-28 12:39:12 +02:00
/>
</div>
2021-09-29 11:32:05 +02:00
<div v-if="!withGpx">
<div class="workout-date-duration">
<div class="form-item">
2021-10-20 17:38:25 +02:00
<label>{{ $t('workouts.WORKOUT_DATE') }}:</label>
2021-09-29 11:32:05 +02:00
<div class="workout-date-time">
<input
id="workout-date"
name="workout-date"
type="date"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutDate"
2021-09-29 11:32:05 +02:00
/>
<input
id="workout-time"
name="workout-time"
class="workout-time"
type="time"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutTime"
2021-09-29 11:32:05 +02:00
/>
</div>
</div>
<div class="form-item">
2021-10-20 17:38:25 +02:00
<label>{{ $t('workouts.DURATION') }}:</label>
2021-09-29 11:32:05 +02:00
<div>
<input
id="workout-duration-hour"
name="workout-duration-hour"
class="workout-duration"
type="text"
placeholder="HH"
pattern="^([0-9]*[0-9])$"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutDurationHour"
2021-09-29 11:32:05 +02:00
/>
:
<input
id="workout-duration-minutes"
name="workout-duration-minutes"
class="workout-duration"
type="text"
pattern="^([0-5][0-9])$"
placeholder="MM"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutDurationMinutes"
2021-09-29 11:32:05 +02:00
/>
:
<input
id="workout-duration-seconds"
name="workout-duration-seconds"
class="workout-duration"
type="text"
pattern="^([0-5][0-9])$"
placeholder="SS"
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutDurationSeconds"
2021-09-29 11:32:05 +02:00
/>
</div>
</div>
</div>
<div class="form-item">
2021-10-20 17:38:25 +02:00
<label>{{ $t('workouts.DISTANCE') }} (km):</label>
2021-09-29 11:32:05 +02:00
<input
name="workout-distance"
2021-09-29 11:32:05 +02:00
type="number"
min="0"
2021-11-11 09:41:23 +01:00
step="0.01"
2021-09-29 11:32:05 +02:00
required
@invalid="invalidateForm"
2021-09-29 11:32:05 +02:00
:disabled="loading"
v-model="workoutForm.workoutDistance"
2021-09-29 11:32:05 +02:00
/>
</div>
</div>
2021-09-28 12:39:12 +02:00
<div class="form-item">
2021-10-20 17:38:25 +02:00
<label> {{ $t('workouts.NOTES') }}: </label>
2021-09-28 12:39:12 +02:00
<CustomTextArea
name="notes"
:input="workoutForm.notes"
2021-09-29 11:32:05 +02:00
:disabled="loading"
2021-09-28 12:39:12 +02:00
@updateValue="updateNotes"
/>
</div>
</div>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
2021-09-29 11:32:05 +02:00
<div v-if="loading">
<Loader />
</div>
<div v-else class="form-buttons">
<button class="confirm" type="submit" :disabled="loading">
2021-10-20 17:38:25 +02:00
{{ $t('buttons.SUBMIT') }}
2021-09-28 12:39:12 +02:00
</button>
2021-09-29 11:32:05 +02:00
<button class="cancel" @click.prevent="onCancel">
2021-10-20 17:38:25 +02:00
{{ $t('buttons.CANCEL') }}
2021-09-28 12:39:12 +02:00
</button>
</div>
</form>
</div>
</template>
</Card>
</div>
</template>
<script setup lang="ts">
2021-09-28 12:39:12 +02:00
import {
ComputedRef,
computed,
reactive,
2021-09-29 11:32:05 +02:00
ref,
toRefs,
2021-09-28 12:39:12 +02:00
watch,
2021-09-29 11:32:05 +02:00
onMounted,
2021-09-28 12:39:12 +02:00
onUnmounted,
withDefaults,
2021-09-28 12:39:12 +02:00
} from 'vue'
import { useI18n } from 'vue-i18n'
2021-09-29 11:32:05 +02:00
import { useRouter } from 'vue-router'
2021-09-28 12:39:12 +02:00
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
2021-09-28 12:39:12 +02:00
import { ISport } from '@/types/sports'
2021-10-30 12:01:55 +02:00
import { IUserProfile } from '@/types/user'
2021-09-29 11:32:05 +02:00
import { IWorkout, IWorkoutForm } from '@/types/workouts'
2021-09-28 12:39:12 +02:00
import { useStore } from '@/use/useStore'
2021-09-29 11:32:05 +02:00
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
import { getReadableFileSize } from '@/utils/files'
2021-09-28 12:39:12 +02:00
import { translateSports } from '@/utils/sports'
interface Props {
authUser: IUserProfile
sports: ISport[]
isCreation?: boolean
loading?: boolean
workout?: IWorkout
}
const props = withDefaults(defineProps<Props>(), {
isCreation: false,
loading: false,
workout: () => ({} as IWorkout),
})
2021-09-29 11:32:05 +02:00
const { t } = useI18n()
const store = useStore()
const router = useRouter()
2021-09-28 12:39:12 +02:00
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
const formErrors = ref(false)
2021-09-28 12:39:12 +02:00
onMounted(() => {
if (props.workout.id) {
formatWorkoutForm(props.workout)
}
})
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWithGpx() {
withGpx.value = !withGpx.value
formErrors.value = false
}
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) {
2021-09-29 11:32:05 +02:00
payload.title = workoutForm.title
} else {
formatPayload(payload)
2021-09-29 11:32:05 +02:00
}
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
2021-09-28 12:39:12 +02:00
}
payload.file = gpxFile
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT, payload)
} else {
formatPayload(payload)
store.dispatch(WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX, payload)
2021-09-28 12:39:12 +02:00
}
}
}
function onCancel() {
if (props.workout.id) {
router.push({
name: 'Workout',
params: { workoutId: props.workout.id },
})
} else {
router.go(-1)
}
}
function invalidateForm() {
formErrors.value = true
}
2021-09-28 12:39:12 +02:00
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
2021-09-28 12:39:12 +02:00
watch(
() => props.workout,
async (
newWorkout: IWorkout | undefined,
previousWorkout: IWorkout | undefined
) => {
if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
2021-09-28 12:39:12 +02:00
}
}
)
2021-09-28 12:39:12 +02:00
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
#workout-edition {
@media screen and (max-width: $small-limit) {
&.center-form {
margin: 50px auto;
}
}
2021-09-28 12:39:12 +02:00
::v-deep(.card) {
.card-title {
text-align: center;
text-transform: uppercase;
}
2021-09-29 11:32:05 +02:00
2021-09-28 12:39:12 +02:00
.card-content {
@media screen and (max-width: $medium-limit) {
2021-09-29 11:32:05 +02:00
padding: $default-padding 0;
}
2021-09-28 12:39:12 +02:00
2021-09-29 11:32:05 +02:00
#workout-form {
.form-items {
2021-09-28 12:39:12 +02:00
display: flex;
flex-direction: column;
2021-09-29 11:32:05 +02:00
input {
height: 20px;
}
.workout-date-duration {
display: flex;
flex-direction: row;
justify-content: space-between;
@media screen and (max-width: $medium-limit) {
2021-09-29 11:32:05 +02:00
flex-direction: column;
}
}
.form-item {
display: flex;
flex-direction: column;
padding: $default-padding;
.workout-date-time {
display: flex;
#workout-date {
margin-right: $default-margin;
}
}
.workout-duration {
width: 25px;
}
}
.form-item-radio {
display: flex;
justify-content: space-around;
label {
font-weight: normal;
@media screen and (max-width: $medium-limit) {
font-size: 0.9em;
2021-09-29 11:32:05 +02:00
}
}
input {
margin-top: -2px;
vertical-align: middle;
}
2021-09-28 12:39:12 +02:00
}
}
2021-09-29 11:32:05 +02:00
.form-buttons {
display: flex;
justify-content: flex-end;
button {
margin: $default-padding * 0.5;
}
2021-09-28 12:39:12 +02:00
}
.files-help {
display: flex;
justify-content: space-around;
margin-top: $default-margin;
div {
display: flex;
@media screen and (max-width: $medium-limit) {
flex-direction: column;
}
ul {
margin: 0;
padding: 0 $default-padding * 2;
}
}
}
2021-09-28 12:39:12 +02:00
}
}
}
}
</style>