Client - add user sports preferences

+ minor refactor
This commit is contained in:
Sam 2021-11-12 18:52:08 +01:00
parent 7afdd04d7d
commit 7c49fd31ad
67 changed files with 500 additions and 101 deletions

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.04e24276.css" rel="prefetch"><link href="/static/css/main.c790adb1.css" rel="prefetch"><link href="/static/css/main~workouts.66c5ef05.css" rel="prefetch"><link href="/static/css/profile.b52bc193.css" rel="prefetch"><link href="/static/css/reset.bd9657a8.css" rel="prefetch"><link href="/static/css/workouts.d0767062.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.e5da50b8.js" rel="prefetch"><link href="/static/js/main~workouts.a74990d7.js" rel="prefetch"><link href="/static/js/profile.6a786c1d.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.1c22fd12.js" rel="prefetch"><link href="/static/css/app.534b9c5c.css" rel="preload" as="style"><link href="/static/js/app.9ada5ac5.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.71654064.js" rel="preload" as="script"><link href="/static/css/app.534b9c5c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.71654064.js"></script><script src="/static/js/app.9ada5ac5.js"></script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.dc8b6d66.css" rel="prefetch"><link href="/static/css/main.411e7bd3.css" rel="prefetch"><link href="/static/css/main~workouts.c8c5694b.css" rel="prefetch"><link href="/static/css/profile.314b1d45.css" rel="prefetch"><link href="/static/css/reset.a71577d5.css" rel="prefetch"><link href="/static/css/workouts.773dfff0.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.88fa3c28.js" rel="prefetch"><link href="/static/js/main~workouts.a74990d7.js" rel="prefetch"><link href="/static/js/profile.62578012.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.46dd8fa5.js" rel="prefetch"><link href="/static/css/app.97115085.css" rel="preload" as="style"><link href="/static/js/app.38e0c4d5.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.71654064.js" rel="preload" as="script"><link href="/static/css/app.97115085.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.71654064.js"></script><script src="/static/js/app.38e0c4d5.js"></script></body></html>

@ -64,7 +64,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/img/workouts/mountains.svg"
},
{
"revision": "04e22b3a72bc3a810319eb7a512e4b23",
"revision": "59c5cdf1d1708e7f936a0a30db0bbffb",
"url": "/index.html"
},
{
@ -76,12 +76,12 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/robots.txt"
},
{
"revision": "ca5244fc3bcfc65816b9",
"url": "/static/css/admin.04e24276.css"
"revision": "55e1f50bd31cac2908e3",
"url": "/static/css/admin.dc8b6d66.css"
},
{
"revision": "fe2e967e1efad7b13e67",
"url": "/static/css/app.534b9c5c.css"
"revision": "9ae7710525db019efc86",
"url": "/static/css/app.97115085.css"
},
{
"revision": "82c1118c918377daaa71a320ab8eea42",
@ -92,24 +92,24 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/static/css/leaflet.css"
},
{
"revision": "d3cf46cfc6340753d540",
"url": "/static/css/main.c790adb1.css"
"revision": "12cfbc42bf674769c6f0",
"url": "/static/css/main.411e7bd3.css"
},
{
"revision": "68364924c988a1f11b42",
"url": "/static/css/main~workouts.66c5ef05.css"
"revision": "ce60ed388b792b0e9a0e",
"url": "/static/css/main~workouts.c8c5694b.css"
},
{
"revision": "3438ac3f32223591afd9",
"url": "/static/css/profile.b52bc193.css"
"revision": "74137feddeb35e2de067",
"url": "/static/css/profile.314b1d45.css"
},
{
"revision": "688d813785d3c55a7d33",
"url": "/static/css/reset.bd9657a8.css"
"revision": "6066a5f13daad652feea",
"url": "/static/css/reset.a71577d5.css"
},
{
"revision": "5e13fc66c78986a630a0",
"url": "/static/css/workouts.d0767062.css"
"revision": "9a0901331d45e214aa27",
"url": "/static/css/workouts.773dfff0.css"
},
{
"revision": "e719f9244c69e28e7d00e725ca1e280e",
@ -192,12 +192,12 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/static/img/pt-sans-v9-latin-regular.f1f73e45.svg"
},
{
"revision": "ca5244fc3bcfc65816b9",
"revision": "55e1f50bd31cac2908e3",
"url": "/static/js/admin.2f1d393d.js"
},
{
"revision": "fe2e967e1efad7b13e67",
"url": "/static/js/app.9ada5ac5.js"
"revision": "9ae7710525db019efc86",
"url": "/static/js/app.38e0c4d5.js"
},
{
"revision": "bd7d183c9f68e5f4027d",
@ -224,23 +224,23 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
"url": "/static/js/chunk-vendors.71654064.js"
},
{
"revision": "d3cf46cfc6340753d540",
"url": "/static/js/main.e5da50b8.js"
"revision": "12cfbc42bf674769c6f0",
"url": "/static/js/main.88fa3c28.js"
},
{
"revision": "68364924c988a1f11b42",
"revision": "ce60ed388b792b0e9a0e",
"url": "/static/js/main~workouts.a74990d7.js"
},
{
"revision": "3438ac3f32223591afd9",
"url": "/static/js/profile.6a786c1d.js"
"revision": "74137feddeb35e2de067",
"url": "/static/js/profile.62578012.js"
},
{
"revision": "688d813785d3c55a7d33",
"revision": "6066a5f13daad652feea",
"url": "/static/js/reset.518e646f.js"
},
{
"revision": "5e13fc66c78986a630a0",
"url": "/static/js/workouts.1c22fd12.js"
"revision": "9a0901331d45e214aa27",
"url": "/static/js/workouts.46dd8fa5.js"
}
]);

@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"/precache-manifest.200309f56fa439f700e940d169066203.js"
"/precache-manifest.94b00578e2690280739258ebb12c465f.js"
);
workbox.core.setCacheNameDetails({prefix: "fittrackee_client"});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["profile"],{"0bb3":function(e,t,n){},"36e8":function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("dad5"),o=n("2906"),u=function(e){return Object(c["pushScopeId"])("data-v-d342b648"),e=e(),Object(c["popScopeId"])(),e},a={key:0,id:"profile",class:"container view"},s=u((function(){return Object(c["createElementVNode"])("div",{id:"bottom"},null,-1)})),d=Object(c["defineComponent"])({setup:function(e){var t=Object(o["a"])(),n=Object(c["computed"])((function(){return t.getters[r["a"].GETTERS.AUTH_USER_PROFILE]}));return function(e,t){var r=Object(c["resolveComponent"])("router-view");return Object(c["unref"])(n).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",a,[Object(c["createVNode"])(r,{user:Object(c["unref"])(n)},null,8,["user"]),s])):Object(c["createCommentVNode"])("",!0)}}}),b=(n("6171"),n("6b0d")),i=n.n(b);const f=i()(d,[["__scopeId","data-v-d342b648"]]);t["default"]=f},6171:function(e,t,n){"use strict";n("0bb3")},"9b98":function(e,t,n){"use strict";n("d332")},ad3d:function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("6c02"),o=n("3c44"),u=n("71a7"),a=n("dad5"),s=n("2906"),d={key:0,id:"user",class:"view"},b={class:"box"},i=Object(c["defineComponent"])({setup:function(e){var t=Object(r["c"])(),n=Object(s["a"])(),i=Object(c["computed"])((function(){return n.getters[a["e"].GETTERS.USER]}));return Object(c["onBeforeMount"])((function(){t.params.username&&"string"===typeof t.params.username&&n.dispatch(a["e"].ACTIONS.GET_USER,t.params.username)})),Object(c["onBeforeUnmount"])((function(){n.dispatch(a["e"].ACTIONS.EMPTY_USER)})),function(e,t){return Object(c["unref"])(i).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",d,[Object(c["createVNode"])(o["a"],{user:Object(c["unref"])(i)},null,8,["user"]),Object(c["createElementVNode"])("div",b,[Object(c["createVNode"])(u["a"],{user:Object(c["unref"])(i),"from-admin":!0},null,8,["user"])])])):Object(c["createCommentVNode"])("",!0)}}}),f=(n("9b98"),n("6b0d")),O=n.n(f);const p=O()(i,[["__scopeId","data-v-218f8f1e"]]);t["default"]=p},d332:function(e,t,n){}}]);
//# sourceMappingURL=profile.62578012.js.map

File diff suppressed because one or more lines are too long

@ -1,2 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["profile"],{"36e8":function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("dad5"),o=n("2906"),u={key:0,id:"profile",class:"container view"},a=Object(c["defineComponent"])({setup:function(e){var t=Object(o["a"])(),n=Object(c["computed"])((function(){return t.getters[r["a"].GETTERS.AUTH_USER_PROFILE]}));return function(e,t){var r=Object(c["resolveComponent"])("router-view");return Object(c["unref"])(n).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",u,[Object(c["createVNode"])(r,{user:Object(c["unref"])(n)},null,8,["user"])])):Object(c["createCommentVNode"])("",!0)}}}),s=(n("44ab"),n("6b0d")),b=n.n(s);const i=b()(a,[["__scopeId","data-v-bb090bfa"]]);t["default"]=i},"44ab":function(e,t,n){"use strict";n("4fe6")},"4fe6":function(e,t,n){},"9b98":function(e,t,n){"use strict";n("d332")},ad3d:function(e,t,n){"use strict";n.r(t);var c=n("7a23"),r=n("6c02"),o=n("3c44"),u=n("71a7"),a=n("dad5"),s=n("2906"),b={key:0,id:"user",class:"view"},i={class:"box"},d=Object(c["defineComponent"])({setup:function(e){var t=Object(r["c"])(),n=Object(s["a"])(),d=Object(c["computed"])((function(){return n.getters[a["e"].GETTERS.USER]}));return Object(c["onBeforeMount"])((function(){t.params.username&&"string"===typeof t.params.username&&n.dispatch(a["e"].ACTIONS.GET_USER,t.params.username)})),Object(c["onBeforeUnmount"])((function(){n.dispatch(a["e"].ACTIONS.EMPTY_USER)})),function(e,t){return Object(c["unref"])(d).username?(Object(c["openBlock"])(),Object(c["createElementBlock"])("div",b,[Object(c["createVNode"])(o["a"],{user:Object(c["unref"])(d)},null,8,["user"]),Object(c["createElementVNode"])("div",i,[Object(c["createVNode"])(u["a"],{user:Object(c["unref"])(d),"from-admin":!0},null,8,["user"])])])):Object(c["createCommentVNode"])("",!0)}}}),f=(n("9b98"),n("6b0d")),O=n.n(f);const j=O()(d,[["__scopeId","data-v-218f8f1e"]]);t["default"]=j},d332:function(e,t,n){}}]);
//# sourceMappingURL=profile.6a786c1d.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -35,6 +35,7 @@
<SportImage
:title="sport.translatedLabel"
:sport-label="sport.label"
:color="sport.color"
/>
</td>
<td class="sport-label">
@ -127,9 +128,6 @@
font-style: italic;
padding: 0 $default-padding;
}
.text-left {
text-align: left;
}
.sport-action {
padding-left: $default-padding * 4;
}

@ -1,7 +1,7 @@
<template>
<div
class="sport-img"
:style="{ fill: sportColors[sportLabel] }"
:style="{ fill: color ? color : sportColors[sportLabel] }"
:title="title ? title : $t(`sports.${sportLabel}.LABEL`)"
>
<CyclingSport v-if="sportLabel === 'Cycling (Sport)'" />
@ -37,12 +37,13 @@
interface Props {
sportLabel: string
color: string | null
title?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '',
})
const { sportLabel, title } = toRefs(props)
const { color, sportLabel, title } = toRefs(props)
const sportColors = inject('sportColors')
</script>

@ -5,7 +5,11 @@
$router.push({ name: 'Workout', params: { workoutId: workout.id } })
"
>
<SportImage :sport-label="sportLabel" :title="workout.title" />
<SportImage
:sport-label="sportLabel"
:title="workout.title"
:color="sportColor"
/>
<sup>
<i
v-if="workout.records.length > 0"
@ -28,6 +32,7 @@
interface Props {
workout: IWorkout
sportLabel: string
sportColor: string | null
}
const props = defineProps<Props>()

@ -10,6 +10,7 @@
:key="index"
:workout="workout"
:sportLabel="getSportLabel(workout, sports)"
:sportColor="getSportColor(workout, sports)"
/>
</div>
<div v-else class="donut-display">
@ -41,7 +42,7 @@
import CalendarWorkoutsChart from '@/components/Dashboard/UserCalendar/CalendarWorkoutsChart.vue'
import { ISport } from '@/types/sports'
import { IWorkout } from '@/types/workouts'
import { getSportLabel, sportIdColors } from '@/utils/sports'
import { getSportColor, getSportLabel, sportIdColors } from '@/utils/sports'
import { getDonutDatasets } from '@/utils/workouts'
interface Props {

@ -2,7 +2,7 @@
<div class="records-card">
<Card>
<template #title>
<SportImage :sport-label="records.label" />
<SportImage :sport-label="records.label" :color="records.color" />
{{ sportTranslatedLabel }}
</template>
<template #content>

@ -13,7 +13,7 @@
:checked="selectedSportIds.includes(sport.id)"
@input="updateSelectedSportIds(sport.id)"
/>
<SportImage :sport-label="sport.label" />
<SportImage :sport-label="sport.label" :color="sport.color" />
<span class="sport-label">{{ sport.translatedLabel }}</span>
</label>
</div>

@ -91,9 +91,5 @@
.user-bio {
white-space: pre-wrap;
}
.profile-buttons {
display: flex;
gap: $default-padding;
}
}
</style>

@ -38,13 +38,3 @@
props.user.timezone ? props.user.timezone : 'Europe/Paris'
)
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
#user-preferences {
.profile-buttons {
display: flex;
gap: $default-padding;
}
}
</style>

@ -22,7 +22,7 @@
const props = defineProps<Props>()
const { user, tab } = toRefs(props)
const tabs = ['PROFILE', 'PREFERENCES']
const tabs = ['PROFILE', 'PREFERENCES', 'SPORTS']
</script>
<style lang="scss" scoped>

@ -34,7 +34,7 @@
const store = useStore()
const { user, tab } = toRefs(props)
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES']
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES', 'SPORTS']
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)

@ -38,7 +38,10 @@
case 'PICTURE':
return '/profile/edit/picture'
case 'PREFERENCES':
return `/profile${props.edition ? '/edit' : ''}/preferences`
case 'SPORTS':
return `/profile${
props.edition ? '/edit' : ''
}/${tab.toLocaleLowerCase()}`
default:
case 'PROFILE':
return `/profile${props.edition ? '/edit' : ''}`

@ -0,0 +1,280 @@
<template>
<div id="user-sport-preferences">
<div class="responsive-table" v-if="sports.length > 0">
<div class="mobile-display">
<div v-if="isEdition" class="profile-buttons mobile-display">
<button
class="cancel"
@click.prevent="$router.push('/profile/sports')"
>
{{ $t('buttons.BACK') }}
</button>
</div>
<div v-else class="profile-buttons">
<button @click="$router.push('/profile/edit/sports')">
{{ $t('user.PROFILE.EDIT_SPORTS_PREFERENCES') }}
</button>
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
</div>
</div>
<table>
<thead>
<tr>
<th>{{ $t('user.PROFILE.SPORT.COLOR') }}</th>
<th class="text-left">{{ $t('workouts.SPORT', 0) }}</th>
<th>{{ $t('user.PROFILE.SPORT.IS_ACTIVE') }}</th>
<th>{{ $t('user.PROFILE.SPORT.STOPPED_SPEED_THRESHOLD') }}</th>
<th v-if="isEdition">{{ $t('user.PROFILE.SPORT.ACTION') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="sport in translatedSports" :key="sport.id">
<td>
<span class="cell-heading">
{{ $t('user.PROFILE.SPORT.COLOR') }}
</span>
<input
v-if="isSportInEdition(sport.id)"
class="sport-color"
type="color"
:value="sportPayload.color"
@input="updateColor"
/>
<SportImage
v-else
:title="sport.translatedLabel"
:sport-label="sport.label"
:color="sport.color ? sport.color : sportColors[sport.label]"
/>
</td>
<td class="sport-label">
<span class="cell-heading">
{{ $t('user.PROFILE.SPORT.LABEL') }}
</span>
{{ sport.translatedLabel }}
<i
v-if="loading && isSportInEdition(sport.id)"
class="fa fa-refresh fa-spin fa-fw"
/>
<ErrorMessage
:message="errorMessages"
v-if="errorMessages && sportPayload.sport_id === sport.id"
/>
</td>
<td class="text-center">
<span class="cell-heading">
{{ $t('user.PROFILE.SPORT.IS_ACTIVE') }}
</span>
<input
v-if="isSportInEdition(sport.id)"
type="checkbox"
:checked="sport.is_active_for_user"
@change="updateIsActive"
/>
<i
v-else
:class="`fa fa${
sport.is_active_for_user ? '-check' : ''
}-square-o`"
aria-hidden="true"
/>
</td>
<td class="text-center">
<span class="cell-heading">
{{ $t('user.PROFILE.SPORT.STOPPED_SPEED_THRESHOLD') }}
</span>
<input
class="threshold-input"
v-if="isSportInEdition(sport.id)"
type="number"
min="0"
step="0.1"
:value="sportPayload.stopped_speed_threshold"
@input="updateThreshold"
/>
<span v-else>
{{ sport.stopped_speed_threshold }}
</span>
</td>
<td v-if="isEdition" class="action-buttons">
<span class="cell-heading">
{{ $t('user.PROFILE.SPORT.ACTION') }}
</span>
<button
v-if="sportPayload.sport_id === 0"
@click="updateSportInEdition(sport)"
>
{{ $t('buttons.EDIT') }}
</button>
<div v-if="isSportInEdition(sport.id)" class="edition-buttons">
<button :disabled="loading" @click="updateSport">
{{ $t('buttons.SUBMIT') }}
</button>
<button :disabled="loading" @click="updateSportInEdition(null)">
{{ $t('buttons.CANCEL') }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="isEdition" class="profile-buttons">
<button class="cancel" @click.prevent="$router.push('/profile/sports')">
{{ $t('buttons.BACK') }}
</button>
</div>
<div v-else class="profile-buttons">
<button @click="$router.push('/profile/edit/sports')">
{{ $t('user.PROFILE.EDIT_SPORTS_PREFERENCES') }}
</button>
<button @click="$router.push('/')">{{ $t('common.HOME') }}</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ComputedRef, computed, inject, reactive, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { AUTH_USER_STORE, ROOT_STORE, SPORTS_STORE } from '@/store/constants'
import { ISport, ITranslatedSport } from '@/types/sports'
import { IUserSportPreferencesPayload } from '@/types/user'
import { useStore } from '@/use/useStore'
import { translateSports } from '@/utils/sports'
interface Props {
isEdition: boolean
}
const props = defineProps<Props>()
const store = useStore()
const { t } = useI18n()
const { isEdition } = toRefs(props)
const sportColors = inject('sportColors')
const sports: ComputedRef<ISport[]> = computed(
() => store.getters[SPORTS_STORE.GETTERS.SPORTS]
)
const translatedSports: ComputedRef<ITranslatedSport[]> = computed(() =>
translateSports(sports.value, t)
)
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const sportPayload: IUserSportPreferencesPayload = reactive({
sport_id: 0,
color: null,
is_active: true,
stopped_speed_threshold: 1,
})
function updateSportInEdition(sport: ISport | null) {
if (sport !== null) {
sportPayload.sport_id = sport.id
sportPayload.color = sport.color ? sport.color : sportColors[sport.label]
sportPayload.is_active = sport.is_active_for_user
sportPayload.stopped_speed_threshold = sport.stopped_speed_threshold
} else {
resetSportPayload()
}
}
function isSportInEdition(sportId: number) {
return sportPayload.sport_id === sportId
}
function updateColor(event: Event & { target: HTMLInputElement }) {
sportPayload.color = event.target.value
}
function updateThreshold(event: Event & { target: HTMLInputElement }) {
sportPayload.stopped_speed_threshold = parseFloat(event.target.value)
}
function updateIsActive(event: Event & { target: HTMLInputElement }) {
sportPayload.is_active = event.target.checked
}
function resetSportPayload() {
sportPayload.sport_id = 0
sportPayload.color = null
sportPayload.is_active = true
sportPayload.stopped_speed_threshold = 1
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
}
function updateSport(event: Event) {
event.preventDefault()
store.dispatch(
AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES,
sportPayload
)
}
watch(
() => loading.value,
(newIsLoading) => {
if (!newIsLoading && !errorMessages.value) {
resetSportPayload()
}
}
)
</script>
<style lang="scss" scoped>
@import '~@/scss/base.scss';
#user-sport-preferences {
.sport-img {
height: 35px;
width: 35px;
margin: 0 auto;
}
.sport-color {
border: none;
margin: 6px 1px 6px 0;
padding: 0;
width: 40px;
}
.sport-label {
width: 170px;
}
.action-buttons {
width: 70px;
}
.edition-buttons {
display: flex;
flex-wrap: wrap;
gap: $default-padding * 0.5;
line-height: 1.3em;
button {
text-align: center;
min-width: 80px;
}
}
.threshold-input {
padding: $default-padding * 0.5;
width: 50px;
}
.mobile-display {
display: none;
}
div.error-message {
margin: 0;
}
@media screen and (max-width: $small-limit) {
.sport-label {
width: 100%;
}
.action-buttons {
width: 100%;
}
.edition-buttons {
justify-content: center;
}
.mobile-display {
display: flex;
margin: $default-margin * 2 0 $default-margin;
}
}
}
</style>

@ -75,7 +75,11 @@
"
>
<div class="img">
<SportImage v-if="sport.label" :sport-label="sport.label" />
<SportImage
v-if="sport.label"
:sport-label="sport.label"
:color="sport.color"
/>
</div>
<div class="data">
<i class="fa fa-clock-o" aria-hidden="true" />

@ -17,7 +17,7 @@
<i class="fa fa-chevron-left" aria-hidden="true" />
</div>
<div class="workout-card-title">
<SportImage :sport-label="sport.label" />
<SportImage :sport-label="sport.label" :color="sport.color" />
<div class="workout-title-date">
<div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
{{ workoutObject.title }}

@ -46,7 +46,7 @@
v-model="workoutForm.sport_id"
>
<option
v-for="sport in translatedSports.filter((s) => s.is_active)"
v-for="sport in translatedSports"
:value="sport.id"
:key="sport.id"
>
@ -259,7 +259,7 @@
const { workout, isCreation, loading } = toRefs(props)
const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t)
translateSports(props.sports, t, true)
)
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]

@ -52,6 +52,9 @@
:sport-label="
sports.filter((s) => s.id === workout.sport_id)[0].label
"
:color="
sports.filter((s) => s.id === workout.sport_id)[0].color
"
/>
</td>
<td

@ -23,6 +23,7 @@
"BIRTH_DATE": "Birth date",
"EDIT": "Edit profile",
"EDIT_PREFERENCES": "Edit preferences",
"EDIT_SPORTS_PREFERENCES": "Edit sports preferences",
"FIRST_NAME": "First name",
"FIRST_DAY_OF_WEEK": "First day of week",
"LANGUAGE": "Language",
@ -36,11 +37,20 @@
"PREFERENCES_EDITION": "Preferences edition",
"PROFILE_EDITION": "Profile edition",
"REGISTRATION_DATE": "Registration date",
"SPORTS_EDITION": "Sports preferences edition",
"SUNDAY": "Sunday",
"TABS": {
"PICTURE": "picture",
"PREFERENCES": "preferences",
"PROFILE": "profile"
"PROFILE": "profile",
"SPORTS": "sports"
},
"SPORT": {
"ACTION": "action",
"COLOR": "color",
"IS_ACTIVE": "active",
"LABEL": "label",
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
},
"TIMEZONE": "Timezone"
},

@ -23,6 +23,7 @@
"BIRTH_DATE": "Date de naissance",
"EDIT": "Modifier le profil",
"EDIT_PREFERENCES": "Modifier les préférences",
"EDIT_SPORTS_PREFERENCES": "Modifier les préférences des sports",
"FIRST_DAY_OF_WEEK": "Premier jour de la semaine",
"FIRST_NAME": "Prénom",
"LANGUAGE": "Langue",
@ -36,11 +37,20 @@
"PREFERENCES_EDITION": "Mise à jour des préférences",
"PROFILE_EDITION": "Mise à jour du profil",
"REGISTRATION_DATE": "Date d'inscription",
"SPORTS_EDITION": "Mise à jour des préférences des sports",
"SUNDAY": "Dimanche",
"TABS": {
"PICTURE": "image",
"PREFERENCES": "préférences",
"PROFILE": "profil"
"PROFILE": "profil",
"SPORTS": "sports"
},
"SPORT": {
"ACTION": "action",
"COLOR": "couleur",
"IS_ACTIVE": "actif",
"LABEL": "label",
"STOPPED_SPEED_THRESHOLD": "seuil de vitesse arrêtée"
},
"TIMEZONE": "Fuseau horaire"
},
@ -49,4 +59,4 @@
"RESET_PASSWORD": "Réinitialiser votre mot de passe",
"USER_PICTURE": "photo de l'utilisateur",
"USERNAME": "Nom d'utilisateur"
}
}

@ -11,6 +11,7 @@ import ProfileEdition from '@/components/User/ProfileEdition/index.vue'
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
import UserSportPreferences from '@/components/User/UserSportPreferences.vue'
import store from '@/store'
import { AUTH_USER_STORE } from '@/store/constants'
@ -101,6 +102,12 @@ const routes: Array<RouteRecordRaw> = [
name: 'UserPreferences',
component: UserPreferences,
},
{
path: 'sports',
name: 'UserSportPreferences',
component: UserSportPreferences,
props: { isEdition: false },
},
],
},
{
@ -126,6 +133,12 @@ const routes: Array<RouteRecordRaw> = [
name: 'UserPreferencesEdition',
component: UserPreferencesEdition,
},
{
path: 'sports',
name: 'UserSportPreferencesEdition',
component: UserSportPreferences,
props: { isEdition: true },
},
],
},
],

@ -254,9 +254,12 @@ button {
}
}
.center-text {
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.responsive-table {
margin-bottom: 15px;
@ -337,3 +340,14 @@ button {
}
}
}
.profile-buttons {
display: flex;
gap: $default-padding;
}
.medium-sport-img {
height: 35px;
width: 35px;
margin: 0 auto;
}

@ -25,6 +25,7 @@ import {
IUserPayload,
IUserPicturePayload,
IUserPreferencesPayload,
IUserSportPreferencesPayload,
} from '@/types/user'
import { handleError } from '@/utils'
@ -172,6 +173,26 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
)
},
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserSportPreferencesPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, true)
authApi
.post('auth/profile/edit/sports', payload)
.then((res) => {
if (res.data.status === 'success') {
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
} else {
handleError(context, null)
}
})
.catch((error) => {
handleError(context, error)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
})
},
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserPicturePayload

@ -10,6 +10,7 @@ export enum AuthUserActions {
UPDATE_USER_PICTURE = 'UPDATE_USER_PICTURE',
UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE',
UPDATE_USER_PREFERENCES = 'UPDATE_USER_PREFERENCES',
UPDATE_USER_SPORT_PREFERENCES = 'UPDATE_USER_SPORT_PREFERENCES',
}
export enum AuthUserGetters {

@ -16,6 +16,7 @@ import {
IUserPayload,
IUserPicturePayload,
IUserPreferencesPayload,
IUserSportPreferencesPayload,
} from '@/types/user'
export interface IAuthUserState {
@ -52,6 +53,11 @@ export interface IAuthUserActions {
payload: IUserPreferencesPayload
): void
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_SPORT_PREFERENCES](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserSportPreferencesPayload
): void
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PICTURE](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserPicturePayload

@ -1,7 +1,7 @@
import { ActionContext, ActionTree } from 'vuex'
import authApi from '@/api/authApi'
import { ROOT_STORE, SPORTS_STORE } from '@/store/constants'
import { AUTH_USER_STORE, ROOT_STORE, SPORTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types'
import { ISportsActions, ISportsState } from '@/store/modules/sports/types'
import { ISportPayload } from '@/types/sports'
@ -20,6 +20,7 @@ export const actions: ActionTree<ISportsState, IRootState> & ISportsActions = {
SPORTS_STORE.MUTATIONS.SET_SPORTS,
res.data.data.sports
)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
} else {
handleError(context, null)
}

@ -1,9 +1,12 @@
export interface ISport {
color: string | null
has_workouts: boolean
id: number
img: string
is_active: boolean
is_active_for_user: boolean
label: string
stopped_speed_threshold: number
}
export interface ITranslatedSport extends ISport {

@ -45,6 +45,13 @@ export interface IUserPreferencesPayload {
weekm: boolean
}
export interface IUserSportPreferencesPayload {
sport_id: number
color: string | null
is_active: boolean
stopped_speed_threshold: number
}
export interface IUserPicturePayload {
picture: File
}

@ -27,8 +27,9 @@ export interface IRecord {
}
export interface IRecordsBySport {
[key: string]: string | Record<string, string | number>[]
[key: string]: string | Record<string, string | number>[] | null
label: string
color: string | null
records: Record<string, string | number>[]
}

@ -44,6 +44,7 @@ export const getRecordsBySports = (
if (sportList[sport.translatedLabel] === void 0) {
sportList[sport.translatedLabel] = {
label: sport.label,
color: sport.color,
records: [],
}
}

@ -38,7 +38,7 @@ export const translateSports = (
onlyActive = false
): ITranslatedSport[] =>
sports
.filter((sport) => (onlyActive ? sport.is_active : true))
.filter((sport) => (onlyActive ? sport.is_active_for_user : true))
.map((sport) => ({
...sport,
translatedLabel: t(`sports.${sport.label}.LABEL`),
@ -50,3 +50,12 @@ export const getSportLabel = (workout: IWorkout, sports: ISport[]): string => {
.filter((sport) => sport.id === workout.sport_id)
.map((sport) => sport.label)[0]
}
export const getSportColor = (
workout: IWorkout,
sports: ISport[]
): string | null => {
return sports
.filter((sport) => sport.id === workout.sport_id)
.map((sport) => sport.color)[0]
}

@ -1,6 +1,7 @@
<template>
<div id="profile" class="container view" v-if="authUser.username">
<router-view :user="authUser"></router-view>
<div id="bottom" />
</div>
</template>

@ -5,25 +5,34 @@ import { translateSports } from '@/utils/sports'
const { t } = createI18n.global
export const sports: ISport[] = [
{
color: null,
has_workouts: false,
id: 1,
img: '/img/sports/cycling-sport.png',
is_active: true,
is_active_for_user: true,
label: 'Cycling (Sport)',
stopped_speed_threshold: 1,
},
{
color: '#000000',
has_workouts: false,
id: 2,
img: '/img/sports/cycling-transport.png',
is_active: false,
is_active_for_user: false,
label: 'Cycling (Transport)',
stopped_speed_threshold: 1,
},
{
color: null,
has_workouts: true,
id: 3,
img: '/img/sports/hiking.png',
is_active: true,
is_active_for_user: false,
label: 'Hiking',
stopped_speed_threshold: 0.1,
},
]

@ -157,6 +157,7 @@ describe('getRecordsBySports', () => {
},
expected: {
'Cycling (Sport)': {
color: null,
label: 'Cycling (Sport)',
records: [
{
@ -206,6 +207,7 @@ describe('getRecordsBySports', () => {
},
expected: {
'Cycling (Sport)': {
color: null,
label: 'Cycling (Sport)',
records: [
{
@ -225,6 +227,7 @@ describe('getRecordsBySports', () => {
],
},
'Cycling (Transport)': {
color: '#000000',
label: 'Cycling (Transport)',
records: [
{

@ -18,27 +18,36 @@ describe('sortSports', () => {
},
expected: [
{
color: null,
has_workouts: false,
id: 1,
img: '/img/sports/cycling-sport.png',
is_active: true,
is_active_for_user: true,
label: 'Cycling (Sport)',
stopped_speed_threshold: 1,
translatedLabel: 'Cycling (Sport)',
},
{
color: '#000000',
has_workouts: false,
id: 2,
img: '/img/sports/cycling-transport.png',
is_active: false,
is_active_for_user: false,
label: 'Cycling (Transport)',
stopped_speed_threshold: 1,
translatedLabel: 'Cycling (Transport)',
},
{
color: null,
has_workouts: true,
id: 3,
img: '/img/sports/hiking.png',
is_active: true,
is_active_for_user: false,
label: 'Hiking',
stopped_speed_threshold: 0.1,
translatedLabel: 'Hiking',
},
],
@ -53,21 +62,16 @@ describe('sortSports', () => {
},
expected: [
{
color: null,
has_workouts: false,
id: 1,
img: '/img/sports/cycling-sport.png',
is_active: true,
is_active_for_user: true,
label: 'Cycling (Sport)',
stopped_speed_threshold: 1,
translatedLabel: 'Cycling (Sport)',
},
{
has_workouts: true,
id: 3,
img: '/img/sports/hiking.png',
is_active: true,
label: 'Hiking',
translatedLabel: 'Hiking',
},
],
},
{
@ -88,27 +92,36 @@ describe('sortSports', () => {
},
expected: [
{
color: null,
has_workouts: true,
id: 3,
img: '/img/sports/hiking.png',
is_active: true,
is_active_for_user: false,
label: 'Hiking',
stopped_speed_threshold: 0.1,
translatedLabel: 'Randonnée',
},
{
color: null,
has_workouts: false,
id: 1,
img: '/img/sports/cycling-sport.png',
is_active: true,
is_active_for_user: true,
label: 'Cycling (Sport)',
stopped_speed_threshold: 1,
translatedLabel: 'Vélo (Sport)',
},
{
color: '#000000',
has_workouts: false,
id: 2,
img: '/img/sports/cycling-transport.png',
is_active: false,
is_active_for_user: false,
label: 'Cycling (Transport)',
stopped_speed_threshold: 1,
translatedLabel: 'Vélo (Transport)',
},
],
@ -123,19 +136,14 @@ describe('sortSports', () => {
},
expected: [
{
has_workouts: true,
id: 3,
img: '/img/sports/hiking.png',
is_active: true,
label: 'Hiking',
translatedLabel: 'Randonnée',
},
{
color: null,
has_workouts: false,
id: 1,
img: '/img/sports/cycling-sport.png',
is_active: true,
is_active_for_user: true,
label: 'Cycling (Sport)',
stopped_speed_threshold: 1,
translatedLabel: 'Vélo (Sport)',
},
],