Merge pull request #392 from SamR1/improve-ui

Improve UI
This commit is contained in:
Sam 2023-07-14 15:39:48 +02:00 committed by GitHub
commit 675561d654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 749 additions and 274 deletions

View File

@ -22,7 +22,7 @@ class TestLogin:
assert inputs[1].get_attribute('id') == 'password'
assert inputs[1].get_attribute('type') == 'password'
button = selenium.find_element(By.TAG_NAME, 'button')
button = selenium.find_elements(By.TAG_NAME, 'button')[-1]
assert button.get_attribute('type') == 'submit'
assert 'Log in' in button.text
@ -47,4 +47,7 @@ class TestLogin:
assert 'Statistics' in nav
assert 'Add a workout' in nav
assert user['username'] in nav
assert 'Logout' in nav
logout_button = selenium.find_elements(By.CLASS_NAME, 'logout-button')[
0
]
assert logout_button

View File

@ -6,14 +6,19 @@ from .utils import register_valid_user
class TestLogout:
def test_user_can_log_out(self, selenium):
user = register_valid_user(selenium)
user_menu = selenium.find_element(By.CLASS_NAME, 'nav-items-user-menu')
logout_link = user_menu.find_elements(By.CLASS_NAME, 'nav-item')[2]
logout_button = selenium.find_elements(By.CLASS_NAME, 'logout-button')[
0
]
logout_link.click()
logout_button.click()
modal = selenium.find_element(By.ID, 'modal')
confirm_button = modal.find_elements(By.CLASS_NAME, 'confirm')[0]
confirm_button.click()
selenium.implicitly_wait(1)
nav = selenium.find_element(By.ID, 'nav').text
assert 'Register' in nav
assert 'Login' in nav
assert user['username'] not in nav
assert 'Logout' not in nav
buttons = selenium.find_elements(By.CLASS_NAME, 'logout-button')
assert buttons == []

View File

@ -35,7 +35,7 @@ class TestRegistration:
assert form_infos[1].text == 'Enter a valid email address.'
assert form_infos[2].text == 'At least 8 characters required.'
button = selenium.find_element(By.TAG_NAME, 'button')
button = selenium.find_elements(By.TAG_NAME, 'button')[-1]
assert button.get_attribute('type') == 'submit'
assert 'Register' in button.text
@ -84,7 +84,11 @@ class TestRegistration:
register(selenium, user)
assert selenium.current_url == URL
errors = selenium.find_element(By.CLASS_NAME, 'error-message').text
errors = (
selenium.find_element(By.ID, 'user-form')
.find_element(By.CLASS_NAME, 'error-message')
.text
)
assert 'Sorry, that username is already taken.' in errors
def test_user_does_not_return_error_if_email_is_already_taken(

View File

@ -30,7 +30,7 @@ class TestWorkout:
)
selenium.find_element(By.NAME, 'workout-distance').send_keys('10')
confirm_button = selenium.find_element(By.CLASS_NAME, 'confirm')
confirm_button = selenium.find_elements(By.CLASS_NAME, 'confirm')[-1]
confirm_button.click()
WebDriverWait(selenium, 10).until(

View File

@ -34,7 +34,7 @@ def register(selenium, user):
password.send_keys(user.get('password'))
accepted_policy = selenium.find_element(By.ID, 'accepted_policy')
accepted_policy.click()
submit_button = selenium.find_element(By.TAG_NAME, 'button')
submit_button = selenium.find_elements(By.TAG_NAME, 'button')[-1]
submit_button.click()
@ -45,7 +45,7 @@ def login(selenium, user):
email.send_keys(user.get('email'))
password = selenium.find_element(By.ID, 'password')
password.send_keys(user.get('password'))
submit_button = selenium.find_element(By.TAG_NAME, 'button')
submit_button = selenium.find_elements(By.TAG_NAME, 'button')[-1]
submit_button.click()
@ -65,8 +65,12 @@ def register_valid_user(selenium):
def register_valid_user_and_logout(selenium):
user = register_valid_user(selenium)
user_menu = selenium.find_element(By.CLASS_NAME, 'nav-items-user-menu')
logout_link = user_menu.find_elements(By.CLASS_NAME, 'nav-item')[2]
logout_link.click()
logout_button = user_menu.find_elements(By.CLASS_NAME, 'logout-button')[0]
logout_button.click()
modal = selenium.find_element(By.ID, 'modal')
confirm_button = modal.find_elements(By.CLASS_NAME, 'confirm')[0]
confirm_button.click()
selenium.implicitly_wait(1)
return user

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><script defer="defer" src="/static/js/chunk-vendors.b1590c25.js"></script><script defer="defer" src="/static/js/app.d394b3e5.js"></script><link href="/static/css/app.ce91f57c.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></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><script defer="defer" src="/static/js/chunk-vendors.1d7b137c.js"></script><script defer="defer" src="/static/js/app.6bf88dea.js"></script><link href="/static/css/app.33d241fd.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></body></html>

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

@ -1 +0,0 @@
.chart-menu[data-v-22d55de2]{display:flex}.chart-menu .chart-arrow[data-v-22d55de2],.chart-menu .time-frames[data-v-22d55de2]{flex-grow:1;text-align:center}.chart-menu .chart-arrow[data-v-22d55de2]{cursor:pointer}.sports-menu{display:flex;flex-wrap:wrap;padding:10px}.sports-menu label{display:flex;align-items:center;font-size:.9em;font-weight:400;min-width:120px;padding:10px}@media screen and (max-width:1000px){.sports-menu label{min-width:100px}}@media screen and (max-width:500px){.sports-menu label{min-width:20px}.sports-menu label .sport-label{display:none}}.sports-menu .sport-img{padding:3px;width:20px;height:20px}#user-statistics.stats-disabled[data-v-30799d13]{opacity:.3;pointer-events:none}#user-statistics[data-v-30799d13] .chart-radio{justify-content:space-around;padding:30px 10px 10px 10px}#statistics[data-v-2e341d4e]{display:flex;width:100%}#statistics .container[data-v-2e341d4e]{display:flex;flex-direction:column;width:100%}

View File

@ -0,0 +1 @@
.chart-menu[data-v-361ef577]{display:flex;align-items:center}.chart-menu .chart-arrow[data-v-361ef577],.chart-menu .time-frames[data-v-361ef577]{flex-grow:1;text-align:center}.chart-menu .chart-arrow[data-v-361ef577]{cursor:pointer}.sports-menu{display:flex;flex-wrap:wrap;padding:10px}.sports-menu label{display:flex;align-items:center;font-size:.9em;font-weight:400;min-width:120px;padding:10px}@media screen and (max-width:1000px){.sports-menu label{min-width:100px}}@media screen and (max-width:500px){.sports-menu label{min-width:20px}.sports-menu label .sport-label{display:none}}.sports-menu .sport-img{padding:3px;width:20px;height:20px}#user-statistics.stats-disabled[data-v-47c262da]{opacity:.3;pointer-events:none}#user-statistics[data-v-47c262da] .chart-radio{justify-content:space-around;padding:30px 10px 10px 10px}#statistics[data-v-19ce09a2]{display:flex;width:100%}#statistics .container[data-v-19ce09a2]{display:flex;flex-direction:column;width:100%}

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

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[845],{4264:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});r(7658);var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),c=r(5801),i=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"AccountConfirmationView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,i.o)(),d=(0,n.Fl)((()=>_.getters[c.SY.GETTERS.ERROR_MESSAGES])),S=(0,n.Fl)((()=>t.query.token));function m(){S.value?_.dispatch(c.YN.ACTIONS.CONFIRM_ACCOUNT,{token:S.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>_.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(d)?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n.Wm)(r,{class:"links",to:"/account-confirmation/resend"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("buttons.ACCOUNT-CONFIRMATION-RESEND"))+"? ",1)])),_:1})])])):(0,n.kq)("",!0)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-785df978"]]);var m=S},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});r(7658);var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),c=r(5801),i=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"EmailUpdateView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,i.o)(),d=(0,n.Fl)((()=>_.getters[c.YN.GETTERS.AUTH_USER_PROFILE])),S=(0,n.Fl)((()=>_.getters[c.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>_.getters[c.SY.GETTERS.ERROR_MESSAGES])),p=(0,n.Fl)((()=>t.query.token));function R(){p.value?_.dispatch(c.YN.ACTIONS.CONFIRM_EMAIL,{token:p.value,refreshUser:S.value}):r.push("/")}return(0,n.wF)((()=>R())),(0,n.Ah)((()=>_.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{d.value.username&&e&&r.push("/")})),(e,t)=>{const r=(0,n.up)("router-link"),u=(0,n.up)("i18n-t");return(0,a.SU)(m)&&!(0,a.SU)(d).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n._)("span",null,[(0,n.Wm)(u,{keypath:"user.PROFILE.ERRORED_EMAIL_UPDATE"},{default:(0,n.w5)((()=>[(0,n.Wm)(r,{to:"/login"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("user.LOG_IN")),1)])),_:1})])),_:1})])])])):(0,n.kq)("",!0)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-8c2ec9ce"]]);var m=S},3537:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(6252),a=r(2262),s=r(5801),u=r(9917);const o=e=>((0,n.dD)("data-v-0c3c0394"),e=e(),(0,n.Cn)(),e),c={key:0,id:"profile",class:"view"},i=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({__name:"ProfileView",setup(e){const t=(0,u.o)(),r=(0,n.Fl)((()=>t.getters[s.YN.GETTERS.AUTH_USER_PROFILE]));return(e,t)=>{const s=(0,n.up)("router-view");return(0,a.SU)(r).username?((0,n.wg)(),(0,n.iD)("div",c,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),i])):(0,n.kq)("",!0)}}}),E=r(3744);const _=(0,E.Z)(l,[["__scopeId","data-v-0c3c0394"]]);var d=_},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(2201),u=r(2179),o=r(1585),c=r(5801),i=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var _=(0,n.aZ)({__name:"UserView",props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),_=(0,s.yj)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.RT.GETTERS.USER]));return(0,n.wF)((()=>{_.params.username&&"string"===typeof _.params.username&&d.dispatch(c.RT.ACTIONS.GET_USER,_.params.username)})),(0,n.Jd)((()=>{d.dispatch(c.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(S)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(S),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-af7007f4"]]);var m=S}}]);
//# sourceMappingURL=profile.d4857496.js.map
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[845],{4264:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});r(7658);var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),c=r(5801),i=r(9917);const l={key:0,id:"account-confirmation",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"AccountConfirmationView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,i.o)(),d=(0,n.Fl)((()=>_.getters[c.SY.GETTERS.ERROR_MESSAGES])),S=(0,n.Fl)((()=>t.query.token));function m(){S.value?_.dispatch(c.YN.ACTIONS.CONFIRM_ACCOUNT,{token:S.value}):r.push("/")}return(0,n.wF)((()=>m())),(0,n.Ah)((()=>_.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(e,t)=>{const r=(0,n.up)("router-link");return(0,a.SU)(d)?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n.Wm)(r,{class:"links",to:"/account-confirmation/resend"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("buttons.ACCOUNT-CONFIRMATION-RESEND"))+"? ",1)])),_:1})])])):(0,n.kq)("",!0)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-785df978"]]);var m=S},8160:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});r(7658);var n=r(6252),a=r(2262),s=r(3577),u=r(2201),o=r(7167),c=r(5801),i=r(9917);const l={key:0,id:"email-update",class:"center-card with-margin"},E={class:"error-message"};var _=(0,n.aZ)({__name:"EmailUpdateView",setup(e){const t=(0,u.yj)(),r=(0,u.tv)(),_=(0,i.o)(),d=(0,n.Fl)((()=>_.getters[c.YN.GETTERS.AUTH_USER_PROFILE])),S=(0,n.Fl)((()=>_.getters[c.YN.GETTERS.IS_AUTHENTICATED])),m=(0,n.Fl)((()=>_.getters[c.SY.GETTERS.ERROR_MESSAGES])),p=(0,n.Fl)((()=>t.query.token));function R(){p.value?_.dispatch(c.YN.ACTIONS.CONFIRM_EMAIL,{token:p.value,refreshUser:S.value}):r.push("/")}return(0,n.wF)((()=>R())),(0,n.Ah)((()=>_.commit(c.SY.MUTATIONS.EMPTY_ERROR_MESSAGES))),(0,n.YP)((()=>m.value),(e=>{d.value.username&&e&&r.push("/")})),(e,t)=>{const r=(0,n.up)("router-link"),u=(0,n.up)("i18n-t");return(0,a.SU)(m)&&!(0,a.SU)(d).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(o.Z),(0,n._)("p",E,[(0,n._)("span",null,(0,s.zw)(e.$t("error.SOMETHING_WRONG"))+".",1),(0,n._)("span",null,[(0,n.Wm)(u,{keypath:"user.PROFILE.ERRORED_EMAIL_UPDATE"},{default:(0,n.w5)((()=>[(0,n.Wm)(r,{to:"/login"},{default:(0,n.w5)((()=>[(0,n.Uk)((0,s.zw)(e.$t("user.LOG_IN")),1)])),_:1})])),_:1})])])])):(0,n.kq)("",!0)}}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-8c2ec9ce"]]);var m=S},3537:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(6252),a=r(2262),s=r(5801),u=r(9917);const o=e=>((0,n.dD)("data-v-0c3c0394"),e=e(),(0,n.Cn)(),e),c={key:0,id:"profile",class:"view"},i=o((()=>(0,n._)("div",{id:"bottom"},null,-1)));var l=(0,n.aZ)({__name:"ProfileView",setup(e){const t=(0,u.o)(),r=(0,n.Fl)((()=>t.getters[s.YN.GETTERS.AUTH_USER_PROFILE]));return(e,t)=>{const s=(0,n.up)("router-view");return(0,a.SU)(r).username?((0,n.wg)(),(0,n.iD)("div",c,[(0,n.Wm)(s,{user:(0,a.SU)(r)},null,8,["user"]),i])):(0,n.kq)("",!0)}}}),E=r(3744);const _=(0,E.Z)(l,[["__scopeId","data-v-0c3c0394"]]);var d=_},9453:function(e,t,r){r.r(t),r.d(t,{default:function(){return m}});var n=r(6252),a=r(2262),s=r(2201),u=r(2179),o=r(8732),c=r(5801),i=r(9917);const l={key:0,id:"user",class:"view"},E={class:"box"};var _=(0,n.aZ)({__name:"UserView",props:{fromAdmin:{type:Boolean}},setup(e){const t=e,{fromAdmin:r}=(0,a.BK)(t),_=(0,s.yj)(),d=(0,i.o)(),S=(0,n.Fl)((()=>d.getters[c.RT.GETTERS.USER]));return(0,n.wF)((()=>{_.params.username&&"string"===typeof _.params.username&&d.dispatch(c.RT.ACTIONS.GET_USER,_.params.username)})),(0,n.Jd)((()=>{d.dispatch(c.RT.ACTIONS.EMPTY_USER)})),(e,t)=>(0,a.SU)(S).username?((0,n.wg)(),(0,n.iD)("div",l,[(0,n.Wm)(u.Z,{user:(0,a.SU)(S)},null,8,["user"]),(0,n._)("div",E,[(0,n.Wm)(o.Z,{user:(0,a.SU)(S),"from-admin":(0,a.SU)(r)},null,8,["user","from-admin"])])])):(0,n.kq)("",!0)}}),d=r(3744);const S=(0,d.Z)(_,[["__scopeId","data-v-af7007f4"]]);var m=S}}]);
//# sourceMappingURL=profile.168ed5aa.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{7885:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});var a=t(6252),r=t(2262),l=t(3577),o=(t(7658),t(9150)),n=t(436);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({__name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({__name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-30799d13"]]);var Z=F,x=t(5630),D=t(5801),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
//# sourceMappingURL=statistics.ec64386f.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,24 +1,26 @@
<template>
<div id="top" />
<NavBar @menuInteraction="updateHideScrollBar" />
<div v-if="appLoading" class="app-container">
<div class="app-loading">
<Loader />
<main>
<div v-if="appLoading" class="app-container">
<div class="app-loading">
<Loader />
</div>
</div>
</div>
<div v-else class="app-container" :class="{ 'hide-scroll': hideScrollBar }">
<router-view v-if="appConfig" />
<NoConfig v-else />
</div>
<div class="container scroll">
<div
class="scroll-button"
:class="{ 'display-button': displayScrollButton }"
@click="scrollToTop"
>
<i class="fa fa-chevron-up" aria-hidden="true"></i>
<div v-else class="app-container" :class="{ 'hide-scroll': hideScrollBar }">
<router-view v-if="appConfig" />
<NoConfig v-else />
</div>
</div>
<div class="container scroll">
<div
class="scroll-button"
:class="{ 'display-button': displayScrollButton }"
@click="scrollToTop"
>
<i class="fa fa-chevron-up" aria-hidden="true"></i>
</div>
</div>
</main>
<Footer
v-if="appConfig"
:version="appConfig ? appConfig.version : ''"

View File

@ -7,7 +7,7 @@
<div class="admin-menu description-list">
<dl>
<dt>
<router-link to="/admin/application">
<router-link id="adminLink" to="/admin/application">
{{ $t('admin.APPLICATION') }}
</router-link>
</dt>
@ -54,7 +54,7 @@
</template>
<script setup lang="ts">
import { capitalize, toRefs, withDefaults } from 'vue'
import { capitalize, onMounted, toRefs, withDefaults } from 'vue'
import AppStatsCards from '@/components/Administration/AppStatsCards.vue'
import Card from '@/components/Common/Card.vue'
@ -69,6 +69,13 @@
})
const { appConfig, appStatistics } = toRefs(props)
onMounted(() => {
const applicationLink = document.getElementById('adminLink')
if (applicationLink) {
applicationLink.focus()
}
})
</script>
<style lang="scss" scoped>

View File

@ -61,13 +61,15 @@
<span class="cell-heading">
{{ $t('user.PROFILE.REGISTRATION_DATE') }}
</span>
{{
formatDate(
user.created_at,
authUser.timezone,
authUser.date_format
)
}}
<time>
{{
formatDate(
user.created_at,
authUser.timezone,
authUser.date_format
)
}}
</time>
</td>
<td class="text-center">
<span class="cell-heading">

View File

@ -1,15 +1,23 @@
<template>
<div class="dropdown-wrapper">
<div class="dropdown-selected" @click="toggleDropdown">
<button
:aria-label="buttonLabel"
:aria-expanded="isOpen"
class="dropdown-selector transparent"
@click="toggleDropdown"
>
<slot></slot>
</div>
</button>
<ul class="dropdown-list" v-if="isOpen">
<li
class="dropdown-item"
:class="{ selected: option.value === selected }"
v-for="(option, index) in dropdownOptions"
:key="index"
tabindex="0"
@click="updateSelected(option)"
@keydown.enter="updateSelected(option)"
role="button"
>
{{ option.label }}
</li>
@ -25,6 +33,7 @@
interface Props {
options: TDropdownOptions
selected: string
buttonLabel: string
}
const props = defineProps<Props>()
@ -51,36 +60,39 @@
</script>
<style scoped lang="scss">
.dropdown-list {
list-style-type: none;
background-color: #ffffff;
padding: 0 !important;
margin-top: 5px;
margin-left: -20px !important;
position: absolute;
text-align: left;
border: solid 1px lightgrey;
box-shadow: 2px 2px 5px lightgrey;
width: auto !important;
li {
padding: 3px 8px;
}
}
.dropdown-item {
cursor: pointer;
&.selected {
font-weight: bold;
@import '~@/scss/vars.scss';
.dropdown-wrapper {
.dropdown-selector {
margin: 0;
padding: $default-padding * 0.5;
}
&.selected::after {
content: ' ✔';
}
.dropdown-list {
list-style-type: none;
background-color: #ffffff;
padding: 0 !important;
margin-top: 5px;
margin-left: -20px !important;
position: absolute;
text-align: left;
border: solid 1px lightgrey;
box-shadow: 2px 2px 5px lightgrey;
width: auto !important;
&:hover {
background-color: var(--dropdown-hover-color);
.dropdown-item {
padding: 3px 12px;
&.selected {
font-weight: bold;
}
&.selected::after {
content: ' ✔';
}
&:hover {
background-color: var(--dropdown-hover-color);
}
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div id="modal">
<div id="modal" role="dialog">
<div class="custom-modal">
<Card>
<template #title>
@ -21,7 +21,12 @@
>
{{ $t('buttons.YES') }}
</button>
<button class="cancel" @click="emit('cancelAction')">
<button
:tabindex="0"
:id="`${name}-cancel-button`"
class="cancel"
@click="emit('cancelAction')"
>
{{ $t(`buttons.${errorMessages ? 'CANCEL' : 'NO'}`) }}
</button>
</div>
@ -41,9 +46,11 @@
title: string
message: string
strongMessage?: string | null
name?: string | null
}
const props = withDefaults(defineProps<Props>(), {
strongMessage: () => null,
name: 'modal',
})
const emit = defineEmits(['cancelAction', 'confirmAction'])

View File

@ -7,6 +7,7 @@
class="page-link"
:to="{ path, query: getQuery(pagination.page, -1) }"
:disabled="!pagination.has_prev"
:tabindex="pagination.has_prev ? 0 : -1"
>
<slot @click="pagination.has_next ? navigate : null">
{{ $t('api.PAGINATION.PREVIOUS') }}
@ -35,6 +36,7 @@
class="page-link"
:to="{ path, query: getQuery(pagination.page, 1) }"
:disabled="!pagination.has_next"
:tabindex="pagination.has_next ? 0 : -1"
>
<slot @click="pagination.has_next ? navigate : null">
{{ $t('api.PAGINATION.NEXT') }}

View File

@ -11,13 +11,15 @@
@input="updatePassword"
@invalid="invalidPassword"
/>
<div class="show-password" @click="togglePassword">
{{ $t(`user.${showPassword ? 'HIDE' : 'SHOW'}_PASSWORD`) }}
<i
class="fa"
:class="`fa-eye${showPassword ? '-slash' : ''}`"
aria-hidden="true"
/>
<div class="show-password">
<button class="transparent" @click.prevent="togglePassword" type="button">
{{ $t(`user.${showPassword ? 'HIDE' : 'SHOW'}_PASSWORD`) }}
<i
class="fa"
:class="`fa-eye${showPassword ? '-slash' : ''}`"
aria-hidden="true"
/>
</button>
</div>
<div v-if="checkStrength" class="form-info">
<i class="fa fa-info-circle" aria-hidden="true" />
@ -71,6 +73,7 @@
(newPassword) => {
if (newPassword === '') {
passwordValue.value = ''
showPassword.value = false
}
}
)
@ -84,12 +87,15 @@
flex-direction: column;
.show-password {
font-style: italic;
font-size: 0.85em;
text-align: right;
margin-top: -0.75 * $default-margin;
padding-right: $default-padding;
cursor: pointer;
margin-top: -0.5 * $default-margin;
display: flex;
justify-content: right;
button {
font-style: italic;
font-size: 0.85em;
padding: $default-padding * 0.5 $default-padding;
cursor: pointer;
}
}
}
</style>

View File

@ -9,6 +9,7 @@
min="0"
max="4"
step="1"
:tabindex="-1"
/>
<div v-if="passwordStrength" class="password-strength-details">
<span class="password-strength-value">

View File

@ -10,6 +10,7 @@
type="radio"
name="total_distance"
:checked="displayedData === 'total_distance'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.DISTANCE') }}
@ -19,6 +20,7 @@
type="radio"
name="total_duration"
:checked="displayedData === 'total_duration'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.DURATION') }}
@ -28,6 +30,7 @@
type="radio"
name="nb_workouts"
:checked="displayedData === 'nb_workouts'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.WORKOUT', 2) }}
@ -37,6 +40,7 @@
type="radio"
name="average_speed"
:checked="displayedData === 'average_speed'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.AVERAGE_SPEED') }}
@ -46,6 +50,7 @@
type="radio"
name="total_ascent"
:checked="displayedData === 'total_ascent'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.ASCENT') }}
@ -55,6 +60,7 @@
type="radio"
name="total_descent"
:checked="displayedData === 'total_descent'"
:disabled="isDisabled"
@click="updateDisplayData"
/>
{{ $t('workouts.DESCENT') }}
@ -130,6 +136,10 @@
type: Boolean,
default: false,
},
isDisabled: {
type: Boolean,
default: false,
},
},
setup(props) {
const store = useStore()

View File

@ -22,7 +22,7 @@
params: { workoutId: record.workout_id },
}"
>
{{ record.workout_date }}
<time>{{ record.workout_date }}</time>
</router-link>
</span>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div id="footer">
<footer id="footer">
<div class="footer-items">
<div class="footer-item">
<strong>FitTrackee</strong>
@ -22,7 +22,7 @@
</router-link>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">

View File

@ -1,22 +1,31 @@
<template>
<div id="nav">
<header id="nav">
<Modal
v-show="displayModal"
:title="$t('common.CONFIRMATION')"
:message="$t('user.LOGOUT_CONFIRMATION')"
@confirmAction="logout"
@cancelAction="updateDisplayModal(false)"
@keydown.esc="updateDisplayModal(false)"
/>
<div class="nav-container">
<div class="nav-app-name">
<div class="nav-item app-name" @click="$router.push('/')">
FitTrackee
</div>
<router-link class="nav-item app-name" to="/">FitTrackee</router-link>
</div>
<div class="nav-icon-open" :class="{ 'menu-open': isMenuOpen }">
<i class="fa fa-bars hamburger-icon" @click="openMenu()"></i>
<button class="menu-button transparent" @click="openMenu()">
<i class="fa fa-bars hamburger-icon"></i>
</button>
</div>
<div class="nav-items" :class="{ 'menu-open': isMenuOpen }">
<div class="nav-items-close">
<div class="app-name">FitTrackee</div>
<i
class="fa fa-close close-icon nav-item"
:class="{ 'menu-closed': !isMenuOpen }"
@click="closeMenu()"
/>
<button class="menu-button transparent" @click="closeMenu()">
<i
class="fa fa-close close-icon nav-item"
:class="{ 'menu-closed': !isMenuOpen }"
/>
</button>
</div>
<div class="nav-items-app-menu" @click="closeMenu()">
<div class="nav-items-group" v-if="isAuthenticated">
@ -50,9 +59,14 @@
<router-link class="nav-item" to="/profile" @click="closeMenu">
{{ authUser.username }}
</router-link>
<div class="nav-item nav-link" @click="logout">
{{ $t('user.LOGOUT') }}
</div>
<button
class="logout-button transparent"
@click="updateDisplayModal(true)"
:aria-label="$t('user.LOGOUT')"
>
<i class="fa fa-sign-out logout-fa" aria-hidden="true" />
<span class="logout-text">{{ $t('user.LOGOUT') }}</span>
</button>
</div>
<div class="nav-items-group" v-else>
<router-link class="nav-item" to="/login" @click="closeMenu">
@ -68,17 +82,18 @@
:options="availableLanguages"
:selected="language"
@selected="updateLanguage"
:buttonLabel="$t('user.REGISTER')"
>
<i class="fa fa-language"></i>
</Dropdown>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { ComputedRef, computed, ref, capitalize } from 'vue'
import { ComputedRef, Ref, computed, ref, capitalize } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -100,7 +115,8 @@
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
const isMenuOpen = ref(false)
const isMenuOpen: Ref<boolean> = ref(false)
const displayModal: Ref<boolean> = ref(false)
function openMenu() {
isMenuOpen.value = true
@ -118,6 +134,16 @@
}
function logout() {
store.dispatch(AUTH_USER_STORE.ACTIONS.LOGOUT)
displayModal.value = false
}
function updateDisplayModal(display: boolean) {
displayModal.value = display
if (display) {
const button = document.getElementById('modal-cancel-button')
if (button) {
button.focus()
}
}
}
</script>
@ -154,10 +180,7 @@
font-size: 1.2em;
font-weight: bold;
margin-right: 10px;
&:hover {
cursor: pointer;
}
line-height: 1.6em;
}
.fa {
@ -172,12 +195,15 @@
.close-icon {
display: none;
}
.menu-button {
padding: 0;
}
.nav-items {
display: flex;
flex: 1;
justify-content: space-between;
line-height: 1.8em;
line-height: 2em;
width: 100%;
.nav-items-close {
@ -196,11 +222,15 @@
}
.nav-item {
padding: 0 10px;
::v-deep(.dropdown-list) {
z-index: 1000;
margin-left: -160px !important;
width: 180px !important;
height: 28px;
&.dropdown-wrapper {
padding: 0;
margin-left: 2px;
::v-deep(.dropdown-list) {
z-index: 1000;
margin-left: -150px !important;
width: 180px !important;
}
}
}
@ -226,6 +256,16 @@
.nav-separator {
display: none;
}
.logout-button {
padding: $default-padding * 0.5 $default-padding * 0.75;
margin-left: 2px;
.logout-fa {
display: block;
}
.logout-text {
display: none;
}
}
}
@media screen and (max-width: $medium-limit) {
@ -286,14 +326,31 @@
.nav-items-group {
display: flex;
flex-direction: column;
.logout-button {
padding: $default-padding $default-padding $default-padding
$default-padding * 2.4;
color: var(--app-a-color);
text-align: left;
.logout-fa {
display: none;
}
.logout-text {
display: block;
}
}
}
.nav-item {
padding: 7px 25px;
::v-deep(.dropdown-list) {
margin-left: initial !important;
width: auto !important;
&.dropdown-wrapper {
padding-left: $default-padding * 2;
::v-deep(.dropdown-list) {
margin-left: initial !important;
width: auto !important;
height: 200px;
overflow-y: scroll;
}
}
}
@ -308,10 +365,6 @@
padding: 0;
}
}
.nav-items-user-menu :nth-child(1) {
order: 1;
}
}
}
</style>

View File

@ -4,19 +4,20 @@
{{ capitalize($t('privacy_policy.TITLE')) }}
</h1>
<p class="last-update">
{{ $t('privacy_policy.LAST_UPDATE')}}: {{ private_policy_date }}
{{ $t('privacy_policy.LAST_UPDATE') }}:
<time>{{ privatePolicyDate }}</time>
</p>
<template v-if="appConfig.privacy_policy">
<div
v-html="snarkdown(linkifyAndClean(appConfig.privacy_policy))"
/>
<div v-html="snarkdown(linkifyAndClean(appConfig.privacy_policy))" />
</template>
<template v-else>
<template v-for="paragraph in paragraphs" :key="paragraph">
<h2>
{{ $t(`privacy_policy.CONTENT.${paragraph}.TITLE`)}}
{{ $t(`privacy_policy.CONTENT.${paragraph}.TITLE`) }}
</h2>
<p v-html="snarkdown($t(`privacy_policy.CONTENT.${paragraph}.CONTENT`))" />
<p
v-html="snarkdown($t(`privacy_policy.CONTENT.${paragraph}.CONTENT`))"
/>
</template>
</template>
</div>
@ -34,7 +35,7 @@
import { linkifyAndClean } from '@/utils/inputs'
const store = useStore()
const fittrackee_private_policy_date = 'Sun, 26 Feb 2023 17:00:00 GMT'
const fittrackeePrivatePolicyDate = 'Sun, 26 Feb 2023 17:00:00 GMT'
const appConfig: ComputedRef<TAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
@ -46,19 +47,24 @@
)
const dateFormat = computed(() => getDateFormat())
const timezone = computed(() => getTimezone())
const private_policy_date = computed(() => getPolicyDate())
const privatePolicyDate = computed(() => getPolicyDate())
const paragraphs = [
'DATA_COLLECTED', 'INFORMATION_USAGE', 'INFORMATION_PROTECTION',
'INFORMATION_DISCLOSURE', 'SITE_USAGE_BY_CHILDREN', 'YOUR_CONSENT',
'ACCOUNT_DELETION', 'CHANGES_TO_OUR_PRIVACY_POLICY'
'DATA_COLLECTED',
'INFORMATION_USAGE',
'INFORMATION_PROTECTION',
'INFORMATION_DISCLOSURE',
'SITE_USAGE_BY_CHILDREN',
'YOUR_CONSENT',
'ACCOUNT_DELETION',
'CHANGES_TO_OUR_PRIVACY_POLICY',
]
function getTimezone() {
return authUser.value.timezone
? authUser.value.timezone
: Intl.DateTimeFormat().resolvedOptions().timeZone
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'Europe/Paris'
? authUser.value.timezone
: Intl.DateTimeFormat().resolvedOptions().timeZone
? Intl.DateTimeFormat().resolvedOptions().timeZone
: 'Europe/Paris'
}
function getDateFormat() {
return dateStringFormats[language.value]
@ -66,14 +72,13 @@
function getPolicyDate() {
return formatDate(
appConfig.value.privacy_policy && appConfig.value.privacy_policy_date
? `${appConfig.value.privacy_policy_date}`
: fittrackee_private_policy_date,
? `${appConfig.value.privacy_policy_date}`
: fittrackeePrivatePolicyDate,
timezone.value,
dateFormat.value,
false,
)
false
)
}
</script>
<style lang="scss" scoped>

View File

@ -1,12 +1,13 @@
<template>
<div class="chart-menu">
<div class="chart-arrow">
<i
class="fa fa-chevron-left"
aria-hidden="true"
@click="emit('arrowClick', true)"
/>
</div>
<button
class="chart-arrow transparent"
@click="emit('arrowClick', true)"
@keydown.enter="emit('arrowClick', true)"
:disabled="isDisabled"
>
<i class="fa fa-chevron-left" aria-hidden="true" />
</button>
<div class="time-frames custom-checkboxes-group">
<div class="time-frames-checkboxes custom-checkboxes">
<div
@ -21,24 +22,38 @@
:name="frame"
:checked="selectedTimeFrame === frame"
@input="onUpdateTimeFrame(frame)"
:disabled="isDisabled"
/>
<span>{{ $t(`statistics.TIME_FRAMES.${frame}`) }}</span>
<span
:id="`frame-${frame}`"
:tabindex="isDisabled ? -1 : 0"
role="button"
@keydown.enter="onUpdateTimeFrame(frame)"
>
{{ $t(`statistics.TIME_FRAMES.${frame}`) }}
</span>
</label>
</div>
</div>
</div>
<div class="chart-arrow">
<i
class="fa fa-chevron-right"
aria-hidden="true"
@click="emit('arrowClick', false)"
/>
</div>
<button
class="chart-arrow transparent"
@click="emit('arrowClick', false)"
@keydown.enter="emit('arrowClick', false)"
:disabled="isDisabled"
>
<i class="fa fa-chevron-right" aria-hidden="true" />
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, ref, toRefs } from 'vue'
interface Props {
isDisabled: boolean
}
const props = defineProps<Props>()
const { isDisabled } = toRefs(props)
const emit = defineEmits(['arrowClick', 'timeFrameUpdate'])
@ -49,11 +64,21 @@
selectedTimeFrame.value = timeFrame
emit('timeFrameUpdate', timeFrame)
}
onMounted(() => {
if (!isDisabled.value) {
const input = document.getElementById('frame-month')
if (input) {
input.focus()
}
}
})
</script>
<style lang="scss" scoped>
.chart-menu {
display: flex;
align-items: center;
.chart-arrow,
.time-frames {

View File

@ -3,6 +3,7 @@
<StatsMenu
@timeFrameUpdate="updateTimeFrame"
@arrowClick="handleOnClickArrows"
:isDisabled="isDisabled"
/>
<StatChart
:sports="sports"
@ -10,6 +11,7 @@
:chartParams="chartParams"
:displayed-sport-ids="selectedSportIds"
:fullStats="true"
:isDisabled="isDisabled"
/>
<SportsMenu
:selected-sport-ids="selectedSportIds"
@ -35,6 +37,7 @@
interface Props {
sports: ISport[]
user: IAuthUserProfile
isDisabled: boolean
}
const props = defineProps<Props>()

View File

@ -2,6 +2,7 @@
<div id="user-infos" class="description-list">
<Modal
v-if="displayModal"
name="user"
:title="$t('common.CONFIRMATION')"
:message="
displayModal === 'delete'
@ -15,6 +16,7 @@
: resetUserPassword(user.username)
"
@cancelAction="updateDisplayModal('')"
@keydown.esc="updateDisplayModal('')"
/>
<div class="info-box success-message" v-if="isSuccess">
{{
@ -58,13 +60,17 @@
<div v-else>
<dl>
<dt>{{ $t('user.PROFILE.REGISTRATION_DATE') }}:</dt>
<dd>{{ registrationDate }}</dd>
<dd>
<time>{{ registrationDate }}</time>
</dd>
<dt>{{ $t('user.PROFILE.FIRST_NAME') }}:</dt>
<dd>{{ user.first_name }}</dd>
<dt>{{ $t('user.PROFILE.LAST_NAME') }}:</dt>
<dd>{{ user.last_name }}</dd>
<dt>{{ $t('user.PROFILE.BIRTH_DATE') }}:</dt>
<dd>{{ birthDate }}</dd>
<dd>
<time v-if="birthDate">{{ birthDate }}</time>
</dd>
<dt>{{ $t('user.PROFILE.LOCATION') }}:</dt>
<dd>{{ user.location }}</dd>
<dt>{{ $t('user.PROFILE.BIO') }}:</dt>
@ -186,6 +192,10 @@
function updateDisplayModal(value: string) {
displayModal.value = value
if (value !== '') {
const button = document.getElementById('user-cancel-button')
if (button) {
button.focus()
}
store.commit(USERS_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false)
}
}

View File

@ -2,10 +2,12 @@
<div id="user-infos-edition">
<Modal
v-if="displayModal"
name="account"
:title="$t('common.CONFIRMATION')"
:message="$t('user.CONFIRM_ACCOUNT_DELETION')"
@confirmAction="deleteAccount(user.username)"
@cancelAction="updateDisplayModal(false)"
@keydown.esc="updateDisplayModal(false)"
/>
<div class="profile-form form-box">
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
@ -62,7 +64,11 @@
<button class="danger" @click.prevent="updateDisplayModal(true)">
{{ $t('buttons.DELETE_MY_ACCOUNT') }}
</button>
<button class="confirm" v-if="canRequestExport()" @click.prevent="requestExport">
<button
class="confirm"
v-if="canRequestExport()"
@click.prevent="requestExport"
>
{{ $t('buttons.REQUEST_DATA_EXPORT') }}
</button>
</div>
@ -73,22 +79,22 @@
{{ $t('user.EXPORT_REQUEST.ONLY_ONE_EXPORT_PER_DAY') }}
</span>
<div v-if="exportRequest" class="data-export-archive">
{{$t('user.EXPORT_REQUEST.DATA_EXPORT')}}
{{ $t('user.EXPORT_REQUEST.DATA_EXPORT') }}
({{ exportRequestDate }}):
<span
v-if="exportRequest.status=== 'successful'"
v-if="exportRequest.status === 'successful'"
class="archive-link"
@click.prevent="downloadArchive(exportRequest.file_name)"
>
<i class="fa fa-download" aria-hidden="true" />
{{ $t("user.EXPORT_REQUEST.DOWNLOAD_ARCHIVE") }}
{{ $t('user.EXPORT_REQUEST.DOWNLOAD_ARCHIVE') }}
({{ getReadableFileSize(exportRequest.file_size) }})
</span>
<span v-else>
{{ $t(`user.EXPORT_REQUEST.STATUS.${exportRequest.status}`)}}
{{ $t(`user.EXPORT_REQUEST.STATUS.${exportRequest.status}`) }}
</span>
<span v-if="generatingLink">
{{ $t(`user.EXPORT_REQUEST.GENERATING_LINK`)}}
{{ $t(`user.EXPORT_REQUEST.GENERATING_LINK`) }}
<i class="fa fa-spinner fa-pulse" aria-hidden="true" />
</span>
</div>
@ -111,11 +117,15 @@
onUnmounted,
} from 'vue'
import authApi from "@/api/authApi";
import authApi from '@/api/authApi'
import PasswordInput from '@/components/Common/PasswordInput.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
import {IAuthUserProfile, IUserAccountPayload, IExportRequest} from '@/types/user'
import {
IAuthUserProfile,
IUserAccountPayload,
IExportRequest,
} from '@/types/user'
import { useStore } from '@/use/useStore'
import { formatDate } from '@/utils/dates'
import { getReadableFileSize } from '@/utils/files'
@ -150,8 +160,8 @@
const exportRequest: ComputedRef<IExportRequest | null> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.EXPORT_REQUEST]
)
const exportRequestDate: ComputedRef<string | null> = computed(
() => getExportRequestDate()
const exportRequestDate: ComputedRef<string | null> = computed(() =>
getExportRequestDate()
)
const generatingLink: Ref<boolean> = ref(false)
@ -175,19 +185,22 @@
userForm.new_password = new_password
}
function getExportRequestDate() {
return exportRequest.value ? formatDate(
exportRequest.value.created_at,
user.value.timezone,
user.value.date_format,
true,
null, true
) : null
return exportRequest.value
? formatDate(
exportRequest.value.created_at,
user.value.timezone,
user.value.date_format,
true,
null,
true
)
: null
}
function canRequestExport() {
return exportRequestDate.value
? isBefore(new Date(exportRequestDate.value), subDays(new Date(), 1))
: true
? isBefore(new Date(exportRequestDate.value), subDays(new Date(), 1))
: true
}
function updateProfile() {
const payload: IUserAccountPayload = {
@ -202,6 +215,12 @@
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
if (displayModal.value) {
const button = document.getElementById('account-cancel-button')
if (button) {
button.focus()
}
}
}
function deleteAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
@ -225,7 +244,7 @@
document.body.appendChild(archive_link)
archive_link.click()
})
.finally(() => generatingLink.value = false)
.finally(() => (generatingLink.value = false))
}
onUnmounted(() => {
@ -284,8 +303,8 @@
.data-export {
padding: $default-padding 0;
.data-export-archive {
padding-top: $default-padding*2;
font-size: .9em;
padding-top: $default-padding * 2;
font-size: 0.9em;
.archive-link {
color: var(--app-a-color);

View File

@ -2,10 +2,12 @@
<div id="oauth2-app" class="description-list">
<Modal
v-if="displayModal"
name="app"
:title="$t('common.CONFIRMATION')"
:message="$t(messageToDisplay)"
@confirmAction="confirmAction(client.id)"
@cancelAction="updateDisplayModal(false)"
@keydown.esc="updateDisplayModal(false)"
/>
<div v-if="client && client.client_id">
<div
@ -49,13 +51,15 @@
</dd>
<dt>{{ capitalize($t('oauth2.APP.ISSUE_AT')) }}:</dt>
<dd>
{{
formatDate(
client.issued_at,
authUser.timezone,
authUser.date_format
)
}}
<time>
{{
formatDate(
client.issued_at,
authUser.timezone,
authUser.date_format
)
}}
</time>
</dd>
<dt>{{ $t('oauth2.APP.NAME') }}:</dt>
<dd>{{ client.name }}</dd>
@ -176,6 +180,11 @@
displayModal.value = value
if (!value) {
messageToDisplay.value = null
} else {
const button = document.getElementById('app-cancel-button')
if (button) {
button.focus()
}
}
}
function confirmAction(clientId: number) {

View File

@ -8,13 +8,15 @@
</router-link>
<span class="app-issued-at">
{{ $t('oauth2.APP.ISSUE_AT') }}
{{
formatDate(
client.issued_at,
authUser.timezone,
authUser.date_format
)
}}
<time>
{{
formatDate(
client.issued_at,
authUser.timezone,
authUser.date_format
)
}}
</time>
</span>
</li>
</ul>

View File

@ -11,7 +11,14 @@
:disabled="disabled"
@input="$router.push(getPath(tab))"
/>
<span>{{ $t(`user.PROFILE.TABS.${tab}`) }}</span>
<span
:id="`tab-${tab}`"
:tabindex="0"
role="button"
@keydown.enter="$router.push(getPath(tab))"
>
{{ $t(`user.PROFILE.TABS.${tab}`) }}
</span>
</label>
</div>
</div>
@ -19,7 +26,7 @@
</template>
<script setup lang="ts">
import { toRefs, withDefaults } from 'vue'
import { onMounted, toRefs, withDefaults } from 'vue'
interface Props {
tabs: string[]
@ -33,6 +40,13 @@
const { tabs, selectedTab, disabled } = toRefs(props)
onMounted(() => {
const input = document.getElementById(`tab-${tabs.value[0]}`)
if (input) {
input.focus()
}
})
function getPath(tab: string) {
switch (tab) {
case 'ACCOUNT':

View File

@ -1,8 +1,9 @@
<template>
<div id="workout-card-title">
<div
class="workout-previous workout-arrow"
<button
class="workout-previous workout-arrow transparent"
:class="{ inactive: !workoutObject.previousUrl }"
:disabled="!workoutObject.previousUrl"
:title="
workoutObject.previousUrl
? $t(`workouts.PREVIOUS_${workoutObject.type}`)
@ -15,33 +16,40 @@
"
>
<i class="fa fa-chevron-left" aria-hidden="true" />
</div>
</button>
<div class="workout-card-title">
<SportImage :sport-label="sport.label" :color="sport.color" />
<div class="workout-title-date">
<div class="workout-title" v-if="workoutObject.type === 'WORKOUT'">
<span>{{ workoutObject.title }}</span>
<i
class="fa fa-edit"
aria-hidden="true"
<button
class="transparent icon-button"
@click="
$router.push({
name: 'EditWorkout',
params: { workoutId: workoutObject.workoutId },
})
"
/>
<i
:aria-label="$t(`workouts.EDIT_WORKOUT`)"
>
<i class="fa fa-edit" aria-hidden="true" />
</button>
<button
v-if="workoutObject.with_gpx"
class="fa fa-download"
aria-hidden="true"
class="transparent icon-button"
@click.prevent="downloadGpx(workoutObject.workoutId)"
/>
<i
class="fa fa-trash"
aria-hidden="true"
@click="emit('displayModal', true)"
/>
:aria-label="$t(`workouts.DOWNLOAD_WORKOUT`)"
>
<i class="fa fa-download" aria-hidden="true" />
</button>
<button
id="delete-workout-button"
class="transparent icon-button"
@click="displayDeleteModal"
:aria-label="$t(`workouts.DELETE_WORKOUT`)"
>
<i class="fa fa-trash" aria-hidden="true" />
</button>
</div>
<div class="workout-title" v-else>
{{ workoutObject.title }}
@ -53,14 +61,15 @@
</span>
</div>
<div class="workout-date">
{{ workoutObject.workoutDate }} -
{{ workoutObject.workoutTime }}
<time>
{{ workoutObject.workoutDate }} - {{ workoutObject.workoutTime }}
</time>
<span class="workout-link">
<router-link
v-if="workoutObject.type === 'SEGMENT'"
:to="{
name: 'Workout',
params: { workoutId: workoutObject.workoutId },
params: { workoutId: workoutObject.workoutId }
}"
>
> {{ $t('workouts.BACK_TO_WORKOUT') }}
@ -69,9 +78,10 @@
</div>
</div>
</div>
<div
class="workout-next workout-arrow"
<button
class="workout-next workout-arrow transparent"
:class="{ inactive: !workoutObject.nextUrl }"
:disabled="!workoutObject.nextUrl"
:title="
workoutObject.nextUrl
? $t(`workouts.NEXT_${workoutObject.type}`)
@ -82,7 +92,7 @@
"
>
<i class="fa fa-chevron-right" aria-hidden="true" />
</div>
</button>
</div>
</template>
@ -119,6 +129,10 @@
gpxLink.click()
})
}
function displayDeleteModal(event: Event & { target: HTMLInputElement }) {
event.target.blur()
emit('displayModal', true)
}
</script>
<style lang="scss" scoped>
@ -166,9 +180,13 @@
}
.fa {
cursor: pointer;
padding: 0 $default-padding * 0.3;
}
.icon-button {
cursor: pointer;
padding: 0;
margin-left: 2px;
}
}
@media screen and (max-width: $small-limit) {

View File

@ -18,12 +18,24 @@
@ready="fitBounds(bounds)"
>
<LControlLayers />
<LControl position="topleft" class="map-control" @click="resetZoom">
<LControl
position="topleft"
class="map-control"
tabindex="0"
role="button"
:aria-label="$t('workouts.RESET_ZOOM')"
@click="resetZoom"
>
<i class="fa fa-refresh" aria-hidden="true" />
</LControl>
<LControl
position="topleft"
class="map-control"
tabindex="0"
role="button"
:aria-label="
$t(`workouts.${isFullscreen ? 'EXIT' : 'VIEW'}_FULLSCREEN`)
"
@click="toggleFullscreen"
>
<i

View File

@ -2,10 +2,12 @@
<div class="workout-detail">
<Modal
v-if="displayModal"
name="workout"
:title="$t('common.CONFIRMATION')"
:message="$t('workouts.WORKOUT_DELETION_CONFIRMATION')"
@confirmAction="deleteWorkout(workoutObject.workoutId)"
@cancelAction="updateDisplayModal(false)"
@cancelAction="cancelDelete"
@keydown.esc="cancelDelete"
/>
<Card>
<template #title>
@ -35,6 +37,7 @@
ComputedRef,
Ref,
computed,
nextTick,
ref,
toRefs,
watch,
@ -161,18 +164,50 @@
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
if (displayModal.value) {
nextTick(() => {
const button = document.getElementById('workout-cancel-button')
if (button) {
button.focus()
}
})
}
}
function cancelDelete() {
updateDisplayModal(false)
const button = document.getElementById('delete-workout-button')
if (button) {
button.focus()
}
}
function deleteWorkout(workoutId: string) {
updateDisplayModal(false)
store.dispatch(WORKOUTS_STORE.ACTIONS.DELETE_WORKOUT, {
workoutId: workoutId,
})
}
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
watch(
() => route.params.segmentId,
async (newSegmentId) => {
if (newSegmentId) {
segmentId.value = +newSegmentId
scrollToTop()
}
}
)
watch(
() => route.params.workoutId,
async (workoutId) => {
if (workoutId) {
displayModal.value = false
scrollToTop()
}
}
)

View File

@ -352,8 +352,15 @@
const payloadErrorMessages: Ref<string[]> = ref([])
onMounted(() => {
let element
if (props.workout.id) {
formatWorkoutForm(props.workout)
element = document.getElementById('sport')
} else {
element = document.getElementById('withGpx')
}
if (element) {
element.focus()
}
})

View File

@ -1,12 +1,13 @@
<template>
<div class="workouts-filters">
<div class="box">
<form v-on:submit.prevent="onSubmit" class="form">
<form v-on:submit.prevent="onFilter" class="form">
<div class="form-all-items">
<div class="form-items-group">
<div class="form-item">
<label> {{ $t('workouts.FROM') }}: </label>
<input
id="from"
name="from"
type="date"
:value="$route.query.from"
@ -31,6 +32,7 @@
name="sport_id"
:value="$route.query.sport_id"
@change="handleFilterChange"
@keyup.enter="onFilter"
>
<option value="" />
<option
@ -54,9 +56,9 @@
@change="handleFilterChange"
placeholder=""
type="text"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
</div>
</div>
</div>
</div>
@ -71,7 +73,7 @@
step="0.1"
:value="$route.query.distance_from"
@change="handleFilterChange"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -81,7 +83,7 @@
step="0.1"
:value="$route.query.distance_to"
@change="handleFilterChange"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
</div>
</div>
@ -98,7 +100,7 @@
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -108,7 +110,7 @@
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
</div>
</div>
@ -125,7 +127,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -135,7 +137,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
</div>
</div>
@ -153,7 +155,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -163,7 +165,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
@keyup.enter="onFilter"
/>
</div>
</div>
@ -184,7 +186,7 @@
</template>
<script setup lang="ts">
import { ComputedRef, computed, toRefs, watch } from 'vue'
import { ComputedRef, computed, toRefs, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { LocationQuery, useRoute, useRouter } from 'vue-router'
@ -215,6 +217,13 @@
)
let params: LocationQuery = Object.assign({}, route.query)
onMounted(() => {
const filter = document.getElementById('from')
if (filter) {
filter.focus()
}
})
function handleFilterChange(event: Event & { target: HTMLInputElement }) {
if (event.target.value === '') {
delete params[event.target.name]
@ -317,7 +326,8 @@
height: 100%;
.form-item {
label, span {
label,
span {
font-size: 0.9em;
}
@ -395,7 +405,6 @@
}
.form {
.form-all-items {
.form-items-group {
.form-item-title {
padding-top: $default-padding;

View File

@ -83,13 +83,15 @@
<span class="cell-heading">
{{ $t('workouts.DATE') }}
</span>
{{
formatDate(
workout.workout_date,
user.timezone,
user.date_format
)
}}
<time>
{{
formatDate(
workout.workout_date,
user.timezone,
user.date_format
)
}}
</time>
</td>
<td class="text-right">
<span class="cell-heading">

View File

@ -59,7 +59,8 @@
"TABLE": {
"ADD_ADMIN_RIGHTS": "Add admin rights",
"REMOVE_ADMIN_RIGHTS": "Remove admin rights"
}
},
"TITLE": "Administration - Users"
},
"USER_EMAIL_UPDATE_SUCCESSFUL": "The email address has been updated."
}

View File

@ -1,4 +1,5 @@
{
"ACCOUNT_CONFIRMATION": "Account confirmation",
"ACCOUNT_CONFIRMATION_NOT_RECEIVED": "Didn't received instructions?",
"ACCOUNT_CONFIRMATION_SENT": "Check your email. A new confirmation email has been sent to the address provided.",
"ADMIN": "Admin",
@ -7,6 +8,7 @@
"CURRENT_PASSWORD": "Current password",
"EMAIL": "Email",
"EMAIL_INFO": "Enter a valid email address.",
"EMAIL_UPDATE": "Email update",
"ENTER_PASSWORD": "Enter a password",
"EXPORT_REQUEST": {
"DATA_EXPORT": "Data export",
@ -26,6 +28,7 @@
"LAST_PRIVACY_POLICY_TO_VALIDATE": "The privacy policy has been updated, please {0} it before proceeding.",
"LOGIN": "Login",
"LOGOUT": "Logout",
"LOGOUT_CONFIRMATION": "Are you sure you want to log out?",
"LOG_IN": "log in",
"NEW_PASSWORD": "New password",
"NO_USERS_FOUND": "No users found.",

View File

@ -6,13 +6,16 @@
"AVE_SPEED": "ave. speed",
"BACK_TO_WORKOUT": "back to workout",
"DATE": "date",
"DELETE_WORKOUT": "Delete the workout",
"DESCENT": "descent",
"DISPLAY_FILTERS": "display filters",
"DISTANCE": "distance",
"DOWNLOAD_WORKOUT": "Download the workout",
"DURATION": "duration",
"EDIT_WORKOUT": "Edit the workout",
"ELEVATION": "elevation",
"END": "end",
"EXIT_FULLSCREEN": "Exit Fullscreen",
"FROM": "from",
"GPX_FILE": ".gpx file",
"HIDE_FILTERS": "hide filters",
@ -50,6 +53,7 @@
"RECORD_LD": "Longest duration",
"RECORD_MS": "Max. speed",
"REMAINING_CHARS": "remaining characters",
"RESET_ZOOM": "Reset zoom",
"SEGMENT": "segment | segments",
"SPEED": "speed",
"SPORT": "sport | sports",
@ -60,6 +64,7 @@
"TO": "to",
"TOTAL_DURATION": "total duration",
"UPLOAD_FIRST_WORKOUT": "Upload one!",
"VIEW_FULLSCREEN": "View Fullscreen",
"WEATHER": {
"DARK_SKY": {
"clear-day": "clear day",

View File

@ -59,7 +59,8 @@
"TABLE": {
"ADD_ADMIN_RIGHTS": "Ajouter les droits d'admin",
"REMOVE_ADMIN_RIGHTS": "Retirer les droits d'admin"
}
},
"TITLE": "Administration - Utilisateurs"
},
"USER_EMAIL_UPDATE_SUCCESSFUL": "L'adresse email a été mise à jour."
}

View File

@ -1,4 +1,5 @@
{
"ACCOUNT_CONFIRMATION": "Confirmation du compte",
"ACCOUNT_CONFIRMATION_NOT_RECEIVED": "Vous n'avez pas reçu les instructions ?",
"ACCOUNT_CONFIRMATION_SENT": "Vérifiez vos courriels. Un nouveau courriel de confirmation a été envoyé à l'adresse électronique fournie.",
"ADMIN": "Admin",
@ -7,6 +8,7 @@
"CURRENT_PASSWORD": "Mot de passe actuel",
"EMAIL": "Courriel",
"EMAIL_INFO": "Saisissez une adresse électronique valide.",
"EMAIL_UPDATE": "Mise à jour de l'adresse électronique",
"ENTER_PASSWORD": "Saisissez un mot de passe",
"EXPORT_REQUEST": {
"DATA_EXPORT": "Export des données",
@ -26,6 +28,7 @@
"LAST_PRIVACY_POLICY_TO_VALIDATE": "La politique de confidentialité a été mise à jour. Veuillez l'{0} avant de poursuivre.",
"LOGIN": "Se connecter",
"LOGOUT": "Se déconnecter",
"LOGOUT_CONFIRMATION": "Etes-vous sûr de vouloir vous déconnecter ?",
"LOG_IN": "connecter",
"NEW_PASSWORD": "Nouveau mot de passe",
"NO_USERS_FOUND": "Aucun utilisateur trouvé.",

View File

@ -6,13 +6,16 @@
"AVE_SPEED": "vitesse moy.",
"BACK_TO_WORKOUT": "revenir à la séance",
"DATE": "date",
"DELETE_WORKOUT": "Supprimer la séance",
"DESCENT": "dénivelé négatif",
"DISPLAY_FILTERS": "afficher les filtres",
"DISTANCE": "distance",
"DOWNLOAD_WORKOUT": "Télécharger la séance",
"DURATION": "durée",
"EDIT_WORKOUT": "Modifier la séance",
"ELEVATION": "altitude",
"END": "fin",
"EXIT_FULLSCREEN": "Sortir du plein-écran",
"FROM": "à partir de",
"GPX_FILE": "fichier .gpx",
"HIDE_FILTERS": "masquer les filtres",
@ -50,6 +53,7 @@
"RECORD_LD": "Durée la + longue",
"RECORD_MS": "Vitesse max.",
"REMAINING_CHARS": "nombre de caractères restants ",
"RESET_ZOOM": "Réinitialiser le zoom",
"SEGMENT": "segment | segments",
"SPEED": "vitesse",
"SPORT": "sport | sports",
@ -60,6 +64,7 @@
"TO": "jusqu'au",
"TOTAL_DURATION": "durée totale",
"UPLOAD_FIRST_WORKOUT": "Ajoutez votre première séance !",
"VIEW_FULLSCREEN": "Afficher en plein-écran",
"WEATHER": {
"DARK_SKY": {
"clear-day": "ensoleillé",

View File

@ -1,3 +1,4 @@
import { capitalize } from 'vue'
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import AdminApplication from '@/components/Administration/AdminApplication.vue'
@ -19,6 +20,7 @@ import UserApps from '@/components/User/UserApps/index.vue'
import UserApp from '@/components/User/UserApps/UserApp.vue'
import UserAppsList from '@/components/User/UserApps/UserAppsList.vue'
import UserSportPreferences from '@/components/User/UserSportPreferences.vue'
import createI18n from '@/i18n'
import store from '@/store'
import { AUTH_USER_STORE } from '@/store/constants'
import AboutView from '@/views/AboutView.vue'
@ -27,6 +29,8 @@ import NotFoundView from '@/views/NotFoundView.vue'
import PrivacyPolicyView from '@/views/PrivacyPolicyView.vue'
import LoginOrRegister from '@/views/user/LoginOrRegister.vue'
const { t } = createI18n.global
const getTabFromPath = (path: string): string => {
const regex = /(\/profile)(\/edit)*(\/*)/
const tag = path.replace(regex, '').toUpperCase()
@ -38,18 +42,27 @@ const routes: Array<RouteRecordRaw> = [
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
title: 'dashboard.DASHBOARD',
},
},
{
path: '/login',
name: 'Login',
component: LoginOrRegister,
props: { action: 'login' },
meta: {
title: 'user.LOGIN',
},
},
{
path: '/register',
name: 'Register',
component: LoginOrRegister,
props: { action: 'register' },
meta: {
title: 'user.REGISTER',
},
},
{
path: '/account-confirmation',
@ -58,6 +71,9 @@ const routes: Array<RouteRecordRaw> = [
import(
/* webpackChunkName: 'profile' */ '@/views/user/AccountConfirmationView.vue'
),
meta: {
title: 'user.ACCOUNT_CONFIRMATION',
},
},
{
path: '/account-confirmation/resend',
@ -67,6 +83,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/AccountConfirmationResendView.vue'
),
props: { action: 'account-confirmation-resend' },
meta: {
title: 'buttons.ACCOUNT-CONFIRMATION-RESEND',
},
},
{
path: '/account-confirmation/email-sent',
@ -76,6 +95,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/AccountConfirmationResendView.vue'
),
props: { action: 'email-sent' },
meta: {
title: 'buttons.ACCOUNT-CONFIRMATION-RESEND',
},
},
{
path: '/password-reset/sent',
@ -85,6 +107,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/PasswordResetView.vue'
),
props: { action: 'request-sent' },
meta: {
title: 'user.PASSWORD_RESET',
},
},
{
path: '/password-reset/request',
@ -94,6 +119,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/PasswordResetView.vue'
),
props: { action: 'reset-request' },
meta: {
title: 'user.PASSWORD_RESET',
},
},
{
path: '/password-reset/password-updated',
@ -103,6 +131,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/PasswordResetView.vue'
),
props: { action: 'password-updated' },
meta: {
title: 'user.PASSWORD_RESET',
},
},
{
path: '/password-reset',
@ -112,6 +143,9 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: 'reset' */ '@/views/user/PasswordResetView.vue'
),
props: { action: 'reset' },
meta: {
title: 'user.PASSWORD_RESET',
},
},
{
path: '/email-update',
@ -120,6 +154,9 @@ const routes: Array<RouteRecordRaw> = [
import(
/* webpackChunkName: 'profile' */ '@/views/user/EmailUpdateView.vue'
),
meta: {
title: 'user.EMAIL_UPDATE',
},
},
{
path: '/profile',
@ -139,17 +176,26 @@ const routes: Array<RouteRecordRaw> = [
path: '',
name: 'UserInfos',
component: UserInfos,
meta: {
title: 'user.PROFILE.TABS.PROFILE',
},
},
{
path: 'preferences',
name: 'UserPreferences',
component: UserPreferences,
meta: {
title: 'user.PROFILE.TABS.PREFERENCES',
},
},
{
path: 'sports',
name: 'UserSportPreferences',
component: UserSportPreferences,
props: { isEdition: false },
meta: {
title: 'user.PROFILE.TABS.SPORTS',
},
},
{
path: 'apps',
@ -160,27 +206,42 @@ const routes: Array<RouteRecordRaw> = [
path: '',
name: 'UserAppsList',
component: UserAppsList,
meta: {
title: 'user.PROFILE.TABS.APPS',
},
},
{
path: ':id',
name: 'UserApp',
component: UserApp,
meta: {
title: 'user.PROFILE.TABS.APPS',
},
},
{
path: ':id/created',
name: 'CreatedUserApp',
component: UserApp,
props: { afterCreation: true },
meta: {
title: 'user.PROFILE.TABS.APPS',
},
},
{
path: 'new',
name: 'AddUserApp',
component: AddUserApp,
meta: {
title: 'user.PROFILE.TABS.APPS',
},
},
{
path: 'authorize',
name: 'AuthorizeUserApp',
component: AuthorizeUserApp,
meta: {
title: 'user.PROFILE.TABS.APPS',
},
},
],
},
@ -198,32 +259,50 @@ const routes: Array<RouteRecordRaw> = [
path: '',
name: 'UserInfosEdition',
component: UserInfosEdition,
meta: {
title: 'user.PROFILE.EDIT',
},
},
{
path: 'account',
name: 'UserAccountEdition',
component: UserAccountEdition,
meta: {
title: 'user.PROFILE.ACCOUNT_EDITION',
},
},
{
path: 'picture',
name: 'UserPictureEdition',
component: UserPictureEdition,
meta: {
title: 'user.PROFILE.PICTURE_EDITION',
},
},
{
path: 'preferences',
name: 'UserPreferencesEdition',
component: UserPreferencesEdition,
meta: {
title: 'user.PROFILE.EDIT_PREFERENCES',
},
},
{
path: 'sports',
name: 'UserSportPreferencesEdition',
component: UserSportPreferences,
props: { isEdition: true },
meta: {
title: 'user.PROFILE.EDIT_SPORTS_PREFERENCES',
},
},
{
path: 'privacy-policy',
name: 'UserPrivacyPolicy',
component: UserPrivacyPolicyValidation,
meta: {
title: 'user.PROFILE.PRIVACY-POLICY_EDITION',
},
},
],
},
@ -234,12 +313,18 @@ const routes: Array<RouteRecordRaw> = [
name: 'Statistics',
component: () =>
import(/* webpackChunkName: 'statistics' */ '@/views/StatisticsView.vue'),
meta: {
title: 'statistics.STATISTICS',
},
},
{
path: '/users/:username',
name: 'User',
component: () =>
import(/* webpackChunkName: 'profile' */ '@/views/user/UserView.vue'),
meta: {
title: 'administration.USER',
},
},
{
path: '/workouts',
@ -248,6 +333,10 @@ const routes: Array<RouteRecordRaw> = [
import(
/* webpackChunkName: 'workouts' */ '@/views/workouts/WorkoutsView.vue'
),
meta: {
title: 'workouts.WORKOUT',
count: 0,
},
},
{
path: '/workouts/:workoutId',
@ -255,6 +344,9 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: 'workouts' */ '@/views/workouts/Workout.vue'),
props: { displaySegment: false },
meta: {
title: 'workouts.WORKOUT',
},
},
{
path: '/workouts/:workoutId/edit',
@ -263,6 +355,9 @@ const routes: Array<RouteRecordRaw> = [
import(
/* webpackChunkName: 'workouts' */ '@/views/workouts/EditWorkout.vue'
),
meta: {
title: 'workouts.EDIT_WORKOUT',
},
},
{
path: '/workouts/:workoutId/segment/:segmentId',
@ -270,6 +365,10 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: 'workouts' */ '@/views/workouts/Workout.vue'),
props: { displaySegment: true },
meta: {
title: 'workouts.SEGMENT',
count: 0,
},
},
{
path: '/workouts/add',
@ -278,6 +377,9 @@ const routes: Array<RouteRecordRaw> = [
import(
/* webpackChunkName: 'workouts' */ '@/views/workouts/AddWorkout.vue'
),
meta: {
title: 'workouts.ADD_WORKOUT',
},
},
{
path: '/admin',
@ -289,22 +391,34 @@ const routes: Array<RouteRecordRaw> = [
path: '',
name: 'AdministrationMenu',
component: AdminMenu,
meta: {
title: 'admin.ADMINISTRATION',
},
},
{
path: 'application',
name: 'ApplicationAdministration',
component: AdminApplication,
meta: {
title: 'admin.APP_CONFIG.TITLE',
},
},
{
path: 'application/edit',
name: 'ApplicationAdministrationEdition',
component: AdminApplication,
props: { edition: true },
meta: {
title: 'admin.APPLICATION',
},
},
{
path: 'sports',
name: 'SportsAdministration',
component: AdminSports,
meta: {
title: 'admin.SPORTS.TITLE',
},
},
{
path: 'users/:username',
@ -312,11 +426,18 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: 'profile' */ '@/views/user/UserView.vue'),
props: { fromAdmin: true },
meta: {
title: 'admin.USER',
count: 1,
},
},
{
path: 'users',
name: 'UsersAdministration',
component: AdminUsers,
meta: {
title: 'admin.USERS.TITLE',
},
},
],
},
@ -324,16 +445,25 @@ const routes: Array<RouteRecordRaw> = [
path: '/about',
name: 'About',
component: AboutView,
meta: {
title: 'common.ABOUT',
},
},
{
path: '/privacy-policy',
name: 'PrivacyPolicy',
component: PrivacyPolicyView,
meta: {
title: 'privacy_policy.TITLE',
},
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundView,
meta: {
title: 'error.NOT_FOUND.PAGE',
},
},
]
@ -357,6 +487,17 @@ const pathsWithoutAuthentication = [
const pathsWithoutChecks = ['/email-update', '/about', '/privacy-policy']
router.beforeEach((to, from, next) => {
if ('title' in to.meta) {
const title = typeof to.meta.title === 'string' ? to.meta.title : ''
const translatedTitle = title
? typeof to.meta.count === 'number'
? t(title, +to.meta.count)
: t(title)
: ''
window.document.title = `FitTrackee${
title ? ` - ${capitalize(translatedTitle)}` : ''
}`
}
store
.dispatch(AUTH_USER_STORE.ACTIONS.CHECK_AUTH_USER)
.then(() => {

View File

@ -79,6 +79,30 @@ button {
color: var(--app-color);
padding: 6px 14px;
&.transparent {
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
font-size: 1em;
border-color: transparent;
box-shadow: none;
&:hover, &:disabled {
background: transparent;
}
&:hover {
color: var(--app-color);
}
&:enabled:active {
box-shadow: none;
}
&:disabled, &.confirm:disabled {
border-color: transparent;
color: var(--disabled-color);
}
}
&:hover {
background: var(--app-color);
color: var(--button-hover-color);

View File

@ -1,7 +1,7 @@
:root {
--app-background-color: #FFFFFF;
--app-color: #2c3e50;
--app-color-light: #808b96;
--app-color-light: #6f7070;
--app-a-color: #40578a;
--app-shadow-color: lightgrey;
--app-loading-color: #f3f3f3;
@ -23,8 +23,8 @@
--input-error-color: #dc3545;
--dropdown-hover-color: #eff0f5;
--custom-checkbox-border-color: #9da3af;
--custom-checkbox-checked-bg-color: #9da3af;
--custom-checkbox-border-color: #6d797a;
--custom-checkbox-checked-bg-color: #6d797a;
--custom-checkbox-checked-color: #FFFFFF;
--calendar-border-color: #c4c7cf;
@ -42,7 +42,7 @@
--footer-background-color: #FFFFFF;
--footer-border-color: #ebeef3;
--footer-color: #8b8c8c;
--footer-color: #6f7070;
--alert-background-color: #d6dde3;
--alert-color: #3f3f3f;
@ -54,7 +54,7 @@
--success-color: #306430;
--disabled-background-color: #e0e0e0;
--disabled-color: #a3a3a3;
--disabled-color: #727272;
--disabled-sport-color: #616161;
--scroll-button-bg-color: rgba(255, 255, 255, .7);
@ -63,7 +63,7 @@
--workout-img-color: invert(22%) sepia(25%) saturate(646%) hue-rotate(169deg)
brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #666666;
--workout-no-map-color: #585959;
--cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969;
@ -76,6 +76,6 @@
--password-color-good: #acc578;
--password-color-strong: #57c255;
--scroll-thumb-color: #b9bcbd;
--scroll-thumb-color: #949697;
}

View File

@ -5,9 +5,10 @@
<template #title>{{ $t('statistics.STATISTICS') }}</template>
<template #content>
<Statistics
:class="{ 'stats-disabled': authUser.nb_workouts === 0 }"
:class="{ 'stats-disabled': isDisabled }"
:user="authUser"
:sports="sports"
:isDisabled="isDisabled"
/>
</template>
</Card>
@ -36,6 +37,9 @@
authUser.value.sports_list.includes(sport.id)
)
)
const isDisabled: ComputedRef<boolean> = computed(
() => authUser.value.nb_workouts === 0
)
</script>
<style lang="scss" scoped>