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('id') == 'password'
assert inputs[1].get_attribute('type') == '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 button.get_attribute('type') == 'submit'
assert 'Log in' in button.text assert 'Log in' in button.text
@ -47,4 +47,7 @@ class TestLogin:
assert 'Statistics' in nav assert 'Statistics' in nav
assert 'Add a workout' in nav assert 'Add a workout' in nav
assert user['username'] 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: class TestLogout:
def test_user_can_log_out(self, selenium): def test_user_can_log_out(self, selenium):
user = register_valid_user(selenium) user = register_valid_user(selenium)
user_menu = selenium.find_element(By.CLASS_NAME, 'nav-items-user-menu') logout_button = selenium.find_elements(By.CLASS_NAME, 'logout-button')[
logout_link = user_menu.find_elements(By.CLASS_NAME, 'nav-item')[2] 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) selenium.implicitly_wait(1)
nav = selenium.find_element(By.ID, 'nav').text nav = selenium.find_element(By.ID, 'nav').text
assert 'Register' in nav assert 'Register' in nav
assert 'Login' in nav assert 'Login' in nav
assert user['username'] not 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[1].text == 'Enter a valid email address.'
assert form_infos[2].text == 'At least 8 characters required.' 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 button.get_attribute('type') == 'submit'
assert 'Register' in button.text assert 'Register' in button.text
@ -84,7 +84,11 @@ class TestRegistration:
register(selenium, user) register(selenium, user)
assert selenium.current_url == URL 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 assert 'Sorry, that username is already taken.' in errors
def test_user_does_not_return_error_if_email_is_already_taken( 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') 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() confirm_button.click()
WebDriverWait(selenium, 10).until( WebDriverWait(selenium, 10).until(

View File

@ -34,7 +34,7 @@ def register(selenium, user):
password.send_keys(user.get('password')) password.send_keys(user.get('password'))
accepted_policy = selenium.find_element(By.ID, 'accepted_policy') accepted_policy = selenium.find_element(By.ID, 'accepted_policy')
accepted_policy.click() 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() submit_button.click()
@ -45,7 +45,7 @@ def login(selenium, user):
email.send_keys(user.get('email')) email.send_keys(user.get('email'))
password = selenium.find_element(By.ID, 'password') password = selenium.find_element(By.ID, 'password')
password.send_keys(user.get('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() submit_button.click()
@ -65,8 +65,12 @@ def register_valid_user(selenium):
def register_valid_user_and_logout(selenium): def register_valid_user_and_logout(selenium):
user = register_valid_user(selenium) user = register_valid_user(selenium)
user_menu = selenium.find_element(By.CLASS_NAME, 'nav-items-user-menu') 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 = user_menu.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)
return user 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}}]); "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.d4857496.js.map //# 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> <template>
<div id="top" /> <div id="top" />
<NavBar @menuInteraction="updateHideScrollBar" /> <NavBar @menuInteraction="updateHideScrollBar" />
<div v-if="appLoading" class="app-container"> <main>
<div class="app-loading"> <div v-if="appLoading" class="app-container">
<Loader /> <div class="app-loading">
<Loader />
</div>
</div> </div>
</div> <div v-else class="app-container" :class="{ 'hide-scroll': hideScrollBar }">
<div v-else class="app-container" :class="{ 'hide-scroll': hideScrollBar }"> <router-view v-if="appConfig" />
<router-view v-if="appConfig" /> <NoConfig v-else />
<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> </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 <Footer
v-if="appConfig" v-if="appConfig"
:version="appConfig ? appConfig.version : ''" :version="appConfig ? appConfig.version : ''"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="chart-menu"> <div class="chart-menu">
<div class="chart-arrow"> <button
<i class="chart-arrow transparent"
class="fa fa-chevron-left" @click="emit('arrowClick', true)"
aria-hidden="true" @keydown.enter="emit('arrowClick', true)"
@click="emit('arrowClick', true)" :disabled="isDisabled"
/> >
</div> <i class="fa fa-chevron-left" aria-hidden="true" />
</button>
<div class="time-frames custom-checkboxes-group"> <div class="time-frames custom-checkboxes-group">
<div class="time-frames-checkboxes custom-checkboxes"> <div class="time-frames-checkboxes custom-checkboxes">
<div <div
@ -21,24 +22,38 @@
:name="frame" :name="frame"
:checked="selectedTimeFrame === frame" :checked="selectedTimeFrame === frame"
@input="onUpdateTimeFrame(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> </label>
</div> </div>
</div> </div>
</div> </div>
<div class="chart-arrow"> <button
<i class="chart-arrow transparent"
class="fa fa-chevron-right" @click="emit('arrowClick', false)"
aria-hidden="true" @keydown.enter="emit('arrowClick', false)"
@click="emit('arrowClick', false)" :disabled="isDisabled"
/> >
</div> <i class="fa fa-chevron-right" aria-hidden="true" />
</button>
</div> </div>
</template> </template>
<script setup lang="ts"> <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']) const emit = defineEmits(['arrowClick', 'timeFrameUpdate'])
@ -49,11 +64,21 @@
selectedTimeFrame.value = timeFrame selectedTimeFrame.value = timeFrame
emit('timeFrameUpdate', timeFrame) emit('timeFrameUpdate', timeFrame)
} }
onMounted(() => {
if (!isDisabled.value) {
const input = document.getElementById('frame-month')
if (input) {
input.focus()
}
}
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chart-menu { .chart-menu {
display: flex; display: flex;
align-items: center;
.chart-arrow, .chart-arrow,
.time-frames { .time-frames {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,7 +59,8 @@
"TABLE": { "TABLE": {
"ADD_ADMIN_RIGHTS": "Add admin rights", "ADD_ADMIN_RIGHTS": "Add admin rights",
"REMOVE_ADMIN_RIGHTS": "Remove admin rights" "REMOVE_ADMIN_RIGHTS": "Remove admin rights"
} },
"TITLE": "Administration - Users"
}, },
"USER_EMAIL_UPDATE_SUCCESSFUL": "The email address has been updated." "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_NOT_RECEIVED": "Didn't received instructions?",
"ACCOUNT_CONFIRMATION_SENT": "Check your email. A new confirmation email has been sent to the address provided.", "ACCOUNT_CONFIRMATION_SENT": "Check your email. A new confirmation email has been sent to the address provided.",
"ADMIN": "Admin", "ADMIN": "Admin",
@ -7,6 +8,7 @@
"CURRENT_PASSWORD": "Current password", "CURRENT_PASSWORD": "Current password",
"EMAIL": "Email", "EMAIL": "Email",
"EMAIL_INFO": "Enter a valid email address.", "EMAIL_INFO": "Enter a valid email address.",
"EMAIL_UPDATE": "Email update",
"ENTER_PASSWORD": "Enter a password", "ENTER_PASSWORD": "Enter a password",
"EXPORT_REQUEST": { "EXPORT_REQUEST": {
"DATA_EXPORT": "Data export", "DATA_EXPORT": "Data export",
@ -26,6 +28,7 @@
"LAST_PRIVACY_POLICY_TO_VALIDATE": "The privacy policy has been updated, please {0} it before proceeding.", "LAST_PRIVACY_POLICY_TO_VALIDATE": "The privacy policy has been updated, please {0} it before proceeding.",
"LOGIN": "Login", "LOGIN": "Login",
"LOGOUT": "Logout", "LOGOUT": "Logout",
"LOGOUT_CONFIRMATION": "Are you sure you want to log out?",
"LOG_IN": "log in", "LOG_IN": "log in",
"NEW_PASSWORD": "New password", "NEW_PASSWORD": "New password",
"NO_USERS_FOUND": "No users found.", "NO_USERS_FOUND": "No users found.",

View File

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

View File

@ -59,7 +59,8 @@
"TABLE": { "TABLE": {
"ADD_ADMIN_RIGHTS": "Ajouter les droits d'admin", "ADD_ADMIN_RIGHTS": "Ajouter les droits d'admin",
"REMOVE_ADMIN_RIGHTS": "Retirer 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." "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_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.", "ACCOUNT_CONFIRMATION_SENT": "Vérifiez vos courriels. Un nouveau courriel de confirmation a été envoyé à l'adresse électronique fournie.",
"ADMIN": "Admin", "ADMIN": "Admin",
@ -7,6 +8,7 @@
"CURRENT_PASSWORD": "Mot de passe actuel", "CURRENT_PASSWORD": "Mot de passe actuel",
"EMAIL": "Courriel", "EMAIL": "Courriel",
"EMAIL_INFO": "Saisissez une adresse électronique valide.", "EMAIL_INFO": "Saisissez une adresse électronique valide.",
"EMAIL_UPDATE": "Mise à jour de l'adresse électronique",
"ENTER_PASSWORD": "Saisissez un mot de passe", "ENTER_PASSWORD": "Saisissez un mot de passe",
"EXPORT_REQUEST": { "EXPORT_REQUEST": {
"DATA_EXPORT": "Export des données", "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.", "LAST_PRIVACY_POLICY_TO_VALIDATE": "La politique de confidentialité a été mise à jour. Veuillez l'{0} avant de poursuivre.",
"LOGIN": "Se connecter", "LOGIN": "Se connecter",
"LOGOUT": "Se déconnecter", "LOGOUT": "Se déconnecter",
"LOGOUT_CONFIRMATION": "Etes-vous sûr de vouloir vous déconnecter ?",
"LOG_IN": "connecter", "LOG_IN": "connecter",
"NEW_PASSWORD": "Nouveau mot de passe", "NEW_PASSWORD": "Nouveau mot de passe",
"NO_USERS_FOUND": "Aucun utilisateur trouvé.", "NO_USERS_FOUND": "Aucun utilisateur trouvé.",

View File

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

View File

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

View File

@ -79,6 +79,30 @@ button {
color: var(--app-color); color: var(--app-color);
padding: 6px 14px; 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 { &:hover {
background: var(--app-color); background: var(--app-color);
color: var(--button-hover-color); color: var(--button-hover-color);

View File

@ -1,7 +1,7 @@
:root { :root {
--app-background-color: #FFFFFF; --app-background-color: #FFFFFF;
--app-color: #2c3e50; --app-color: #2c3e50;
--app-color-light: #808b96; --app-color-light: #6f7070;
--app-a-color: #40578a; --app-a-color: #40578a;
--app-shadow-color: lightgrey; --app-shadow-color: lightgrey;
--app-loading-color: #f3f3f3; --app-loading-color: #f3f3f3;
@ -23,8 +23,8 @@
--input-error-color: #dc3545; --input-error-color: #dc3545;
--dropdown-hover-color: #eff0f5; --dropdown-hover-color: #eff0f5;
--custom-checkbox-border-color: #9da3af; --custom-checkbox-border-color: #6d797a;
--custom-checkbox-checked-bg-color: #9da3af; --custom-checkbox-checked-bg-color: #6d797a;
--custom-checkbox-checked-color: #FFFFFF; --custom-checkbox-checked-color: #FFFFFF;
--calendar-border-color: #c4c7cf; --calendar-border-color: #c4c7cf;
@ -42,7 +42,7 @@
--footer-background-color: #FFFFFF; --footer-background-color: #FFFFFF;
--footer-border-color: #ebeef3; --footer-border-color: #ebeef3;
--footer-color: #8b8c8c; --footer-color: #6f7070;
--alert-background-color: #d6dde3; --alert-background-color: #d6dde3;
--alert-color: #3f3f3f; --alert-color: #3f3f3f;
@ -54,7 +54,7 @@
--success-color: #306430; --success-color: #306430;
--disabled-background-color: #e0e0e0; --disabled-background-color: #e0e0e0;
--disabled-color: #a3a3a3; --disabled-color: #727272;
--disabled-sport-color: #616161; --disabled-sport-color: #616161;
--scroll-button-bg-color: rgba(255, 255, 255, .7); --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) --workout-img-color: invert(22%) sepia(25%) saturate(646%) hue-rotate(169deg)
brightness(97%) contrast(96%); brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea; --workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #666666; --workout-no-map-color: #585959;
--cell-heading-bg-color: #eeeeee; --cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969; --cell-heading-color: #696969;
@ -76,6 +76,6 @@
--password-color-good: #acc578; --password-color-good: #acc578;
--password-color-strong: #57c255; --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 #title>{{ $t('statistics.STATISTICS') }}</template>
<template #content> <template #content>
<Statistics <Statistics
:class="{ 'stats-disabled': authUser.nb_workouts === 0 }" :class="{ 'stats-disabled': isDisabled }"
:user="authUser" :user="authUser"
:sports="sports" :sports="sports"
:isDisabled="isDisabled"
/> />
</template> </template>
</Card> </Card>
@ -36,6 +37,9 @@
authUser.value.sports_list.includes(sport.id) authUser.value.sports_list.includes(sport.id)
) )
) )
const isDisabled: ComputedRef<boolean> = computed(
() => authUser.value.nb_workouts === 0
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>