Client - add password reset + refacto

This commit is contained in:
Sam 2021-10-20 17:38:25 +02:00
parent c4eb9bdbf8
commit 8d93024a5f
66 changed files with 797 additions and 318 deletions

View File

@ -22,7 +22,7 @@
"register-service-worker": "^1.7.1",
"vue": "^3.0.0",
"vue-chart-3": "^0.5.8",
"vue-i18n": "^9.1.0",
"vue-i18n": "^9.1.9",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},

View File

@ -1,22 +1,17 @@
<template>
<div class="alert-message">
<div v-html="t(message)" />
<div v-html="$t(message)" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'AlertMessage',
props: {
message: String,
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -9,14 +9,13 @@
@input="updateText"
/>
<div class="remaining-chars">
{{ t('workouts.REMAINING_CHARS') }}: {{ text.length }}/{{ charLimit }}
{{ $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',
@ -40,7 +39,6 @@
},
emits: ['updateValue'],
setup(props, { emit }) {
const { t } = useI18n()
let text = ref('')
function updateText(event: Event & { target: HTMLInputElement }) {
@ -54,7 +52,7 @@
}
)
return { t, text, updateText }
return { text, updateText }
},
})
</script>

View File

@ -2,26 +2,21 @@
<div class="error-message">
<ul v-if="Array.isArray(message)">
<li v-for="(subMessage, index) in message" :key="index">
{{ t(subMessage) }}
{{ $t(subMessage) }}
</li>
</ul>
<div v-else>{{ t(message) }}</div>
<div v-else>{{ $t(message) }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'ErrorMessage',
props: {
message: [String, Array],
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -0,0 +1,63 @@
<template>
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 345.834 345.834"
style="enable-background: new 0 0 345.834 345.834"
xml:space="preserve"
>
<g>
<path
d="M339.798,260.429c0.13-0.026,0.257-0.061,0.385-0.094c0.109-0.028,0.219-0.051,0.326-0.084
c0.125-0.038,0.247-0.085,0.369-0.129c0.108-0.039,0.217-0.074,0.324-0.119c0.115-0.048,0.226-0.104,0.338-0.157
c0.109-0.052,0.22-0.1,0.327-0.158c0.107-0.057,0.208-0.122,0.312-0.184c0.107-0.064,0.215-0.124,0.319-0.194
c0.111-0.074,0.214-0.156,0.321-0.236c0.09-0.067,0.182-0.13,0.27-0.202c0.162-0.133,0.316-0.275,0.466-0.421
c0.027-0.026,0.056-0.048,0.083-0.075c0.028-0.028,0.052-0.059,0.079-0.088c0.144-0.148,0.284-0.3,0.416-0.46
c0.077-0.094,0.144-0.192,0.216-0.289c0.074-0.1,0.152-0.197,0.221-0.301c0.074-0.111,0.139-0.226,0.207-0.34
c0.057-0.096,0.118-0.19,0.171-0.289c0.062-0.115,0.114-0.234,0.169-0.351c0.049-0.104,0.101-0.207,0.146-0.314
c0.048-0.115,0.086-0.232,0.128-0.349c0.041-0.114,0.085-0.227,0.12-0.343c0.036-0.118,0.062-0.238,0.092-0.358
c0.029-0.118,0.063-0.234,0.086-0.353c0.028-0.141,0.045-0.283,0.065-0.425c0.014-0.1,0.033-0.199,0.043-0.3
c0.025-0.249,0.038-0.498,0.038-0.748V92.76c0-4.143-3.357-7.5-7.5-7.5h-236.25c-0.066,0-0.13,0.008-0.196,0.01
c-0.143,0.004-0.285,0.01-0.427,0.022c-0.113,0.009-0.225,0.022-0.337,0.037c-0.128,0.016-0.255,0.035-0.382,0.058
c-0.119,0.021-0.237,0.046-0.354,0.073c-0.119,0.028-0.238,0.058-0.356,0.092c-0.117,0.033-0.232,0.069-0.346,0.107
c-0.117,0.04-0.234,0.082-0.349,0.128c-0.109,0.043-0.216,0.087-0.322,0.135c-0.118,0.053-0.235,0.11-0.351,0.169
c-0.099,0.051-0.196,0.103-0.292,0.158c-0.116,0.066-0.23,0.136-0.343,0.208c-0.093,0.06-0.184,0.122-0.274,0.185
c-0.106,0.075-0.211,0.153-0.314,0.235c-0.094,0.075-0.186,0.152-0.277,0.231c-0.09,0.079-0.179,0.158-0.266,0.242
c-0.099,0.095-0.194,0.194-0.288,0.294c-0.047,0.05-0.097,0.094-0.142,0.145c-0.027,0.03-0.048,0.063-0.074,0.093
c-0.094,0.109-0.182,0.223-0.27,0.338c-0.064,0.084-0.13,0.168-0.19,0.254c-0.078,0.112-0.15,0.227-0.222,0.343
c-0.059,0.095-0.12,0.189-0.174,0.286c-0.063,0.112-0.118,0.227-0.175,0.342c-0.052,0.105-0.106,0.21-0.153,0.317
c-0.049,0.113-0.092,0.23-0.135,0.345c-0.043,0.113-0.087,0.225-0.124,0.339c-0.037,0.115-0.067,0.232-0.099,0.349
c-0.032,0.12-0.066,0.239-0.093,0.36c-0.025,0.113-0.042,0.228-0.062,0.342c-0.022,0.13-0.044,0.26-0.06,0.39
c-0.013,0.108-0.019,0.218-0.027,0.328c-0.01,0.14-0.019,0.28-0.021,0.421c-0.001,0.041-0.006,0.081-0.006,0.122v46.252
c0,4.143,3.357,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-29.595l66.681,59.037c-0.348,0.245-0.683,0.516-0.995,0.827l-65.687,65.687v-49.288
c0-4.143-3.357-7.5-7.5-7.5s-7.5,3.357-7.5,7.5v9.164h-38.75c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h38.75v43.231
c0,4.143,3.357,7.5,7.5,7.5h236.25c0.247,0,0.494-0.013,0.74-0.037c0.115-0.011,0.226-0.033,0.339-0.049
C339.542,260.469,339.67,260.454,339.798,260.429z M330.834,234.967l-65.688-65.687c-0.042-0.042-0.087-0.077-0.13-0.117
l49.383-41.897c3.158-2.68,3.546-7.412,0.866-10.571c-2.678-3.157-7.41-3.547-10.571-0.866l-84.381,71.59l-98.444-87.158h208.965
V234.967z M185.878,179.888c0.535-0.535,0.969-1.131,1.308-1.765l28.051,24.835c1.418,1.255,3.194,1.885,4.972,1.885
c1.726,0,3.451-0.593,4.853-1.781l28.587-24.254c0.26,0.38,0.553,0.743,0.89,1.08l65.687,65.687H120.191L185.878,179.888z"
/>
<path
d="M7.5,170.676h126.667c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H7.5c-4.143,0-7.5,3.357-7.5,7.5
S3.357,170.676,7.5,170.676z"
/>
<path
d="M20.625,129.345H77.5c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H20.625c-4.143,0-7.5,3.357-7.5,7.5
S16.482,129.345,20.625,129.345z"
/>
<path
d="M62.5,226.51h-55c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h55c4.143,0,7.5-3.357,7.5-7.5S66.643,226.51,62.5,226.51z"
/>
</g>
</svg>
</template>
<script>
export default {
name: 'EmailSent',
}
</script>

View File

@ -0,0 +1,94 @@
<template>
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 512.001 512.001"
style="enable-background: new 0 0 512.001 512.001"
xml:space="preserve"
>
<g>
<g>
<path
d="M468.683,287.265h-69.07c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h69.07
c4.147,0,7.508-3.361,7.508-7.508C476.191,290.626,472.83,287.265,468.683,287.265z"
/>
</g>
</g>
<g>
<g>
<path
d="M105.012,268.377L85.781,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L58.034,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.775
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C109.507,275.266,108.499,270.62,105.012,268.377z"
/>
</g>
</g>
<g>
<g>
<path
d="M194.441,268.377L175.21,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
l-17.47,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.776l-17.471-11.243
c-3.487-2.245-8.133-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L147.463,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.776
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.47,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C198.936,275.266,197.928,270.62,194.441,268.377z"
/>
</g>
</g>
<g>
<g>
<path
d="M283.871,268.377L264.64,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.471-11.243
c-3.486-2.245-8.134-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L236.892,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.775
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.886-1.216,6.32-3.446C288.366,275.266,287.358,270.62,283.871,268.377z"
/>
</g>
</g>
<g>
<g>
<path
d="M373.3,268.377L354.069,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L326.322,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.776
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C377.795,275.266,376.787,270.62,373.3,268.377z"
/>
</g>
</g>
<g>
<g>
<path
d="M271.792,330.359H15.016V181.642h93.1c4.147,0,7.508-3.361,7.508-7.508c0-4.147-3.361-7.508-7.508-7.508H12.513
C5.613,166.626,0,172.24,0,179.14v153.722c0,6.9,5.613,12.513,12.513,12.513h259.278c4.147,0,7.508-3.361,7.508-7.508
C279.299,333.72,275.939,330.359,271.792,330.359z"
/>
</g>
</g>
<g>
<g>
<path
d="M499.487,166.626H162.174c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h334.811v148.716H323.848
c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h175.64c6.9,0,12.513-5.613,12.513-12.513V179.14
C512.001,172.24,506.387,166.626,499.487,166.626z"
/>
</g>
</g>
</svg>
</template>
<script>
export default {
name: 'Password',
}
</script>

View File

@ -17,12 +17,12 @@
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import CyclingSport from '@/components/Common/SportImage/CyclingSport.vue'
import CyclingTransport from '@/components/Common/SportImage/CyclingTransport.vue'
import Hiking from '@/components/Common/SportImage/Hiking.vue'
import MountainBiking from '@/components/Common/SportImage/MountainBiking.vue'
import Running from '@/components/Common/SportImage/Running.vue'
import Walking from '@/components/Common/SportImage/Walking.vue'
import CyclingSport from '@/components/Common/Images/SportImage/CyclingSport.vue'
import CyclingTransport from '@/components/Common/Images/SportImage/CyclingTransport.vue'
import Hiking from '@/components/Common/Images/SportImage/Hiking.vue'
import MountainBiking from '@/components/Common/Images/SportImage/MountainBiking.vue'
import Running from '@/components/Common/Images/SportImage/Running.vue'
import Walking from '@/components/Common/Images/SportImage/Walking.vue'
import { sportColors } from '@/utils/sports'
export default defineComponent({

View File

@ -10,10 +10,10 @@
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<div class="modal-buttons">
<button class="confirm" @click="emit('confirmAction')">
{{ t('buttons.YES') }}
{{ $t('buttons.YES') }}
</button>
<button class="cancel" @click="emit('cancelAction')">
{{ t('buttons.NO') }}
{{ $t('buttons.NO') }}
</button>
</div>
</template>
@ -24,7 +24,6 @@
<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'
@ -49,13 +48,12 @@
},
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 }
return { errorMessages, emit }
},
})
</script>

View File

@ -1,14 +1,13 @@
<template>
<Error
title="404"
:message="t(`error.NOT_FOUND.${target}`)"
:message="$t(`error.NOT_FOUND.${target}`)"
:button-text="t('common.HOME')"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import Error from '@/components/Common/Error.vue'
@ -23,9 +22,5 @@
default: 'PAGE',
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="start-chart">
<div v-if="hideChartIfNoData && emptyStats">
{{ t('workouts.NO_WORKOUTS') }}
{{ $t('workouts.NO_WORKOUTS') }}
</div>
<div v-else>
<div class="chart-radio">
@ -12,7 +12,7 @@
:checked="displayedData === 'total_distance'"
@click="updateDisplayData"
/>
{{ t('workouts.DISTANCE') }}
{{ $t('workouts.DISTANCE') }}
</label>
<label>
<input
@ -21,7 +21,7 @@
:checked="displayedData === 'total_duration'"
@click="updateDisplayData"
/>
{{ t('workouts.DURATION') }}
{{ $t('workouts.DURATION') }}
</label>
<label>
<input
@ -30,7 +30,7 @@
:checked="displayedData === 'nb_workouts'"
@click="updateDisplayData"
/>
{{ t('workouts.WORKOUT', 2) }}
{{ $t('workouts.WORKOUT', 2) }}
</label>
</div>
<Chart
@ -57,7 +57,6 @@
watch,
onBeforeMount,
} from 'vue'
import { useI18n } from 'vue-i18n'
import Chart from '@/components/Common/StatsChart/Chart.vue'
import { STATS_STORE } from '@/store/constants'
@ -106,7 +105,6 @@
},
setup(props) {
const store = useStore()
const { t } = useI18n()
let displayedData: Ref<TStatisticsDatasetKeys> = ref('total_distance')
const statistics: ComputedRef<TStatisticsFromApi> = computed(
@ -168,7 +166,6 @@
labels: computed(() => formattedStats.value.labels),
emptyStats: computed(() => Object.keys(statistics.value).length === 0),
displayedData,
t,
updateDisplayData,
}
},

View File

@ -1,6 +1,6 @@
<template>
<div id="timeline">
<div class="section-title">{{ t('workouts.LATEST_WORKOUTS') }}</div>
<div class="section-title">{{ $t('workouts.LATEST_WORKOUTS') }}</div>
<div v-if="user.nb_workouts > 0 && workouts.length === 0">
<WorkoutCard
v-for="index in [...Array(initWorkoutsCount).keys()]"
@ -23,7 +23,7 @@
<NoWorkouts v-if="workouts.length === 0" />
<div v-if="moreWorkoutsExist" class="more-workouts">
<button @click="loadMoreWorkouts">
{{ t('workouts.LOAD_MORE_WORKOUT') }}
{{ $t('workouts.LOAD_MORE_WORKOUT') }}
</button>
</div>
</div>
@ -39,7 +39,6 @@
ref,
onBeforeMount,
} from 'vue'
import { useI18n } from 'vue-i18n'
import WorkoutCard from '@/components/Workout/WorkoutCard.vue'
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
@ -67,7 +66,6 @@
},
setup(props) {
const store = useStore()
const { t } = useI18n()
let page = ref(1)
const per_page = 5
@ -103,7 +101,6 @@
moreWorkoutsExist,
per_page,
workouts,
t,
loadMoreWorkouts,
}
},

View File

@ -25,7 +25,7 @@
import { defineComponent, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import { IWorkout } from '@/types/workouts'
export default defineComponent({

View File

@ -18,7 +18,6 @@
<script lang="ts">
import { endOfMonth, startOfMonth } from 'date-fns'
import { PropType, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import StatChart from '@/components/Common/StatsChart/index.vue'
@ -42,7 +41,6 @@
},
},
setup(props) {
const { t } = useI18n()
const date = new Date()
return {
chartParams: {
@ -51,7 +49,6 @@
end: endOfMonth(date),
},
selectedSportIds: props.sports.map((sport) => sport.id),
t,
}
},
})

View File

@ -31,7 +31,7 @@
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import { IRecord } from '@/types/workouts'
export default defineComponent({

View File

@ -2,11 +2,11 @@
<div class="user-records-section">
<div class="section-title">
<i class="fa fa-trophy custom-fa-small" aria-hidden="true" />
{{ t('workouts.RECORD', 2) }}
{{ $t('workouts.RECORD', 2) }}
</div>
<div class="user-records">
<div v-if="Object.keys(recordsBySport).length === 0" class="no-records">
{{ t('workouts.NO_RECORDS') }}
{{ $t('workouts.NO_RECORDS') }}
</div>
<RecordsCard
v-for="sportTranslatedLabel in Object.keys(recordsBySport).sort()"
@ -52,7 +52,7 @@
props.user.timezone
)
)
return { recordsBySport, t }
return { recordsBySport }
},
})
</script>

View File

@ -3,12 +3,12 @@
<UserStatCard
icon="calendar"
:value="user.nb_workouts"
:text="t('workouts.WORKOUT', user.nb_workouts)"
:text="$t('workouts.WORKOUT', user.nb_workouts)"
/>
<UserStatCard
icon="road"
:value="Number(user.total_distance).toFixed(2)"
:text="t('workouts.KM')"
:text="$t('workouts.KM')"
/>
<UserStatCard
icon="clock-o"
@ -18,7 +18,7 @@
<UserStatCard
icon="tags"
:value="user.nb_sports"
:text="t('workouts.SPORT', user.nb_sports)"
:text="$t('workouts.SPORT', user.nb_sports)"
/>
</div>
</template>
@ -63,10 +63,7 @@
}
}
return {
t,
total_duration: computed(() => get_duration(total_duration)),
}
return { total_duration: computed(() => get_duration(total_duration)) }
},
})
</script>

View File

@ -21,19 +21,19 @@
<div class="nav-items-app-menu" @click="closeMenu()">
<div class="nav-items-group" v-if="isAuthenticated">
<router-link class="nav-item" to="/">{{
t('dashboard.DASHBOARD')
$t('dashboard.DASHBOARD')
}}</router-link>
<router-link class="nav-item" to="/workouts">
{{ capitalize(t('workouts.WORKOUT', 2)) }}
{{ capitalize($t('workouts.WORKOUT', 2)) }}
</router-link>
<router-link class="nav-item" to="/statistics">
{{ t('statistics.STATISTICS') }}
{{ $t('statistics.STATISTICS') }}
</router-link>
<div v-if="isAuthenticated && authUser.admin" class="nav-item">
{{ t('administration.ADMIN') }}
{{ $t('administration.ADMIN') }}
</div>
<router-link class="nav-item" to="/workouts/add">
{{ t('workouts.ADD_WORKOUT') }}
{{ $t('workouts.ADD_WORKOUT') }}
</router-link>
<div class="nav-item nav-separator" />
</div>
@ -47,15 +47,15 @@
{{ authUser.username }}
</router-link>
<div class="nav-item nav-link" @click="logout">
{{ t('user.LOGOUT') }}
{{ $t('user.LOGOUT') }}
</div>
</div>
<div class="nav-items-group" v-else>
<router-link class="nav-item" to="/login" @click="closeMenu">{{
t('user.LOGIN')
$t('user.LOGIN')
}}</router-link>
<router-link class="nav-item" to="/register" @click="closeMenu">{{
t('user.REGISTER')
$t('user.REGISTER')
}}</router-link>
</div>
<Dropdown
@ -93,7 +93,7 @@
},
emits: ['menuInteraction'],
setup(props, { emit }) {
const { t, locale, availableLocales } = useI18n()
const { locale, availableLocales } = useI18n()
const store = useStore()
const availableLanguages = availableLocales.map((l) => {
@ -140,7 +140,6 @@
isAuthenticated,
isMenuOpen,
language,
t,
capitalize,
openMenu,
closeMenu,

View File

@ -66,7 +66,7 @@
stroke: none;
fill-rule: nonzero;
fill: var(--app-color);
filter: drop-shadow(10px 10px 10px var(--app-shadow-color));
filter: var(--svg-filter);
}
}
}

View File

@ -22,7 +22,7 @@
:checked="selectedTimeFrame === frame"
@input="onUpdateTimeFrame(frame)"
/>
<span>{{ t(`statistics.TIME_FRAMES.${frame}`) }}</span>
<span>{{ $t(`statistics.TIME_FRAMES.${frame}`) }}</span>
</label>
</div>
</div>
@ -39,13 +39,11 @@
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'StatsMenu',
emits: ['arrowClick', 'timeFrameUpdate'],
setup(props, { emit }) {
const { t } = useI18n()
let selectedTimeFrame = ref('month')
const timeFrames = ['week', 'month', 'year']
@ -56,7 +54,6 @@
return {
selectedTimeFrame,
t,
timeFrames,
onUpdateTimeFrame,
emit,

View File

@ -23,7 +23,7 @@
import { ComputedRef, PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import { ISport, ITranslatedSport } from '@/types/sports'
import { translateSports, sportColors } from '@/utils/sports'

View File

@ -107,7 +107,6 @@
chartParams,
selectedTimeFrame,
sportColors,
t,
timeFrames,
translatedSports,
selectedSportIds,

View File

@ -0,0 +1,69 @@
<template>
<div id="password-action-done">
<EmailSent v-if="action === 'request-sent'" />
<Password v-else />
<div class="password-message">
<span v-if="action === 'request-sent'"
>{{ $t('user.PASSWORD_SENT_EMAIL_TEXT') }}
</span>
<i18n-t v-else keypath="user.PASSWORD_UPDATED">
<router-link to="/login">
{{ $t('common.HERE') }}
</router-link>
</i18n-t>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import EmailSent from '@/components/Common/Images/EmailSent.vue'
import Password from '@/components/Common/Images/Password.vue'
export default defineComponent({
name: 'PasswordActionDone',
components: {
EmailSent,
Password,
},
props: {
action: {
type: String,
required: true,
},
},
})
</script>
<style scoped lang="scss">
@import '~@/scss/base';
#password-action-done {
display: flex;
flex-direction: column;
align-items: center;
margin: 100px auto;
width: 700px;
@media screen and (max-width: $medium-limit) {
width: 100%;
}
svg {
stroke: none;
fill-rule: nonzero;
fill: var(--app-color);
filter: var(--svg-filter);
width: 100px;
}
.password-message {
font-size: 1.1em;
text-align: center;
@media screen and (max-width: $medium-limit) {
font-size: 1em;
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div id="password-reset-request">
<Card>
<template #title>{{ $t('user.RESET_PASSWORD') }}</template>
<template #content>
<UserAuthForm :action="action" :token="token" />
</template>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Card from '@/components/Common/Card.vue'
import UserAuthForm from '@/components/User/UserAuthForm.vue'
export default defineComponent({
name: 'PasswordResetForm',
components: {
Card,
UserAuthForm,
},
props: {
action: {
type: String,
required: true,
},
token: {
type: String,
default: '',
},
},
})
</script>
<style scoped lang="scss">
@import '~@/scss/base';
#password-reset-request {
margin: 100px auto;
width: 700px;
@media screen and (max-width: $medium-limit) {
width: 100%;
}
::v-deep(.card) {
.card-content {
#user-form {
width: 100%;
}
}
}
}
</style>

View File

@ -1,36 +1,36 @@
<template>
<div id="user-infos" class="description-list">
<dl>
<dt>{{ t('user.PROFILE.REGISTRATION_DATE') }}:</dt>
<dt>{{ $t('user.PROFILE.REGISTRATION_DATE') }}:</dt>
<dd>{{ registrationDate }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.FIRST_NAME') }}:</dt>
<dt>{{ $t('user.PROFILE.FIRST_NAME') }}:</dt>
<dd>{{ user.first_name }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.LAST_NAME') }}:</dt>
<dt>{{ $t('user.PROFILE.LAST_NAME') }}:</dt>
<dd>{{ user.last_name }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.BIRTH_DATE') }}:</dt>
<dt>{{ $t('user.PROFILE.BIRTH_DATE') }}:</dt>
<dd>{{ birthDate }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.LOCATION') }}:</dt>
<dt>{{ $t('user.PROFILE.LOCATION') }}:</dt>
<dd>{{ user.location }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.BIO') }}:</dt>
<dt>{{ $t('user.PROFILE.BIO') }}:</dt>
<dd class="user-bio">
{{ user.bio }}
</dd>
</dl>
<div class="profile-buttons">
<button @click="$router.push('/profile/edit')">
{{ t('user.PROFILE.EDIT') }}
{{ $t('user.PROFILE.EDIT') }}
</button>
<button @click="$router.push('/')">{{ t('common.HOME') }}</button>
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
</div>
</div>
</template>
@ -38,7 +38,6 @@
<script lang="ts">
import { format } from 'date-fns'
import { PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { IAuthUserProfile } from '@/types/user'
@ -51,7 +50,6 @@
},
},
setup(props) {
const { t } = useI18n()
const registrationDate = computed(() =>
props.user.created_at
? format(new Date(props.user.created_at), 'dd/MM/yyyy HH:mm')
@ -62,7 +60,7 @@
? format(new Date(props.user.birth_date), 'dd/MM/yyyy')
: ''
)
return { birthDate, registrationDate, t }
return { birthDate, registrationDate }
},
})
</script>

View File

@ -1,29 +1,28 @@
<template>
<div id="user-preferences" class="description-list">
<dl>
<dt>{{ t('user.PROFILE.LANGUAGE') }}:</dt>
<dt>{{ $t('user.PROFILE.LANGUAGE') }}:</dt>
<dd>{{ language }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.TIMEZONE') }}:</dt>
<dt>{{ $t('user.PROFILE.TIMEZONE') }}:</dt>
<dd>{{ timezone }}</dd>
</dl>
<dl>
<dt>{{ t('user.PROFILE.FIRST_DAY_OF_WEEK') }}:</dt>
<dd>{{ t(`user.PROFILE.${fistDayOfWeek}`) }}</dd>
<dt>{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}:</dt>
<dd>{{ $t(`user.PROFILE.${fistDayOfWeek}`) }}</dd>
</dl>
<div class="profile-buttons">
<button @click="$router.push('/profile/edit/preferences')">
{{ t('user.PROFILE.EDIT_PREFERENCES') }}
{{ $t('user.PROFILE.EDIT_PREFERENCES') }}
</button>
<button @click="$router.push('/')">{{ t('common.HOME') }}</button>
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
</div>
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { IAuthUserProfile } from '@/types/user'
@ -36,7 +35,6 @@
},
},
setup(props) {
const { t } = useI18n()
const language = computed(() =>
props.user.language ? props.user.language.toUpperCase() : 'EN'
)
@ -46,7 +44,7 @@
const timezone = computed(() =>
props.user.timezone ? props.user.timezone : 'Europe/Paris'
)
return { fistDayOfWeek, language, t, timezone }
return { fistDayOfWeek, language, timezone }
},
})
</script>

View File

@ -8,7 +8,7 @@
<div class="user-stat">
<span class="stat-number">{{ user.nb_workouts }}</span>
<span class="stat-label">
{{ t('workouts.WORKOUT', user.nb_workouts) }}
{{ $t('workouts.WORKOUT', user.nb_workouts) }}
</span>
</div>
<div class="user-stat">
@ -20,7 +20,7 @@
<div class="user-stat hide-small">
<span class="stat-number">{{ user.nb_sports }}</span>
<span class="stat-label">
{{ t('workouts.SPORT', user.nb_sports) }}
{{ $t('workouts.SPORT', user.nb_sports) }}
</span>
</div>
</div>
@ -36,7 +36,6 @@
<script lang="ts">
import { ComputedRef, PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
@ -64,14 +63,13 @@
},
},
setup(props) {
const { t } = useI18n()
const tabs = ['PROFILE', 'PREFERENCES']
const authUserPictureUrl: ComputedRef<string> = computed(() =>
props.user.picture
? `${getApiUrl()}/users/${props.user.username}/picture?${Date.now()}`
: ''
)
return { authUserPictureUrl, t, tabs }
return { authUserPictureUrl, tabs }
},
})
</script>

View File

@ -2,8 +2,8 @@
<div id="user-infos-edition">
<Modal
v-if="displayModal"
:title="t('common.CONFIRMATION')"
:message="t('user.CONFIRM_ACCOUNT_DELETION')"
:title="$t('common.CONFIRMATION')"
:message="$t('user.CONFIRM_ACCOUNT_DELETION')"
@confirmAction="deleteAccount(user.username)"
@cancelAction="updateDisplayModal(false)"
/>
@ -11,15 +11,15 @@
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<form @submit.prevent="updateProfile">
<label class="form-items" for="email">
{{ t('user.EMAIL') }}
{{ $t('user.EMAIL') }}
<input id="email" :value="user.email" disabled />
</label>
<label class="form-items" for="registrationDate">
{{ t('user.PROFILE.REGISTRATION_DATE') }}
{{ $t('user.PROFILE.REGISTRATION_DATE') }}
<input id="registrationDate" :value="registrationDate" disabled />
</label>
<label class="form-items" for="password">
{{ t('user.PASSWORD') }}
{{ $t('user.PASSWORD') }}
<input
id="password"
type="password"
@ -28,7 +28,7 @@
/>
</label>
<label class="form-items" for="passwordConfirmation">
{{ t('user.PASSWORD_CONFIRMATION') }}
{{ $t('user.PASSWORD_CONFIRMATION') }}
<input
id="passwordConfirmation"
type="password"
@ -38,7 +38,7 @@
</label>
<hr />
<label class="form-items" for="first_name">
{{ t('user.PROFILE.FIRST_NAME') }}
{{ $t('user.PROFILE.FIRST_NAME') }}
<input
id="first_name"
v-model="userForm.first_name"
@ -46,11 +46,11 @@
/>
</label>
<label class="form-items" for="last_name">
{{ t('user.PROFILE.LAST_NAME') }}
{{ $t('user.PROFILE.LAST_NAME') }}
<input id="last_name" v-model="userForm.last_name" />
</label>
<label class="form-items" for="birth_date">
{{ t('user.PROFILE.BIRTH_DATE') }}
{{ $t('user.PROFILE.BIRTH_DATE') }}
<input
id="birth_date"
type="date"
@ -60,7 +60,7 @@
/>
</label>
<label class="form-items" for="location">
{{ t('user.PROFILE.LOCATION') }}
{{ $t('user.PROFILE.LOCATION') }}
<input
id="location"
v-model="userForm.location"
@ -68,7 +68,7 @@
/>
</label>
<label class="form-items">
{{ t('user.PROFILE.BIO') }}
{{ $t('user.PROFILE.BIO') }}
<CustomTextArea
name="bio"
:charLimit="200"
@ -79,13 +79,13 @@
</label>
<div class="form-buttons">
<button class="confirm" type="submit">
{{ t('buttons.SUBMIT') }}
{{ $t('buttons.SUBMIT') }}
</button>
<button class="cancel" @click.prevent="$router.go(-1)">
{{ t('buttons.CANCEL') }}
{{ $t('buttons.CANCEL') }}
</button>
<button class="danger" @click.prevent="updateDisplayModal(true)">
{{ t('buttons.DELETE_MY_ACCOUNT') }}
{{ $t('buttons.DELETE_MY_ACCOUNT') }}
</button>
</div>
</form>
@ -105,7 +105,6 @@
ref,
onMounted,
} from 'vue'
import { useI18n } from 'vue-i18n'
import CustomTextArea from '@/components/Common/CustomTextArea.vue'
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
@ -128,7 +127,6 @@
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const userForm: IUserPayload = reactive({
password: '',
@ -185,7 +183,6 @@
errorMessages,
loading,
registrationDate,
t,
userForm,
deleteAccount,
updateBio,

View File

@ -12,16 +12,16 @@
/>
<div class="picture-buttons">
<button type="submit" :disabled="!pictureFile">
{{ t('user.PROFILE.PICTURE_UPDATE') }}
{{ $t('user.PROFILE.PICTURE_UPDATE') }}
</button>
<button class="danger" v-if="user.picture" @click="deleteUserPicture">
{{ t('user.PROFILE.PICTURE_REMOVE') }}
{{ $t('user.PROFILE.PICTURE_REMOVE') }}
</button>
<button class="cancel" @click="$router.push('/profile')">
{{ t('user.PROFILE.BACK_TO_PROFILE') }}
{{ $t('user.PROFILE.BACK_TO_PROFILE') }}
</button>
</div>
<span>{{ t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}</span>
<span>{{ $t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}</span>
</form>
</div>
</div>
@ -36,7 +36,6 @@
computed,
ref,
} from 'vue'
import { useI18n } from 'vue-i18n'
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import UserPicture from '@/components/User/UserPicture.vue'
@ -59,7 +58,6 @@
},
},
setup() {
const { t } = useI18n()
const store = useStore()
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
@ -92,7 +90,6 @@
errorMessages,
fileSizeLimit,
pictureFile,
t,
deleteUserPicture,
updateUserPicture,
updatePictureFile,

View File

@ -4,7 +4,7 @@
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<form @submit.prevent="updateProfile">
<label class="form-items">
{{ t('user.PROFILE.LANGUAGE') }}
{{ $t('user.PROFILE.LANGUAGE') }}
<select id="language" v-model="userForm.language" :disabled="loading">
<option
v-for="lang in availableLanguages"
@ -16,7 +16,7 @@
</select>
</label>
<label class="form-items" for="timezone">
{{ t('user.PROFILE.TIMEZONE') }}
{{ $t('user.PROFILE.TIMEZONE') }}
<input
id="timezone"
v-model="userForm.timezone"
@ -24,23 +24,23 @@
/>
</label>
<label class="form-items">
{{ t('user.PROFILE.FIRST_DAY_OF_WEEK') }}
{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}
<select id="weekm" v-model="userForm.weekm" :disabled="loading">
<option
v-for="start in weekStart"
:value="start.value"
:key="start.value"
>
{{ t(`user.PROFILE.${start.label}`) }}
{{ $t(`user.PROFILE.${start.label}`) }}
</option>
</select>
</label>
<div class="form-buttons">
<button class="confirm" type="submit">
{{ t('buttons.SUBMIT') }}
{{ $t('buttons.SUBMIT') }}
</button>
<button class="cancel" @click.prevent="$router.go(-1)">
{{ t('buttons.CANCEL') }}
{{ $t('buttons.CANCEL') }}
</button>
</div>
</form>
@ -76,7 +76,7 @@
},
},
setup(props) {
const { t, availableLocales } = useI18n()
const { availableLocales } = useI18n()
const store = useStore()
const userForm: IUserPreferencesPayload = reactive({
language: '',
@ -122,7 +122,6 @@
availableLanguages,
errorMessages,
loading,
t,
userForm,
weekStart,
updateProfile,

View File

@ -1,7 +1,7 @@
<template>
<div id="user-profile-edition">
<Card>
<template #title>{{ t(`user.PROFILE.${tab}_EDITION`) }}</template>
<template #title>{{ $t(`user.PROFILE.${tab}_EDITION`) }}</template>
<template #content>
<UserProfileTabs
:tabs="tabs"
@ -18,8 +18,7 @@
</template>
<script lang="ts">
import { PropType, defineComponent, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { PropType, computed, defineComponent, ref } from 'vue'
import Card from '@/components/Common/Card.vue'
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
@ -50,14 +49,13 @@
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES']
const selectedTab = ref(props.tab)
const loading = computed(
() => store.getters[USER_STORE.GETTERS.USER_LOADING]
)
return { loading, selectedTab, t, tabs }
return { loading, selectedTab, tabs }
},
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<div id="login-or-register-form">
<div id="user-auth-form">
<div id="user-form">
<div
class="form-box"
@ -19,38 +19,57 @@
:disabled="registration_disabled"
required
v-model="formData.username"
:placeholder="t('user.USERNAME')"
:placeholder="$t('user.USERNAME')"
/>
<input
v-if="action !== 'reset'"
id="email"
:disabled="registration_disabled"
required
type="email"
v-model="formData.email"
:placeholder="t('user.EMAIL')"
:placeholder="
action === 'reset-request'
? $t('user.ENTER_EMAIL')
: $t('user.EMAIL')
"
/>
<input
v-if="action !== 'reset-request'"
id="password"
:disabled="registration_disabled"
required
type="password"
v-model="formData.password"
:placeholder="t('user.PASSWORD')"
:placeholder="
action === 'reset'
? $t('user.ENTER_PASSWORD')
: $t('user.PASSWORD')
"
/>
<input
v-if="action === 'register'"
v-if="['register', 'reset'].includes(action)"
id="confirm-password"
:disabled="registration_disabled"
type="password"
required
v-model="formData.password_conf"
:placeholder="t('user.PASSWORD_CONFIRM')"
:placeholder="
action === 'reset'
? $t('user.ENTER_PASSWORD_CONFIRMATION')
: $t('user.PASSWORD_CONFIRM')
"
/>
</div>
<button type="submit" :disabled="registration_disabled">
{{ t(buttonText) }}
{{ $t(buttonText) }}
</button>
</form>
<div v-if="action === 'login'">
<router-link class="password-forgotten" to="/password-reset/request">
{{ $t('user.PASSWORD_FORGOTTEN') }}
</router-link>
</div>
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
</div>
</div>
@ -59,19 +78,17 @@
<script lang="ts">
import { ComputedRef, computed, defineComponent, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import AlertMessage from '@/components/Common/AlertMessage.vue'
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import router from '@/router'
import { ROOT_STORE, USER_STORE } from '@/store/constants'
import { IAppConfig } from '@/types/application'
import { ILoginRegisterFormData } from '@/types/user'
import { useStore } from '@/use/useStore'
export default defineComponent({
name: 'LoginOrRegisterForm',
name: 'UserAuthForm',
components: {
AlertMessage,
ErrorMessage,
@ -81,6 +98,10 @@
type: String,
required: true,
},
token: {
type: String,
default: '',
},
},
setup(props) {
const formData: ILoginRegisterFormData = reactive({
@ -89,12 +110,11 @@
password: '',
password_conf: '',
})
const { t } = useI18n()
const route = useRoute()
const store = useStore()
const buttonText: ComputedRef<string> = computed(() =>
props.action === 'register' ? 'buttons.REGISTER' : 'buttons.LOGIN'
getButtonText(props.action)
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
@ -108,11 +128,42 @@
!appConfig.value.is_registration_enabled
)
function getButtonText(action: string): string {
switch (action) {
case 'reset-request':
case 'reset':
return 'buttons.SUBMIT'
default:
return `buttons.${props.action.toUpperCase()}`
}
}
function onSubmit(actionType: string) {
return store.dispatch(USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
actionType,
formData,
})
switch (actionType) {
case 'reset':
if (!props.token) {
return store.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
'user.INVALID_TOKEN'
)
}
return store.dispatch(USER_STORE.ACTIONS.RESET_USER_PASSWORD, {
password: formData.password,
password_conf: formData.password_conf,
token: props.token,
})
case 'reset-request':
return store.dispatch(
USER_STORE.ACTIONS.SEND_PASSWORD_RESET_REQUEST,
{
email: formData.email,
}
)
default:
store.dispatch(USER_STORE.ACTIONS.LOGIN_OR_REGISTER, {
actionType,
formData,
})
}
}
function resetFormData() {
formData.username = ''
@ -128,13 +179,11 @@
}
)
return {
t,
appConfig,
buttonText,
errorMessages,
formData,
registration_disabled,
router,
onSubmit,
}
},
@ -144,7 +193,7 @@
<style scoped lang="scss">
@import '~@/scss/base';
#login-or-register-form {
#user-auth-form {
display: flex;
align-items: center;
@ -154,6 +203,12 @@
#user-form {
width: 60%;
.password-forgotten {
font-size: 0.9em;
font-style: italic;
padding-left: $default-padding;
}
button {
margin: $default-margin;
border: solid 1px var(--app-color);

View File

@ -3,7 +3,7 @@
<img
v-if="authUserPictureUrl !== ''"
class="nav-profile-user-img"
:alt="t('user.USER_PICTURE')"
:alt="$t('user.USER_PICTURE')"
:src="authUserPictureUrl"
/>
<div v-else class="no-picture">
@ -14,10 +14,10 @@
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import { IAuthUserProfile } from '@/types/user'
import { getApiUrl } from '@/utils'
export default defineComponent({
name: 'UserPicture',
props: {
@ -27,14 +27,12 @@
},
},
setup(props) {
const { t } = useI18n()
return {
authUserPictureUrl: computed(() =>
props.user.picture
? `${getApiUrl()}users/${props.user.username}/picture?${Date.now()}`
: ''
),
t,
}
},
})

View File

@ -11,7 +11,7 @@
:disabled="disabled"
@input="$router.push(getPath(tab))"
/>
<span>{{ t(`user.PROFILE.TABS.${tab}`) }}</span>
<span>{{ $t(`user.PROFILE.TABS.${tab}`) }}</span>
</label>
</div>
</div>
@ -20,7 +20,6 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'UserProfileTabs',
@ -43,7 +42,6 @@
},
},
setup(props) {
const { t } = useI18n()
function getPath(tab: string) {
switch (tab) {
case 'PICTURE':
@ -55,7 +53,7 @@
return `/profile${props.edition ? '/edit' : ''}`
}
}
return { t, getPath }
return { getPath }
},
})
</script>

View File

@ -6,7 +6,7 @@
<img
class="profile-img"
v-if="userPictureUrl !== ''"
:alt="t('user.USER_PICTURE')"
:alt="$t('user.USER_PICTURE')"
:src="userPictureUrl"
/>
<div v-else class="no-picture">
@ -49,7 +49,7 @@
<div v-if="workout">
<StaticMap v-if="workout.with_gpx" :workout="workout" />
<div v-else class="no-map">
{{ t('workouts.NO_MAP') }}
{{ $t('workouts.NO_MAP') }}
</div>
</div>
</div>
@ -78,9 +78,8 @@
<script lang="ts">
import { Locale, format, formatDistance } from 'date-fns'
import { PropType, defineComponent, ComputedRef, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import { ROOT_STORE } from '@/store/constants'
import { ISport } from '@/types/sports'
@ -111,7 +110,6 @@
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
const userPictureUrl: ComputedRef<string> = computed(() =>
@ -128,7 +126,6 @@
formatDistance,
getDateWithTZ,
locale,
t,
userPictureUrl,
}
},

View File

@ -1,7 +1,7 @@
<template>
<div id="workout-chart">
<Card>
<template #title>{{ t('workouts.ANALYSIS') }} </template>
<template #title>{{ $t('workouts.ANALYSIS') }} </template>
<template #content>
<div class="chart-radio">
<label>
@ -11,7 +11,7 @@
:checked="displayDistance"
@click="updateDisplayDistance"
/>
{{ t('workouts.DISTANCE') }}
{{ $t('workouts.DISTANCE') }}
</label>
<label>
<input
@ -20,7 +20,7 @@
:checked="!displayDistance"
@click="updateDisplayDistance"
/>
{{ t('workouts.DURATION') }}
{{ $t('workouts.DURATION') }}
</label>
</div>
<LineChart
@ -29,7 +29,7 @@
@mouseleave="emitEmptyCoordinates"
/>
<div class="no-data-cleaning">
{{ t('workouts.NO_DATA_CLEANING') }}
{{ $t('workouts.NO_DATA_CLEANING') }}
</div>
</template>
</Card>
@ -201,7 +201,6 @@
return {
displayDistance,
lineChartProps,
t,
emitEmptyCoordinates,
updateDisplayDistance,
}

View File

@ -5,8 +5,8 @@
:class="{ inactive: !workoutObject.previousUrl }"
:title="
workoutObject.previousUrl
? t(`workouts.PREVIOUS_${workoutObject.type}`)
: t(`workouts.NO_PREVIOUS_${workoutObject.type}`)
? $t(`workouts.PREVIOUS_${workoutObject.type}`)
: $t(`workouts.NO_PREVIOUS_${workoutObject.type}`)
"
@click="
workoutObject.previousUrl
@ -42,7 +42,7 @@
<span class="workout-segment">
<i class="fa fa-map-marker" aria-hidden="true" />
{{ t('workouts.SEGMENT') }}
{{ $t('workouts.SEGMENT') }}
{{ workoutObject.segmentId + 1 }}
</span>
</div>
@ -57,7 +57,7 @@
params: { workoutId: workoutObject.workoutId },
}"
>
> {{ t('workouts.BACK_TO_WORKOUT') }}
> {{ $t('workouts.BACK_TO_WORKOUT') }}
</router-link></span
>
</div>
@ -68,8 +68,8 @@
:class="{ inactive: !workoutObject.nextUrl }"
:title="
workoutObject.nextUrl
? t(`workouts.NEXT_${workoutObject.type}`)
: t(`workouts.NO_NEXT_${workoutObject.type}`)
? $t(`workouts.NEXT_${workoutObject.type}`)
: $t(`workouts.NO_NEXT_${workoutObject.type}`)
"
@click="
workoutObject.nextUrl ? $router.push(workoutObject.nextUrl) : null
@ -82,9 +82,8 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import { ISport } from '@/types/sports'
import { IWorkoutObject } from '@/types/workouts'
@ -105,8 +104,7 @@
},
emits: ['displayModal'],
setup(props, { emit }) {
const { t } = useI18n()
return { t, emit }
return { emit }
},
})
</script>

View File

@ -2,25 +2,26 @@
<div id="workout-info">
<div class="workout-data">
<i class="fa fa-clock-o" aria-hidden="true" />
{{ t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
{{ $t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="LD" />
<div v-if="withPause">
({{ t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
{{ t('workouts.TOTAL_DURATION') }}:
({{ $t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
{{ $t('workouts.TOTAL_DURATION') }}:
<span>{{ workoutObject.duration }})</span>
</div>
</div>
<div class="workout-data">
<i class="fa fa-road" aria-hidden="true" />
{{ t('workouts.DISTANCE') }}: <span>{{ workoutObject.distance }} km</span>
{{ $t('workouts.DISTANCE') }}:
<span>{{ workoutObject.distance }} km</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="FD" />
</div>
<div class="workout-data">
<i class="fa fa-tachometer" aria-hidden="true" />
{{ t('workouts.AVERAGE_SPEED') }}:
{{ $t('workouts.AVERAGE_SPEED') }}:
<span>{{ workoutObject.aveSpeed }} km/h</span
><WorkoutRecord :workoutObject="workoutObject" record_type="AS" /><br />
{{ t('workouts.MAX_SPEED') }}:
{{ $t('workouts.MAX_SPEED') }}:
<span>{{ workoutObject.maxSpeed }} km/h</span>
<WorkoutRecord :workoutObject="workoutObject" record_type="MS" />
</div>
@ -31,11 +32,11 @@
<img
class="mountains"
src="/img/workouts/mountains.svg"
:alt="t('workouts.ELEVATION')"
:alt="$t('workouts.ELEVATION')"
/>
{{ t('workouts.MIN_ALTITUDE') }}: <span>{{ workoutObject.minAlt }} m</span
><br />
{{ t('workouts.MAX_ALTITUDE') }}:
{{ $t('workouts.MIN_ALTITUDE') }}:
<span>{{ workoutObject.minAlt }} m</span><br />
{{ $t('workouts.MAX_ALTITUDE') }}:
<span>{{ workoutObject.maxAlt }} m</span>
</div>
<div
@ -43,9 +44,9 @@
v-if="workoutObject.ascent !== null && workoutObject.descent !== null"
>
<i class="fa fa-location-arrow" aria-hidden="true" />
{{ t('workouts.ASCENT') }}: <span>{{ workoutObject.ascent }} m</span
{{ $t('workouts.ASCENT') }}: <span>{{ workoutObject.ascent }} m</span
><br />
{{ t('workouts.DESCENT') }}: <span>{{ workoutObject.descent }} m</span>
{{ $t('workouts.DESCENT') }}: <span>{{ workoutObject.descent }} m</span>
</div>
<WorkoutWeather :workoutObject="workoutObject" />
</div>
@ -53,7 +54,6 @@
<script lang="ts">
import { PropType, computed, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import WorkoutRecord from '@/components/Workout/WorkoutDetail/WorkoutRecord.vue'
import WorkoutWeather from '@/components/Workout/WorkoutDetail/WorkoutWeather.vue'
@ -72,14 +72,12 @@
},
},
setup(props) {
const { t } = useI18n()
return {
withPause: computed(
() =>
props.workoutObject.pauses !== '0:00:00' &&
props.workoutObject.pauses !== null
),
t,
}
},
})

View File

@ -23,7 +23,7 @@
/>
</LMap>
</div>
<div v-else class="no-map">{{ t('workouts.NO_MAP') }}</div>
<div v-else class="no-map">{{ $t('workouts.NO_MAP') }}</div>
</div>
</div>
</template>
@ -32,7 +32,6 @@
import { gpx } from '@tmcw/togeojson'
import { LGeoJson, LMap, LMarker, LTileLayer } from '@vue-leaflet/vue-leaflet'
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ROOT_STORE } from '@/store/constants'
import { IAppConfig } from '@/types/application'
@ -59,7 +58,6 @@
},
},
setup(props) {
const { t } = useI18n()
const store = useStore()
function getGeoJson(gpxContent: string): GeoJSONData {
@ -120,7 +118,6 @@
bounds,
center,
geoJson,
t,
workoutMap,
fitBounds,
getApiUrl,

View File

@ -9,17 +9,17 @@
<th />
<th>
<div class="weather-th">
{{ t('workouts.START') }}
{{ $t('workouts.START') }}
<img
class="weather-img"
:src="`/img/weather/${workoutObject.weatherStart.icon}.svg`"
:alt="
t(
$t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherStart.icon}`
)
"
:title="
t(
$t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherStart.icon}`
)
"
@ -28,17 +28,17 @@
</th>
<th>
<div class="weather-th">
{{ t('workouts.END') }}
{{ $t('workouts.END') }}
<img
class="weather-img"
:src="`/img/weather/${workoutObject.weatherEnd.icon}.svg`"
:alt="
t(
$t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherEnd.icon}`
)
"
:title="
t(
$t(
`workouts.WEATHER.DARK_SKY.${workoutObject.weatherEnd.icon}`
)
"
@ -53,8 +53,8 @@
<img
class="weather-img weather-img-small"
src="/img/weather/temperature.svg"
:alt="t(`workouts.WEATHER.TEMPERATURE`)"
:title="t(`workouts.WEATHER.TEMPERATURE`)"
:alt="$t(`workouts.WEATHER.TEMPERATURE`)"
:title="$t(`workouts.WEATHER.TEMPERATURE`)"
/>
</td>
<td>
@ -69,8 +69,8 @@
<img
class="weather-img weather-img-small"
src="/img/weather/pour-rain.svg"
:alt="t(`workouts.WEATHER.HUMIDITY`)"
:title="t(`workouts.WEATHER.HUMIDITY`)"
:alt="$t(`workouts.WEATHER.HUMIDITY`)"
:title="$t(`workouts.WEATHER.HUMIDITY`)"
/>
</td>
<td>
@ -85,8 +85,8 @@
<img
class="weather-img weather-img-small"
src="/img/weather/breeze.svg"
:alt="t(`workouts.WEATHER.WIND`)"
:title="t(`workouts.WEATHER.WIND`)"
:alt="$t(`workouts.WEATHER.WIND`)"
:title="$t(`workouts.WEATHER.WIND`)"
/>
</td>
<td>{{ Number(workoutObject.weatherStart.wind).toFixed(1) }}m/s</td>
@ -99,9 +99,9 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { IWorkoutObject } from '@/types/workouts'
export default defineComponent({
name: 'WorkoutWeather',
props: {
@ -110,10 +110,6 @@
required: true,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -2,8 +2,8 @@
<div class="workout-detail">
<Modal
v-if="displayModal"
:title="t('common.CONFIRMATION')"
:message="t('workouts.WORKOUT_DELETION_CONFIRMATION')"
:title="$t('common.CONFIRMATION')"
:message="$t('workouts.WORKOUT_DELETION_CONFIRMATION')"
@confirmAction="deleteWorkout(workoutObject.workoutId)"
@cancelAction="updateDisplayModal(false)"
/>
@ -36,7 +36,6 @@
ref,
watch,
} from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import Card from '@/components/Common/Card.vue'
@ -90,7 +89,6 @@
setup(props) {
const route = useRoute()
const store = useStore()
const { t } = useI18n()
function getWorkoutObjectUrl(
workout: IWorkout,
@ -196,7 +194,6 @@
getWorkoutObject(workout.value, segment.value)
),
displayModal,
t,
deleteWorkout,
updateDisplayModal,
}

View File

@ -5,7 +5,7 @@
>
<Card>
<template #title>{{
t(`workouts.${isCreation ? 'ADD' : 'EDIT'}_WORKOUT`)
$t(`workouts.${isCreation ? 'ADD' : 'EDIT'}_WORKOUT`)
}}</template>
<template #content>
<div id="workout-form">
@ -20,7 +20,7 @@
:disabled="loading"
@click="updateWithGpx"
/>
<label for="withGpx">{{ t('workouts.WITH_GPX') }}</label>
<label for="withGpx">{{ $t('workouts.WITH_GPX') }}</label>
</div>
<div>
<input
@ -31,12 +31,12 @@
@click="updateWithGpx"
/>
<label for="withoutGpx">{{
t('workouts.WITHOUT_GPX')
$t('workouts.WITHOUT_GPX')
}}</label>
</div>
</div>
<div class="form-item">
<label> {{ t('workouts.SPORT', 1) }}: </label>
<label> {{ $t('workouts.SPORT', 1) }}: </label>
<select
id="sport"
required
@ -54,8 +54,8 @@
</div>
<div class="form-item" v-if="isCreation && withGpx">
<label for="gpxFile">
{{ t('workouts.GPX_FILE') }}
{{ t('workouts.ZIP_ARCHIVE_DESCRIPTION') }}:
{{ $t('workouts.GPX_FILE') }}
{{ $t('workouts.ZIP_ARCHIVE_DESCRIPTION') }}:
</label>
<input
id="gpxFile"
@ -67,26 +67,28 @@
/>
<div class="files-help">
<div>
<strong>{{ t('workouts.GPX_FILE') }}:</strong>
<strong>{{ $t('workouts.GPX_FILE') }}:</strong>
<ul>
<li>{{ t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}</li>
<li>
{{ $t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}
</li>
</ul>
</div>
<div>
<strong>{{ t('workouts.ZIP_ARCHIVE') }}:</strong>
<strong>{{ $t('workouts.ZIP_ARCHIVE') }}:</strong>
<ul>
<li>{{ t('workouts.NO_FOLDER') }}</li>
<li>{{ $t('workouts.NO_FOLDER') }}</li>
<li>
{{ t('workouts.MAX_FILES') }}: {{ gpx_limit_import }}
{{ $t('workouts.MAX_FILES') }}: {{ gpx_limit_import }}
</li>
<li>{{ t('workouts.MAX_SIZE') }}: {{ zipSizeLimit }}</li>
<li>{{ $t('workouts.MAX_SIZE') }}: {{ zipSizeLimit }}</li>
</ul>
</div>
</div>
</div>
<div class="form-item" v-else>
<label for="title"> {{ t('workouts.TITLE') }}: </label>
<label for="title"> {{ $t('workouts.TITLE') }}: </label>
<input
id="title"
name="title"
@ -99,7 +101,7 @@
<div v-if="!withGpx">
<div class="workout-date-duration">
<div class="form-item">
<label>{{ t('workouts.WORKOUT_DATE') }}:</label>
<label>{{ $t('workouts.WORKOUT_DATE') }}:</label>
<div class="workout-date-time">
<input
id="workout-date"
@ -121,7 +123,7 @@
</div>
</div>
<div class="form-item">
<label>{{ t('workouts.DURATION') }}:</label>
<label>{{ $t('workouts.DURATION') }}:</label>
<div>
<input
id="workout-duration-hour"
@ -162,7 +164,7 @@
</div>
</div>
<div class="form-item">
<label>{{ t('workouts.DISTANCE') }} (km):</label>
<label>{{ $t('workouts.DISTANCE') }} (km):</label>
<input
type="number"
min="0"
@ -174,7 +176,7 @@
</div>
</div>
<div class="form-item">
<label> {{ t('workouts.NOTES') }}: </label>
<label> {{ $t('workouts.NOTES') }}: </label>
<CustomTextArea
name="notes"
:input="workoutDataObject.notes"
@ -189,10 +191,10 @@
</div>
<div v-else class="form-buttons">
<button class="confirm" type="submit" :disabled="loading">
{{ t('buttons.SUBMIT') }}
{{ $t('buttons.SUBMIT') }}
</button>
<button class="cancel" @click.prevent="onCancel">
{{ t('buttons.CANCEL') }}
{{ $t('buttons.CANCEL') }}
</button>
</div>
</form>
@ -406,7 +408,6 @@
errorMessages,
fileSizeLimit,
gpx_limit_import,
t,
translatedSports,
withGpx,
zipSizeLimit,

View File

@ -1,9 +1,9 @@
<template>
<div id="workout-note">
<Card>
<template #title>{{ t('workouts.NOTES') }}</template>
<template #title>{{ $t('workouts.NOTES') }}</template>
<template #content>
{{ notes && notes !== '' ? notes : t('workouts.NO_NOTES') }}</template
{{ notes && notes !== '' ? notes : $t('workouts.NO_NOTES') }}</template
>
</Card>
</div>
@ -11,7 +11,6 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
@ -26,10 +25,6 @@
required: false,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -1,7 +1,7 @@
<template>
<div id="workout-segments">
<Card>
<template #title>{{ t('workouts.SEGMENT', 2) }}</template>
<template #title>{{ $t('workouts.SEGMENT', 2) }}</template>
<template #content>
<ul>
<li v-for="(segment, index) in segments" :key="segment.segment_id">
@ -13,10 +13,10 @@
segmentId: index + 1,
},
}"
>{{ t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
>{{ $t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
>
({{ t('workouts.DISTANCE') }}: {{ segment.distance }} km,
{{ t('workouts.DURATION') }}: {{ segment.duration }})
({{ $t('workouts.DISTANCE') }}: {{ segment.distance }} km,
{{ $t('workouts.DURATION') }}: {{ segment.duration }})
</li>
</ul>
</template>
@ -26,7 +26,6 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
import Card from '@/components/Common/Card.vue'
import { IWorkoutSegment } from '@/types/workouts'
@ -42,10 +41,6 @@
required: true,
},
},
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -1,9 +1,9 @@
<template>
<div class="no-workouts box">
<div>
{{ t('workouts.NO_WORKOUTS') }}
{{ $t('workouts.NO_WORKOUTS') }}
<router-link to="/workouts/add">
{{ t('workouts.UPLOAD_FIRST_WORKOUT') }}
{{ $t('workouts.UPLOAD_FIRST_WORKOUT') }}
</router-link>
</div>
</div>
@ -11,14 +11,9 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: 'NoWorkouts',
setup() {
const { t } = useI18n()
return { t }
},
})
</script>

View File

@ -4,18 +4,18 @@
<div class="form">
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.FROM') }}: </label>
<label> {{ $t('workouts.FROM') }}: </label>
<input name="from" type="date" @change="handleFilterChange" />
</div>
<div class="form-item">
<label> {{ t('workouts.TO') }}: </label>
<label> {{ $t('workouts.TO') }}: </label>
<input name="to" type="date" @change="handleFilterChange" />
</div>
</div>
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.SPORT', 1) }}:</label>
<label> {{ $t('workouts.SPORT', 1) }}:</label>
<select name="sport_id" @change="handleFilterChange">
<option value="" />
<option
@ -31,7 +31,7 @@
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.DISTANCE') }} (km): </label>
<label> {{ $t('workouts.DISTANCE') }} (km): </label>
<div class="form-inputs-group">
<input
name="distance_from"
@ -40,7 +40,7 @@
step="1"
@change="handleFilterChange"
/>
<span>{{ t('workouts.TO') }}</span>
<span>{{ $t('workouts.TO') }}</span>
<input
name="distance_to"
type="number"
@ -54,7 +54,7 @@
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.DURATION') }} (km): </label>
<label> {{ $t('workouts.DURATION') }} (km): </label>
<div class="form-inputs-group">
<input
name="duration_from"
@ -63,7 +63,7 @@
placeholder="hh:mm"
type="text"
/>
<span>{{ t('workouts.TO') }}</span>
<span>{{ $t('workouts.TO') }}</span>
<input
name="duration_to"
@change="handleFilterChange"
@ -77,7 +77,7 @@
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.AVE_SPEED') }} (km): </label>
<label> {{ $t('workouts.AVE_SPEED') }} (km): </label>
<div class="form-inputs-group">
<input
min="0"
@ -86,7 +86,7 @@
step="1"
type="number"
/>
<span>{{ t('workouts.TO') }}</span>
<span>{{ $t('workouts.TO') }}</span>
<input
min="0"
name="ave_speed_to"
@ -100,7 +100,7 @@
<div class="form-items-group">
<div class="form-item">
<label> {{ t('workouts.MAX_SPEED') }} (km): </label>
<label> {{ $t('workouts.MAX_SPEED') }} (km): </label>
<div class="form-inputs-group">
<input
@ -110,7 +110,7 @@
step="1"
type="number"
/>
<span>{{ t('workouts.TO') }}</span>
<span>{{ $t('workouts.TO') }}</span>
<input
min="0"
name="max_speed_to"
@ -125,7 +125,7 @@
<div class="form-button">
<button class="confirm" @click="onFilter">
{{ t('buttons.FILTER') }}
{{ $t('buttons.FILTER') }}
</button>
</div>
</div>
@ -166,7 +166,7 @@
emit('filter', { ...params })
}
return { t, translatedSports, onFilter, handleFilterChange }
return { translatedSports, onFilter, handleFilterChange }
},
})
</script>

View File

@ -6,19 +6,19 @@
<thead>
<tr>
<th class="sport-col" />
<th>{{ capitalize(t('workouts.WORKOUT', 1)) }}</th>
<th>{{ capitalize(t('workouts.DATE')) }}</th>
<th>{{ capitalize(t('workouts.DISTANCE')) }}</th>
<th>{{ capitalize(t('workouts.DURATION')) }}</th>
<th>{{ capitalize(t('workouts.AVE_SPEED')) }}</th>
<th>{{ capitalize(t('workouts.MAX_SPEED')) }}</th>
<th>{{ capitalize($t('workouts.WORKOUT', 1)) }}</th>
<th>{{ capitalize($t('workouts.DATE')) }}</th>
<th>{{ capitalize($t('workouts.DISTANCE')) }}</th>
<th>{{ capitalize($t('workouts.DURATION')) }}</th>
<th>{{ capitalize($t('workouts.AVE_SPEED')) }}</th>
<th>{{ capitalize($t('workouts.MAX_SPEED')) }}</th>
</tr>
</thead>
<tbody>
<tr v-for="workout in workouts" :key="workout.id">
<td class="sport-col">
<span class="cell-heading">
{{ t('workouts.SPORT', 1) }}
{{ $t('workouts.SPORT', 1) }}
</span>
<SportImage
:title="
@ -32,7 +32,7 @@
</td>
<td class="workout-title">
<span class="cell-heading">
{{ capitalize(t('workouts.WORKOUT', 1)) }}
{{ capitalize($t('workouts.WORKOUT', 1)) }}
</span>
<router-link
class="nav-item"
@ -53,7 +53,7 @@
</td>
<td>
<span class="cell-heading">
{{ t('workouts.DATE') }}
{{ $t('workouts.DATE') }}
</span>
{{
format(
@ -64,25 +64,25 @@
</td>
<td class="text-right">
<span class="cell-heading">
{{ t('workouts.DISTANCE') }}
{{ $t('workouts.DISTANCE') }}
</span>
{{ Number(workout.distance).toFixed(2) }} km
</td>
<td class="text-right">
<span class="cell-heading">
{{ t('workouts.DURATION') }}
{{ $t('workouts.DURATION') }}
</span>
{{ workout.moving }}
</td>
<td class="text-right">
<span class="cell-heading">
{{ t('workouts.AVE_SPEED') }}
{{ $t('workouts.AVE_SPEED') }}
</span>
{{ workout.ave_speed }} km/h
</td>
<td class="text-right">
<span class="cell-heading">
{{ t('workouts.MAX_SPEED') }}
{{ $t('workouts.MAX_SPEED') }}
</span>
{{ workout.max_speed }} km/h
</td>
@ -94,7 +94,7 @@
<NoWorkouts v-if="workouts.length === 0" />
<div v-if="moreWorkoutsExist" class="more-workouts">
<button @click="loadMoreWorkouts">
{{ t('workouts.LOAD_MORE_WORKOUT') }}
{{ $t('workouts.LOAD_MORE_WORKOUT') }}
</button>
</div>
<div id="bottom" />
@ -112,9 +112,8 @@
watch,
onBeforeMount,
} from 'vue'
import { useI18n } from 'vue-i18n'
import SportImage from '@/components/Common/SportImage/index.vue'
import SportImage from '@/components/Common/Images/SportImage/index.vue'
import StaticMap from '@/components/Common/StaticMap.vue'
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
import { WORKOUTS_STORE } from '@/store/constants'
@ -147,7 +146,6 @@
},
setup(props) {
const store = useStore()
const { t } = useI18n()
const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
)
@ -189,7 +187,6 @@
return {
moreWorkoutsExist,
t,
workouts,
capitalize,
format,

View File

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

View File

@ -1,12 +1,20 @@
{
"CONFIRM_ACCOUNT_DELETION": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone",
"EMAIL": "Email",
"ENTER_EMAIL": "Enter an email address",
"ENTER_PASSWORD": "Enter a password",
"ENTER_PASSWORD_CONFIRMATION": "Confirm the password",
"INVALID_TOKEN": "Invalid token, please request a new password reset.",
"LANGUAGE": "Language",
"LOGIN": "Login",
"LOGOUT": "Logout",
"PASSWORD": "Password",
"PASSWORD_CONFIRM": "Confirm Password",
"PASSWORD_CONFIRMATION": "Password confirmation",
"PASSWORD_FORGOTTEN": "Forgot password?",
"PASSWORD_RESET": "Password reset",
"PASSWORD_SENT_EMAIL_TEXT": "Check your email. If your address is in our database, you'll received an email with a link to reset your password.",
"PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.",
"PROFILE": {
"BACK_TO_PROFILE": "Back to profile",
"BIO": "Bio",
@ -36,6 +44,7 @@
},
"REGISTER": "Register",
"REGISTER_DISABLED": "Sorry, registration is disabled.",
"RESET_PASSWORD": "Reset your password",
"USER_PICTURE": "user picture",
"USERNAME": "Username"
}

View File

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

View File

@ -1,12 +1,20 @@
{
"CONFIRM_ACCOUNT_DELETION": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.",
"EMAIL": "Email",
"ENTER_EMAIL": "Saisir une adresse email",
"ENTER_PASSWORD": "Saisir un mot de passe",
"ENTER_PASSWORD_CONFIRMATION": "Confirmer le mot de passe",
"INVALID_TOKEN": "Jeton invalide, veullez demander une nouvelle réinitialisation de mot de passe.",
"LANGUAGE": "Langue",
"LOGIN": "Se connecter",
"LOGOUT": "Se déconnecter",
"PASSWORD": "Mot de passe",
"PASSWORD_CONFIRM": "Confirmation du mot de passe",
"PASSWORD_CONFIRMATION": "Confirmation du mot de passe",
"PASSWORD_FORGOTTEN": "Mot de passe oublié ?",
"PASSWORD_RESET": "Réinitialisation du mot de passe",
"PASSWORD_SENT_EMAIL_TEXT": "Vérifiez vore boite mail. Si vote adresse est dans notre base de données, vous recevrez un email avec un lien pour réinitialiser votre mot de passe.",
"PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.",
"PROFILE": {
"BACK_TO_PROFILE": "Revenir au profil",
"BIO": "Bio",
@ -36,6 +44,7 @@
},
"REGISTER": "S'inscrire",
"REGISTER_DISABLED": "Désolé, les inscriptions sont désactivées.",
"RESET_PASSWORD": "Réinitialiser votre mot de passe",
"USER_PICTURE": "photo de l'utilisateur",
"USERNAME": "Nom d'utilisateur"
}

View File

@ -6,6 +6,7 @@ import AddWorkout from '@/views/AddWorkout.vue'
import Dashboard from '@/views/DashBoard.vue'
import LoginOrRegister from '@/views/LoginOrRegister.vue'
import NotFoundView from '@/views/NotFoundView.vue'
import PasswordResetView from '@/views/PasswordResetView.vue'
import ProfileView from '@/views/ProfileView.vue'
import StatisticsView from '@/views/StatisticsView.vue'
import EditWorkout from '@/views/workouts/EditWorkout.vue'
@ -36,6 +37,30 @@ const routes: Array<RouteRecordRaw> = [
component: ProfileView,
props: { edition: false, tab: 'PROFILE' },
},
{
path: '/password-reset/sent',
name: 'PasswordEmailSent',
component: PasswordResetView,
props: { action: 'request-sent' },
},
{
path: '/password-reset/request',
name: 'PasswordResetRequest',
component: PasswordResetView,
props: { action: 'reset-request' },
},
{
path: '/password-reset/password-updated',
name: 'PasswordUpdated',
component: PasswordResetView,
props: { action: 'password-updated' },
},
{
path: '/password-reset',
name: 'PasswordReset',
component: PasswordResetView,
props: { action: 'reset' },
},
{
path: '/profile/edit/picture',
name: 'UserPictureEdition',
@ -100,18 +125,27 @@ const router = createRouter({
routes,
})
const pathsWithoutAuthentication = [
'/login',
'/password-reset',
'/password-reset/password-updated',
'/password-reset/request',
'/password-reset/sent',
'/register',
]
router.beforeEach((to, from, next) => {
store
.dispatch(USER_STORE.ACTIONS.CHECK_AUTH_USER)
.then(() => {
if (
store.getters[USER_STORE.GETTERS.IS_AUTHENTICATED] &&
['/login', '/register'].includes(to.path)
pathsWithoutAuthentication.includes(to.path)
) {
return next('/')
} else if (
!store.getters[USER_STORE.GETTERS.IS_AUTHENTICATED] &&
!['/login', '/register'].includes(to.path)
!pathsWithoutAuthentication.includes(to.path)
) {
const path =
to.path === '/'

View File

@ -62,4 +62,6 @@
--cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969;
--svg-filter: drop-shadow(10px 10px 10px var(--app-shadow-color))
}

View File

@ -16,6 +16,8 @@ import { IUserActions, IUserState } from '@/store/modules/user/types'
import {
ILoginOrRegisterData,
IUserDeletionPayload,
IUserPasswordPayload,
IUserPasswordResetPayload,
IUserPayload,
IUserPicturePayload,
IUserPreferencesPayload,
@ -214,4 +216,36 @@ export const actions: ActionTree<IUserState, IRootState> & IUserActions = {
context.commit(USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
)
},
[USER_STORE.ACTIONS.SEND_PASSWORD_RESET_REQUEST](
context: ActionContext<IUserState, IRootState>,
payload: IUserPasswordPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
api
.post('auth/password/reset-request', payload)
.then((res) => {
if (res.data.status === 'success') {
router.push('/password-reset/sent')
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
[USER_STORE.ACTIONS.RESET_USER_PASSWORD](
context: ActionContext<IUserState, IRootState>,
payload: IUserPasswordResetPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
api
.post('auth/password/update', payload)
.then((res) => {
if (res.data.status === 'success') {
router.push('/password-reset/password-updated')
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
},
}

View File

@ -5,6 +5,8 @@ export enum UserActions {
GET_USER_PROFILE = 'GET_USER_PROFILE',
LOGIN_OR_REGISTER = 'LOGIN_OR_REGISTER',
LOGOUT = 'LOGOUT',
SEND_PASSWORD_RESET_REQUEST = 'SEND_PASSWORD_RESET_REQUEST',
RESET_USER_PASSWORD = 'RESET_USER_PASSWORD',
UPDATE_USER_PICTURE = 'UPDATE_USER_PICTURE',
UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE',
UPDATE_USER_PREFERENCES = 'UPDATE_USER_PREFERENCES',

View File

@ -11,6 +11,8 @@ import {
IAuthUserProfile,
ILoginOrRegisterData,
IUserDeletionPayload,
IUserPasswordPayload,
IUserPasswordResetPayload,
IUserPayload,
IUserPicturePayload,
IUserPreferencesPayload,
@ -55,6 +57,16 @@ export interface IUserActions {
payload: IUserPicturePayload
): void
[USER_STORE.ACTIONS.SEND_PASSWORD_RESET_REQUEST](
context: ActionContext<IUserState, IRootState>,
payload: IUserPasswordPayload
): void
[USER_STORE.ACTIONS.RESET_USER_PASSWORD](
context: ActionContext<IUserState, IRootState>,
payload: IUserPasswordResetPayload
): void
[USER_STORE.ACTIONS.DELETE_ACCOUNT](
context: ActionContext<IUserState, IRootState>,
payload: IUserDeletionPayload

View File

@ -42,6 +42,16 @@ export interface IUserPicturePayload {
picture: File
}
export interface IUserPasswordPayload {
email: string
}
export interface IUserPasswordResetPayload {
password: string
password_conf: string
token: string
}
export interface IUserDeletionPayload {
username: string
}

View File

@ -15,7 +15,7 @@
import { defineComponent } from 'vue'
import BikePic from '@/components/BikePic.vue'
import LoginOrRegisterForm from '@/components/User/LoginOrRegisterForm.vue'
import LoginOrRegisterForm from '@/components/User/UserAuthForm.vue'
export default defineComponent({
name: 'NavBar',

View File

@ -0,0 +1,64 @@
<template>
<div id="password-reset">
<div class="container">
<PasswordResetRequest
v-if="action.startsWith('reset')"
:action="action"
:token="token"
/>
<PasswordEmailSent v-else :action="action" />
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PasswordEmailSent from '@/components/User/PasswordReset/PasswordActionDone.vue'
import PasswordResetRequest from '@/components/User/PasswordReset/PasswordResetForm.vue'
export default defineComponent({
name: 'PasswordResetView',
components: {
PasswordEmailSent,
PasswordResetRequest,
},
props: {
action: {
type: String,
required: true,
},
},
setup(props) {
const route = useRoute()
const router = useRouter()
const token = computed(() => route.query.token)
onBeforeMount(() => {
if (props.action === 'reset' && !token.value) {
router.push('/')
}
})
return { token }
},
})
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
#password-reset {
display: flex;
.container {
display: flex;
justify-content: center;
width: 50%;
@media screen and (max-width: $small-limit) {
width: 100%;
margin: 0 auto 50px auto;
}
}
}
</style>

View File

@ -11,7 +11,7 @@
aria-hidden="true"
/>
<span>
{{ t(`workouts.${hiddenFilters ? 'DISPLAY' : 'HIDE'}_FILTERS`) }}
{{ $t(`workouts.${hiddenFilters ? 'DISPLAY' : 'HIDE'}_FILTERS`) }}
</span>
</div>
</div>
@ -71,7 +71,6 @@
authUser,
hiddenFilters,
params,
t,
translatedSports,
toggleFilters,
updateParams,

View File

@ -976,6 +976,18 @@
"@intlify/shared" "9.1.7"
"@intlify/vue-devtools" "9.1.7"
"@intlify/core-base@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.1.9.tgz#e4e8c951010728e4af3a0d13d74cf3f9e7add7f6"
integrity sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==
dependencies:
"@intlify/devtools-if" "9.1.9"
"@intlify/message-compiler" "9.1.9"
"@intlify/message-resolver" "9.1.9"
"@intlify/runtime" "9.1.9"
"@intlify/shared" "9.1.9"
"@intlify/vue-devtools" "9.1.9"
"@intlify/core@^9.1.6":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/core/-/core-9.1.7.tgz#69c00dc31111f1b61d79fbd9ad1838196e73c94a"
@ -990,6 +1002,13 @@
dependencies:
"@intlify/shared" "9.1.7"
"@intlify/devtools-if@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.1.9.tgz#a30e1dd1256ff2c5c98d8d75d075384fba898e5d"
integrity sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==
dependencies:
"@intlify/shared" "9.1.9"
"@intlify/message-compiler@9.1.7", "@intlify/message-compiler@^9.1.6":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.1.7.tgz#4663fcc2a190f3cc6970e12565c8d6f22beeb719"
@ -999,11 +1018,25 @@
"@intlify/shared" "9.1.7"
source-map "0.6.1"
"@intlify/message-compiler@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.1.9.tgz#1193cbd224a71c2fb981455b8534a3c766d2948d"
integrity sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==
dependencies:
"@intlify/message-resolver" "9.1.9"
"@intlify/shared" "9.1.9"
source-map "0.6.1"
"@intlify/message-resolver@9.1.7":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/message-resolver/-/message-resolver-9.1.7.tgz#a95d13866c8de85784358039c8845668152e4162"
integrity sha512-WTK+OaXJYjyquLGhuCyDvU2WHkG+kXzXeHagmVFHn+s118Jf2143zzkLLUrapP5CtZ/csuyjmYg7b3xQRQAmvw==
"@intlify/message-resolver@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/message-resolver/-/message-resolver-9.1.9.tgz#3155ccd2f5e6d0dc16cad8b7f1d8e97fcda05bfc"
integrity sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==
"@intlify/runtime@9.1.7":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/runtime/-/runtime-9.1.7.tgz#67e0d6b2fd85a5b0b301a151c2f436f93154c3c6"
@ -1013,11 +1046,25 @@
"@intlify/message-resolver" "9.1.7"
"@intlify/shared" "9.1.7"
"@intlify/runtime@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/runtime/-/runtime-9.1.9.tgz#2c12ce29518a075629efed0a8ed293ee740cb285"
integrity sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==
dependencies:
"@intlify/message-compiler" "9.1.9"
"@intlify/message-resolver" "9.1.9"
"@intlify/shared" "9.1.9"
"@intlify/shared@9.1.7", "@intlify/shared@^9.1.6":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.1.7.tgz#e7d8bc90cb59dc17dd7b4c85a73db16fcb7891fc"
integrity sha512-zt0zlUdalumvT9AjQNxPXA36UgOndUyvBMplh8uRZU0fhWHAwhnJTcf0NaG9Qvr8I1n3HPSs96+kLb/YdwTavQ==
"@intlify/shared@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.1.9.tgz#0baaf96128b85560666bec784ffb01f6623cc17a"
integrity sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==
"@intlify/vue-devtools@9.1.7":
version "9.1.7"
resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.1.7.tgz#b08d39bb5f21ba9b1954eab9466e9408129425a7"
@ -1027,6 +1074,15 @@
"@intlify/runtime" "9.1.7"
"@intlify/shared" "9.1.7"
"@intlify/vue-devtools@9.1.9":
version "9.1.9"
resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz#2be8f4dbe7f7ed4115676eb32348141d411e426b"
integrity sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==
dependencies:
"@intlify/message-resolver" "9.1.9"
"@intlify/runtime" "9.1.9"
"@intlify/shared" "9.1.9"
"@intlify/vue-i18n-loader@^2.1.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@intlify/vue-i18n-loader/-/vue-i18n-loader-2.1.2.tgz#91a0858e26275dfc2c9c27aef9883028cada45ae"
@ -9877,14 +9933,14 @@ vue-i18n@^8.17.0:
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.25.0.tgz#1037d9295fa2845a230b771de473481edb2cfc4c"
integrity sha512-ynhcL+PmTxuuSE1T10htiSXzjBozxYIE3ffbM1RfgAkVbr/v1SP+9Mi/7/uv8ZVV1yGuKjFAYp9BXq+X7op6MQ==
vue-i18n@^9.1.0:
version "9.1.7"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.1.7.tgz#6f28dd2135197066508e2e65ab204a019750d773"
integrity sha512-ujuuDanoHqtEd4GejWrbG/fXE9nrP51ElsEGxp0WBHfv+/ki0/wyUqkO+4fLikki2obGtXdviTPH0VNpas5K6g==
vue-i18n@^9.1.9:
version "9.1.9"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.1.9.tgz#cb53e06ab5cc5b7eed59332f151caf48d47be9bb"
integrity sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==
dependencies:
"@intlify/core-base" "9.1.7"
"@intlify/shared" "9.1.7"
"@intlify/vue-devtools" "9.1.7"
"@intlify/core-base" "9.1.9"
"@intlify/shared" "9.1.9"
"@intlify/vue-devtools" "9.1.9"
"@vue/devtools-api" "^6.0.0-beta.7"
"vue-loader-v16@npm:vue-loader@^16.1.0":