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

View File

@ -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>

View File

@ -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"
}
]);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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>

View File

@ -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>()

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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]
)

View File

@ -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' : ''}`

View File

@ -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>

View File

@ -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" />

View File

@ -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 }}

View File

@ -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]

View File

@ -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

View File

@ -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"
},

View File

@ -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"
}
}

View File

@ -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 },
},
],
},
],

View File

@ -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;
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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
}

View 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>[]
}

View File

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

View File

@ -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]
}

View File

@ -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>

View File

@ -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,
},
]

View File

@ -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: [
{

View File

@ -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)',
},
],