Client - edit a workout

This commit is contained in:
Sam 2021-09-28 12:39:12 +02:00
parent 3dbdd5cb6b
commit fab8cae3b2
15 changed files with 396 additions and 3 deletions

View File

@ -0,0 +1,67 @@
<template>
<div class="custom-textarea">
<textarea
:id="name"
:name="name"
:maxLenght="charLimit"
v-model="text"
@input="updateText"
/>
<div class="remaining-chars">
{{ t('workouts.REMAINING_CHARS') }}: {{ text.length }}/{{ charLimit }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'CustomTextarea',
props: {
charLimit: {
type: Number,
default: 500,
},
input: {
type: String,
default: '',
},
name: {
type: String,
required: true,
},
},
emits: ['updateValue'],
setup(props, { emit }) {
const { t } = useI18n()
let text = ref('')
function updateText(event: Event & { target: HTMLInputElement }) {
emit('updateValue', event.target.value)
}
watch(
() => props.input,
(value) => {
text.value = value
}
)
return { t, text, updateText }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
.custom-textarea {
display: flex;
flex-direction: column;
.remaining-chars {
font-size: 0.8em;
font-style: italic;
}
}
</style>

View File

@ -23,6 +23,16 @@
<div class="workout-title-date"> <div class="workout-title-date">
<div class="workout-title" v-if="workoutObject.type === 'WORKOUT'"> <div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
{{ workoutObject.title }} {{ workoutObject.title }}
<i
class="fa fa-edit"
aria-hidden="true"
@click="
$router.push({
name: 'EditWorkout',
params: { workoutId: workoutObject.workoutId },
})
"
/>
<i <i
class="fa fa-trash" class="fa fa-trash"
aria-hidden="true" aria-hidden="true"
@ -135,6 +145,10 @@
.workout-link { .workout-link {
padding-left: $default-padding; padding-left: $default-padding;
} }
.fa {
padding: 0 $default-padding * 0.3;
}
} }
} }
</style> </style>

View File

@ -0,0 +1,195 @@
<template>
<div id="workout-edition">
<Card :without-title="false">
<template #title>{{ t('workouts.EDIT_WORKOUT') }}</template>
<template #content>
<div id="workout-form">
<form @submit.prevent="updateWorkout">
<div class="form-items">
<div class="form-item">
<label> {{ t('workouts.SPORT', 1) }}: </label>
<select id="sport" v-model="workoutDataObject.sport_id">
<option
v-for="sport in translatedSports"
:value="sport.id"
:key="sport.id"
>
{{ sport.label }}
</option>
</select>
</div>
<div class="form-item">
<label for="title"> {{ t('workouts.TITLE') }}: </label>
<input
id="title"
name="title"
type="text"
v-model="workoutDataObject.title"
/>
</div>
<div class="form-item">
<label> {{ t('workouts.NOTES') }}: </label>
<CustomTextArea
name="notes"
:input="workoutDataObject.notes"
@updateValue="updateNotes"
/>
</div>
</div>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<div class="form-buttons">
<button class="confirm" type="submit">
{{ t('buttons.SUBMIT') }}
</button>
<button
class="cancel"
@click="
$router.push({
name: 'Workout',
params: { workoutId: workout.id },
})
"
>
{{ t('buttons.CANCEL') }}
</button>
</div>
</form>
</div>
</template>
</Card>
</div>
</template>
<script lang="ts">
import {
ComputedRef,
PropType,
defineComponent,
computed,
reactive,
watch,
onUnmounted,
} from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import CustomTextArea from '@/components/Common/CustomTextArea.vue'
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
import { useStore } from '@/use/useStore'
import { translateSports } from '@/utils/sports'
export default defineComponent({
name: 'AddOrEditWorkout',
components: {
Card,
CustomTextArea,
ErrorMessage,
},
props: {
sports: {
type: Object as PropType<ISport[]>,
required: true,
},
workout: {
type: Object as PropType<IWorkout>,
required: true,
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const workoutForm = reactive({
sport_id: 0,
title: '',
notes: '',
})
function updateNotes(value: string) {
workoutForm.notes = value
}
function updateWorkout() {
if (props.workout) {
store.dispatch(WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT, {
workoutId: props.workout.id,
data: workoutForm,
})
}
}
watch(
() => props.workout,
async (newWorkout: IWorkout | undefined) => {
if (newWorkout && newWorkout.id) {
workoutForm.sport_id = newWorkout.sport_id
workoutForm.title = newWorkout.title
workoutForm.notes = newWorkout.notes
}
}
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return {
errorMessages,
t,
translatedSports,
workoutDataObject: workoutForm,
updateNotes,
updateWorkout,
}
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
#workout-edition {
margin: 25% auto;
width: 700px;
::v-deep(.card) {
.card-title {
text-align: center;
text-transform: uppercase;
}
.card-content {
.form-items {
display: flex;
flex-direction: column;
.form-item {
display: flex;
flex-direction: column;
padding: $default-padding;
label {
text-transform: capitalize;
}
}
}
.form-buttons {
display: flex;
justify-content: flex-end;
button {
margin: $default-padding * 0.5;
}
}
}
}
@media screen and (max-width: $small-limit) {
width: 100%;
}
}
</style>

View File

@ -1,6 +1,8 @@
{ {
"CANCEL": "Cancel",
"LOGIN": "Log in", "LOGIN": "Log in",
"NO": "No", "NO": "No",
"REGISTER": "Register", "REGISTER": "Register",
"SUBMIT": "Submit",
"YES": "Yes" "YES": "Yes"
} }

View File

@ -7,6 +7,7 @@
"DESCENT": "descent", "DESCENT": "descent",
"DISTANCE": "distance", "DISTANCE": "distance",
"DURATION": "duration", "DURATION": "duration",
"EDIT_WORKOUT": "Edit the workout",
"ELEVATION": "elevation", "ELEVATION": "elevation",
"END": "end", "END": "end",
"KM": "km", "KM": "km",
@ -35,10 +36,12 @@
"RECORD_FD": "Farest distance", "RECORD_FD": "Farest distance",
"RECORD_LD": "Longest duration", "RECORD_LD": "Longest duration",
"RECORD_MS": "Max. speed", "RECORD_MS": "Max. speed",
"REMAINING_CHARS": "remaining characters",
"SEGMENT": "segment | segments", "SEGMENT": "segment | segments",
"SPEED": "speed", "SPEED": "speed",
"SPORT": "sport | sports", "SPORT": "sport | sports",
"START": "start", "START": "start",
"TITLE": "title",
"TOTAL_DURATION": "total duration", "TOTAL_DURATION": "total duration",
"WEATHER": { "WEATHER": {
"HUMIDITY": "humidity", "HUMIDITY": "humidity",

View File

@ -1,6 +1,8 @@
{ {
"CANCEL": "Annuler",
"LOGIN": "Se connecter", "LOGIN": "Se connecter",
"NO": "Non", "NO": "Non",
"REGISTER": "S'inscrire", "REGISTER": "S'inscrire",
"SUBMIT": "Valider",
"YES": "Oui" "YES": "Oui"
} }

View File

@ -7,6 +7,7 @@
"DESCENT": "dénivelé négatif", "DESCENT": "dénivelé négatif",
"DISTANCE": "distance", "DISTANCE": "distance",
"DURATION": "durée", "DURATION": "durée",
"EDIT_WORKOUT": "Modifier la séance",
"ELEVATION": "altitude", "ELEVATION": "altitude",
"END": "fin", "END": "fin",
"KM": "km", "KM": "km",
@ -35,10 +36,12 @@
"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.",
"REMAINING_CHARS": "nombre de caractères restants ",
"SEGMENT": "segment | segments", "SEGMENT": "segment | segments",
"SPEED": "vitesse", "SPEED": "vitesse",
"SPORT": "sport | sports", "SPORT": "sport | sports",
"START": "début", "START": "début",
"TITLE": "titre",
"TOTAL_DURATION": "durée totale", "TOTAL_DURATION": "durée totale",
"WEATHER": { "WEATHER": {
"HUMIDITY": "humidité", "HUMIDITY": "humidité",

View File

@ -3,6 +3,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 Dashboard from '@/views/DashBoard.vue' import Dashboard from '@/views/DashBoard.vue'
import EditWorkout from '@/views/EditWorkout.vue'
import LoginOrRegister from '@/views/LoginOrRegister.vue' import LoginOrRegister from '@/views/LoginOrRegister.vue'
import NotFoundView from '@/views/NotFoundView.vue' import NotFoundView from '@/views/NotFoundView.vue'
import Workout from '@/views/Workout.vue' import Workout from '@/views/Workout.vue'
@ -31,6 +32,11 @@ const routes: Array<RouteRecordRaw> = [
component: Workout, component: Workout,
props: { displaySegment: false }, props: { displaySegment: false },
}, },
{
path: '/workouts/:workoutId/edit',
name: 'EditWorkout',
component: EditWorkout,
},
{ {
path: '/workouts/:workoutId/segment/:segmentId', path: '/workouts/:workoutId/segment/:segmentId',
name: 'WorkoutSegment', name: 'WorkoutSegment',

View File

@ -27,14 +27,14 @@ img {
max-width: 100%; max-width: 100%;
} }
input { input, textarea, select {
border-radius: $border-radius; border-radius: $border-radius;
border: solid 1px var(--input-border-color); border: solid 1px var(--input-border-color);
padding: $default-padding;
&:disabled { &:disabled {
border-color: var(--disabled-color); border-color: var(--disabled-color);
} }
} }
label { label {

View File

@ -123,4 +123,27 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
handleError(context, error) handleError(context, error)
}) })
}, },
[WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.patch(`workouts/${payload.workoutId}`, payload.data)
.then(() => {
context
.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, {
workoutId: payload.workoutId,
})
.then(() =>
router.push({
name: 'Workout',
params: { workoutId: payload.workoutId },
})
)
})
.catch((error) => {
handleError(context, error)
})
},
} }

View File

@ -1,5 +1,6 @@
export enum WorkoutsActions { export enum WorkoutsActions {
DELETE_WORKOUT = 'DELETE_WORKOUT', DELETE_WORKOUT = 'DELETE_WORKOUT',
EDIT_WORKOUT = 'EDIT_WORKOUT',
GET_CALENDAR_WORKOUTS = 'GET_CALENDAR_WORKOUTS', GET_CALENDAR_WORKOUTS = 'GET_CALENDAR_WORKOUTS',
GET_USER_WORKOUTS = 'GET_USER_WORKOUTS', GET_USER_WORKOUTS = 'GET_USER_WORKOUTS',
GET_WORKOUT_DATA = 'GET_WORKOUT_DATA', GET_WORKOUT_DATA = 'GET_WORKOUT_DATA',

View File

@ -38,6 +38,10 @@ export interface IWorkoutsActions {
context: ActionContext<IWorkoutsState, IRootState>, context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload payload: IWorkoutPayload
): void ): void
[WORKOUTS_STORE.ACTIONS.EDIT_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload
): void
} }
export interface IWorkoutsGetters { export interface IWorkoutsGetters {

View File

@ -97,9 +97,16 @@ export interface IWorkoutObject {
workoutTime: string workoutTime: string
} }
export interface IWorkoutForm {
sport_id: number | null
title: string
notes: string
}
export interface IWorkoutPayload { export interface IWorkoutPayload {
workoutId: string | string[] workoutId: string | string[]
segmentId?: string | string[] segmentId?: string | string[]
data?: IWorkoutForm
} }
export interface IWorkoutsPayload { export interface IWorkoutsPayload {

View File

@ -0,0 +1,63 @@
<template>
<div id="edit-workout">
<div class="container">
<AddOrEditWorkout :sports="sports" :workout="workoutData.workout" />
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
watch,
onBeforeMount,
ComputedRef,
} from 'vue'
import { useRoute } from 'vue-router'
import AddOrEditWorkout from '@/components/Workout/WorkoutEdition.vue'
import { SPORTS_STORE, WORKOUTS_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
import { IWorkoutData } from '@/types/workouts'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'EditWorkout',
components: {
AddOrEditWorkout,
},
setup() {
const route = useRoute()
const store = useStore()
onBeforeMount(() => {
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_WORKOUT_DATA, {
workoutId: route.params.workoutId,
})
})
const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
)
const workoutData: ComputedRef<IWorkoutData> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]
)
watch(
() => route.params.workoutId,
async (newWorkoutId) => {
if (!newWorkoutId) {
store.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
}
}
)
return { sports, workoutData }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
</style>

View File

@ -55,6 +55,7 @@
import WorkoutNotes from '@/components/Workout/WorkoutNotes.vue' import WorkoutNotes from '@/components/Workout/WorkoutNotes.vue'
import WorkoutSegments from '@/components/Workout/WorkoutSegments.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 { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { IWorkoutData, IWorkoutPayload, TCoordinates } from '@/types/workouts' import { IWorkoutData, IWorkoutPayload, TCoordinates } from '@/types/workouts'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
@ -92,7 +93,9 @@
const authUser: ComputedRef<IAuthUserProfile> = computed( const authUser: ComputedRef<IAuthUserProfile> = computed(
() => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE] () => store.getters[USER_STORE.GETTERS.AUTH_USER_PROFILE]
) )
const sports = computed(() => store.getters[SPORTS_STORE.GETTERS.SPORTS]) const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
)
let markerCoordinates: Ref<TCoordinates> = ref({ let markerCoordinates: Ref<TCoordinates> = ref({
latitude: null, latitude: null,
longitude: null, longitude: null,