Client - add a workout + fix

This commit is contained in:
Sam 2021-09-29 11:32:05 +02:00
parent 991a7acc03
commit 6bfcb24133
14 changed files with 515 additions and 74 deletions

View File

@ -4,6 +4,7 @@
:id="name" :id="name"
:name="name" :name="name"
:maxLenght="charLimit" :maxLenght="charLimit"
:disabled="disabled"
v-model="text" v-model="text"
@input="updateText" @input="updateText"
/> />
@ -24,6 +25,10 @@
type: Number, type: Number,
default: 500, default: 500,
}, },
disabled: {
type: Boolean,
default: false,
},
input: { input: {
type: String, type: String,
default: '', default: '',

View File

@ -2,15 +2,15 @@
<div id="timeline"> <div id="timeline">
<div class="section-title">{{ t('workouts.LATEST_WORKOUTS') }}</div> <div class="section-title">{{ t('workouts.LATEST_WORKOUTS') }}</div>
<WorkoutCard <WorkoutCard
v-for="index in [...Array(workoutsToDisplayCount()).keys()]" v-for="workout in workouts"
:workout="workouts.length > 0 ? workouts[index] : null" :workout="workout"
:sport=" :sport="
workouts.length > 0 workouts.length > 0
? sports.filter((s) => s.id === workouts[index].sport_id)[0] ? sports.filter((s) => s.id === workout.sport_id)[0]
: null : null
" "
:user="user" :user="user"
:key="index" :key="workout.id"
/> />
<div v-if="workouts.length === 0" class="no-workouts"> <div v-if="workouts.length === 0" class="no-workouts">
{{ t('workouts.NO_WORKOUTS') }} {{ t('workouts.NO_WORKOUTS') }}
@ -56,7 +56,7 @@
required: true, required: true,
}, },
}, },
setup(props) { setup() {
const store = useStore() const store = useStore()
const { t } = useI18n() const { t } = useI18n()
@ -64,9 +64,6 @@
const per_page = 5 const per_page = 5
onBeforeMount(() => loadWorkouts()) onBeforeMount(() => loadWorkouts())
const initWorkoutsCount =
props.user.nb_workouts >= per_page ? per_page : props.user.nb_workouts
const workouts: ComputedRef<IWorkout[]> = computed( const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS] () => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
) )
@ -86,20 +83,13 @@
page.value += 1 page.value += 1
loadWorkouts() loadWorkouts()
} }
function workoutsToDisplayCount() {
return workouts.value.length > initWorkoutsCount
? workouts.value.length
: initWorkoutsCount
}
return { return {
initWorkoutsCount,
moreWorkoutsExist, moreWorkoutsExist,
per_page, per_page,
workouts, workouts,
t, t,
loadMoreWorkouts, loadMoreWorkouts,
workoutsToDisplayCount,
} }
}, },
}) })

View File

@ -28,7 +28,9 @@
</div> </div>
<div class="nav-item">{{ t('statistics.STATISTICS') }}</div> <div class="nav-item">{{ t('statistics.STATISTICS') }}</div>
<div class="nav-item">{{ t('administration.ADMIN') }}</div> <div class="nav-item">{{ t('administration.ADMIN') }}</div>
<div class="nav-item">{{ t('workouts.ADD_WORKOUT') }}</div> <router-link class="nav-item" to="/workouts/add">
{{ t('workouts.ADD_WORKOUT') }}
</router-link>
<div class="nav-item nav-separator" /> <div class="nav-item nav-separator" />
</div> </div>
</div> </div>

View File

@ -1,14 +1,45 @@
<template> <template>
<div id="workout-edition"> <div id="workout-edition">
<Card :without-title="false"> <Card :without-title="false">
<template #title>{{ t('workouts.EDIT_WORKOUT') }}</template> <template #title>{{
t(`workouts.${isCreation ? 'ADD' : 'EDIT'}_WORKOUT`)
}}</template>
<template #content> <template #content>
<div id="workout-form"> <div id="workout-form">
<form @submit.prevent="updateWorkout"> <form @submit.prevent="updateWorkout">
<div class="form-items"> <div class="form-items">
<div class="form-item-radio" v-if="isCreation">
<div class="radio">
<input
id="withGpx"
type="radio"
:checked="withGpx"
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withGpx">{{ t('workouts.WITH_GPX') }}</label>
</div>
<div class="radio">
<input
id="withoutGpx"
type="radio"
:checked="!withGpx"
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withoutGpx">{{
t('workouts.WITHOUT_GPX')
}}</label>
</div>
</div>
<div class="form-item"> <div class="form-item">
<label> {{ t('workouts.SPORT', 1) }}: </label> <label> {{ t('workouts.SPORT', 1) }}: </label>
<select id="sport" v-model="workoutDataObject.sport_id"> <select
id="sport"
required
:disabled="loading"
v-model="workoutDataObject.sport_id"
>
<option <option
v-for="sport in translatedSports" v-for="sport in translatedSports"
:value="sport.id" :value="sport.id"
@ -18,38 +49,133 @@
</option> </option>
</select> </select>
</div> </div>
<div class="form-item"> <div class="form-item" v-if="isCreation && withGpx">
<label for="gpxFile">
{{ t('workouts.GPX_FILE') }}
<sup>
<i class="fa fa-question-circle" aria-hidden="true" />
</sup>
{{ t('workouts.ZIP_FILE') }}
<sup
><i class="fa fa-question-circle" aria-hidden="true" /></sup
>:
</label>
<input
id="gpxFile"
name="gpxFile"
type="file"
accept=".gpx, .zip"
:disabled="loading"
@input="updateFile"
/>
</div>
<div class="form-item" v-else>
<label for="title"> {{ t('workouts.TITLE') }}: </label> <label for="title"> {{ t('workouts.TITLE') }}: </label>
<input <input
id="title" id="title"
name="title" name="title"
type="text" type="text"
:required="!isCreation"
:disabled="loading"
v-model="workoutDataObject.title" v-model="workoutDataObject.title"
/> />
</div> </div>
<div v-if="!withGpx">
<div class="workout-date-duration">
<div class="form-item">
<label>{{ t('workouts.WORKOUT_DATE') }}:</label>
<div class="workout-date-time">
<input
id="workout-date"
name="workout-date"
type="date"
required
:disabled="loading"
v-model="workoutDataObject.workoutDate"
/>
<input
id="workout-time"
name="workout-time"
class="workout-time"
type="time"
required
:disabled="loading"
v-model="workoutDataObject.workoutTime"
/>
</div>
</div>
<div class="form-item">
<label>{{ t('workouts.DURATION') }}:</label>
<div>
<input
id="workout-duration-hour"
name="workout-duration-hour"
class="workout-duration"
type="text"
placeholder="HH"
pattern="^([0-9]*[0-9])$"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationHour"
/>
:
<input
id="workout-duration-minutes"
name="workout-duration-minutes"
class="workout-duration"
type="text"
pattern="^([0-5][0-9])$"
placeholder="MM"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationMinutes"
/>
:
<input
id="workout-duration-seconds"
name="workout-duration-seconds"
class="workout-duration"
type="text"
pattern="^([0-5][0-9])$"
placeholder="SS"
required
:disabled="loading"
v-model="workoutDataObject.workoutDurationSeconds"
/>
</div>
</div>
</div>
<div class="form-item">
<label>{{ t('workouts.DISTANCE') }} (km):</label>
<input
type="number"
min="0"
step="0.1"
required
:disabled="loading"
v-model="workoutDataObject.workoutDistance"
/>
</div>
</div>
<div class="form-item"> <div class="form-item">
<label> {{ t('workouts.NOTES') }}: </label> <label> {{ t('workouts.NOTES') }}: </label>
<CustomTextArea <CustomTextArea
name="notes" name="notes"
:input="workoutDataObject.notes" :input="workoutDataObject.notes"
:disabled="loading"
@updateValue="updateNotes" @updateValue="updateNotes"
/> />
</div> </div>
</div> </div>
<ErrorMessage :message="errorMessages" v-if="errorMessages" /> <ErrorMessage :message="errorMessages" v-if="errorMessages" />
<div class="form-buttons"> <div v-if="loading">
<button class="confirm" type="submit"> <Loader />
</div>
<div v-else class="form-buttons">
<button class="confirm" type="submit" :disabled="loading">
{{ t('buttons.SUBMIT') }} {{ t('buttons.SUBMIT') }}
</button> </button>
<button <button class="cancel" @click.prevent="onCancel">
class="cancel"
@click="
$router.push({
name: 'Workout',
params: { workoutId: workout.id },
})
"
>
{{ t('buttons.CANCEL') }} {{ t('buttons.CANCEL') }}
</button> </button>
</div> </div>
@ -67,18 +193,24 @@
defineComponent, defineComponent,
computed, computed,
reactive, reactive,
ref,
watch, watch,
onMounted,
onUnmounted, onUnmounted,
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import Card from '@/components/Common/Card.vue' import Card from '@/components/Common/Card.vue'
import CustomTextArea from '@/components/Common/CustomTextArea.vue' import CustomTextArea from '@/components/Common/CustomTextArea.vue'
import ErrorMessage from '@/components/Common/ErrorMessage.vue' import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import Loader from '@/components/Common/Loader.vue'
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants' import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports' import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts' import { IAuthUserProfile } from '@/types/user'
import { IWorkout, IWorkoutForm } from '@/types/workouts'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
import { translateSports } from '@/utils/sports' import { translateSports } from '@/utils/sports'
export default defineComponent({ export default defineComponent({
@ -87,20 +219,40 @@
Card, Card,
CustomTextArea, CustomTextArea,
ErrorMessage, ErrorMessage,
Loader,
}, },
props: { props: {
authUser: {
type: Object as PropType<IAuthUserProfile>,
required: true,
},
isCreation: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
sports: { sports: {
type: Object as PropType<ISport[]>, type: Object as PropType<ISport[]>,
required: true, required: true,
}, },
workout: { workout: {
type: Object as PropType<IWorkout>, type: Object as PropType<IWorkout>,
required: true, required: false,
}, },
}, },
setup(props) { setup(props) {
const { t } = useI18n() const { t } = useI18n()
const store = useStore() const store = useStore()
const router = useRouter()
onMounted(() => {
if (props.workout && props.workout.id) {
formatWorkoutForm(props.workout)
}
})
const translatedSports: ComputedRef<ISport[]> = computed(() => const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t) translateSports(props.sports, t)
@ -109,30 +261,112 @@
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES] () => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
) )
const workoutForm = reactive({ const workoutForm = reactive({
sport_id: 0, sport_id: '',
title: '', title: '',
notes: '', 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) { function updateNotes(value: string) {
workoutForm.notes = value 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() { function updateWorkout() {
const payload: IWorkoutForm = {
sport_id: +workoutForm.sport_id,
notes: workoutForm.notes,
}
if (props.workout) { if (props.workout) {
if (props.workout.with_gpx) {
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, { store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id, workoutId: props.workout.id,
data: workoutForm, data: payload,
}) })
} else {
formatPayload(payload)
store.dispatch(
WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX,
payload
)
}
} else {
if (withGpx.value) {
if (!gpxFile) {
throw new Error('No file provided !!')
}
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( watch(
() => props.workout, () => props.workout,
async (newWorkout: IWorkout | undefined) => { async (
if (newWorkout && newWorkout.id) { newWorkout: IWorkout | undefined,
workoutForm.sport_id = newWorkout.sport_id previousWorkout: IWorkout | undefined
workoutForm.title = newWorkout.title ) => {
workoutForm.notes = newWorkout.notes if (newWorkout !== previousWorkout && newWorkout && newWorkout.id) {
formatWorkoutForm(newWorkout)
} }
} }
) )
@ -143,8 +377,12 @@
errorMessages, errorMessages,
t, t,
translatedSports, translatedSports,
withGpx,
workoutDataObject: workoutForm, workoutDataObject: workoutForm,
onCancel,
updateFile,
updateNotes, updateNotes,
updateWithGpx,
updateWorkout, updateWorkout,
} }
}, },
@ -155,29 +393,75 @@
@import '~@/scss/base'; @import '~@/scss/base';
#workout-edition { #workout-edition {
margin: 25% auto; margin: 100px auto;
width: 700px; width: 700px;
@media screen and (max-width: $small-limit) {
width: 100%;
margin: 0 auto 50px auto;
}
::v-deep(.card) { ::v-deep(.card) {
.card-title { .card-title {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
} }
.card-content { .card-content {
@media screen and (max-width: $small-limit) {
padding: $default-padding 0;
}
#workout-form {
.form-items { .form-items {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
input {
height: 20px;
}
.workout-date-duration {
display: flex;
flex-direction: row;
justify-content: space-between;
@media screen and (max-width: $small-limit) {
flex-direction: column;
}
}
.form-item { .form-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: $default-padding; 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;
.radio {
label { label {
text-transform: capitalize; font-weight: normal;
}
input {
margin-top: -2px;
vertical-align: middle;
} }
} }
} }
}
.form-buttons { .form-buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -187,10 +471,6 @@
} }
} }
} }
@media screen and (max-width: $small-limit) {
width: 100%;
margin: 15% auto;
} }
} }
</style> </style>

View File

@ -1,5 +1,5 @@
{ {
"ADD_WORKOUT": "Add workout", "ADD_WORKOUT": "Add a workout",
"ANALYSIS": "analysis", "ANALYSIS": "analysis",
"ASCENT": "ascent", "ASCENT": "ascent",
"AVERAGE_SPEED": "average speed", "AVERAGE_SPEED": "average speed",
@ -10,6 +10,7 @@
"EDIT_WORKOUT": "Edit the workout", "EDIT_WORKOUT": "Edit the workout",
"ELEVATION": "elevation", "ELEVATION": "elevation",
"END": "end", "END": "end",
"GPX_FILE": "fichier .gpx",
"KM": "km", "KM": "km",
"LATEST_WORKOUTS": "Latest workouts", "LATEST_WORKOUTS": "Latest workouts",
"LOAD_MORE_WORKOUT": "Load more workouts", "LOAD_MORE_WORKOUT": "Load more workouts",
@ -27,7 +28,7 @@
"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_SEGMENT": "Previous segment",
"PREVIOUS_WORKOUT": "Previous workout", "PREVIOUS_WORKOUT": "Previous workout",
@ -60,6 +61,10 @@
"wind": "wind" "wind": "wind"
} }
}, },
"WITH_GPX": "with .gpx file",
"WITHOUT_GPX": "without .gpx file",
"WORKOUT": "workout | workouts", "WORKOUT": "workout | workouts",
"WORKOUT_DELETION_CONFIRMATION": "Are you sure you want to delete this workout?" "WORKOUT_DATE": "workout date",
"WORKOUT_DELETION_CONFIRMATION": "Are you sure you want to delete this workout?",
"ZIP_FILE": "or .zip file containing .gpx files"
} }

View File

@ -10,6 +10,7 @@
"EDIT_WORKOUT": "Modifier la séance", "EDIT_WORKOUT": "Modifier la séance",
"ELEVATION": "altitude", "ELEVATION": "altitude",
"END": "fin", "END": "fin",
"GPX_FILE": ".gpx file",
"KM": "km", "KM": "km",
"LATEST_WORKOUTS": "Séances récentes", "LATEST_WORKOUTS": "Séances récentes",
"LOAD_MORE_WORKOUT": "Charger les séances suivantes", "LOAD_MORE_WORKOUT": "Charger les séances suivantes",
@ -27,7 +28,7 @@
"NO_PREVIOUS_WORKOUT": "Pas de séance précédente", "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_SEGMENT": "Segment précédent",
"PREVIOUS_WORKOUT": "Séance précédente", "PREVIOUS_WORKOUT": "Séance précédente",
@ -60,6 +61,10 @@
"wind": "venteux" "wind": "venteux"
} }
}, },
"WITH_GPX": "avec un fichier .gpx",
"WITHOUT_GPX": "sans fichier .gpx",
"WORKOUT": "séance | séances", "WORKOUT": "séance | séances",
"WORKOUT_DELETION_CONFIRMATION": "Etes-vous sûr de vouloir supprimer cette séance ?" "WORKOUT_DATE": "date de la séance",
"WORKOUT_DELETION_CONFIRMATION": "Etes-vous sûr de vouloir supprimer cette séance ?",
"ZIP_FILE": "ou une archive .zip contenant des fichiers .gpx"
} }

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import store from '@/store' import store from '@/store'
import { USER_STORE } from '@/store/constants' import { USER_STORE } from '@/store/constants'
import AddWorkout from '@/views/AddWorkout.vue'
import Dashboard from '@/views/DashBoard.vue' import Dashboard from '@/views/DashBoard.vue'
import EditWorkout from '@/views/EditWorkout.vue' import EditWorkout from '@/views/EditWorkout.vue'
import LoginOrRegister from '@/views/LoginOrRegister.vue' import LoginOrRegister from '@/views/LoginOrRegister.vue'
@ -43,6 +44,11 @@ const routes: Array<RouteRecordRaw> = [
component: Workout, component: Workout,
props: { displaySegment: true }, props: { displaySegment: true },
}, },
{
path: '/workouts/add',
name: 'AddWorkout',
component: AddWorkout,
},
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView }, { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView },
] ]

View File

@ -33,6 +33,7 @@ input, textarea, select {
padding: $default-padding; padding: $default-padding;
&:disabled { &:disabled {
background-color: var(--disabled-background-color);
border-color: var(--disabled-color); border-color: var(--disabled-color);
} }
} }

View File

@ -2,13 +2,18 @@ import { ActionContext, ActionTree } from 'vuex'
import authApi from '@/api/authApi' import authApi from '@/api/authApi'
import router from '@/router' import router from '@/router'
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants' import { ROOT_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { import {
IWorkoutsActions, IWorkoutsActions,
IWorkoutsState, IWorkoutsState,
} from '@/store/modules/workouts/types' } from '@/store/modules/workouts/types'
import { IWorkout, IWorkoutPayload, IWorkoutsPayload } from '@/types/workouts' import {
IWorkout,
IWorkoutForm,
IWorkoutPayload,
IWorkoutsPayload,
} from '@/types/workouts'
import { handleError } from '@/utils' import { handleError } from '@/utils'
const getWorkouts = ( const getWorkouts = (
@ -113,37 +118,108 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
payload: IWorkoutPayload 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)
authApi authApi
.delete(`workouts/${payload.workoutId}`) .delete(`workouts/${payload.workoutId}`)
.then(() => { .then(() => {
context.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT) context.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
context.dispatch(USER_STORE.ACTIONS.GET_USER_PROFILE)
router.push('/') router.push('/')
}) })
.catch((error) => { .catch((error) => {
handleError(context, error) handleError(context, error)
}) })
.finally(() =>
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
)
}, },
[WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT]( [WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>, context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload 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)
authApi authApi
.patch(`workouts/${payload.workoutId}`, payload.data) .patch(`workouts/${payload.workoutId}`, payload.data)
.then(() => { .then(() => {
context.dispatch(USER_STORE.ACTIONS.GET_USER_PROFILE)
context context
.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, { .dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, {
workoutId: payload.workoutId, workoutId: payload.workoutId,
}) })
.then(() => .then(() => {
router.push({ router.push({
name: 'Workout', name: 'Workout',
params: { workoutId: payload.workoutId }, params: { workoutId: payload.workoutId },
}) })
) })
}) })
.catch((error) => { .catch((error) => {
handleError(context, error) handleError(context, error)
}) })
.finally(() =>
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
)
},
[WORKOUTS_STORE.ACTIONS.ADD_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutForm
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true)
if (!payload.file) {
throw new Error('No gpx file provided')
}
const form = new FormData()
form.append('file', payload.file)
form.append(
'data',
`{"sport_id": ${payload.sport_id}, "notes": "${payload.notes}"}`
)
authApi
.post('workouts', form, {
headers: {
'content-type': 'multipart/form-data',
},
})
.then((res) => {
if (res.data.status === 'created') {
context.dispatch(USER_STORE.ACTIONS.GET_USER_PROFILE)
const workout: IWorkout = res.data.data.workouts[0]
router.push(
res.data.data.workouts.length === 1
? `/workouts/${workout.id}`
: '/'
)
}
})
.catch((error) => {
handleError(context, error)
})
.finally(() =>
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
)
},
[WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutForm
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true)
authApi
.post('workouts/no_gpx', payload)
.then((res) => {
if (res.data.status === 'created') {
context.dispatch(USER_STORE.ACTIONS.GET_USER_PROFILE)
const workout: IWorkout = res.data.data.workouts[0]
router.push(`/workouts/${workout.id}`)
}
})
.catch((error) => {
handleError(context, error)
})
.finally(() =>
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
)
}, },
} }

View File

@ -1,4 +1,6 @@
export enum WorkoutsActions { export enum WorkoutsActions {
ADD_WORKOUT = 'ADD_WORKOUT',
ADD_WORKOUT_WITHOUT_GPX = 'ADD_WORKOUT_WITHOUT_GPX',
DELETE_WORKOUT = 'DELETE_WORKOUT', DELETE_WORKOUT = 'DELETE_WORKOUT',
EDIT_WORKOUT = 'EDIT_WORKOUT', EDIT_WORKOUT = 'EDIT_WORKOUT',
GET_CALENDAR_WORKOUTS = 'GET_CALENDAR_WORKOUTS', GET_CALENDAR_WORKOUTS = 'GET_CALENDAR_WORKOUTS',

View File

@ -13,6 +13,7 @@ import {
IWorkoutsPayload, IWorkoutsPayload,
IWorkoutData, IWorkoutData,
IWorkoutPayload, IWorkoutPayload,
IWorkoutForm,
} from '@/types/workouts' } from '@/types/workouts'
export interface IWorkoutsState { export interface IWorkoutsState {
@ -42,6 +43,14 @@ export interface IWorkoutsActions {
context: ActionContext<IWorkoutsState, IRootState>, context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload payload: IWorkoutPayload
): void ): void
[WORKOUTS_STORE.ACTIONS.ADD_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutForm
): void
[WORKOUTS_STORE.ACTIONS.ADD_WORKOUT_WITHOUT_GPX](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutForm
): void
} }
export interface IWorkoutsGetters { export interface IWorkoutsGetters {

View File

@ -99,8 +99,12 @@ export interface IWorkoutObject {
export interface IWorkoutForm { export interface IWorkoutForm {
sport_id: number | null sport_id: number | null
title: string
notes: string notes: string
title?: string
workout_date?: string
distance?: number
duration?: number
file?: Blob
} }
export interface IWorkoutPayload { export interface IWorkoutPayload {

View File

@ -0,0 +1,47 @@
<template>
<div id="add-workout">
<div class="container">
<WorkoutEdition
:authUser="authUser"
:sports="sports"
:isCreation="true"
:loading="workoutData.loading"
/>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ComputedRef } from 'vue'
import WorkoutEdition from '@/components/Workout/WorkoutEdition.vue'
import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user'
import { IWorkoutData } from '@/types/workouts'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'AddWorkout',
components: {
WorkoutEdition,
},
setup() {
const store = useStore()
const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
)
const authUser: ComputedRef<IAuthUserProfile> = computed(
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const workoutData: ComputedRef<IWorkoutData> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]
)
return { authUser, sports, workoutData }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
</style>

View File

@ -1,7 +1,12 @@
<template> <template>
<div id="edit-workout"> <div id="edit-workout">
<div class="container"> <div class="container">
<WorkoutEdition :sports="sports" :workout="workoutData.workout" /> <WorkoutEdition
:authUser="authUser"
:sports="sports"
:workout="workoutData.workout"
:loading="workoutData.loading"
/>
</div> </div>
</div> </div>
</template> </template>
@ -17,8 +22,9 @@
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import WorkoutEdition from '@/components/Workout/WorkoutEdition.vue' import WorkoutEdition from '@/components/Workout/WorkoutEdition.vue'
import { SPORTS_STORE, WORKOUTS_STORE } from '@/store/constants' import { SPORTS_STORE, USER_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports' import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user'
import { IWorkoutData } from '@/types/workouts' import { IWorkoutData } from '@/types/workouts'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
@ -37,6 +43,9 @@
}) })
}) })
const authUser: ComputedRef<IAuthUserProfile> = computed(
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
)
const sports: ComputedRef<ISport[]> = computed( const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS] () => store.getters[SPORTS_STORE.GETTERS.SPORTS]
) )
@ -53,7 +62,7 @@
} }
) )
return { sports, workoutData } return { authUser, sports, workoutData }
}, },
}) })
</script> </script>