Client - delete a workout

This commit is contained in:
Sam 2021-09-28 09:10:01 +02:00
parent bde004f83b
commit 3dbdd5cb6b
20 changed files with 234 additions and 20 deletions

View File

@ -0,0 +1,113 @@
<template>
<div id="modal">
<div class="custom-modal">
<Card :without-title="false">
<template #title>
{{ title }}
</template>
<template #content>
<div class="modal-message">{{ message }}</div>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<div class="modal-buttons">
<button class="confirm" @click="emit('confirmAction')">
{{ t('buttons.YES') }}
</button>
<button class="cancel" @click="emit('cancelAction')">
{{ t('buttons.NO') }}
</button>
</div>
</template>
</Card>
</div>
</div>
</template>
<script lang="ts">
import { ComputedRef, computed, defineComponent, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import { ROOT_STORE } from '@/store/constants'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'Modal',
components: {
Card,
ErrorMessage,
},
props: {
title: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
},
emits: ['cancelAction', 'confirmAction'],
setup(props, { emit }) {
const { t } = useI18n()
const store = useStore()
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return { errorMessages, t, emit }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base';
#modal {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--modal-background-color);
padding: $default-padding;
z-index: 1240;
.custom-modal {
background-color: var(--app-background-color);
border-radius: $border-radius;
max-width: 500px;
margin: 25% auto;
z-index: 1250;
::v-deep(.card) {
border: 0;
margin: 0;
.card-content {
display: flex;
flex-direction: column;
.modal-message {
padding: $default-padding;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
button {
margin: $default-padding * 0.5;
}
}
}
}
}
@media screen and (max-width: $small-limit) {
.custom-modal {
margin: 20% 0;
width: 100%;
}
}
}
</style>

View File

@ -23,6 +23,11 @@
<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-trash"
aria-hidden="true"
@click="emit('displayModal', true)"
/>
</div> </div>
<div class="workout-title" v-else> <div class="workout-title" v-else>
{{ workoutObject.title }} {{ workoutObject.title }}
@ -86,9 +91,10 @@
required: true, required: true,
}, },
}, },
setup() { emits: ['displayModal'],
setup(props, { emit }) {
const { t } = useI18n() const { t } = useI18n()
return { t } return { t, emit }
}, },
}) })
</script> </script>

View File

@ -1,8 +1,19 @@
<template> <template>
<div class="workout-detail"> <div class="workout-detail">
<Modal
v-if="displayModal"
:title="t('common.CONFIRMATION')"
:message="t('workouts.WORKOUT_DELETION_CONFIRMATION')"
@confirmAction="deleteWorkout(workoutObject.workoutId)"
@cancelAction="updateDisplayModal(false)"
/>
<Card :without-title="false"> <Card :without-title="false">
<template #title> <template #title>
<WorkoutCardTitle :sport="sport" :workoutObject="workoutObject" /> <WorkoutCardTitle
:sport="sport"
:workoutObject="workoutObject"
@displayModal="updateDisplayModal(true)"
/>
</template> </template>
<template #content> <template #content>
<WorkoutMap <WorkoutMap
@ -29,9 +40,11 @@
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 Modal from '@/components/Common/Modal.vue'
import WorkoutCardTitle from '@/components/Workout/WorkoutDetail/WorkoutCardTitle.vue' import WorkoutCardTitle from '@/components/Workout/WorkoutDetail/WorkoutCardTitle.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 { import {
@ -41,12 +54,14 @@
IWorkoutSegment, IWorkoutSegment,
TCoordinates, TCoordinates,
} from '@/types/workouts' } from '@/types/workouts'
import { useStore } from '@/use/useStore'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates' import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
export default defineComponent({ export default defineComponent({
name: 'WorkoutDetail', name: 'WorkoutDetail',
components: { components: {
Card, Card,
Modal,
WorkoutCardTitle, WorkoutCardTitle,
WorkoutData, WorkoutData,
WorkoutMap, WorkoutMap,
@ -74,6 +89,7 @@
}, },
setup(props) { setup(props) {
const route = useRoute() const route = useRoute()
const store = useStore()
const { t } = useI18n() const { t } = useI18n()
function getWorkoutObjectUrl( function getWorkoutObjectUrl(
@ -137,6 +153,14 @@
workoutTime: workoutDate.workout_time, 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( const workout: ComputedRef<IWorkout> = computed(
() => props.workoutData.workout () => props.workoutData.workout
@ -149,6 +173,7 @@
? workout.value.segments[+segmentId.value - 1] ? workout.value.segments[+segmentId.value - 1]
: null : null
) )
let displayModal: Ref<boolean> = ref(false)
watch( watch(
() => route.params.segmentId, () => route.params.segmentId,
@ -170,7 +195,10 @@
workoutObject: computed(() => workoutObject: computed(() =>
getWorkoutObject(workout.value, segment.value) getWorkoutObject(workout.value, segment.value)
), ),
displayModal,
t, t,
deleteWorkout,
updateDisplayModal,
} }
}, },
}) })

View File

@ -1,6 +1,7 @@
{ {
"ERROR": { "ERROR": {
"UNKNOWN": "Error. Please try again or contact the administrator.", "UNKNOWN": "Error. Please try again or contact the administrator.",
"Error, Please try again or contact the administrator": "Error. Please try again or contact the administrator.",
"Invalid credentials": "Invalid credentials.", "Invalid credentials": "Invalid credentials.",
"Network Error": "Network Error", "Network Error": "Network Error",
"Password and password confirmation don't match": "Password and password confirmation don't match.", "Password and password confirmation don't match": "Password and password confirmation don't match.",

View File

@ -1,4 +1,6 @@
{ {
"LOGIN": "Log in", "LOGIN": "Log in",
"REGISTER": "Register" "NO": "No",
"REGISTER": "Register",
"YES": "Yes"
} }

View File

@ -1,4 +1,5 @@
{ {
"CONFIRMATION": "Confirmation",
"DAY": "day | days", "DAY": "day | days",
"HOME": "Home" "HOME": "Home"
} }

View File

@ -57,5 +57,6 @@
"wind": "wind" "wind": "wind"
} }
}, },
"WORKOUT": "workout | workouts" "WORKOUT": "workout | workouts",
"WORKOUT_DELETION_CONFIRMATION": "Are you sure you want to delete this workout?"
} }

View File

@ -1,6 +1,7 @@
{ {
"ERROR": { "ERROR": {
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.", "UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
"Error, Please try again or contact the administrator": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
"Invalid credentials": "Identifiants invalides.", "Invalid credentials": "Identifiants invalides.",
"Network Error": "Erreur Réseau", "Network Error": "Erreur Réseau",
"Password and password confirmation don't match": "Les mots de passe saisis sont différents.", "Password and password confirmation don't match": "Les mots de passe saisis sont différents.",

View File

@ -1,4 +1,6 @@
{ {
"LOGIN": "Se connecter", "LOGIN": "Se connecter",
"REGISTER": "S'inscrire" "NO": "Non",
"REGISTER": "S'inscrire",
"YES": "Oui"
} }

View File

@ -1,4 +1,5 @@
{ {
"CONFIRMATION": "Confirmation",
"DAY": "jour | jours", "DAY": "jour | jours",
"HOME": "Accueil" "HOME": "Accueil"
} }

View File

@ -57,5 +57,6 @@
"wind": "venteux" "wind": "venteux"
} }
}, },
"WORKOUT": "séance | séances" "WORKOUT": "séance | séances",
"WORKOUT_DELETION_CONFIRMATION": "Etes-vous sûr de vouloir supprimer cette séance ?"
} }

View File

@ -51,7 +51,7 @@ button {
&:hover { &:hover {
background: var(--app-color); background: var(--app-color);
color: #FFFFFF; color: var(--button-hover-color);
} }
&:enabled:active { &:enabled:active {
@ -64,6 +64,25 @@ button {
border-color: var(--disabled-color); border-color: var(--disabled-color);
color: var(--disabled-color); color: var(--disabled-color);
} }
&.cancel {
background: var(--button-cancel-bg-color);
color: var(--button-cancel-color);
&:hover {
background: var(--app-color);
color: var(--button-hover-color);
}
}
&.confirm {
background: var(--button-confirm-bg-color);
color: var(--button-confirm-color);
&:hover {
background: var(--app-color);
color: var(--button-hover-color);
}
}
} }
.container { .container {

View File

@ -7,6 +7,12 @@
--app-loading-color: #f3f3f3; --app-loading-color: #f3f3f3;
--app-loading-top-color: var(--app-color); --app-loading-top-color: var(--app-color);
--button-hover-color: #FFFFFF;
--button-cancel-bg-color: #FFFFFF;
--button-cancel-color: var(--app-color);
--button-confirm-bg-color: #FFFFFF;
--button-confirm-color: var(--app-color);
--card-border-color: #c4c7cf; --card-border-color: #c4c7cf;
--input-border-color: #9da3af; --input-border-color: #9da3af;
@ -14,6 +20,8 @@
--calendar-week-end-color: #f5f5f5; --calendar-week-end-color: #f5f5f5;
--calendar-today-color: #eff1f3; --calendar-today-color: #eff1f3;
--modal-background-color: rgba(0, 0, 0, 0.3);
--nav-bar-background-color: #FFFFFF; --nav-bar-background-color: #FFFFFF;
--nav-bar-link-active: #485b6e; --nav-bar-link-active: #485b6e;
--nav-border-color: #c5ccdb; --nav-border-color: #c5ccdb;

View File

@ -1,6 +1,7 @@
import { ActionContext, ActionTree } from 'vuex' import { ActionContext, ActionTree } from 'vuex'
import authApi from '@/api/authApi' import authApi from '@/api/authApi'
import router from '@/router'
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants' import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { import {
@ -107,4 +108,19 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false) context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, false)
) )
}, },
[WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT](
context: ActionContext<IWorkoutsState, IRootState>,
payload: IWorkoutPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
authApi
.delete(`workouts/${payload.workoutId}`)
.then(() => {
context.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT)
router.push('/')
})
.catch((error) => {
handleError(context, error)
})
},
} }

View File

@ -1,4 +1,5 @@
export enum WorkoutsActions { export enum WorkoutsActions {
DELETE_WORKOUT = 'DELETE_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

@ -1,7 +1,6 @@
import { MutationTree } from 'vuex' import { MutationTree } from 'vuex'
import { WORKOUTS_STORE } from '@/store/constants' import { WORKOUTS_STORE } from '@/store/constants'
import { initialWorkoutValue } from '@/store/modules/workouts/state'
import { import {
IWorkoutsState, IWorkoutsState,
TWorkoutsMutations, TWorkoutsMutations,
@ -50,6 +49,11 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
state.user_workouts = [] state.user_workouts = []
}, },
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: IWorkoutsState) { [WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: IWorkoutsState) {
state.workoutData = initialWorkoutValue state.workoutData = {
gpx: '',
loading: false,
workout: <IWorkout>{},
chartData: [],
}
}, },
} }

View File

@ -1,15 +1,13 @@
import { IWorkoutsState } from '@/store/modules/workouts/types' import { IWorkoutsState } from '@/store/modules/workouts/types'
import { IWorkout } from '@/types/workouts' import { IWorkout } from '@/types/workouts'
export const initialWorkoutValue = {
gpx: '',
loading: false,
workout: <IWorkout>{},
chartData: [],
}
export const workoutsState: IWorkoutsState = { export const workoutsState: IWorkoutsState = {
calendar_workouts: [], calendar_workouts: [],
user_workouts: [], user_workouts: [],
workoutData: initialWorkoutValue, workoutData: {
gpx: '',
loading: false,
workout: <IWorkout>{},
chartData: [],
},
} }

View File

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

View File

@ -17,6 +17,9 @@ export const getApiUrl = (): string => {
// TODO: update api error messages to remove these workarounds // TODO: update api error messages to remove these workarounds
const removeLastEndOfLine = (text: string): string => text.replace(/\n$/gm, '') const removeLastEndOfLine = (text: string): string => text.replace(/\n$/gm, '')
const removeLastDot = (text: string): string => text.replace(/\.$/gm, '') const removeLastDot = (text: string): string => text.replace(/\.$/gm, '')
const removeErrorDot = (text: string): string =>
text.replace(/^Error\./gm, 'Error,')
export const handleError = ( export const handleError = (
context: context:
| ActionContext<IRootState, IRootState> | ActionContext<IRootState, IRootState>
@ -37,6 +40,7 @@ export const handleError = (
? error.message ? error.message
: msg : msg
errorMessages = removeLastEndOfLine(errorMessages) errorMessages = removeLastEndOfLine(errorMessages)
errorMessages = removeErrorDot(errorMessages)
context.commit( context.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES, ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
errorMessages.includes('\n') errorMessages.includes('\n')

View File

@ -17,14 +17,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent } from 'vue' import { computed, ComputedRef, defineComponent, onUnmounted } from 'vue'
import Timeline from '@/components/Dashboard/Timeline/index.vue' import Timeline from '@/components/Dashboard/Timeline/index.vue'
import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue' import UserCalendar from '@/components/Dashboard/UserCalendar/index.vue'
import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue' import UserMonthStats from '@/components/Dashboard/UserMonthStats.vue'
import UserRecords from '@/components/Dashboard/UserRecords/index.vue' import UserRecords from '@/components/Dashboard/UserRecords/index.vue'
import UserStatsCards from '@/components/Dashboard/UserStatsCards/index.vue' import UserStatsCards from '@/components/Dashboard/UserStatsCards/index.vue'
import { SPORTS_STORE, USER_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 { IAuthUserProfile } from '@/types/user'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
@ -46,6 +46,9 @@
const sports: ComputedRef<ISport[]> = computed( const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS] () => store.getters[SPORTS_STORE.GETTERS.SPORTS]
) )
onUnmounted(() => {
store.commit(WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS)
})
return { authUser, sports } return { authUser, sports }
}, },
}) })