Merge pull request #111 from SamR1/imperial-units
Display workouts with imperial units
This commit is contained in:
commit
c836c0da7a
@ -6,6 +6,7 @@
|
||||
|
||||
#### New Features
|
||||
|
||||
* [#99](https://github.com/SamR1/FitTrackee/issues/99) - Display workout with imperial units
|
||||
* [#91](https://github.com/SamR1/FitTrackee/issues/91) - Display elevation chart with min and max altitude of workout
|
||||
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
||||
@ -23,7 +24,7 @@
|
||||
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
|
||||
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports
|
||||
|
||||
In this release 5 issue were closed.
|
||||
In this release 6 issue were closed.
|
||||
|
||||
|
||||
## Version 0.4.9 (2021/07/16)
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
#### New Features
|
||||
|
||||
* [#99](https://github.com/SamR1/FitTrackee/issues/99) - Display workout with imperial units
|
||||
* [#91](https://github.com/SamR1/FitTrackee/issues/91) - Display elevation chart with min and max altitude of workout
|
||||
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
||||
@ -23,7 +24,7 @@
|
||||
* [#98/#109](https://github.com/SamR1/FitTrackee/pull/109) - Added stopped_speed_threshold to support slow movement
|
||||
* [#84/#93](https://github.com/SamR1/FitTrackee/pull/93) - Add elevation data and new sports
|
||||
|
||||
In this release 5 issue were closed.
|
||||
In this release 6 issue were closed.
|
||||
|
||||
|
||||
## Version 0.4.9 (2021/07/16)
|
||||
|
@ -34,8 +34,9 @@ Administration
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
@ -72,7 +73,7 @@ Workouts
|
||||
It can be overridden in user preferences.
|
||||
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- Workout creation by uploading a gpx file (related data are stored in database with metric system). A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed.
|
||||
- Workout edition and deletion. User can add a note.
|
||||
- User statistics
|
||||
|
@ -319,6 +319,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"sam@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
@ -419,6 +420,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"sam@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
@ -537,6 +539,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"sam@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
|
@ -160,6 +160,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"admin@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
@ -297,6 +298,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"admin@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
@ -442,6 +444,7 @@
|
||||
<span class="nt">"created_at"</span><span class="p">:</span> <span class="s2">"Sun, 14 Jul 2019 14:09:58 GMT"</span><span class="p">,</span>
|
||||
<span class="nt">"email"</span><span class="p">:</span> <span class="s2">"admin@example.com"</span><span class="p">,</span>
|
||||
<span class="nt">"first_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"imperial_units"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
|
||||
<span class="nt">"language"</span><span class="p">:</span> <span class="s2">"en"</span><span class="p">,</span>
|
||||
<span class="nt">"last_name"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
<span class="nt">"location"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
|
||||
|
@ -277,6 +277,7 @@
|
||||
<section id="new-features">
|
||||
<h4>New Features<a class="headerlink" href="#new-features" title="Permalink to this headline">¶</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/99">#99</a> - Display workout with imperial units</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/91">#91</a> - Display elevation chart with min and max altitude of workout</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/90">#90</a> - Add user sports preferences</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/issues/18">#18</a> - Better UI</p></li>
|
||||
@ -301,7 +302,7 @@
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/109">#98/#109</a> - Added stopped_speed_threshold to support slow movement</p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/SamR1/FitTrackee/pull/93">#84/#93</a> - Add elevation data and new sports</p></li>
|
||||
</ul>
|
||||
<p>In this release 5 issue were closed.</p>
|
||||
<p>In this release 6 issue were closed.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="version-0-4-9-2021-07-16">
|
||||
|
@ -178,8 +178,9 @@
|
||||
<h3>Account & preferences<a class="headerlink" href="#account-preferences" title="Permalink to this headline">¶</a></h3>
|
||||
<ul class="simple">
|
||||
<li><p>A user can create, update and deleted his account</p></li>
|
||||
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
|
||||
<li><p>A user can set language, timezone and first day of week.</p></li>
|
||||
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
|
||||
<li><p>A user can choose between metric system and imperial system for distance, elevation and speed display (<em>new in 0.5.0</em>)</p></li>
|
||||
<li><dl class="simple">
|
||||
<dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul>
|
||||
<li><p>change sport color (used for sport image and charts)</p></li>
|
||||
@ -236,7 +237,7 @@
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.</p></li>
|
||||
<li><p>Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).</p></li>
|
||||
<li><p>Workout creation by uploading a gpx file (related data are stored in database with metric system). A workout can even be created without gpx (the user must enter date, time, duration and distance).</p></li>
|
||||
<li><p>A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed.</p></li>
|
||||
<li><p>Workout edition and deletion. User can add a note.</p></li>
|
||||
<li><p>User statistics</p></li>
|
||||
|
File diff suppressed because one or more lines are too long
@ -34,8 +34,9 @@ Administration
|
||||
Account & preferences
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
- A user can create, update and deleted his account
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can set language, timezone and first day of week.
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
@ -72,7 +73,7 @@ Workouts
|
||||
It can be overridden in user preferences.
|
||||
|
||||
- Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.
|
||||
- Workout creation by uploading a gpx file. A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- Workout creation by uploading a gpx file (related data are stored in database with metric system). A workout can even be created without gpx (the user must enter date, time, duration and distance).
|
||||
- A workout with a gpx file can be displayed with map, weather (if the DarkSky API key is provided) and charts (speed and elevation). Segments can be displayed.
|
||||
- Workout edition and deletion. User can add a note.
|
||||
- User statistics
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Dict
|
||||
|
||||
from flask import current_app
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.event import listens_for
|
||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||
@ -25,7 +26,15 @@ class AppConfig(BaseModel):
|
||||
|
||||
@property
|
||||
def is_registration_enabled(self) -> bool:
|
||||
try:
|
||||
nb_users = User.query.count()
|
||||
except exc.ProgrammingError as e:
|
||||
# workaround for user model related migrations
|
||||
if 'psycopg2.errors.UndefinedColumn' in str(e):
|
||||
result = db.engine.execute("SELECT COUNT(*) FROM users;")
|
||||
nb_users = result.fetchone()[0]
|
||||
else:
|
||||
raise e
|
||||
return self.max_users == 0 or nb_users < self.max_users
|
||||
|
||||
@property
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.babfd43e.css" rel="prefetch"><link href="/static/css/main.7229c1ab.css" rel="prefetch"><link href="/static/css/main~workouts.0edb3403.css" rel="prefetch"><link href="/static/css/profile.05400f70.css" rel="prefetch"><link href="/static/css/reset.46776e72.css" rel="prefetch"><link href="/static/css/workouts.1b0a7916.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.db9cee98.js" rel="prefetch"><link href="/static/js/main~workouts.a74990d7.js" rel="prefetch"><link href="/static/js/profile.62578012.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.d69cf48a.js" rel="prefetch"><link href="/static/css/app.e1e7e23c.css" rel="preload" as="style"><link href="/static/js/app.0f3b3ab5.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.71654064.js" rel="preload" as="script"><link href="/static/css/app.e1e7e23c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.71654064.js"></script><script src="/static/js/app.0f3b3ab5.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"><link rel="stylesheet" href="/static/css/leaflet.css"><title>FitTrackee</title><link href="/static/css/admin.babfd43e.css" rel="prefetch"><link href="/static/css/main.f9856c63.css" rel="prefetch"><link href="/static/css/main~workouts.0edb3403.css" rel="prefetch"><link href="/static/css/profile.05400f70.css" rel="prefetch"><link href="/static/css/reset.46776e72.css" rel="prefetch"><link href="/static/css/workouts.84cbed34.css" rel="prefetch"><link href="/static/js/admin.2f1d393d.js" rel="prefetch"><link href="/static/js/chunk-2d0c9189.c81458cc.js" rel="prefetch"><link href="/static/js/chunk-2d0cf391.020c75ea.js" rel="prefetch"><link href="/static/js/chunk-2d0da8f3.c8c3e7e8.js" rel="prefetch"><link href="/static/js/chunk-2d2248b6.d84473c1.js" rel="prefetch"><link href="/static/js/chunk-2d22523a.4b710d99.js" rel="prefetch"><link href="/static/js/main.23f4d3a6.js" rel="prefetch"><link href="/static/js/main~workouts.6afa0411.js" rel="prefetch"><link href="/static/js/profile.62578012.js" rel="prefetch"><link href="/static/js/reset.518e646f.js" rel="prefetch"><link href="/static/js/workouts.ca9449b1.js" rel="prefetch"><link href="/static/css/app.2b8c39ab.css" rel="preload" as="style"><link href="/static/js/app.28d0829a.js" rel="preload" as="script"><link href="/static/js/chunk-vendors.caa4fc1c.js" rel="preload" as="script"><link href="/static/css/app.2b8c39ab.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/static/js/chunk-vendors.caa4fc1c.js"></script><script src="/static/js/app.28d0829a.js"></script></body></html>
|
@ -64,7 +64,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/img/workouts/mountains.svg"
|
||||
},
|
||||
{
|
||||
"revision": "926210f132992651a9543d9c76da25ba",
|
||||
"revision": "a83d95cb780cb551f9c6e0257addee7a",
|
||||
"url": "/index.html"
|
||||
},
|
||||
{
|
||||
@ -80,8 +80,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/css/admin.babfd43e.css"
|
||||
},
|
||||
{
|
||||
"revision": "4f95d958d90a2ac1b9a0",
|
||||
"url": "/static/css/app.e1e7e23c.css"
|
||||
"revision": "580dbac1a3cc1ff6f809",
|
||||
"url": "/static/css/app.2b8c39ab.css"
|
||||
},
|
||||
{
|
||||
"revision": "82c1118c918377daaa71a320ab8eea42",
|
||||
@ -92,11 +92,11 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/css/leaflet.css"
|
||||
},
|
||||
{
|
||||
"revision": "00c35b353719122c16cd",
|
||||
"url": "/static/css/main.7229c1ab.css"
|
||||
"revision": "3e2dd5ce7fd86f47e0e5",
|
||||
"url": "/static/css/main.f9856c63.css"
|
||||
},
|
||||
{
|
||||
"revision": "11b770a11a1cd8dae5f4",
|
||||
"revision": "ac1280c03a31a5894834",
|
||||
"url": "/static/css/main~workouts.0edb3403.css"
|
||||
},
|
||||
{
|
||||
@ -108,8 +108,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/css/reset.46776e72.css"
|
||||
},
|
||||
{
|
||||
"revision": "c78ff76a4bb0919c4b94",
|
||||
"url": "/static/css/workouts.1b0a7916.css"
|
||||
"revision": "03d9a79c5f845c47ef9c",
|
||||
"url": "/static/css/workouts.84cbed34.css"
|
||||
},
|
||||
{
|
||||
"revision": "e719f9244c69e28e7d00e725ca1e280e",
|
||||
@ -196,8 +196,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/js/admin.2f1d393d.js"
|
||||
},
|
||||
{
|
||||
"revision": "4f95d958d90a2ac1b9a0",
|
||||
"url": "/static/js/app.0f3b3ab5.js"
|
||||
"revision": "580dbac1a3cc1ff6f809",
|
||||
"url": "/static/js/app.28d0829a.js"
|
||||
},
|
||||
{
|
||||
"revision": "bd7d183c9f68e5f4027d",
|
||||
@ -220,16 +220,16 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/js/chunk-2d22523a.4b710d99.js"
|
||||
},
|
||||
{
|
||||
"revision": "1631aa1204c2ef00fa57",
|
||||
"url": "/static/js/chunk-vendors.71654064.js"
|
||||
"revision": "c04fcf32d84e5ec5cb38",
|
||||
"url": "/static/js/chunk-vendors.caa4fc1c.js"
|
||||
},
|
||||
{
|
||||
"revision": "00c35b353719122c16cd",
|
||||
"url": "/static/js/main.db9cee98.js"
|
||||
"revision": "3e2dd5ce7fd86f47e0e5",
|
||||
"url": "/static/js/main.23f4d3a6.js"
|
||||
},
|
||||
{
|
||||
"revision": "11b770a11a1cd8dae5f4",
|
||||
"url": "/static/js/main~workouts.a74990d7.js"
|
||||
"revision": "ac1280c03a31a5894834",
|
||||
"url": "/static/js/main~workouts.6afa0411.js"
|
||||
},
|
||||
{
|
||||
"revision": "058a877bc4b9cbf8929f",
|
||||
@ -240,7 +240,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
"url": "/static/js/reset.518e646f.js"
|
||||
},
|
||||
{
|
||||
"revision": "c78ff76a4bb0919c4b94",
|
||||
"url": "/static/js/workouts.d69cf48a.js"
|
||||
"revision": "03d9a79c5f845c47ef9c",
|
||||
"url": "/static/js/workouts.ca9449b1.js"
|
||||
}
|
||||
]);
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
@ -14,7 +14,7 @@
|
||||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
||||
|
||||
importScripts(
|
||||
"/precache-manifest.c1f31e9729586ecf3c442890704f31cc.js"
|
||||
"/precache-manifest.d81ab1e239beb2ec33c92fe076422816.js"
|
||||
);
|
||||
|
||||
workbox.core.setCacheNameDetails({prefix: "fittrackee_client"});
|
||||
|
1
fittrackee/dist/static/css/app.2b8c39ab.css
vendored
Normal file
1
fittrackee/dist/static/css/app.2b8c39ab.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/app.e1e7e23c.css
vendored
1
fittrackee/dist/static/css/app.e1e7e23c.css
vendored
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
1
fittrackee/dist/static/css/workouts.84cbed34.css
vendored
Normal file
1
fittrackee/dist/static/css/workouts.84cbed34.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.0f3b3ab5.js
vendored
2
fittrackee/dist/static/js/app.0f3b3ab5.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/app.28d0829a.js
vendored
Normal file
2
fittrackee/dist/static/js/app.28d0829a.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.28d0829a.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.28d0829a.js.map
vendored
Normal file
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
1
fittrackee/dist/static/js/chunk-vendors.caa4fc1c.js.map
vendored
Normal file
1
fittrackee/dist/static/js/chunk-vendors.caa4fc1c.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main.23f4d3a6.js
vendored
Normal file
2
fittrackee/dist/static/js/main.23f4d3a6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/main.23f4d3a6.js.map
vendored
Normal file
1
fittrackee/dist/static/js/main.23f4d3a6.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main.db9cee98.js
vendored
2
fittrackee/dist/static/js/main.db9cee98.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main~workouts.6afa0411.js
vendored
Normal file
2
fittrackee/dist/static/js/main~workouts.6afa0411.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/main~workouts.6afa0411.js.map
vendored
Normal file
1
fittrackee/dist/static/js/main~workouts.6afa0411.js.map
vendored
Normal file
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
2
fittrackee/dist/static/js/workouts.ca9449b1.js
vendored
Normal file
2
fittrackee/dist/static/js/workouts.ca9449b1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/workouts.ca9449b1.js.map
vendored
Normal file
1
fittrackee/dist/static/js/workouts.ca9449b1.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,31 @@
|
||||
"""add imperial units preferences
|
||||
|
||||
Revision ID: 07188ca7620a
|
||||
Revises: 080acc8ee956
|
||||
Create Date: 2021-11-13 19:11:17.753567
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '07188ca7620a'
|
||||
down_revision = '080acc8ee956'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'users',
|
||||
sa.Column('imperial_units', sa.Boolean(), nullable=True),
|
||||
)
|
||||
op.execute("UPDATE users SET imperial_units = false")
|
||||
op.alter_column('users', 'imperial_units', nullable=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'imperial_units')
|
||||
# ### end Alembic commands ###
|
@ -487,6 +487,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
||||
assert not data['data']['admin']
|
||||
assert data['data']['timezone'] is None
|
||||
assert data['data']['weekm'] is False
|
||||
assert data['data']['imperial_units'] is False
|
||||
assert data['data']['language'] is None
|
||||
assert data['data']['nb_sports'] == 0
|
||||
assert data['data']['nb_workouts'] == 0
|
||||
@ -517,6 +518,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
||||
assert data['data']['last_name'] == 'Doe'
|
||||
assert data['data']['birth_date']
|
||||
assert data['data']['bio'] == 'just a random guy'
|
||||
assert data['data']['imperial_units'] is False
|
||||
assert data['data']['location'] == 'somewhere'
|
||||
assert data['data']['timezone'] == 'America/New_York'
|
||||
assert data['data']['weekm'] is False
|
||||
@ -553,6 +555,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
||||
assert data['data']['created_at']
|
||||
assert not data['data']['admin']
|
||||
assert data['data']['timezone'] is None
|
||||
assert data['data']['imperial_units'] is False
|
||||
assert data['data']['nb_sports'] == 2
|
||||
assert data['data']['nb_workouts'] == 2
|
||||
assert len(data['data']['records']) == 6
|
||||
@ -605,6 +608,7 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
||||
assert data['data']['last_name'] == 'Doe'
|
||||
assert data['data']['birth_date']
|
||||
assert data['data']['bio'] == 'Nothing to tell'
|
||||
assert data['data']['imperial_units'] is False
|
||||
assert data['data']['location'] == 'Somewhere'
|
||||
assert data['data']['timezone'] is None
|
||||
assert data['data']['weekm'] is False
|
||||
@ -648,6 +652,7 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
||||
assert data['data']['last_name'] == 'Doe'
|
||||
assert data['data']['birth_date']
|
||||
assert data['data']['bio'] == 'Nothing to tell'
|
||||
assert data['data']['imperial_units'] is False
|
||||
assert data['data']['location'] == 'Somewhere'
|
||||
assert data['data']['timezone'] is None
|
||||
assert data['data']['weekm'] is False
|
||||
@ -767,6 +772,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
timezone='America/New_York',
|
||||
weekm=True,
|
||||
language='fr',
|
||||
imperial_units=True,
|
||||
)
|
||||
),
|
||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||
@ -784,6 +790,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
||||
assert data['data']['last_name'] is None
|
||||
assert data['data']['birth_date'] is None
|
||||
assert data['data']['bio'] is None
|
||||
assert data['data']['imperial_units']
|
||||
assert data['data']['location'] is None
|
||||
assert data['data']['timezone'] == 'America/New_York'
|
||||
assert data['data']['weekm'] is True
|
||||
|
@ -36,6 +36,7 @@ class TestGetUser(ApiTestCaseMixin):
|
||||
assert user['last_name'] is None
|
||||
assert user['birth_date'] is None
|
||||
assert user['bio'] is None
|
||||
assert user['imperial_units'] is False
|
||||
assert user['location'] is None
|
||||
assert user['timezone'] is None
|
||||
assert user['weekm'] is False
|
||||
@ -77,6 +78,7 @@ class TestGetUser(ApiTestCaseMixin):
|
||||
assert user['last_name'] is None
|
||||
assert user['birth_date'] is None
|
||||
assert user['bio'] is None
|
||||
assert user['imperial_units'] is False
|
||||
assert user['location'] is None
|
||||
assert user['timezone'] is None
|
||||
assert user['weekm'] is False
|
||||
@ -129,6 +131,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert 'test@test.com' in data['data']['users'][0]['email']
|
||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
||||
assert 'sam@test.com' in data['data']['users'][2]['email']
|
||||
assert data['data']['users'][0]['imperial_units'] is False
|
||||
assert data['data']['users'][0]['timezone'] is None
|
||||
assert data['data']['users'][0]['weekm'] is False
|
||||
assert data['data']['users'][0]['language'] is None
|
||||
@ -138,6 +141,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert data['data']['users'][0]['sports_list'] == []
|
||||
assert data['data']['users'][0]['total_distance'] == 0
|
||||
assert data['data']['users'][0]['total_duration'] == '0:00:00'
|
||||
assert data['data']['users'][1]['imperial_units'] is False
|
||||
assert data['data']['users'][1]['timezone'] is None
|
||||
assert data['data']['users'][1]['weekm'] is False
|
||||
assert data['data']['users'][1]['language'] is None
|
||||
@ -147,6 +151,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert data['data']['users'][1]['sports_list'] == []
|
||||
assert data['data']['users'][1]['total_distance'] == 0
|
||||
assert data['data']['users'][1]['total_duration'] == '0:00:00'
|
||||
assert data['data']['users'][2]['imperial_units'] is False
|
||||
assert data['data']['users'][2]['timezone'] is None
|
||||
assert data['data']['users'][2]['weekm'] is True
|
||||
assert data['data']['users'][2]['language'] is None
|
||||
@ -196,6 +201,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert 'test@test.com' in data['data']['users'][0]['email']
|
||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
||||
assert 'sam@test.com' in data['data']['users'][2]['email']
|
||||
assert data['data']['users'][0]['imperial_units'] is False
|
||||
assert data['data']['users'][0]['timezone'] is None
|
||||
assert data['data']['users'][0]['weekm'] is False
|
||||
assert data['data']['users'][0]['nb_sports'] == 2
|
||||
@ -204,6 +210,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert data['data']['users'][0]['sports_list'] == [1, 2]
|
||||
assert data['data']['users'][0]['total_distance'] == 22.0
|
||||
assert data['data']['users'][0]['total_duration'] == '2:40:00'
|
||||
assert data['data']['users'][1]['imperial_units'] is False
|
||||
assert data['data']['users'][1]['timezone'] is None
|
||||
assert data['data']['users'][1]['weekm'] is False
|
||||
assert data['data']['users'][1]['nb_sports'] == 1
|
||||
@ -212,6 +219,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
||||
assert data['data']['users'][1]['sports_list'] == [1]
|
||||
assert data['data']['users'][1]['total_distance'] == 15
|
||||
assert data['data']['users'][1]['total_duration'] == '1:00:00'
|
||||
assert data['data']['users'][2]['imperial_units'] is False
|
||||
assert data['data']['users'][2]['timezone'] is None
|
||||
assert data['data']['users'][2]['weekm'] is True
|
||||
assert data['data']['users'][2]['nb_sports'] == 0
|
||||
|
@ -14,6 +14,7 @@ class TestUserModel:
|
||||
assert serialized_user['admin'] is False
|
||||
assert serialized_user['first_name'] is None
|
||||
assert serialized_user['last_name'] is None
|
||||
assert serialized_user['imperial_units'] is False
|
||||
assert serialized_user['bio'] is None
|
||||
assert serialized_user['location'] is None
|
||||
assert serialized_user['birth_date'] is None
|
||||
|
@ -313,6 +313,7 @@ def get_authenticated_user_profile(
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
@ -412,6 +413,7 @@ def edit_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
@ -574,6 +576,7 @@ def edit_user_preferences(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "sam@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
@ -653,6 +656,7 @@ def edit_user_preferences(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
# get post data
|
||||
post_data = request.get_json()
|
||||
user_mandatory_data = {
|
||||
'imperial_units',
|
||||
'language',
|
||||
'timezone',
|
||||
'weekm',
|
||||
@ -660,12 +664,14 @@ def edit_user_preferences(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
||||
if not post_data or not post_data.keys() >= user_mandatory_data:
|
||||
return InvalidPayloadErrorResponse()
|
||||
|
||||
imperial_units = post_data.get('imperial_units')
|
||||
language = post_data.get('language')
|
||||
timezone = post_data.get('timezone')
|
||||
weekm = post_data.get('weekm')
|
||||
|
||||
try:
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
user.imperial_units = imperial_units
|
||||
user.language = language
|
||||
user.timezone = timezone
|
||||
user.weekm = weekm
|
||||
|
@ -40,6 +40,7 @@ class User(BaseModel):
|
||||
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
||||
)
|
||||
language = db.Column(db.String(50), nullable=True)
|
||||
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<User {self.username!r}>'
|
||||
@ -142,6 +143,7 @@ class User(BaseModel):
|
||||
],
|
||||
'total_distance': float(total[0]),
|
||||
'total_duration': str(total[1]),
|
||||
'imperial_units': self.imperial_units,
|
||||
}
|
||||
|
||||
|
||||
|
@ -64,6 +64,7 @@ def get_users(auth_user_id: int) -> Dict:
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "admin@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
@ -246,6 +247,7 @@ def get_single_user(
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "admin@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
@ -400,6 +402,7 @@ def update_user(
|
||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||
"email": "admin@example.com",
|
||||
"first_name": null,
|
||||
"imperial_units": false,
|
||||
"language": "en",
|
||||
"last_name": null,
|
||||
"location": null,
|
||||
|
62
fittrackee_client/src/components/Common/Distance.vue
Normal file
62
fittrackee_client/src/components/Common/Distance.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<span class="distance" :class="{ strong }">{{ convertedDistance }}</span>
|
||||
{{ ' ' }}
|
||||
<span v-if="displayUnit" class="unit" :class="{ strong }">
|
||||
{{ unitTo }}{{ speed ? '/h' : '' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComputedRef, computed, toRefs, withDefaults } from 'vue'
|
||||
|
||||
import { TUnit } from '@/types/units'
|
||||
import { units, convertDistance } from '@/utils/units'
|
||||
|
||||
interface Props {
|
||||
distance: number
|
||||
unitFrom: TUnit
|
||||
useImperialUnits: boolean
|
||||
digits?: number
|
||||
displayUnit?: boolean
|
||||
speed?: boolean
|
||||
strong?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
digits: 2,
|
||||
displayUnit: true,
|
||||
speed: false,
|
||||
strong: false,
|
||||
})
|
||||
|
||||
const {
|
||||
digits,
|
||||
displayUnit,
|
||||
distance,
|
||||
speed,
|
||||
strong,
|
||||
unitFrom,
|
||||
useImperialUnits,
|
||||
} = toRefs(props)
|
||||
const unitTo: ComputedRef<TUnit> = computed(() =>
|
||||
useImperialUnits.value
|
||||
? units[unitFrom.value].defaultTarget
|
||||
: unitFrom.value
|
||||
)
|
||||
const convertedDistance = computed(() =>
|
||||
useImperialUnits.value
|
||||
? convertDistance(
|
||||
distance.value,
|
||||
unitFrom.value,
|
||||
unitTo.value,
|
||||
digits.value
|
||||
)
|
||||
: parseFloat(distance.value.toFixed(digits.value))
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/scss/base.scss';
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
@ -40,6 +40,10 @@
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
useImperialUnits: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { t } = useI18n()
|
||||
@ -80,7 +84,12 @@
|
||||
ticks: {
|
||||
maxTicksLimit: 6,
|
||||
callback: function (value) {
|
||||
return formatTooltipValue(props.displayedData, +value, false)
|
||||
return formatTooltipValue(
|
||||
props.displayedData,
|
||||
+value,
|
||||
props.useImperialUnits,
|
||||
false
|
||||
)
|
||||
},
|
||||
},
|
||||
afterFit: function (scale: LayoutItem) {
|
||||
@ -108,7 +117,12 @@
|
||||
.reduce((total, value) => getSum(total, value), 0)
|
||||
return context.datasetIndex ===
|
||||
props.displayedSportIds.length - 1 && total > 0
|
||||
? formatTooltipValue(props.displayedData, total, false)
|
||||
? formatTooltipValue(
|
||||
props.displayedData,
|
||||
total,
|
||||
props.useImperialUnits,
|
||||
false
|
||||
)
|
||||
: null
|
||||
},
|
||||
},
|
||||
@ -132,7 +146,8 @@
|
||||
if (context.parsed.y !== null) {
|
||||
label += formatTooltipValue(
|
||||
props.displayedData,
|
||||
context.parsed.y
|
||||
context.parsed.y,
|
||||
props.useImperialUnits
|
||||
)
|
||||
}
|
||||
return label
|
||||
@ -144,7 +159,11 @@
|
||||
})
|
||||
return (
|
||||
`${t('common.TOTAL')}: ` +
|
||||
formatTooltipValue(props.displayedData, sum)
|
||||
formatTooltipValue(
|
||||
props.displayedData,
|
||||
sum,
|
||||
props.useImperialUnits
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
@ -58,6 +58,7 @@
|
||||
:displayedData="displayedData"
|
||||
:displayedSportIds="displayedSportIds"
|
||||
:fullStats="fullStats"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +135,8 @@
|
||||
props.user.weekm,
|
||||
props.sports,
|
||||
props.displayedSportIds,
|
||||
statistics.value
|
||||
statistics.value,
|
||||
props.user.imperial_units
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
<WorkoutCard
|
||||
v-for="index in [...Array(initWorkoutsCount).keys()]"
|
||||
:user="user"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
:key="index"
|
||||
/>
|
||||
</div>
|
||||
@ -18,6 +19,7 @@
|
||||
: null
|
||||
"
|
||||
:user="user"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
:key="workout.id"
|
||||
/>
|
||||
<NoWorkouts v-if="workouts.length === 0" />
|
||||
|
@ -13,6 +13,7 @@
|
||||
:sportTranslatedLabel="sportTranslatedLabel"
|
||||
:records="recordsBySport[sportTranslatedLabel]"
|
||||
:key="sportTranslatedLabel"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -40,7 +41,8 @@
|
||||
getRecordsBySports(
|
||||
props.user.records,
|
||||
translateSports(props.sports, t),
|
||||
props.user.timezone
|
||||
props.user.timezone,
|
||||
props.user.imperial_units
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
@ -7,8 +7,8 @@
|
||||
/>
|
||||
<StatCard
|
||||
icon="road"
|
||||
:value="Number(user.total_distance).toFixed(2)"
|
||||
:text="$t('workouts.KM')"
|
||||
:value="totalDistance"
|
||||
:text="unitTo === 'mi' ? 'miles' : unitTo"
|
||||
/>
|
||||
<StatCard
|
||||
icon="clock-o"
|
||||
@ -28,7 +28,9 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import StatCard from '@/components/Common/StatCard.vue'
|
||||
import { TUnit } from '@/types/units'
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import { convertDistance, units } from '@/utils/units'
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
}
|
||||
@ -41,6 +43,13 @@
|
||||
() => props.user.total_duration
|
||||
)
|
||||
const totalDuration = computed(() => get_duration(userTotalDuration))
|
||||
const defaultUnitFrom: TUnit = 'km'
|
||||
const unitTo: TUnit = user.value.imperial_units
|
||||
? units[defaultUnitFrom].defaultTarget
|
||||
: defaultUnitFrom
|
||||
const totalDistance = user.value.imperial_units
|
||||
? convertDistance(user.value.total_distance, defaultUnitFrom, unitTo, 2)
|
||||
: parseFloat(user.value.total_distance.toFixed(2))
|
||||
|
||||
function get_duration(total_duration: ComputedRef<string>) {
|
||||
const duration = total_duration.value.match(/day/g)
|
||||
|
@ -11,10 +11,16 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="user-stat">
|
||||
<span class="stat-number">{{
|
||||
Number(user.total_distance).toFixed(0)
|
||||
}}</span>
|
||||
<span class="stat-label">km</span>
|
||||
<Distance
|
||||
:distance="user.total_distance"
|
||||
unitFrom="km"
|
||||
:digits="0"
|
||||
:displayUnit="false"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
<span class="stat-label">
|
||||
{{ user.imperial_units ? 'miles' : 'km' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="user-stat hide-small">
|
||||
<span class="stat-number">{{ user.nb_sports }}</span>
|
||||
@ -72,6 +78,7 @@
|
||||
.stat-label {
|
||||
padding: 0 $default-padding * 0.5;
|
||||
}
|
||||
::v-deep(.distance),
|
||||
.stat-number {
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
@ -87,6 +94,7 @@
|
||||
.user-stats {
|
||||
gap: $default-padding * 2;
|
||||
.user-stat {
|
||||
::v-deep(.distance),
|
||||
.stat-number {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
|
@ -7,6 +7,14 @@
|
||||
<dd>{{ timezone }}</dd>
|
||||
<dt>{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}:</dt>
|
||||
<dd>{{ $t(`user.PROFILE.${fistDayOfWeek}`) }}</dd>
|
||||
<dt>{{ $t('user.PROFILE.UNITS.LABEL') }}:</dt>
|
||||
<dd>
|
||||
{{
|
||||
$t(
|
||||
`user.PROFILE.UNITS.${user.imperial_units ? 'IMPERIAL' : 'METRIC'}`
|
||||
)
|
||||
}}
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="profile-buttons">
|
||||
<button @click="$router.push('/profile/edit/preferences')">
|
||||
|
@ -35,6 +35,22 @@
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-items">
|
||||
{{ $t('user.PROFILE.UNITS.LABEL') }}
|
||||
<select
|
||||
id="imperial_units"
|
||||
v-model="userForm.imperial_units"
|
||||
:disabled="loading"
|
||||
>
|
||||
<option
|
||||
v-for="unit in imperialUnits"
|
||||
:value="unit.value"
|
||||
:key="unit.value"
|
||||
>
|
||||
{{ $t(`user.PROFILE.UNITS.${unit.label}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="form-buttons">
|
||||
<button class="confirm" type="submit">
|
||||
{{ $t('buttons.SUBMIT') }}
|
||||
@ -68,6 +84,7 @@
|
||||
const store = useStore()
|
||||
|
||||
const userForm: IUserPreferencesPayload = reactive({
|
||||
imperial_units: false,
|
||||
language: '',
|
||||
timezone: 'Europe/Paris',
|
||||
weekm: false,
|
||||
@ -82,6 +99,16 @@
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
const imperialUnits = [
|
||||
{
|
||||
label: 'IMPERIAL',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: 'METRIC',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
const loading = computed(
|
||||
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
||||
)
|
||||
@ -96,6 +123,7 @@
|
||||
})
|
||||
|
||||
function updateUserForm(user: IUserProfile) {
|
||||
userForm.imperial_units = user.imperial_units ? user.imperial_units : false
|
||||
userForm.language = user.language ? user.language : 'en'
|
||||
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
|
||||
userForm.weekm = user.weekm ? user.weekm : false
|
||||
|
@ -87,7 +87,13 @@
|
||||
</div>
|
||||
<div class="data">
|
||||
<i class="fa fa-road" aria-hidden="true" />
|
||||
<span v-if="workout">{{ workout.distance }} km</span>
|
||||
<Distance
|
||||
v-if="workout.id"
|
||||
:distance="workout.distance"
|
||||
:digits="3"
|
||||
unitFrom="km"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
<div class="data elevation" v-if="workout && workout.with_gpx">
|
||||
<img
|
||||
@ -96,15 +102,37 @@
|
||||
:alt="$t('workouts.ELEVATION')"
|
||||
/>
|
||||
<div class="data-values">
|
||||
<span>{{ workout.min_alt }}/</span>
|
||||
<span>{{ workout.max_alt }} m </span>
|
||||
<Distance
|
||||
v-if="workout.id"
|
||||
:distance="workout.min_alt"
|
||||
unitFrom="m"
|
||||
:displayUnit="false"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>/
|
||||
<Distance
|
||||
v-if="workout.id"
|
||||
:distance="workout.max_alt"
|
||||
unitFrom="m"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data altitude" v-if="workout && workout.with_gpx">
|
||||
<i class="fa fa-location-arrow" aria-hidden="true" />
|
||||
<div class="data-values">
|
||||
<span>+ {{ workout.ascent }}/</span>
|
||||
<span>- {{ workout.descent }} m </span>
|
||||
+<Distance
|
||||
v-if="workout.id"
|
||||
:distance="workout.ascent"
|
||||
unitFrom="m"
|
||||
:displayUnit="false"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>/-
|
||||
<Distance
|
||||
v-if="workout.id"
|
||||
:distance="workout.descent"
|
||||
unitFrom="m"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,6 +155,7 @@
|
||||
|
||||
interface Props {
|
||||
user: IUserProfile
|
||||
useImperialUnits: boolean
|
||||
workout?: IWorkout
|
||||
sport?: ISport
|
||||
}
|
||||
@ -137,7 +166,7 @@
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const { user, workout, sport } = toRefs(props)
|
||||
const { user, workout, sport, useImperialUnits } = toRefs(props)
|
||||
const locale: ComputedRef<Locale> = computed(
|
||||
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
|
||||
)
|
||||
|
@ -55,12 +55,14 @@
|
||||
import { LineChart, useLineChart } from 'vue-chart-3'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { TUnit } from '@/types/units'
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import {
|
||||
IWorkoutChartData,
|
||||
IWorkoutData,
|
||||
TCoordinates,
|
||||
} from '@/types/workouts'
|
||||
import { units } from '@/utils/units'
|
||||
import { getDatasets } from '@/utils/workouts'
|
||||
|
||||
interface Props {
|
||||
@ -76,8 +78,10 @@
|
||||
let displayDistance = ref(true)
|
||||
let beginElevationAtZero = ref(true)
|
||||
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
|
||||
getDatasets(props.workoutData.chartData, t)
|
||||
getDatasets(props.workoutData.chartData, t, props.authUser.imperial_units)
|
||||
)
|
||||
const fromKmUnit = getUnitTo('km')
|
||||
const fromMUnit = getUnitTo('m')
|
||||
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
|
||||
labels: displayDistance.value
|
||||
? datasets.value.distance_labels
|
||||
@ -119,7 +123,7 @@
|
||||
title: {
|
||||
display: true,
|
||||
text: displayDistance.value
|
||||
? t('workouts.DISTANCE') + ' (km)'
|
||||
? t('workouts.DISTANCE') + ` (${fromKmUnit})`
|
||||
: t('workouts.DURATION'),
|
||||
},
|
||||
},
|
||||
@ -130,7 +134,7 @@
|
||||
position: 'left',
|
||||
title: {
|
||||
display: true,
|
||||
text: t('workouts.SPEED') + ' (km/h)',
|
||||
text: t('workouts.SPEED') + ` (${fromKmUnit}/h)`,
|
||||
},
|
||||
},
|
||||
yElevation: {
|
||||
@ -141,7 +145,7 @@
|
||||
position: 'right',
|
||||
title: {
|
||||
display: true,
|
||||
text: t('workouts.ELEVATION') + ' (m)',
|
||||
text: t('workouts.ELEVATION') + ` (${fromMUnit})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -164,8 +168,8 @@
|
||||
label: function (context) {
|
||||
const label = ` ${context.dataset.label}: ${context.formattedValue}`
|
||||
return context.dataset.yAxisID === 'yElevation'
|
||||
? label + ' m'
|
||||
: label + ' km/h'
|
||||
? label + ` ${fromMUnit}`
|
||||
: label + ` ${fromKmUnit}/h`
|
||||
},
|
||||
title: function (tooltipItems) {
|
||||
if (tooltipItems.length > 0) {
|
||||
@ -174,7 +178,9 @@
|
||||
return tooltipItems.length === 0
|
||||
? ''
|
||||
: displayDistance.value
|
||||
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
|
||||
? `${t('workouts.DISTANCE')}: ${
|
||||
tooltipItems[0].label
|
||||
} ${fromKmUnit}`
|
||||
: `${t('workouts.DURATION')}: ${formatDuration(
|
||||
tooltipItems[0].label.replace(',', '')
|
||||
)}`
|
||||
@ -200,6 +206,11 @@
|
||||
function emitEmptyCoordinates() {
|
||||
emitCoordinates({ latitude: null, longitude: null })
|
||||
}
|
||||
function getUnitTo(unitFrom: TUnit): TUnit {
|
||||
return props.authUser.imperial_units
|
||||
? units[unitFrom].defaultTarget
|
||||
: unitFrom
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -2,27 +2,48 @@
|
||||
<div id="workout-info">
|
||||
<div class="workout-data">
|
||||
<i class="fa fa-clock-o" aria-hidden="true" />
|
||||
{{ $t('workouts.DURATION') }}: <span>{{ workoutObject.moving }}</span>
|
||||
<span class="label"> {{ $t('workouts.DURATION') }} </span>:
|
||||
<span class="value">{{ workoutObject.moving }}</span>
|
||||
<WorkoutRecord :workoutObject="workoutObject" recordType="LD" />
|
||||
<div v-if="withPause">
|
||||
({{ $t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
|
||||
({{ $t('workouts.PAUSES') }}:
|
||||
<span class="value">{{ workoutObject.pauses }}</span> -
|
||||
{{ $t('workouts.TOTAL_DURATION') }}:
|
||||
<span>{{ workoutObject.duration }})</span>
|
||||
<span class="value">{{ workoutObject.duration }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workout-data">
|
||||
<i class="fa fa-road" aria-hidden="true" />
|
||||
{{ $t('workouts.DISTANCE') }}:
|
||||
<span>{{ workoutObject.distance }} km</span>
|
||||
<span class="label"> {{ $t('workouts.DISTANCE') }} </span>:
|
||||
<Distance
|
||||
:distance="workoutObject.distance"
|
||||
:digits="3"
|
||||
unitFrom="km"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
<WorkoutRecord :workoutObject="workoutObject" recordType="FD" />
|
||||
</div>
|
||||
<div class="workout-data">
|
||||
<i class="fa fa-tachometer" aria-hidden="true" />
|
||||
{{ $t('workouts.AVERAGE_SPEED') }}:
|
||||
<span>{{ workoutObject.aveSpeed }} km/h</span
|
||||
><WorkoutRecord :workoutObject="workoutObject" recordType="AS" /><br />
|
||||
{{ $t('workouts.MAX_SPEED') }}:
|
||||
<span>{{ workoutObject.maxSpeed }} km/h</span>
|
||||
<span class="label">{{ $t('workouts.AVERAGE_SPEED') }}</span
|
||||
>:
|
||||
<Distance
|
||||
:distance="workoutObject.aveSpeed"
|
||||
unitFrom="km"
|
||||
:speed="true"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
<WorkoutRecord :workoutObject="workoutObject" recordType="AS" /><br />
|
||||
<span class="label"> {{ $t('workouts.MAX_SPEED') }} </span>:
|
||||
<Distance
|
||||
:distance="workoutObject.maxSpeed"
|
||||
unitFrom="km"
|
||||
:speed="true"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
<WorkoutRecord :workoutObject="workoutObject" recordType="MS" />
|
||||
</div>
|
||||
<div
|
||||
@ -34,21 +55,48 @@
|
||||
src="/img/workouts/mountains.svg"
|
||||
:alt="$t('workouts.ELEVATION')"
|
||||
/>
|
||||
{{ $t('workouts.MIN_ALTITUDE') }}:
|
||||
<span>{{ workoutObject.minAlt }} m</span><br />
|
||||
{{ $t('workouts.MAX_ALTITUDE') }}:
|
||||
<span>{{ workoutObject.maxAlt }} m</span>
|
||||
<span class="label">{{ $t('workouts.MIN_ALTITUDE') }}</span
|
||||
>:
|
||||
<Distance
|
||||
:distance="workoutObject.minAlt"
|
||||
unitFrom="m"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/><br />
|
||||
<span class="label">{{ $t('workouts.MAX_ALTITUDE') }}</span
|
||||
>:
|
||||
<Distance
|
||||
:distance="workoutObject.maxAlt"
|
||||
unitFrom="m"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="workout-data"
|
||||
v-if="workoutObject.ascent !== null && workoutObject.descent !== null"
|
||||
>
|
||||
<i class="fa fa-location-arrow" aria-hidden="true" />
|
||||
{{ $t('workouts.ASCENT') }}: <span>{{ workoutObject.ascent }} m</span
|
||||
><br />
|
||||
{{ $t('workouts.DESCENT') }}: <span>{{ workoutObject.descent }} m</span>
|
||||
<span class="label">{{ $t('workouts.ASCENT') }}</span
|
||||
>:
|
||||
<Distance
|
||||
:distance="workoutObject.ascent"
|
||||
unitFrom="m"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/><br />
|
||||
<span class="label"> {{ $t('workouts.DESCENT') }} </span>:
|
||||
<Distance
|
||||
:distance="workoutObject.descent"
|
||||
unitFrom="m"
|
||||
:strong="true"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
<WorkoutWeather :workoutObject="workoutObject" />
|
||||
<WorkoutWeather
|
||||
:workoutObject="workoutObject"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -61,10 +109,11 @@
|
||||
|
||||
interface Props {
|
||||
workoutObject: IWorkoutObject
|
||||
useImperialUnits: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { workoutObject } = toRefs(props)
|
||||
const { workoutObject, useImperialUnits } = toRefs(props)
|
||||
const withPause = computed(
|
||||
() =>
|
||||
props.workoutObject.pauses !== '0:00:00' &&
|
||||
@ -80,11 +129,17 @@
|
||||
padding: $default-padding $default-padding * 2;
|
||||
width: 100%;
|
||||
|
||||
.workout-data {
|
||||
text-transform: capitalize;
|
||||
padding: $default-padding * 0.5 0;
|
||||
.fa,
|
||||
.mountains {
|
||||
padding-right: $default-padding * 0.5;
|
||||
}
|
||||
|
||||
span {
|
||||
.workout-data {
|
||||
padding: $default-padding * 0.5 0;
|
||||
.label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.value {
|
||||
font-weight: bold;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
@ -89,8 +89,26 @@
|
||||
:title="$t(`workouts.WEATHER.WIND`)"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ Number(workoutObject.weatherStart.wind).toFixed(1) }}m/s</td>
|
||||
<td>{{ Number(workoutObject.weatherEnd.wind).toFixed(1) }}m/s</td>
|
||||
<td>
|
||||
<Distance
|
||||
:distance="workoutObject.weatherStart.wind"
|
||||
unitFrom="m"
|
||||
:digits="1"
|
||||
:displayUnit="false"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
{{ useImperialUnits ? 'ft' : 'm' }}/s
|
||||
</td>
|
||||
<td>
|
||||
<Distance
|
||||
:distance="workoutObject.weatherEnd.wind"
|
||||
unitFrom="m"
|
||||
:digits="1"
|
||||
:displayUnit="false"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>
|
||||
{{ useImperialUnits ? 'ft' : 'm' }}/s
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -104,10 +122,11 @@
|
||||
|
||||
interface Props {
|
||||
workoutObject: IWorkoutObject
|
||||
useImperialUnits: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { workoutObject } = toRefs(props)
|
||||
const { useImperialUnits, workoutObject } = toRefs(props)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -20,7 +20,10 @@
|
||||
:workoutData="workoutData"
|
||||
:markerCoordinates="markerCoordinates"
|
||||
/>
|
||||
<WorkoutData :workoutObject="workoutObject" />
|
||||
<WorkoutData
|
||||
:workoutObject="workoutObject"
|
||||
:useImperialUnits="authUser.imperial_units"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
@ -68,7 +71,7 @@
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
const { markerCoordinates, workoutData } = toRefs(props)
|
||||
const { authUser, markerCoordinates, workoutData } = toRefs(props)
|
||||
const workout: ComputedRef<IWorkout> = computed(
|
||||
() => props.workoutData.workout
|
||||
)
|
||||
|
@ -137,7 +137,7 @@
|
||||
class="workout-duration"
|
||||
type="text"
|
||||
placeholder="HH"
|
||||
pattern="^([0-9]*[0-9])$"
|
||||
pattern="^([0-1]?[0-9]|2[0-3])$"
|
||||
required
|
||||
@invalid="invalidateForm"
|
||||
:disabled="loading"
|
||||
@ -173,12 +173,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>{{ $t('workouts.DISTANCE') }} (km):</label>
|
||||
<label>
|
||||
{{ $t('workouts.DISTANCE') }} ({{
|
||||
authUser.imperial_units ? 'mi' : 'km'
|
||||
}}):
|
||||
</label>
|
||||
<input
|
||||
name="workout-distance"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
step="0.001"
|
||||
required
|
||||
@invalid="invalidateForm"
|
||||
:disabled="loading"
|
||||
@ -239,6 +243,7 @@
|
||||
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
||||
import { getReadableFileSize } from '@/utils/files'
|
||||
import { translateSports } from '@/utils/sports'
|
||||
import { convertDistance } from '@/utils/units'
|
||||
|
||||
interface Props {
|
||||
authUser: IUserProfile
|
||||
@ -257,7 +262,7 @@
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const { workout, isCreation, loading } = toRefs(props)
|
||||
const { authUser, workout, isCreation, loading } = toRefs(props)
|
||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||
translateSports(
|
||||
props.sports,
|
||||
@ -324,7 +329,11 @@
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
const duration = workout.duration.split(':')
|
||||
workoutForm.workoutDistance = `${workout.distance}`
|
||||
workoutForm.workoutDistance = `${
|
||||
authUser.value.imperial_units
|
||||
? convertDistance(workout.distance, 'km', 'mi', 2)
|
||||
: parseFloat(workout.distance.toFixed(2))
|
||||
}`
|
||||
workoutForm.workoutDate = workoutDateTime.workout_date
|
||||
workoutForm.workoutTime = workoutDateTime.workout_time
|
||||
workoutForm.workoutDurationHour = duration[0]
|
||||
@ -334,7 +343,9 @@
|
||||
}
|
||||
function formatPayload(payload: IWorkoutForm) {
|
||||
payload.title = workoutForm.title
|
||||
payload.distance = +workoutForm.workoutDistance
|
||||
payload.distance = authUser.value.imperial_units
|
||||
? convertDistance(+workoutForm.workoutDistance, 'mi', 'km', 3)
|
||||
: +workoutForm.workoutDistance
|
||||
payload.duration =
|
||||
+workoutForm.workoutDurationHour * 3600 +
|
||||
+workoutForm.workoutDurationMinutes * 60 +
|
||||
|
@ -15,8 +15,12 @@
|
||||
}"
|
||||
>{{ $t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
|
||||
>
|
||||
({{ $t('workouts.DISTANCE') }}: {{ segment.distance }} km,
|
||||
{{ $t('workouts.DURATION') }}: {{ segment.duration }})
|
||||
({{ $t('workouts.DISTANCE') }}:
|
||||
<Distance
|
||||
:distance="segment.distance"
|
||||
unitFrom="km"
|
||||
:useImperialUnits="useImperialUnits"
|
||||
/>, {{ $t('workouts.DURATION') }}: {{ segment.duration }})
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@ -31,10 +35,11 @@
|
||||
|
||||
interface Props {
|
||||
segments: IWorkoutSegment[]
|
||||
useImperialUnits: boolean
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { segments } = toRefs(props)
|
||||
const { segments, useImperialUnits } = toRefs(props)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -47,7 +47,7 @@
|
||||
|
||||
<div class="form-items-group">
|
||||
<div class="form-item">
|
||||
<label> {{ $t('workouts.DISTANCE') }} (km): </label>
|
||||
<label> {{ $t('workouts.DISTANCE') }} ({{ toUnit }}): </label>
|
||||
<div class="form-inputs-group">
|
||||
<input
|
||||
name="distance_from"
|
||||
@ -72,7 +72,7 @@
|
||||
|
||||
<div class="form-items-group">
|
||||
<div class="form-item">
|
||||
<label> {{ $t('workouts.DURATION') }} (km): </label>
|
||||
<label> {{ $t('workouts.DURATION') }} ({{ toUnit }}): </label>
|
||||
<div class="form-inputs-group">
|
||||
<input
|
||||
name="duration_from"
|
||||
@ -97,7 +97,7 @@
|
||||
|
||||
<div class="form-items-group">
|
||||
<div class="form-item">
|
||||
<label> {{ $t('workouts.AVE_SPEED') }} (km): </label>
|
||||
<label> {{ $t('workouts.AVE_SPEED') }} ({{ toUnit }}): </label>
|
||||
<div class="form-inputs-group">
|
||||
<input
|
||||
min="0"
|
||||
@ -122,7 +122,7 @@
|
||||
|
||||
<div class="form-items-group">
|
||||
<div class="form-item">
|
||||
<label> {{ $t('workouts.MAX_SPEED') }} (km): </label>
|
||||
<label> {{ $t('workouts.MAX_SPEED') }} ({{ toUnit }}): </label>
|
||||
|
||||
<div class="form-inputs-group">
|
||||
<input
|
||||
@ -167,6 +167,7 @@
|
||||
import { ISport } from '@/types/sports'
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import { translateSports } from '@/utils/sports'
|
||||
import { units } from '@/utils/units'
|
||||
|
||||
interface Props {
|
||||
authUser: IUserProfile
|
||||
@ -181,6 +182,10 @@
|
||||
const router = useRouter()
|
||||
|
||||
const { authUser } = toRefs(props)
|
||||
|
||||
const toUnit = authUser.value.imperial_units
|
||||
? units['km'].defaultTarget
|
||||
: 'km'
|
||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||
translateSports(props.sports, t)
|
||||
)
|
||||
|
@ -45,8 +45,9 @@
|
||||
{{ $t('workouts.SPORT', 1) }}
|
||||
</span>
|
||||
<SportImage
|
||||
v-if="sports.length > 0"
|
||||
:title="
|
||||
sports.filter((s) => s.id === workout.sport_id)[0]
|
||||
sports.find((s) => s.id === workout.sport_id)
|
||||
.translatedLabel
|
||||
"
|
||||
:sport-label="getSportLabel(workout, sports)"
|
||||
@ -93,7 +94,11 @@
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.DISTANCE') }}
|
||||
</span>
|
||||
{{ Number(workout.distance).toFixed(2) }} km
|
||||
<Distance
|
||||
:distance="workout.distance"
|
||||
unitFrom="km"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="cell-heading">
|
||||
@ -105,25 +110,45 @@
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.AVE_SPEED') }}
|
||||
</span>
|
||||
{{ workout.ave_speed }} km/h
|
||||
<Distance
|
||||
:distance="workout.ave_speed"
|
||||
unitFrom="km"
|
||||
:speed="true"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.MAX_SPEED') }}
|
||||
</span>
|
||||
{{ workout.max_speed }} km/h
|
||||
<Distance
|
||||
:distance="workout.max_speed"
|
||||
unitFrom="km"
|
||||
:speed="true"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.ASCENT') }}
|
||||
</span>
|
||||
<span v-if="workout.with_gpx">{{ workout.ascent }} m</span>
|
||||
<Distance
|
||||
v-if="workout.with_gpx"
|
||||
:distance="workout.ascent"
|
||||
unitFrom="m"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="cell-heading">
|
||||
{{ $t('workouts.DESCENT') }}
|
||||
</span>
|
||||
<span v-if="workout.with_gpx">{{ workout.descent }} m</span>
|
||||
<Distance
|
||||
v-if="workout.with_gpx"
|
||||
:distance="workout.descent"
|
||||
unitFrom="m"
|
||||
:useImperialUnits="user.imperial_units"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -163,6 +188,7 @@
|
||||
import { getQuery, sortList, workoutsPayloadKeys } from '@/utils/api'
|
||||
import { getDateWithTZ } from '@/utils/dates'
|
||||
import { getSportColor, getSportLabel } from '@/utils/sports'
|
||||
import { convertDistance } from '@/utils/units'
|
||||
import { defaultOrder } from '@/utils/workouts'
|
||||
|
||||
interface Props {
|
||||
@ -196,7 +222,10 @@
|
||||
})
|
||||
|
||||
function loadWorkouts(payload: TWorkoutsPayload) {
|
||||
store.dispatch(WORKOUTS_STORE.ACTIONS.GET_USER_WORKOUTS, payload)
|
||||
store.dispatch(
|
||||
WORKOUTS_STORE.ACTIONS.GET_USER_WORKOUTS,
|
||||
user.value.imperial_units ? getConvertedPayload(payload) : payload
|
||||
)
|
||||
}
|
||||
function reloadWorkouts(queryParam: string, queryValue: string) {
|
||||
const newQuery: LocationQuery = Object.assign({}, route.query)
|
||||
@ -224,6 +253,18 @@
|
||||
return query
|
||||
}
|
||||
|
||||
function getConvertedPayload(payload: TWorkoutsPayload): TWorkoutsPayload {
|
||||
const convertedPayload: TWorkoutsPayload = {
|
||||
...payload,
|
||||
}
|
||||
Object.entries(convertedPayload).map((entry) => {
|
||||
if (entry[0].match('speed|distance')) {
|
||||
convertedPayload[entry[0]] = convertDistance(+entry[1], 'mi', 'km')
|
||||
}
|
||||
})
|
||||
return convertedPayload
|
||||
}
|
||||
|
||||
function onHover(workoutId: string | null) {
|
||||
hoverWorkoutId.value = workoutId
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import AlertMessage from '@/components/Common/AlertMessage.vue'
|
||||
import Card from '@/components/Common/Card.vue'
|
||||
import CustomTextArea from '@/components/Common/CustomTextArea.vue'
|
||||
import Distance from '@/components/Common/Distance.vue'
|
||||
import Dropdown from '@/components/Common/Dropdown.vue'
|
||||
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
|
||||
import SportImage from '@/components/Common/Images/SportImage/index.vue'
|
||||
@ -11,6 +12,7 @@ export const customComponents = [
|
||||
{ target: AlertMessage, name: 'AlertMessage' },
|
||||
{ target: Card, name: 'Card' },
|
||||
{ target: CustomTextArea, name: 'CustomTextArea' },
|
||||
{ target: Distance, name: 'Distance' },
|
||||
{ target: Dropdown, name: 'Dropdown' },
|
||||
{ target: ErrorMessage, name: 'ErrorMessage' },
|
||||
{ target: Loader, name: 'Loader' },
|
||||
|
@ -53,6 +53,11 @@
|
||||
"LABEL": "label",
|
||||
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
|
||||
},
|
||||
"UNITS": {
|
||||
"LABEL": "Units for distance",
|
||||
"IMPERIAL": "Imperial system (ft, mi)",
|
||||
"METRIC": "Metric system (m, km)"
|
||||
},
|
||||
"TIMEZONE": "Timezone"
|
||||
},
|
||||
"REGISTER": "Register",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"FROM": "from",
|
||||
"GPX_FILE": ".gpx file",
|
||||
"HIDE_FILTERS": "hide filters",
|
||||
"KM": "km",
|
||||
"LATEST_WORKOUTS": "Latest workouts",
|
||||
"LOAD_MORE_WORKOUT": "Load more workouts",
|
||||
"MAX_ALTITUDE": "max. altitude",
|
||||
|
@ -45,6 +45,11 @@
|
||||
"PROFILE": "profil",
|
||||
"SPORTS": "sports"
|
||||
},
|
||||
"UNITS": {
|
||||
"LABEL": "Unités pour les distances ",
|
||||
"IMPERIAL": "Système impérial (ft, mi)",
|
||||
"METRIC": "Système métrique (m, km)"
|
||||
},
|
||||
"SPORT": {
|
||||
"ACTION": "action",
|
||||
"COLOR": "couleur",
|
||||
|
@ -16,7 +16,6 @@
|
||||
"FROM": "à partir de",
|
||||
"GPX_FILE": "fichier .gpx",
|
||||
"HIDE_FILTERS": "masquer les filtres",
|
||||
"KM": "km",
|
||||
"LATEST_WORKOUTS": "Séances récentes",
|
||||
"LOAD_MORE_WORKOUT": "Charger les séances suivantes",
|
||||
"MAX_ALTITUDE": "altitude max",
|
||||
|
14
fittrackee_client/src/types/units.ts
Normal file
14
fittrackee_client/src/types/units.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export type TUnitSystem = 'imperial' | 'metric'
|
||||
|
||||
export type TUnit = 'ft' | 'mi' | 'm' | 'km'
|
||||
|
||||
export type TFactor = {
|
||||
[k in string]: Record<string, number>
|
||||
}
|
||||
|
||||
export interface IUnit {
|
||||
unit: TUnit
|
||||
system: TUnitSystem
|
||||
multiplier: number
|
||||
defaultTarget: TUnit
|
||||
}
|
@ -9,6 +9,7 @@ export interface IUserProfile {
|
||||
created_at: string
|
||||
email: string
|
||||
first_name: string | null
|
||||
imperial_units: boolean
|
||||
language: string | null
|
||||
last_name: string | null
|
||||
location: string | null
|
||||
@ -40,6 +41,7 @@ export interface IAdminUserPayload {
|
||||
}
|
||||
|
||||
export interface IUserPreferencesPayload {
|
||||
imperial_units: boolean
|
||||
language: string
|
||||
timezone: string
|
||||
weekm: boolean
|
||||
|
@ -1,19 +1,31 @@
|
||||
import { ITranslatedSport } from '@/types/sports'
|
||||
import { TUnit } from '@/types/units'
|
||||
import { IRecord, IRecordsBySports } from '@/types/workouts'
|
||||
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
||||
import { convertDistance, units } from '@/utils/units'
|
||||
|
||||
export const formatRecord = (
|
||||
record: IRecord,
|
||||
tz: string
|
||||
tz: string,
|
||||
useImperialUnits: boolean
|
||||
): Record<string, string | number> => {
|
||||
const unitFrom: TUnit = 'km'
|
||||
const unitTo: TUnit = useImperialUnits
|
||||
? units[unitFrom].defaultTarget
|
||||
: unitFrom
|
||||
let value
|
||||
switch (record.record_type) {
|
||||
case 'AS':
|
||||
case 'MS':
|
||||
value = `${record.value} km/h`
|
||||
value = `${convertDistance(
|
||||
+record.value,
|
||||
unitFrom,
|
||||
unitTo,
|
||||
2
|
||||
)} ${unitTo}/h`
|
||||
break
|
||||
case 'FD':
|
||||
value = `${record.value} km`
|
||||
value = `${convertDistance(+record.value, unitFrom, unitTo, 3)} ${unitTo}`
|
||||
break
|
||||
case 'LD':
|
||||
value = record.value
|
||||
@ -36,7 +48,8 @@ export const formatRecord = (
|
||||
export const getRecordsBySports = (
|
||||
records: IRecord[],
|
||||
translatedSports: ITranslatedSport[],
|
||||
tz: string
|
||||
tz: string,
|
||||
useImperialUnits: boolean
|
||||
): IRecordsBySports =>
|
||||
records.reduce((sportList: IRecordsBySports, record) => {
|
||||
const sport = translatedSports.find((s) => s.id === record.sport_id)
|
||||
@ -48,7 +61,9 @@ export const getRecordsBySports = (
|
||||
records: [],
|
||||
}
|
||||
}
|
||||
sportList[sport.translatedLabel].records.push(formatRecord(record, tz))
|
||||
sportList[sport.translatedLabel].records.push(
|
||||
formatRecord(record, tz, useImperialUnits)
|
||||
)
|
||||
}
|
||||
return sportList
|
||||
}, {})
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
} from '@/types/statistics'
|
||||
import { incrementDate, getStartDate } from '@/utils/dates'
|
||||
import { sportColors } from '@/utils/sports'
|
||||
import { convertStatsDistance } from '@/utils/units'
|
||||
|
||||
const dateFormats: Record<string, Record<string, string>> = {
|
||||
week: {
|
||||
@ -94,12 +95,34 @@ export const getDatasets = (displayedSports: ISport[]): TStatisticsDatasets => {
|
||||
return datasets
|
||||
}
|
||||
|
||||
export const convertStatsValue = (
|
||||
datasetKey: TStatisticsDatasetKeys,
|
||||
value: number,
|
||||
useImperialUnits: boolean
|
||||
): number => {
|
||||
switch (datasetKey) {
|
||||
case 'total_distance':
|
||||
case 'total_ascent':
|
||||
case 'total_descent':
|
||||
return convertStatsDistance(
|
||||
datasetKey === 'total_distance' ? 'km' : 'm',
|
||||
value,
|
||||
useImperialUnits
|
||||
)
|
||||
default:
|
||||
case 'nb_workouts':
|
||||
case 'total_duration':
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export const formatStats = (
|
||||
params: IStatisticsDateParams,
|
||||
weekStartingMonday: boolean,
|
||||
sports: ISport[],
|
||||
displayedSportsId: number[],
|
||||
apiStats: TStatisticsFromApi
|
||||
apiStats: TStatisticsFromApi,
|
||||
useImperialUnits: boolean
|
||||
): IStatisticsChartData => {
|
||||
const dayKeys = getDateKeys(params, weekStartingMonday)
|
||||
const dateFormat = dateFormats[params.duration]
|
||||
@ -123,7 +146,11 @@ export const formatStats = (
|
||||
apiStats !== {} &&
|
||||
date in apiStats &&
|
||||
sportsId[dataset.label] in apiStats[date]
|
||||
? apiStats[date][sportsId[dataset.label]][datasetKey]
|
||||
? convertStatsValue(
|
||||
datasetKey,
|
||||
apiStats[date][sportsId[dataset.label]][datasetKey],
|
||||
useImperialUnits
|
||||
)
|
||||
: 0
|
||||
)
|
||||
})
|
||||
|
@ -1,19 +1,23 @@
|
||||
import { TStatisticsDatasetKeys } from '@/types/statistics'
|
||||
import { formatDuration } from '@/utils/duration'
|
||||
import { units } from '@/utils/units'
|
||||
|
||||
export const formatTooltipValue = (
|
||||
displayedData: TStatisticsDatasetKeys,
|
||||
value: number,
|
||||
useImperialUnits: boolean,
|
||||
formatWithUnits = true
|
||||
): string => {
|
||||
const unitFrom = 'km'
|
||||
const unitTo = useImperialUnits ? units[unitFrom].defaultTarget : unitFrom
|
||||
switch (displayedData) {
|
||||
case 'total_duration':
|
||||
return formatDuration(value, formatWithUnits)
|
||||
case 'total_distance':
|
||||
return value.toFixed(2) + ' km'
|
||||
return `${value.toFixed(2)} ${unitTo}`
|
||||
case 'total_ascent':
|
||||
case 'total_descent':
|
||||
return (value / 1000).toFixed(2) + ' km'
|
||||
return `${(value / 1000).toFixed(2)} ${unitTo}`
|
||||
default:
|
||||
return value.toString()
|
||||
}
|
||||
|
65
fittrackee_client/src/utils/units.ts
Normal file
65
fittrackee_client/src/utils/units.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { IUnit, TFactor, TUnit } from '@/types/units'
|
||||
|
||||
export const units: Record<string, IUnit> = {
|
||||
ft: {
|
||||
unit: 'ft',
|
||||
system: 'imperial',
|
||||
multiplier: 1,
|
||||
defaultTarget: 'm',
|
||||
},
|
||||
mi: {
|
||||
unit: 'mi',
|
||||
system: 'imperial',
|
||||
multiplier: 5280,
|
||||
defaultTarget: 'km',
|
||||
},
|
||||
m: {
|
||||
unit: 'm',
|
||||
system: 'metric',
|
||||
multiplier: 1,
|
||||
defaultTarget: 'ft',
|
||||
},
|
||||
km: {
|
||||
unit: 'm',
|
||||
system: 'metric',
|
||||
multiplier: 1000,
|
||||
defaultTarget: 'mi',
|
||||
},
|
||||
}
|
||||
|
||||
const factors: TFactor = {
|
||||
metric: {
|
||||
imperial: 3.280839895,
|
||||
metric: 1,
|
||||
},
|
||||
imperial: {
|
||||
metric: 1 / 3.280839895,
|
||||
imperial: 1,
|
||||
},
|
||||
}
|
||||
|
||||
export const convertDistance = (
|
||||
distance: number,
|
||||
from: TUnit,
|
||||
to: TUnit,
|
||||
digits: number | null = 3
|
||||
): number => {
|
||||
const unitFrom = units[from]
|
||||
const unitTo = units[to]
|
||||
const convertedDistance =
|
||||
(distance * unitFrom.multiplier * factors[unitFrom.system][unitTo.system]) /
|
||||
unitTo.multiplier
|
||||
if (digits !== null) {
|
||||
return parseFloat(convertedDistance.toFixed(digits))
|
||||
}
|
||||
return convertedDistance
|
||||
}
|
||||
|
||||
export const convertStatsDistance = (
|
||||
unitFrom: TUnit,
|
||||
value: number,
|
||||
useImperialUnits: boolean
|
||||
): number => {
|
||||
const unitTo = useImperialUnits ? units[unitFrom].defaultTarget : unitFrom
|
||||
return useImperialUnits ? convertDistance(value, unitFrom, unitTo, 2) : value
|
||||
}
|
@ -5,10 +5,12 @@ import {
|
||||
TCoordinates,
|
||||
TWorkoutDatasets,
|
||||
} from '@/types/workouts'
|
||||
import { convertStatsDistance } from '@/utils/units'
|
||||
|
||||
export const getDatasets = (
|
||||
chartData: IWorkoutApiChartData[],
|
||||
t: CallableFunction
|
||||
t: CallableFunction,
|
||||
useImperialUnits: boolean
|
||||
): IWorkoutChartData => {
|
||||
const datasets: TWorkoutDatasets = {
|
||||
speed: {
|
||||
@ -36,8 +38,12 @@ export const getDatasets = (
|
||||
chartData.map((data) => {
|
||||
distance_labels.push(data.distance)
|
||||
duration_labels.push(data.duration)
|
||||
datasets.speed.data.push(data.speed)
|
||||
datasets.elevation.data.push(data.elevation)
|
||||
datasets.speed.data.push(
|
||||
convertStatsDistance('km', data.speed, useImperialUnits)
|
||||
)
|
||||
datasets.elevation.data.push(
|
||||
convertStatsDistance('m', data.elevation, useImperialUnits)
|
||||
)
|
||||
coordinates.push({ latitude: data.latitude, longitude: data.longitude })
|
||||
})
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
<WorkoutSegments
|
||||
v-if="!displaySegment && workoutData.workout.segments.length > 1"
|
||||
:segments="workoutData.workout.segments"
|
||||
:useImperialUnits="authUser.imperial_units"
|
||||
/>
|
||||
<WorkoutNotes
|
||||
v-if="!displaySegment"
|
||||
|
@ -100,7 +100,113 @@ describe('formatRecord', () => {
|
||||
assert.deepEqual(
|
||||
formatRecord(
|
||||
testParams.inputParams.record,
|
||||
testParams.inputParams.timezone
|
||||
testParams.inputParams.timezone,
|
||||
false
|
||||
),
|
||||
testParams.expected
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatRecord after conversion', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: "return formatted record for 'Average speed'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
value: '11.18 mi/h',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "return formatted record for 'Farest distance'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 10,
|
||||
record_type: 'FD',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 10,
|
||||
record_type: 'FD',
|
||||
value: '11.185 mi',
|
||||
workout_date: '2019/07/08',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "return formatted record for 'Longest duration'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 11,
|
||||
record_type: 'LD',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: '1:01:00',
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 11,
|
||||
record_type: 'LD',
|
||||
value: '1:01:00',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "return formatted record for 'Max. speed'",
|
||||
inputParams: {
|
||||
record: {
|
||||
id: 12,
|
||||
record_type: 'MS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
timezone: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
id: 12,
|
||||
record_type: 'MS',
|
||||
value: '11.18 mi/h',
|
||||
workout_date: '2019/07/08',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
},
|
||||
]
|
||||
testsParams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
assert.deepEqual(
|
||||
formatRecord(
|
||||
testParams.inputParams.record,
|
||||
testParams.inputParams.timezone,
|
||||
true
|
||||
),
|
||||
testParams.expected
|
||||
)
|
||||
@ -121,7 +227,8 @@ describe('formatRecord (invalid record type)', () => {
|
||||
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
'Europe/Paris'
|
||||
'Europe/Paris',
|
||||
false
|
||||
)
|
||||
).to.throw(
|
||||
'Invalid record type, expected: "AS", "FD", "LD", "MD", got: "M"'
|
||||
@ -248,7 +355,138 @@ describe('getRecordsBySports', () => {
|
||||
getRecordsBySports(
|
||||
testParams.input.records,
|
||||
translatedSports,
|
||||
testParams.input.tz
|
||||
testParams.input.tz,
|
||||
false
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
testParams.expected
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('getRecordsBySports after conversion', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: 'returns empty object if no records',
|
||||
input: {
|
||||
records: [],
|
||||
tz: 'Europe/Paris',
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
description: 'returns record grouped by Sport',
|
||||
input: {
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
tz: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
'Cycling (Sport)': {
|
||||
color: null,
|
||||
label: 'Cycling (Sport)',
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
value: '11.18 mi/h',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'returns record grouped by Sport',
|
||||
input: {
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
record_type: 'FD',
|
||||
sport_id: 2,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
||||
workout_id: 'n6JcLPQt3QtZWFfiSnYm4C',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
record_type: 'MS',
|
||||
sport_id: 1,
|
||||
user: 'admin',
|
||||
value: 18,
|
||||
workout_date: 'Sun, 07 Jul 2019 08:00:00 GMT',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
tz: 'Europe/Paris',
|
||||
},
|
||||
expected: {
|
||||
'Cycling (Sport)': {
|
||||
color: null,
|
||||
label: 'Cycling (Sport)',
|
||||
records: [
|
||||
{
|
||||
id: 9,
|
||||
record_type: 'AS',
|
||||
value: '11.18 mi/h',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
record_type: 'MS',
|
||||
value: '11.18 mi/h',
|
||||
workout_date: '2019/07/07',
|
||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||
},
|
||||
],
|
||||
},
|
||||
'Cycling (Transport)': {
|
||||
color: '#000000',
|
||||
label: 'Cycling (Transport)',
|
||||
records: [
|
||||
{
|
||||
id: 10,
|
||||
record_type: 'FD',
|
||||
value: '11.185 mi',
|
||||
workout_date: '2019/07/08',
|
||||
workout_id: 'n6JcLPQt3QtZWFfiSnYm4C',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
testsParams.map((testParams) =>
|
||||
it(testParams.description, () => {
|
||||
assert.deepEqual(
|
||||
getRecordsBySports(
|
||||
testParams.input.records,
|
||||
translatedSports,
|
||||
testParams.input.tz,
|
||||
true
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
@ -316,7 +316,7 @@ describe('formatStats', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [], inputStats),
|
||||
formatStats(inputParams, false, sports, [], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -369,7 +369,7 @@ describe('formatStats', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [2], inputStats),
|
||||
formatStats(inputParams, false, sports, [2], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -427,7 +427,7 @@ describe('formatStats', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [], inputStats),
|
||||
formatStats(inputParams, false, sports, [], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -515,7 +515,7 @@ describe('formatStats', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [1], inputStats),
|
||||
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -605,7 +605,7 @@ describe('formatStats (duration)', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [1], inputStats),
|
||||
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -692,7 +692,7 @@ describe('formatStats (duration)', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [1], inputStats),
|
||||
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -780,7 +780,7 @@ describe('formatStats (duration)', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [1], inputStats),
|
||||
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
@ -868,7 +868,95 @@ describe('formatStats (duration)', () => {
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, true, sports, [1], inputStats),
|
||||
formatStats(inputParams, true, sports, [1], inputStats, false),
|
||||
expected
|
||||
)
|
||||
})
|
||||
|
||||
it('returns datasets after conversion to imperial units', () => {
|
||||
const inputStats: TStatisticsFromApi = {
|
||||
'2021-10-03': {
|
||||
1: {
|
||||
nb_workouts: 1,
|
||||
total_distance: 10,
|
||||
total_duration: 3000,
|
||||
total_ascent: 150,
|
||||
total_descent: 100,
|
||||
},
|
||||
},
|
||||
'2021-10-10': {
|
||||
1: {
|
||||
nb_workouts: 1,
|
||||
total_distance: 15,
|
||||
total_duration: 3500,
|
||||
total_ascent: 250,
|
||||
total_descent: 150,
|
||||
},
|
||||
2: {
|
||||
nb_workouts: 2,
|
||||
total_distance: 20,
|
||||
total_duration: 3000,
|
||||
total_ascent: 150,
|
||||
total_descent: 200,
|
||||
},
|
||||
},
|
||||
'2021-10-17': {
|
||||
3: {
|
||||
nb_workouts: 2,
|
||||
total_distance: 20,
|
||||
total_duration: 3000,
|
||||
total_ascent: 100,
|
||||
total_descent: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
const inputParams = {
|
||||
duration: 'week',
|
||||
start: new Date('October 03, 2021 00:00:00'),
|
||||
end: new Date('October 23, 2021 23:59:59.999'),
|
||||
}
|
||||
const expected: IStatisticsChartData = {
|
||||
labels: ['03/10/2021', '10/10/2021', '17/10/2021'],
|
||||
datasets: {
|
||||
nb_workouts: [
|
||||
{
|
||||
label: 'Cycling (Sport)',
|
||||
backgroundColor: ['#4c9792'],
|
||||
data: [1, 1, 0],
|
||||
},
|
||||
],
|
||||
total_distance: [
|
||||
{
|
||||
label: 'Cycling (Sport)',
|
||||
backgroundColor: ['#4c9792'],
|
||||
data: [6.21, 9.32, 0],
|
||||
},
|
||||
],
|
||||
total_duration: [
|
||||
{
|
||||
label: 'Cycling (Sport)',
|
||||
backgroundColor: ['#4c9792'],
|
||||
data: [3000, 3500, 0],
|
||||
},
|
||||
],
|
||||
total_ascent: [
|
||||
{
|
||||
label: 'Cycling (Sport)',
|
||||
backgroundColor: ['#4c9792'],
|
||||
data: [492.13, 820.21, 0],
|
||||
},
|
||||
],
|
||||
total_descent: [
|
||||
{
|
||||
label: 'Cycling (Sport)',
|
||||
backgroundColor: ['#4c9792'],
|
||||
data: [328.08, 492.13, 0],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
assert.deepEqual(
|
||||
formatStats(inputParams, false, sports, [1], inputStats, true),
|
||||
expected
|
||||
)
|
||||
})
|
||||
|
@ -42,7 +42,56 @@ describe('formatTooltipValue', () => {
|
||||
assert.equal(
|
||||
formatTooltipValue(
|
||||
testParams.inputDisplayedData,
|
||||
testParams.inputValue
|
||||
testParams.inputValue,
|
||||
false
|
||||
),
|
||||
testParams.expectedResult
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatTooltipValue after conversion to imperial units', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: 'returns 30 if input is workouts count',
|
||||
inputDisplayedData: datasetKeys[0], // 'nb_workouts'
|
||||
inputValue: 30,
|
||||
expectedResult: '30',
|
||||
},
|
||||
{
|
||||
description: 'returns 00m:03s if input is total duration',
|
||||
inputDisplayedData: datasetKeys[1], // 'total_duration'
|
||||
inputValue: 30,
|
||||
expectedResult: '00m 30s',
|
||||
},
|
||||
{
|
||||
description: 'returns 30 mi if input is total distance',
|
||||
inputDisplayedData: datasetKeys[2], // 'total_distance'
|
||||
inputValue: 30,
|
||||
expectedResult: '30.00 mi',
|
||||
},
|
||||
{
|
||||
description: 'returns 0.03 mi if input is total ascent',
|
||||
inputDisplayedData: datasetKeys[3], // 'total_distance'
|
||||
inputValue: 30,
|
||||
expectedResult: '0.03 mi',
|
||||
},
|
||||
{
|
||||
description: 'returns 0.03 mi if input is total descent',
|
||||
inputDisplayedData: datasetKeys[4], // 'total_distance'
|
||||
inputValue: 30,
|
||||
expectedResult: '0.03 mi',
|
||||
},
|
||||
]
|
||||
|
||||
testsParams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
assert.equal(
|
||||
formatTooltipValue(
|
||||
testParams.inputDisplayedData,
|
||||
testParams.inputValue,
|
||||
true
|
||||
),
|
||||
testParams.expectedResult
|
||||
)
|
||||
@ -90,6 +139,7 @@ describe('formatTooltipValue (formatWithUnits = false)', () => {
|
||||
formatTooltipValue(
|
||||
testParams.inputDisplayedData,
|
||||
testParams.inputValue,
|
||||
false,
|
||||
false
|
||||
),
|
||||
testParams.expectedResult
|
||||
|
58
fittrackee_client/tests/unit/utils/units.spec.ts
Normal file
58
fittrackee_client/tests/unit/utils/units.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { TUnit } from '@/types/units'
|
||||
import { convertDistance } from '@/utils/units'
|
||||
|
||||
describe('convertDistance', () => {
|
||||
const testsParams: [number, TUnit, TUnit, number][] = [
|
||||
[0, 'm', 'ft', 0],
|
||||
[5, 'm', 'ft', 16.404],
|
||||
[5, 'm', 'mi', 0.003],
|
||||
[5, 'm', 'm', 5.0],
|
||||
[5, 'm', 'km', 0.005],
|
||||
[5, 'km', 'ft', 16404.199],
|
||||
[5, 'km', 'mi', 3.107],
|
||||
[5, 'km', 'm', 5000.0],
|
||||
[5, 'km', 'km', 5.0],
|
||||
[5, 'ft', 'ft', 5.0],
|
||||
[5, 'ft', 'mi', 0.001],
|
||||
[5, 'ft', 'm', 1.524],
|
||||
[5, 'ft', 'km', 0.002],
|
||||
[5, 'mi', 'ft', 26400.0],
|
||||
[5, 'mi', 'mi', 5.0],
|
||||
[5, 'mi', 'm', 8046.72],
|
||||
[5, 'mi', 'km', 8.047],
|
||||
]
|
||||
|
||||
testsParams.map((testParams) => {
|
||||
it(`convert ${testParams[0]}${testParams[1]} in ${testParams[2]}}`, () => {
|
||||
assert.equal(
|
||||
convertDistance(testParams[0], testParams[1], testParams[2]),
|
||||
testParams[3]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('convertDistance w/ digits', () => {
|
||||
const testsParams: [number, TUnit, TUnit, number | null, number][] = [
|
||||
[5, 'km', 'mi', null, 3.106855961174243],
|
||||
[5, 'km', 'mi', 0, 3],
|
||||
[5, 'km', 'mi', 1, 3.1],
|
||||
[5, 'km', 'mi', 2, 3.11],
|
||||
]
|
||||
|
||||
testsParams.map((testParams) => {
|
||||
it(`convert ${testParams[0]}${testParams[1]} in ${testParams[2]}}`, () => {
|
||||
assert.equal(
|
||||
convertDistance(
|
||||
testParams[0],
|
||||
testParams[1],
|
||||
testParams[2],
|
||||
testParams[3]
|
||||
),
|
||||
testParams[4]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -13,6 +13,7 @@ describe('getDatasets', () => {
|
||||
inputParams: {
|
||||
charData: [],
|
||||
locale: 'fr',
|
||||
useImperialUnits: false,
|
||||
},
|
||||
expected: {
|
||||
distance_labels: [],
|
||||
@ -72,6 +73,7 @@ describe('getDatasets', () => {
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
useImperialUnits: false,
|
||||
},
|
||||
expected: {
|
||||
distance_labels: [0, 0, 0.01],
|
||||
@ -102,12 +104,80 @@ describe('getDatasets', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'returns datasets w/ units conversion',
|
||||
inputParams: {
|
||||
charData: [
|
||||
{
|
||||
distance: 0,
|
||||
duration: 0,
|
||||
elevation: 83.6,
|
||||
latitude: 48.845574,
|
||||
longitude: 2.373723,
|
||||
speed: 2.89,
|
||||
time: 'Sun, 12 Sep 2021 13:29:24 GMT',
|
||||
},
|
||||
{
|
||||
distance: 0,
|
||||
duration: 1,
|
||||
elevation: 83.7,
|
||||
latitude: 48.845578,
|
||||
longitude: 2.373732,
|
||||
speed: 1.56,
|
||||
time: 'Sun, 12 Sep 2021 13:29:25 GMT',
|
||||
},
|
||||
{
|
||||
distance: 0.01,
|
||||
duration: 96,
|
||||
elevation: 84.3,
|
||||
latitude: 48.845591,
|
||||
longitude: 2.373811,
|
||||
speed: 14.73,
|
||||
time: 'Sun, 12 Sep 2021 13:31:00 GMT',
|
||||
},
|
||||
],
|
||||
locale: 'en',
|
||||
useImperialUnits: true,
|
||||
},
|
||||
expected: {
|
||||
distance_labels: [0, 0, 0.01],
|
||||
duration_labels: [0, 1, 96],
|
||||
datasets: {
|
||||
speed: {
|
||||
label: 'speed',
|
||||
backgroundColor: ['#FFFFFF'],
|
||||
borderColor: ['#8884d8'],
|
||||
borderWidth: 2,
|
||||
data: [1.8, 0.97, 9.15],
|
||||
yAxisID: 'ySpeed',
|
||||
},
|
||||
elevation: {
|
||||
label: 'elevation',
|
||||
backgroundColor: ['#e5e5e5'],
|
||||
borderColor: ['#cccccc'],
|
||||
borderWidth: 1,
|
||||
fill: true,
|
||||
data: [274.28, 274.61, 276.57],
|
||||
yAxisID: 'yElevation',
|
||||
},
|
||||
},
|
||||
coordinates: [
|
||||
{ latitude: 48.845574, longitude: 2.373723 },
|
||||
{ latitude: 48.845578, longitude: 2.373732 },
|
||||
{ latitude: 48.845591, longitude: 2.373811 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
testparams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
locale.value = testParams.inputParams.locale
|
||||
assert.deepEqual(
|
||||
getDatasets(testParams.inputParams.charData, t),
|
||||
getDatasets(
|
||||
testParams.inputParams.charData,
|
||||
t,
|
||||
testParams.inputParams.useImperialUnits
|
||||
),
|
||||
testParams.expected
|
||||
)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user