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
|
#### 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
|
* [#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
|
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
* [#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
|
* [#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
|
* [#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)
|
## Version 0.4.9 (2021/07/16)
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#### New Features
|
#### 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
|
* [#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
|
* [#90](https://github.com/SamR1/FitTrackee/issues/90) - Add user sports preferences
|
||||||
* [#18](https://github.com/SamR1/FitTrackee/issues/18) - Better UI
|
* [#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
|
* [#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
|
* [#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)
|
## Version 0.4.9 (2021/07/16)
|
||||||
|
@ -34,8 +34,9 @@ Administration
|
|||||||
Account & preferences
|
Account & preferences
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
- A user can create, update and deleted his account
|
- 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 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*):
|
- A user can set sport preferences (*new in 0.5.0*):
|
||||||
- change sport color (used for sport image and charts)
|
- change sport color (used for sport image and charts)
|
||||||
- can override stopped speed threshold (for next uploaded gpx files)
|
- can override stopped speed threshold (for next uploaded gpx files)
|
||||||
@ -72,7 +73,7 @@ Workouts
|
|||||||
It can be overridden in user preferences.
|
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.
|
- 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.
|
- 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.
|
- Workout edition and deletion. User can add a note.
|
||||||
- User statistics
|
- 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">"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">"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">"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">"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">"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>
|
<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">"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">"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">"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">"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">"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>
|
<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">"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">"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">"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">"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">"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>
|
<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">"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">"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">"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">"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">"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>
|
<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">"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">"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">"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">"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">"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>
|
<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">"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">"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">"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">"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">"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>
|
<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">
|
<section id="new-features">
|
||||||
<h4>New Features<a class="headerlink" href="#new-features" title="Permalink to this headline">¶</a></h4>
|
<h4>New Features<a class="headerlink" href="#new-features" title="Permalink to this headline">¶</a></h4>
|
||||||
<ul class="simple">
|
<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/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/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>
|
<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/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>
|
<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>
|
</ul>
|
||||||
<p>In this release 5 issue were closed.</p>
|
<p>In this release 6 issue were closed.</p>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<section id="version-0-4-9-2021-07-16">
|
<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>
|
<h3>Account & preferences<a class="headerlink" href="#account-preferences" title="Permalink to this headline">¶</a></h3>
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li><p>A user can create, update and deleted his account</p></li>
|
<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 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">
|
<li><dl class="simple">
|
||||||
<dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul>
|
<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>
|
<li><p>change sport color (used for sport image and charts)</p></li>
|
||||||
@ -236,7 +237,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul class="simple">
|
<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>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>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>Workout edition and deletion. User can add a note.</p></li>
|
||||||
<li><p>User statistics</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
|
Account & preferences
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
- A user can create, update and deleted his account
|
- 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 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*):
|
- A user can set sport preferences (*new in 0.5.0*):
|
||||||
- change sport color (used for sport image and charts)
|
- change sport color (used for sport image and charts)
|
||||||
- can override stopped speed threshold (for next uploaded gpx files)
|
- can override stopped speed threshold (for next uploaded gpx files)
|
||||||
@ -72,7 +73,7 @@ Workouts
|
|||||||
It can be overridden in user preferences.
|
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.
|
- 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.
|
- 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.
|
- Workout edition and deletion. User can add a note.
|
||||||
- User statistics
|
- User statistics
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from sqlalchemy import exc
|
||||||
from sqlalchemy.engine.base import Connection
|
from sqlalchemy.engine.base import Connection
|
||||||
from sqlalchemy.event import listens_for
|
from sqlalchemy.event import listens_for
|
||||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||||
@ -25,7 +26,15 @@ class AppConfig(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_registration_enabled(self) -> bool:
|
def is_registration_enabled(self) -> bool:
|
||||||
|
try:
|
||||||
nb_users = User.query.count()
|
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
|
return self.max_users == 0 or nb_users < self.max_users
|
||||||
|
|
||||||
@property
|
@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"
|
"url": "/img/workouts/mountains.svg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "926210f132992651a9543d9c76da25ba",
|
"revision": "a83d95cb780cb551f9c6e0257addee7a",
|
||||||
"url": "/index.html"
|
"url": "/index.html"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,8 +80,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/css/admin.babfd43e.css"
|
"url": "/static/css/admin.babfd43e.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "4f95d958d90a2ac1b9a0",
|
"revision": "580dbac1a3cc1ff6f809",
|
||||||
"url": "/static/css/app.e1e7e23c.css"
|
"url": "/static/css/app.2b8c39ab.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "82c1118c918377daaa71a320ab8eea42",
|
"revision": "82c1118c918377daaa71a320ab8eea42",
|
||||||
@ -92,11 +92,11 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/css/leaflet.css"
|
"url": "/static/css/leaflet.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "00c35b353719122c16cd",
|
"revision": "3e2dd5ce7fd86f47e0e5",
|
||||||
"url": "/static/css/main.7229c1ab.css"
|
"url": "/static/css/main.f9856c63.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "11b770a11a1cd8dae5f4",
|
"revision": "ac1280c03a31a5894834",
|
||||||
"url": "/static/css/main~workouts.0edb3403.css"
|
"url": "/static/css/main~workouts.0edb3403.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,8 +108,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/css/reset.46776e72.css"
|
"url": "/static/css/reset.46776e72.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "c78ff76a4bb0919c4b94",
|
"revision": "03d9a79c5f845c47ef9c",
|
||||||
"url": "/static/css/workouts.1b0a7916.css"
|
"url": "/static/css/workouts.84cbed34.css"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "e719f9244c69e28e7d00e725ca1e280e",
|
"revision": "e719f9244c69e28e7d00e725ca1e280e",
|
||||||
@ -196,8 +196,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/js/admin.2f1d393d.js"
|
"url": "/static/js/admin.2f1d393d.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "4f95d958d90a2ac1b9a0",
|
"revision": "580dbac1a3cc1ff6f809",
|
||||||
"url": "/static/js/app.0f3b3ab5.js"
|
"url": "/static/js/app.28d0829a.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "bd7d183c9f68e5f4027d",
|
"revision": "bd7d183c9f68e5f4027d",
|
||||||
@ -220,16 +220,16 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/js/chunk-2d22523a.4b710d99.js"
|
"url": "/static/js/chunk-2d22523a.4b710d99.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "1631aa1204c2ef00fa57",
|
"revision": "c04fcf32d84e5ec5cb38",
|
||||||
"url": "/static/js/chunk-vendors.71654064.js"
|
"url": "/static/js/chunk-vendors.caa4fc1c.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "00c35b353719122c16cd",
|
"revision": "3e2dd5ce7fd86f47e0e5",
|
||||||
"url": "/static/js/main.db9cee98.js"
|
"url": "/static/js/main.23f4d3a6.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "11b770a11a1cd8dae5f4",
|
"revision": "ac1280c03a31a5894834",
|
||||||
"url": "/static/js/main~workouts.a74990d7.js"
|
"url": "/static/js/main~workouts.6afa0411.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "058a877bc4b9cbf8929f",
|
"revision": "058a877bc4b9cbf8929f",
|
||||||
@ -240,7 +240,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([
|
|||||||
"url": "/static/js/reset.518e646f.js"
|
"url": "/static/js/reset.518e646f.js"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"revision": "c78ff76a4bb0919c4b94",
|
"revision": "03d9a79c5f845c47ef9c",
|
||||||
"url": "/static/js/workouts.d69cf48a.js"
|
"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("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
||||||
|
|
||||||
importScripts(
|
importScripts(
|
||||||
"/precache-manifest.c1f31e9729586ecf3c442890704f31cc.js"
|
"/precache-manifest.d81ab1e239beb2ec33c92fe076422816.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
workbox.core.setCacheNameDetails({prefix: "fittrackee_client"});
|
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 not data['data']['admin']
|
||||||
assert data['data']['timezone'] is None
|
assert data['data']['timezone'] is None
|
||||||
assert data['data']['weekm'] is False
|
assert data['data']['weekm'] is False
|
||||||
|
assert data['data']['imperial_units'] is False
|
||||||
assert data['data']['language'] is None
|
assert data['data']['language'] is None
|
||||||
assert data['data']['nb_sports'] == 0
|
assert data['data']['nb_sports'] == 0
|
||||||
assert data['data']['nb_workouts'] == 0
|
assert data['data']['nb_workouts'] == 0
|
||||||
@ -517,6 +518,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
|||||||
assert data['data']['last_name'] == 'Doe'
|
assert data['data']['last_name'] == 'Doe'
|
||||||
assert data['data']['birth_date']
|
assert data['data']['birth_date']
|
||||||
assert data['data']['bio'] == 'just a random guy'
|
assert data['data']['bio'] == 'just a random guy'
|
||||||
|
assert data['data']['imperial_units'] is False
|
||||||
assert data['data']['location'] == 'somewhere'
|
assert data['data']['location'] == 'somewhere'
|
||||||
assert data['data']['timezone'] == 'America/New_York'
|
assert data['data']['timezone'] == 'America/New_York'
|
||||||
assert data['data']['weekm'] is False
|
assert data['data']['weekm'] is False
|
||||||
@ -553,6 +555,7 @@ class TestUserProfile(ApiTestCaseMixin):
|
|||||||
assert data['data']['created_at']
|
assert data['data']['created_at']
|
||||||
assert not data['data']['admin']
|
assert not data['data']['admin']
|
||||||
assert data['data']['timezone'] is None
|
assert data['data']['timezone'] is None
|
||||||
|
assert data['data']['imperial_units'] is False
|
||||||
assert data['data']['nb_sports'] == 2
|
assert data['data']['nb_sports'] == 2
|
||||||
assert data['data']['nb_workouts'] == 2
|
assert data['data']['nb_workouts'] == 2
|
||||||
assert len(data['data']['records']) == 6
|
assert len(data['data']['records']) == 6
|
||||||
@ -605,6 +608,7 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
|||||||
assert data['data']['last_name'] == 'Doe'
|
assert data['data']['last_name'] == 'Doe'
|
||||||
assert data['data']['birth_date']
|
assert data['data']['birth_date']
|
||||||
assert data['data']['bio'] == 'Nothing to tell'
|
assert data['data']['bio'] == 'Nothing to tell'
|
||||||
|
assert data['data']['imperial_units'] is False
|
||||||
assert data['data']['location'] == 'Somewhere'
|
assert data['data']['location'] == 'Somewhere'
|
||||||
assert data['data']['timezone'] is None
|
assert data['data']['timezone'] is None
|
||||||
assert data['data']['weekm'] is False
|
assert data['data']['weekm'] is False
|
||||||
@ -648,6 +652,7 @@ class TestUserProfileUpdate(ApiTestCaseMixin):
|
|||||||
assert data['data']['last_name'] == 'Doe'
|
assert data['data']['last_name'] == 'Doe'
|
||||||
assert data['data']['birth_date']
|
assert data['data']['birth_date']
|
||||||
assert data['data']['bio'] == 'Nothing to tell'
|
assert data['data']['bio'] == 'Nothing to tell'
|
||||||
|
assert data['data']['imperial_units'] is False
|
||||||
assert data['data']['location'] == 'Somewhere'
|
assert data['data']['location'] == 'Somewhere'
|
||||||
assert data['data']['timezone'] is None
|
assert data['data']['timezone'] is None
|
||||||
assert data['data']['weekm'] is False
|
assert data['data']['weekm'] is False
|
||||||
@ -767,6 +772,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
timezone='America/New_York',
|
timezone='America/New_York',
|
||||||
weekm=True,
|
weekm=True,
|
||||||
language='fr',
|
language='fr',
|
||||||
|
imperial_units=True,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
@ -784,6 +790,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
|
|||||||
assert data['data']['last_name'] is None
|
assert data['data']['last_name'] is None
|
||||||
assert data['data']['birth_date'] is None
|
assert data['data']['birth_date'] is None
|
||||||
assert data['data']['bio'] is None
|
assert data['data']['bio'] is None
|
||||||
|
assert data['data']['imperial_units']
|
||||||
assert data['data']['location'] is None
|
assert data['data']['location'] is None
|
||||||
assert data['data']['timezone'] == 'America/New_York'
|
assert data['data']['timezone'] == 'America/New_York'
|
||||||
assert data['data']['weekm'] is True
|
assert data['data']['weekm'] is True
|
||||||
|
@ -36,6 +36,7 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
assert user['last_name'] is None
|
assert user['last_name'] is None
|
||||||
assert user['birth_date'] is None
|
assert user['birth_date'] is None
|
||||||
assert user['bio'] is None
|
assert user['bio'] is None
|
||||||
|
assert user['imperial_units'] is False
|
||||||
assert user['location'] is None
|
assert user['location'] is None
|
||||||
assert user['timezone'] is None
|
assert user['timezone'] is None
|
||||||
assert user['weekm'] is False
|
assert user['weekm'] is False
|
||||||
@ -77,6 +78,7 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
assert user['last_name'] is None
|
assert user['last_name'] is None
|
||||||
assert user['birth_date'] is None
|
assert user['birth_date'] is None
|
||||||
assert user['bio'] is None
|
assert user['bio'] is None
|
||||||
|
assert user['imperial_units'] is False
|
||||||
assert user['location'] is None
|
assert user['location'] is None
|
||||||
assert user['timezone'] is None
|
assert user['timezone'] is None
|
||||||
assert user['weekm'] is False
|
assert user['weekm'] is False
|
||||||
@ -129,6 +131,7 @@ class TestGetUsers(ApiTestCaseMixin):
|
|||||||
assert 'test@test.com' in data['data']['users'][0]['email']
|
assert 'test@test.com' in data['data']['users'][0]['email']
|
||||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
||||||
assert 'sam@test.com' in data['data']['users'][2]['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]['timezone'] is None
|
||||||
assert data['data']['users'][0]['weekm'] is False
|
assert data['data']['users'][0]['weekm'] is False
|
||||||
assert data['data']['users'][0]['language'] is None
|
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]['sports_list'] == []
|
||||||
assert data['data']['users'][0]['total_distance'] == 0
|
assert data['data']['users'][0]['total_distance'] == 0
|
||||||
assert data['data']['users'][0]['total_duration'] == '0:00:00'
|
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]['timezone'] is None
|
||||||
assert data['data']['users'][1]['weekm'] is False
|
assert data['data']['users'][1]['weekm'] is False
|
||||||
assert data['data']['users'][1]['language'] is None
|
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]['sports_list'] == []
|
||||||
assert data['data']['users'][1]['total_distance'] == 0
|
assert data['data']['users'][1]['total_distance'] == 0
|
||||||
assert data['data']['users'][1]['total_duration'] == '0:00:00'
|
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]['timezone'] is None
|
||||||
assert data['data']['users'][2]['weekm'] is True
|
assert data['data']['users'][2]['weekm'] is True
|
||||||
assert data['data']['users'][2]['language'] is None
|
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 'test@test.com' in data['data']['users'][0]['email']
|
||||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
||||||
assert 'sam@test.com' in data['data']['users'][2]['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]['timezone'] is None
|
||||||
assert data['data']['users'][0]['weekm'] is False
|
assert data['data']['users'][0]['weekm'] is False
|
||||||
assert data['data']['users'][0]['nb_sports'] == 2
|
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]['sports_list'] == [1, 2]
|
||||||
assert data['data']['users'][0]['total_distance'] == 22.0
|
assert data['data']['users'][0]['total_distance'] == 22.0
|
||||||
assert data['data']['users'][0]['total_duration'] == '2:40:00'
|
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]['timezone'] is None
|
||||||
assert data['data']['users'][1]['weekm'] is False
|
assert data['data']['users'][1]['weekm'] is False
|
||||||
assert data['data']['users'][1]['nb_sports'] == 1
|
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]['sports_list'] == [1]
|
||||||
assert data['data']['users'][1]['total_distance'] == 15
|
assert data['data']['users'][1]['total_distance'] == 15
|
||||||
assert data['data']['users'][1]['total_duration'] == '1:00:00'
|
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]['timezone'] is None
|
||||||
assert data['data']['users'][2]['weekm'] is True
|
assert data['data']['users'][2]['weekm'] is True
|
||||||
assert data['data']['users'][2]['nb_sports'] == 0
|
assert data['data']['users'][2]['nb_sports'] == 0
|
||||||
|
@ -14,6 +14,7 @@ class TestUserModel:
|
|||||||
assert serialized_user['admin'] is False
|
assert serialized_user['admin'] is False
|
||||||
assert serialized_user['first_name'] is None
|
assert serialized_user['first_name'] is None
|
||||||
assert serialized_user['last_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['bio'] is None
|
||||||
assert serialized_user['location'] is None
|
assert serialized_user['location'] is None
|
||||||
assert serialized_user['birth_date'] 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",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": 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",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": 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",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -653,6 +656,7 @@ def edit_user_preferences(auth_user_id: int) -> Union[Dict, HttpResponse]:
|
|||||||
# get post data
|
# get post data
|
||||||
post_data = request.get_json()
|
post_data = request.get_json()
|
||||||
user_mandatory_data = {
|
user_mandatory_data = {
|
||||||
|
'imperial_units',
|
||||||
'language',
|
'language',
|
||||||
'timezone',
|
'timezone',
|
||||||
'weekm',
|
'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:
|
if not post_data or not post_data.keys() >= user_mandatory_data:
|
||||||
return InvalidPayloadErrorResponse()
|
return InvalidPayloadErrorResponse()
|
||||||
|
|
||||||
|
imperial_units = post_data.get('imperial_units')
|
||||||
language = post_data.get('language')
|
language = post_data.get('language')
|
||||||
timezone = post_data.get('timezone')
|
timezone = post_data.get('timezone')
|
||||||
weekm = post_data.get('weekm')
|
weekm = post_data.get('weekm')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = User.query.filter_by(id=auth_user_id).first()
|
user = User.query.filter_by(id=auth_user_id).first()
|
||||||
|
user.imperial_units = imperial_units
|
||||||
user.language = language
|
user.language = language
|
||||||
user.timezone = timezone
|
user.timezone = timezone
|
||||||
user.weekm = weekm
|
user.weekm = weekm
|
||||||
|
@ -40,6 +40,7 @@ class User(BaseModel):
|
|||||||
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
'Record', lazy=True, backref=db.backref('user', lazy='joined')
|
||||||
)
|
)
|
||||||
language = db.Column(db.String(50), nullable=True)
|
language = db.Column(db.String(50), nullable=True)
|
||||||
|
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<User {self.username!r}>'
|
return f'<User {self.username!r}>'
|
||||||
@ -142,6 +143,7 @@ class User(BaseModel):
|
|||||||
],
|
],
|
||||||
'total_distance': float(total[0]),
|
'total_distance': float(total[0]),
|
||||||
'total_duration': str(total[1]),
|
'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",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -246,6 +247,7 @@ def get_single_user(
|
|||||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -400,6 +402,7 @@ def update_user(
|
|||||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": 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,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
useImperialUnits: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -80,7 +84,12 @@
|
|||||||
ticks: {
|
ticks: {
|
||||||
maxTicksLimit: 6,
|
maxTicksLimit: 6,
|
||||||
callback: function (value) {
|
callback: function (value) {
|
||||||
return formatTooltipValue(props.displayedData, +value, false)
|
return formatTooltipValue(
|
||||||
|
props.displayedData,
|
||||||
|
+value,
|
||||||
|
props.useImperialUnits,
|
||||||
|
false
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
afterFit: function (scale: LayoutItem) {
|
afterFit: function (scale: LayoutItem) {
|
||||||
@ -108,7 +117,12 @@
|
|||||||
.reduce((total, value) => getSum(total, value), 0)
|
.reduce((total, value) => getSum(total, value), 0)
|
||||||
return context.datasetIndex ===
|
return context.datasetIndex ===
|
||||||
props.displayedSportIds.length - 1 && total > 0
|
props.displayedSportIds.length - 1 && total > 0
|
||||||
? formatTooltipValue(props.displayedData, total, false)
|
? formatTooltipValue(
|
||||||
|
props.displayedData,
|
||||||
|
total,
|
||||||
|
props.useImperialUnits,
|
||||||
|
false
|
||||||
|
)
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -132,7 +146,8 @@
|
|||||||
if (context.parsed.y !== null) {
|
if (context.parsed.y !== null) {
|
||||||
label += formatTooltipValue(
|
label += formatTooltipValue(
|
||||||
props.displayedData,
|
props.displayedData,
|
||||||
context.parsed.y
|
context.parsed.y,
|
||||||
|
props.useImperialUnits
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return label
|
return label
|
||||||
@ -144,7 +159,11 @@
|
|||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
`${t('common.TOTAL')}: ` +
|
`${t('common.TOTAL')}: ` +
|
||||||
formatTooltipValue(props.displayedData, sum)
|
formatTooltipValue(
|
||||||
|
props.displayedData,
|
||||||
|
sum,
|
||||||
|
props.useImperialUnits
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
:displayedData="displayedData"
|
:displayedData="displayedData"
|
||||||
:displayedSportIds="displayedSportIds"
|
:displayedSportIds="displayedSportIds"
|
||||||
:fullStats="fullStats"
|
:fullStats="fullStats"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,7 +135,8 @@
|
|||||||
props.user.weekm,
|
props.user.weekm,
|
||||||
props.sports,
|
props.sports,
|
||||||
props.displayedSportIds,
|
props.displayedSportIds,
|
||||||
statistics.value
|
statistics.value,
|
||||||
|
props.user.imperial_units
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<WorkoutCard
|
<WorkoutCard
|
||||||
v-for="index in [...Array(initWorkoutsCount).keys()]"
|
v-for="index in [...Array(initWorkoutsCount).keys()]"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
:key="index"
|
:key="index"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -18,6 +19,7 @@
|
|||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
:key="workout.id"
|
:key="workout.id"
|
||||||
/>
|
/>
|
||||||
<NoWorkouts v-if="workouts.length === 0" />
|
<NoWorkouts v-if="workouts.length === 0" />
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
:sportTranslatedLabel="sportTranslatedLabel"
|
:sportTranslatedLabel="sportTranslatedLabel"
|
||||||
:records="recordsBySport[sportTranslatedLabel]"
|
:records="recordsBySport[sportTranslatedLabel]"
|
||||||
:key="sportTranslatedLabel"
|
:key="sportTranslatedLabel"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,7 +41,8 @@
|
|||||||
getRecordsBySports(
|
getRecordsBySports(
|
||||||
props.user.records,
|
props.user.records,
|
||||||
translateSports(props.sports, t),
|
translateSports(props.sports, t),
|
||||||
props.user.timezone
|
props.user.timezone,
|
||||||
|
props.user.imperial_units
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon="road"
|
icon="road"
|
||||||
:value="Number(user.total_distance).toFixed(2)"
|
:value="totalDistance"
|
||||||
:text="$t('workouts.KM')"
|
:text="unitTo === 'mi' ? 'miles' : unitTo"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon="clock-o"
|
icon="clock-o"
|
||||||
@ -28,7 +28,9 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import StatCard from '@/components/Common/StatCard.vue'
|
import StatCard from '@/components/Common/StatCard.vue'
|
||||||
|
import { TUnit } from '@/types/units'
|
||||||
import { IUserProfile } from '@/types/user'
|
import { IUserProfile } from '@/types/user'
|
||||||
|
import { convertDistance, units } from '@/utils/units'
|
||||||
interface Props {
|
interface Props {
|
||||||
user: IUserProfile
|
user: IUserProfile
|
||||||
}
|
}
|
||||||
@ -41,6 +43,13 @@
|
|||||||
() => props.user.total_duration
|
() => props.user.total_duration
|
||||||
)
|
)
|
||||||
const totalDuration = computed(() => get_duration(userTotalDuration))
|
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>) {
|
function get_duration(total_duration: ComputedRef<string>) {
|
||||||
const duration = total_duration.value.match(/day/g)
|
const duration = total_duration.value.match(/day/g)
|
||||||
|
@ -11,10 +11,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-stat">
|
<div class="user-stat">
|
||||||
<span class="stat-number">{{
|
<Distance
|
||||||
Number(user.total_distance).toFixed(0)
|
:distance="user.total_distance"
|
||||||
}}</span>
|
unitFrom="km"
|
||||||
<span class="stat-label">km</span>
|
:digits="0"
|
||||||
|
:displayUnit="false"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
|
/>
|
||||||
|
<span class="stat-label">
|
||||||
|
{{ user.imperial_units ? 'miles' : 'km' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-stat hide-small">
|
<div class="user-stat hide-small">
|
||||||
<span class="stat-number">{{ user.nb_sports }}</span>
|
<span class="stat-number">{{ user.nb_sports }}</span>
|
||||||
@ -72,6 +78,7 @@
|
|||||||
.stat-label {
|
.stat-label {
|
||||||
padding: 0 $default-padding * 0.5;
|
padding: 0 $default-padding * 0.5;
|
||||||
}
|
}
|
||||||
|
::v-deep(.distance),
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
@ -87,6 +94,7 @@
|
|||||||
.user-stats {
|
.user-stats {
|
||||||
gap: $default-padding * 2;
|
gap: $default-padding * 2;
|
||||||
.user-stat {
|
.user-stat {
|
||||||
|
::v-deep(.distance),
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
@ -7,6 +7,14 @@
|
|||||||
<dd>{{ timezone }}</dd>
|
<dd>{{ timezone }}</dd>
|
||||||
<dt>{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}:</dt>
|
<dt>{{ $t('user.PROFILE.FIRST_DAY_OF_WEEK') }}:</dt>
|
||||||
<dd>{{ $t(`user.PROFILE.${fistDayOfWeek}`) }}</dd>
|
<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>
|
</dl>
|
||||||
<div class="profile-buttons">
|
<div class="profile-buttons">
|
||||||
<button @click="$router.push('/profile/edit/preferences')">
|
<button @click="$router.push('/profile/edit/preferences')">
|
||||||
|
@ -35,6 +35,22 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">
|
<div class="form-buttons">
|
||||||
<button class="confirm" type="submit">
|
<button class="confirm" type="submit">
|
||||||
{{ $t('buttons.SUBMIT') }}
|
{{ $t('buttons.SUBMIT') }}
|
||||||
@ -68,6 +84,7 @@
|
|||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const userForm: IUserPreferencesPayload = reactive({
|
const userForm: IUserPreferencesPayload = reactive({
|
||||||
|
imperial_units: false,
|
||||||
language: '',
|
language: '',
|
||||||
timezone: 'Europe/Paris',
|
timezone: 'Europe/Paris',
|
||||||
weekm: false,
|
weekm: false,
|
||||||
@ -82,6 +99,16 @@
|
|||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
const imperialUnits = [
|
||||||
|
{
|
||||||
|
label: 'IMPERIAL',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'METRIC',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
const loading = computed(
|
const loading = computed(
|
||||||
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
|
||||||
)
|
)
|
||||||
@ -96,6 +123,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
function updateUserForm(user: IUserProfile) {
|
function updateUserForm(user: IUserProfile) {
|
||||||
|
userForm.imperial_units = user.imperial_units ? user.imperial_units : false
|
||||||
userForm.language = user.language ? user.language : 'en'
|
userForm.language = user.language ? user.language : 'en'
|
||||||
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
|
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
|
||||||
userForm.weekm = user.weekm ? user.weekm : false
|
userForm.weekm = user.weekm ? user.weekm : false
|
||||||
|
@ -87,7 +87,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="data">
|
<div class="data">
|
||||||
<i class="fa fa-road" aria-hidden="true" />
|
<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>
|
||||||
<div class="data elevation" v-if="workout && workout.with_gpx">
|
<div class="data elevation" v-if="workout && workout.with_gpx">
|
||||||
<img
|
<img
|
||||||
@ -96,15 +102,37 @@
|
|||||||
:alt="$t('workouts.ELEVATION')"
|
:alt="$t('workouts.ELEVATION')"
|
||||||
/>
|
/>
|
||||||
<div class="data-values">
|
<div class="data-values">
|
||||||
<span>{{ workout.min_alt }}/</span>
|
<Distance
|
||||||
<span>{{ workout.max_alt }} m </span>
|
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>
|
</div>
|
||||||
<div class="data altitude" v-if="workout && workout.with_gpx">
|
<div class="data altitude" v-if="workout && workout.with_gpx">
|
||||||
<i class="fa fa-location-arrow" aria-hidden="true" />
|
<i class="fa fa-location-arrow" aria-hidden="true" />
|
||||||
<div class="data-values">
|
<div class="data-values">
|
||||||
<span>+ {{ workout.ascent }}/</span>
|
+<Distance
|
||||||
<span>- {{ workout.descent }} m </span>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,6 +155,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: IUserProfile
|
user: IUserProfile
|
||||||
|
useImperialUnits: boolean
|
||||||
workout?: IWorkout
|
workout?: IWorkout
|
||||||
sport?: ISport
|
sport?: ISport
|
||||||
}
|
}
|
||||||
@ -137,7 +166,7 @@
|
|||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const { user, workout, sport } = toRefs(props)
|
const { user, workout, sport, useImperialUnits } = toRefs(props)
|
||||||
const locale: ComputedRef<Locale> = computed(
|
const locale: ComputedRef<Locale> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
|
() => store.getters[ROOT_STORE.GETTERS.LOCALE]
|
||||||
)
|
)
|
||||||
|
@ -55,12 +55,14 @@
|
|||||||
import { LineChart, useLineChart } from 'vue-chart-3'
|
import { LineChart, useLineChart } from 'vue-chart-3'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { TUnit } from '@/types/units'
|
||||||
import { IUserProfile } from '@/types/user'
|
import { IUserProfile } from '@/types/user'
|
||||||
import {
|
import {
|
||||||
IWorkoutChartData,
|
IWorkoutChartData,
|
||||||
IWorkoutData,
|
IWorkoutData,
|
||||||
TCoordinates,
|
TCoordinates,
|
||||||
} from '@/types/workouts'
|
} from '@/types/workouts'
|
||||||
|
import { units } from '@/utils/units'
|
||||||
import { getDatasets } from '@/utils/workouts'
|
import { getDatasets } from '@/utils/workouts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -76,8 +78,10 @@
|
|||||||
let displayDistance = ref(true)
|
let displayDistance = ref(true)
|
||||||
let beginElevationAtZero = ref(true)
|
let beginElevationAtZero = ref(true)
|
||||||
const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
|
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(() => ({
|
let chartData: ComputedRef<ChartData<'line'>> = computed(() => ({
|
||||||
labels: displayDistance.value
|
labels: displayDistance.value
|
||||||
? datasets.value.distance_labels
|
? datasets.value.distance_labels
|
||||||
@ -119,7 +123,7 @@
|
|||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: displayDistance.value
|
text: displayDistance.value
|
||||||
? t('workouts.DISTANCE') + ' (km)'
|
? t('workouts.DISTANCE') + ` (${fromKmUnit})`
|
||||||
: t('workouts.DURATION'),
|
: t('workouts.DURATION'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -130,7 +134,7 @@
|
|||||||
position: 'left',
|
position: 'left',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: t('workouts.SPEED') + ' (km/h)',
|
text: t('workouts.SPEED') + ` (${fromKmUnit}/h)`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yElevation: {
|
yElevation: {
|
||||||
@ -141,7 +145,7 @@
|
|||||||
position: 'right',
|
position: 'right',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: t('workouts.ELEVATION') + ' (m)',
|
text: t('workouts.ELEVATION') + ` (${fromMUnit})`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -164,8 +168,8 @@
|
|||||||
label: function (context) {
|
label: function (context) {
|
||||||
const label = ` ${context.dataset.label}: ${context.formattedValue}`
|
const label = ` ${context.dataset.label}: ${context.formattedValue}`
|
||||||
return context.dataset.yAxisID === 'yElevation'
|
return context.dataset.yAxisID === 'yElevation'
|
||||||
? label + ' m'
|
? label + ` ${fromMUnit}`
|
||||||
: label + ' km/h'
|
: label + ` ${fromKmUnit}/h`
|
||||||
},
|
},
|
||||||
title: function (tooltipItems) {
|
title: function (tooltipItems) {
|
||||||
if (tooltipItems.length > 0) {
|
if (tooltipItems.length > 0) {
|
||||||
@ -174,7 +178,9 @@
|
|||||||
return tooltipItems.length === 0
|
return tooltipItems.length === 0
|
||||||
? ''
|
? ''
|
||||||
: displayDistance.value
|
: displayDistance.value
|
||||||
? `${t('workouts.DISTANCE')}: ${tooltipItems[0].label} km`
|
? `${t('workouts.DISTANCE')}: ${
|
||||||
|
tooltipItems[0].label
|
||||||
|
} ${fromKmUnit}`
|
||||||
: `${t('workouts.DURATION')}: ${formatDuration(
|
: `${t('workouts.DURATION')}: ${formatDuration(
|
||||||
tooltipItems[0].label.replace(',', '')
|
tooltipItems[0].label.replace(',', '')
|
||||||
)}`
|
)}`
|
||||||
@ -200,6 +206,11 @@
|
|||||||
function emitEmptyCoordinates() {
|
function emitEmptyCoordinates() {
|
||||||
emitCoordinates({ latitude: null, longitude: null })
|
emitCoordinates({ latitude: null, longitude: null })
|
||||||
}
|
}
|
||||||
|
function getUnitTo(unitFrom: TUnit): TUnit {
|
||||||
|
return props.authUser.imperial_units
|
||||||
|
? units[unitFrom].defaultTarget
|
||||||
|
: unitFrom
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -2,27 +2,48 @@
|
|||||||
<div id="workout-info">
|
<div id="workout-info">
|
||||||
<div class="workout-data">
|
<div class="workout-data">
|
||||||
<i class="fa fa-clock-o" aria-hidden="true" />
|
<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" />
|
<WorkoutRecord :workoutObject="workoutObject" recordType="LD" />
|
||||||
<div v-if="withPause">
|
<div v-if="withPause">
|
||||||
({{ $t('workouts.PAUSES') }}: <span>{{ workoutObject.pauses }}</span> -
|
({{ $t('workouts.PAUSES') }}:
|
||||||
|
<span class="value">{{ workoutObject.pauses }}</span> -
|
||||||
{{ $t('workouts.TOTAL_DURATION') }}:
|
{{ $t('workouts.TOTAL_DURATION') }}:
|
||||||
<span>{{ workoutObject.duration }})</span>
|
<span class="value">{{ workoutObject.duration }})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="workout-data">
|
<div class="workout-data">
|
||||||
<i class="fa fa-road" aria-hidden="true" />
|
<i class="fa fa-road" aria-hidden="true" />
|
||||||
{{ $t('workouts.DISTANCE') }}:
|
<span class="label"> {{ $t('workouts.DISTANCE') }} </span>:
|
||||||
<span>{{ workoutObject.distance }} km</span>
|
<Distance
|
||||||
|
:distance="workoutObject.distance"
|
||||||
|
:digits="3"
|
||||||
|
unitFrom="km"
|
||||||
|
:strong="true"
|
||||||
|
:useImperialUnits="useImperialUnits"
|
||||||
|
/>
|
||||||
<WorkoutRecord :workoutObject="workoutObject" recordType="FD" />
|
<WorkoutRecord :workoutObject="workoutObject" recordType="FD" />
|
||||||
</div>
|
</div>
|
||||||
<div class="workout-data">
|
<div class="workout-data">
|
||||||
<i class="fa fa-tachometer" aria-hidden="true" />
|
<i class="fa fa-tachometer" aria-hidden="true" />
|
||||||
{{ $t('workouts.AVERAGE_SPEED') }}:
|
<span class="label">{{ $t('workouts.AVERAGE_SPEED') }}</span
|
||||||
<span>{{ workoutObject.aveSpeed }} km/h</span
|
>:
|
||||||
><WorkoutRecord :workoutObject="workoutObject" recordType="AS" /><br />
|
<Distance
|
||||||
{{ $t('workouts.MAX_SPEED') }}:
|
:distance="workoutObject.aveSpeed"
|
||||||
<span>{{ workoutObject.maxSpeed }} km/h</span>
|
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" />
|
<WorkoutRecord :workoutObject="workoutObject" recordType="MS" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -34,21 +55,48 @@
|
|||||||
src="/img/workouts/mountains.svg"
|
src="/img/workouts/mountains.svg"
|
||||||
:alt="$t('workouts.ELEVATION')"
|
:alt="$t('workouts.ELEVATION')"
|
||||||
/>
|
/>
|
||||||
{{ $t('workouts.MIN_ALTITUDE') }}:
|
<span class="label">{{ $t('workouts.MIN_ALTITUDE') }}</span
|
||||||
<span>{{ workoutObject.minAlt }} m</span><br />
|
>:
|
||||||
{{ $t('workouts.MAX_ALTITUDE') }}:
|
<Distance
|
||||||
<span>{{ workoutObject.maxAlt }} m</span>
|
: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>
|
||||||
<div
|
<div
|
||||||
class="workout-data"
|
class="workout-data"
|
||||||
v-if="workoutObject.ascent !== null && workoutObject.descent !== null"
|
v-if="workoutObject.ascent !== null && workoutObject.descent !== null"
|
||||||
>
|
>
|
||||||
<i class="fa fa-location-arrow" aria-hidden="true" />
|
<i class="fa fa-location-arrow" aria-hidden="true" />
|
||||||
{{ $t('workouts.ASCENT') }}: <span>{{ workoutObject.ascent }} m</span
|
<span class="label">{{ $t('workouts.ASCENT') }}</span
|
||||||
><br />
|
>:
|
||||||
{{ $t('workouts.DESCENT') }}: <span>{{ workoutObject.descent }} m</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>
|
</div>
|
||||||
<WorkoutWeather :workoutObject="workoutObject" />
|
<WorkoutWeather
|
||||||
|
:workoutObject="workoutObject"
|
||||||
|
:useImperialUnits="useImperialUnits"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -61,10 +109,11 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workoutObject: IWorkoutObject
|
workoutObject: IWorkoutObject
|
||||||
|
useImperialUnits: boolean
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const { workoutObject } = toRefs(props)
|
const { workoutObject, useImperialUnits } = toRefs(props)
|
||||||
const withPause = computed(
|
const withPause = computed(
|
||||||
() =>
|
() =>
|
||||||
props.workoutObject.pauses !== '0:00:00' &&
|
props.workoutObject.pauses !== '0:00:00' &&
|
||||||
@ -80,11 +129,17 @@
|
|||||||
padding: $default-padding $default-padding * 2;
|
padding: $default-padding $default-padding * 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.workout-data {
|
.fa,
|
||||||
text-transform: capitalize;
|
.mountains {
|
||||||
padding: $default-padding * 0.5 0;
|
padding-right: $default-padding * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.workout-data {
|
||||||
|
padding: $default-padding * 0.5 0;
|
||||||
|
.label {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
@ -89,8 +89,26 @@
|
|||||||
:title="$t(`workouts.WEATHER.WIND`)"
|
:title="$t(`workouts.WEATHER.WIND`)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ Number(workoutObject.weatherStart.wind).toFixed(1) }}m/s</td>
|
<td>
|
||||||
<td>{{ Number(workoutObject.weatherEnd.wind).toFixed(1) }}m/s</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -104,10 +122,11 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workoutObject: IWorkoutObject
|
workoutObject: IWorkoutObject
|
||||||
|
useImperialUnits: boolean
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const { workoutObject } = toRefs(props)
|
const { useImperialUnits, workoutObject } = toRefs(props)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -20,7 +20,10 @@
|
|||||||
:workoutData="workoutData"
|
:workoutData="workoutData"
|
||||||
:markerCoordinates="markerCoordinates"
|
:markerCoordinates="markerCoordinates"
|
||||||
/>
|
/>
|
||||||
<WorkoutData :workoutObject="workoutObject" />
|
<WorkoutData
|
||||||
|
:workoutObject="workoutObject"
|
||||||
|
:useImperialUnits="authUser.imperial_units"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +71,7 @@
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const { markerCoordinates, workoutData } = toRefs(props)
|
const { authUser, markerCoordinates, workoutData } = toRefs(props)
|
||||||
const workout: ComputedRef<IWorkout> = computed(
|
const workout: ComputedRef<IWorkout> = computed(
|
||||||
() => props.workoutData.workout
|
() => props.workoutData.workout
|
||||||
)
|
)
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
class="workout-duration"
|
class="workout-duration"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="HH"
|
placeholder="HH"
|
||||||
pattern="^([0-9]*[0-9])$"
|
pattern="^([0-1]?[0-9]|2[0-3])$"
|
||||||
required
|
required
|
||||||
@invalid="invalidateForm"
|
@invalid="invalidateForm"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@ -173,12 +173,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label>{{ $t('workouts.DISTANCE') }} (km):</label>
|
<label>
|
||||||
|
{{ $t('workouts.DISTANCE') }} ({{
|
||||||
|
authUser.imperial_units ? 'mi' : 'km'
|
||||||
|
}}):
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
name="workout-distance"
|
name="workout-distance"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.001"
|
||||||
required
|
required
|
||||||
@invalid="invalidateForm"
|
@invalid="invalidateForm"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@ -239,6 +243,7 @@
|
|||||||
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
||||||
import { getReadableFileSize } from '@/utils/files'
|
import { getReadableFileSize } from '@/utils/files'
|
||||||
import { translateSports } from '@/utils/sports'
|
import { translateSports } from '@/utils/sports'
|
||||||
|
import { convertDistance } from '@/utils/units'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
authUser: IUserProfile
|
authUser: IUserProfile
|
||||||
@ -257,7 +262,7 @@
|
|||||||
const store = useStore()
|
const store = useStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const { workout, isCreation, loading } = toRefs(props)
|
const { authUser, workout, isCreation, loading } = toRefs(props)
|
||||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||||
translateSports(
|
translateSports(
|
||||||
props.sports,
|
props.sports,
|
||||||
@ -324,7 +329,11 @@
|
|||||||
'yyyy-MM-dd'
|
'yyyy-MM-dd'
|
||||||
)
|
)
|
||||||
const duration = workout.duration.split(':')
|
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.workoutDate = workoutDateTime.workout_date
|
||||||
workoutForm.workoutTime = workoutDateTime.workout_time
|
workoutForm.workoutTime = workoutDateTime.workout_time
|
||||||
workoutForm.workoutDurationHour = duration[0]
|
workoutForm.workoutDurationHour = duration[0]
|
||||||
@ -334,7 +343,9 @@
|
|||||||
}
|
}
|
||||||
function formatPayload(payload: IWorkoutForm) {
|
function formatPayload(payload: IWorkoutForm) {
|
||||||
payload.title = workoutForm.title
|
payload.title = workoutForm.title
|
||||||
payload.distance = +workoutForm.workoutDistance
|
payload.distance = authUser.value.imperial_units
|
||||||
|
? convertDistance(+workoutForm.workoutDistance, 'mi', 'km', 3)
|
||||||
|
: +workoutForm.workoutDistance
|
||||||
payload.duration =
|
payload.duration =
|
||||||
+workoutForm.workoutDurationHour * 3600 +
|
+workoutForm.workoutDurationHour * 3600 +
|
||||||
+workoutForm.workoutDurationMinutes * 60 +
|
+workoutForm.workoutDurationMinutes * 60 +
|
||||||
|
@ -15,8 +15,12 @@
|
|||||||
}"
|
}"
|
||||||
>{{ $t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
|
>{{ $t('workouts.SEGMENT', 1) }} {{ index + 1 }}</router-link
|
||||||
>
|
>
|
||||||
({{ $t('workouts.DISTANCE') }}: {{ segment.distance }} km,
|
({{ $t('workouts.DISTANCE') }}:
|
||||||
{{ $t('workouts.DURATION') }}: {{ segment.duration }})
|
<Distance
|
||||||
|
:distance="segment.distance"
|
||||||
|
unitFrom="km"
|
||||||
|
:useImperialUnits="useImperialUnits"
|
||||||
|
/>, {{ $t('workouts.DURATION') }}: {{ segment.duration }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
@ -31,10 +35,11 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
segments: IWorkoutSegment[]
|
segments: IWorkoutSegment[]
|
||||||
|
useImperialUnits: boolean
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
const { segments } = toRefs(props)
|
const { segments, useImperialUnits } = toRefs(props)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<div class="form-items-group">
|
<div class="form-items-group">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label> {{ $t('workouts.DISTANCE') }} (km): </label>
|
<label> {{ $t('workouts.DISTANCE') }} ({{ toUnit }}): </label>
|
||||||
<div class="form-inputs-group">
|
<div class="form-inputs-group">
|
||||||
<input
|
<input
|
||||||
name="distance_from"
|
name="distance_from"
|
||||||
@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<div class="form-items-group">
|
<div class="form-items-group">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label> {{ $t('workouts.DURATION') }} (km): </label>
|
<label> {{ $t('workouts.DURATION') }} ({{ toUnit }}): </label>
|
||||||
<div class="form-inputs-group">
|
<div class="form-inputs-group">
|
||||||
<input
|
<input
|
||||||
name="duration_from"
|
name="duration_from"
|
||||||
@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
<div class="form-items-group">
|
<div class="form-items-group">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label> {{ $t('workouts.AVE_SPEED') }} (km): </label>
|
<label> {{ $t('workouts.AVE_SPEED') }} ({{ toUnit }}): </label>
|
||||||
<div class="form-inputs-group">
|
<div class="form-inputs-group">
|
||||||
<input
|
<input
|
||||||
min="0"
|
min="0"
|
||||||
@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
<div class="form-items-group">
|
<div class="form-items-group">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label> {{ $t('workouts.MAX_SPEED') }} (km): </label>
|
<label> {{ $t('workouts.MAX_SPEED') }} ({{ toUnit }}): </label>
|
||||||
|
|
||||||
<div class="form-inputs-group">
|
<div class="form-inputs-group">
|
||||||
<input
|
<input
|
||||||
@ -167,6 +167,7 @@
|
|||||||
import { ISport } from '@/types/sports'
|
import { ISport } from '@/types/sports'
|
||||||
import { IUserProfile } from '@/types/user'
|
import { IUserProfile } from '@/types/user'
|
||||||
import { translateSports } from '@/utils/sports'
|
import { translateSports } from '@/utils/sports'
|
||||||
|
import { units } from '@/utils/units'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
authUser: IUserProfile
|
authUser: IUserProfile
|
||||||
@ -181,6 +182,10 @@
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const { authUser } = toRefs(props)
|
const { authUser } = toRefs(props)
|
||||||
|
|
||||||
|
const toUnit = authUser.value.imperial_units
|
||||||
|
? units['km'].defaultTarget
|
||||||
|
: 'km'
|
||||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||||
translateSports(props.sports, t)
|
translateSports(props.sports, t)
|
||||||
)
|
)
|
||||||
|
@ -45,8 +45,9 @@
|
|||||||
{{ $t('workouts.SPORT', 1) }}
|
{{ $t('workouts.SPORT', 1) }}
|
||||||
</span>
|
</span>
|
||||||
<SportImage
|
<SportImage
|
||||||
|
v-if="sports.length > 0"
|
||||||
:title="
|
:title="
|
||||||
sports.filter((s) => s.id === workout.sport_id)[0]
|
sports.find((s) => s.id === workout.sport_id)
|
||||||
.translatedLabel
|
.translatedLabel
|
||||||
"
|
"
|
||||||
:sport-label="getSportLabel(workout, sports)"
|
:sport-label="getSportLabel(workout, sports)"
|
||||||
@ -93,7 +94,11 @@
|
|||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
{{ $t('workouts.DISTANCE') }}
|
{{ $t('workouts.DISTANCE') }}
|
||||||
</span>
|
</span>
|
||||||
{{ Number(workout.distance).toFixed(2) }} km
|
<Distance
|
||||||
|
:distance="workout.distance"
|
||||||
|
unitFrom="km"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
@ -105,25 +110,45 @@
|
|||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
{{ $t('workouts.AVE_SPEED') }}
|
{{ $t('workouts.AVE_SPEED') }}
|
||||||
</span>
|
</span>
|
||||||
{{ workout.ave_speed }} km/h
|
<Distance
|
||||||
|
:distance="workout.ave_speed"
|
||||||
|
unitFrom="km"
|
||||||
|
:speed="true"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
{{ $t('workouts.MAX_SPEED') }}
|
{{ $t('workouts.MAX_SPEED') }}
|
||||||
</span>
|
</span>
|
||||||
{{ workout.max_speed }} km/h
|
<Distance
|
||||||
|
:distance="workout.max_speed"
|
||||||
|
unitFrom="km"
|
||||||
|
:speed="true"
|
||||||
|
:useImperialUnits="user.imperial_units"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
{{ $t('workouts.ASCENT') }}
|
{{ $t('workouts.ASCENT') }}
|
||||||
</span>
|
</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>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span class="cell-heading">
|
<span class="cell-heading">
|
||||||
{{ $t('workouts.DESCENT') }}
|
{{ $t('workouts.DESCENT') }}
|
||||||
</span>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -163,6 +188,7 @@
|
|||||||
import { getQuery, sortList, workoutsPayloadKeys } from '@/utils/api'
|
import { getQuery, sortList, workoutsPayloadKeys } from '@/utils/api'
|
||||||
import { getDateWithTZ } from '@/utils/dates'
|
import { getDateWithTZ } from '@/utils/dates'
|
||||||
import { getSportColor, getSportLabel } from '@/utils/sports'
|
import { getSportColor, getSportLabel } from '@/utils/sports'
|
||||||
|
import { convertDistance } from '@/utils/units'
|
||||||
import { defaultOrder } from '@/utils/workouts'
|
import { defaultOrder } from '@/utils/workouts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -196,7 +222,10 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
function loadWorkouts(payload: TWorkoutsPayload) {
|
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) {
|
function reloadWorkouts(queryParam: string, queryValue: string) {
|
||||||
const newQuery: LocationQuery = Object.assign({}, route.query)
|
const newQuery: LocationQuery = Object.assign({}, route.query)
|
||||||
@ -224,6 +253,18 @@
|
|||||||
return query
|
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) {
|
function onHover(workoutId: string | null) {
|
||||||
hoverWorkoutId.value = workoutId
|
hoverWorkoutId.value = workoutId
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import AlertMessage from '@/components/Common/AlertMessage.vue'
|
import AlertMessage from '@/components/Common/AlertMessage.vue'
|
||||||
import Card from '@/components/Common/Card.vue'
|
import Card from '@/components/Common/Card.vue'
|
||||||
import CustomTextArea from '@/components/Common/CustomTextArea.vue'
|
import CustomTextArea from '@/components/Common/CustomTextArea.vue'
|
||||||
|
import Distance from '@/components/Common/Distance.vue'
|
||||||
import Dropdown from '@/components/Common/Dropdown.vue'
|
import Dropdown from '@/components/Common/Dropdown.vue'
|
||||||
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
|
import ErrorMessage from '@/components/Common/ErrorMessage.vue'
|
||||||
import SportImage from '@/components/Common/Images/SportImage/index.vue'
|
import SportImage from '@/components/Common/Images/SportImage/index.vue'
|
||||||
@ -11,6 +12,7 @@ export const customComponents = [
|
|||||||
{ target: AlertMessage, name: 'AlertMessage' },
|
{ target: AlertMessage, name: 'AlertMessage' },
|
||||||
{ target: Card, name: 'Card' },
|
{ target: Card, name: 'Card' },
|
||||||
{ target: CustomTextArea, name: 'CustomTextArea' },
|
{ target: CustomTextArea, name: 'CustomTextArea' },
|
||||||
|
{ target: Distance, name: 'Distance' },
|
||||||
{ target: Dropdown, name: 'Dropdown' },
|
{ target: Dropdown, name: 'Dropdown' },
|
||||||
{ target: ErrorMessage, name: 'ErrorMessage' },
|
{ target: ErrorMessage, name: 'ErrorMessage' },
|
||||||
{ target: Loader, name: 'Loader' },
|
{ target: Loader, name: 'Loader' },
|
||||||
|
@ -53,6 +53,11 @@
|
|||||||
"LABEL": "label",
|
"LABEL": "label",
|
||||||
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
|
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
|
||||||
},
|
},
|
||||||
|
"UNITS": {
|
||||||
|
"LABEL": "Units for distance",
|
||||||
|
"IMPERIAL": "Imperial system (ft, mi)",
|
||||||
|
"METRIC": "Metric system (m, km)"
|
||||||
|
},
|
||||||
"TIMEZONE": "Timezone"
|
"TIMEZONE": "Timezone"
|
||||||
},
|
},
|
||||||
"REGISTER": "Register",
|
"REGISTER": "Register",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"FROM": "from",
|
"FROM": "from",
|
||||||
"GPX_FILE": ".gpx file",
|
"GPX_FILE": ".gpx file",
|
||||||
"HIDE_FILTERS": "hide filters",
|
"HIDE_FILTERS": "hide filters",
|
||||||
"KM": "km",
|
|
||||||
"LATEST_WORKOUTS": "Latest workouts",
|
"LATEST_WORKOUTS": "Latest workouts",
|
||||||
"LOAD_MORE_WORKOUT": "Load more workouts",
|
"LOAD_MORE_WORKOUT": "Load more workouts",
|
||||||
"MAX_ALTITUDE": "max. altitude",
|
"MAX_ALTITUDE": "max. altitude",
|
||||||
|
@ -45,6 +45,11 @@
|
|||||||
"PROFILE": "profil",
|
"PROFILE": "profil",
|
||||||
"SPORTS": "sports"
|
"SPORTS": "sports"
|
||||||
},
|
},
|
||||||
|
"UNITS": {
|
||||||
|
"LABEL": "Unités pour les distances ",
|
||||||
|
"IMPERIAL": "Système impérial (ft, mi)",
|
||||||
|
"METRIC": "Système métrique (m, km)"
|
||||||
|
},
|
||||||
"SPORT": {
|
"SPORT": {
|
||||||
"ACTION": "action",
|
"ACTION": "action",
|
||||||
"COLOR": "couleur",
|
"COLOR": "couleur",
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"FROM": "à partir de",
|
"FROM": "à partir de",
|
||||||
"GPX_FILE": "fichier .gpx",
|
"GPX_FILE": "fichier .gpx",
|
||||||
"HIDE_FILTERS": "masquer les filtres",
|
"HIDE_FILTERS": "masquer les filtres",
|
||||||
"KM": "km",
|
|
||||||
"LATEST_WORKOUTS": "Séances récentes",
|
"LATEST_WORKOUTS": "Séances récentes",
|
||||||
"LOAD_MORE_WORKOUT": "Charger les séances suivantes",
|
"LOAD_MORE_WORKOUT": "Charger les séances suivantes",
|
||||||
"MAX_ALTITUDE": "altitude max",
|
"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
|
created_at: string
|
||||||
email: string
|
email: string
|
||||||
first_name: string | null
|
first_name: string | null
|
||||||
|
imperial_units: boolean
|
||||||
language: string | null
|
language: string | null
|
||||||
last_name: string | null
|
last_name: string | null
|
||||||
location: string | null
|
location: string | null
|
||||||
@ -40,6 +41,7 @@ export interface IAdminUserPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserPreferencesPayload {
|
export interface IUserPreferencesPayload {
|
||||||
|
imperial_units: boolean
|
||||||
language: string
|
language: string
|
||||||
timezone: string
|
timezone: string
|
||||||
weekm: boolean
|
weekm: boolean
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
import { ITranslatedSport } from '@/types/sports'
|
import { ITranslatedSport } from '@/types/sports'
|
||||||
|
import { TUnit } from '@/types/units'
|
||||||
import { IRecord, IRecordsBySports } from '@/types/workouts'
|
import { IRecord, IRecordsBySports } from '@/types/workouts'
|
||||||
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
|
||||||
|
import { convertDistance, units } from '@/utils/units'
|
||||||
|
|
||||||
export const formatRecord = (
|
export const formatRecord = (
|
||||||
record: IRecord,
|
record: IRecord,
|
||||||
tz: string
|
tz: string,
|
||||||
|
useImperialUnits: boolean
|
||||||
): Record<string, string | number> => {
|
): Record<string, string | number> => {
|
||||||
|
const unitFrom: TUnit = 'km'
|
||||||
|
const unitTo: TUnit = useImperialUnits
|
||||||
|
? units[unitFrom].defaultTarget
|
||||||
|
: unitFrom
|
||||||
let value
|
let value
|
||||||
switch (record.record_type) {
|
switch (record.record_type) {
|
||||||
case 'AS':
|
case 'AS':
|
||||||
case 'MS':
|
case 'MS':
|
||||||
value = `${record.value} km/h`
|
value = `${convertDistance(
|
||||||
|
+record.value,
|
||||||
|
unitFrom,
|
||||||
|
unitTo,
|
||||||
|
2
|
||||||
|
)} ${unitTo}/h`
|
||||||
break
|
break
|
||||||
case 'FD':
|
case 'FD':
|
||||||
value = `${record.value} km`
|
value = `${convertDistance(+record.value, unitFrom, unitTo, 3)} ${unitTo}`
|
||||||
break
|
break
|
||||||
case 'LD':
|
case 'LD':
|
||||||
value = record.value
|
value = record.value
|
||||||
@ -36,7 +48,8 @@ export const formatRecord = (
|
|||||||
export const getRecordsBySports = (
|
export const getRecordsBySports = (
|
||||||
records: IRecord[],
|
records: IRecord[],
|
||||||
translatedSports: ITranslatedSport[],
|
translatedSports: ITranslatedSport[],
|
||||||
tz: string
|
tz: string,
|
||||||
|
useImperialUnits: boolean
|
||||||
): IRecordsBySports =>
|
): IRecordsBySports =>
|
||||||
records.reduce((sportList: IRecordsBySports, record) => {
|
records.reduce((sportList: IRecordsBySports, record) => {
|
||||||
const sport = translatedSports.find((s) => s.id === record.sport_id)
|
const sport = translatedSports.find((s) => s.id === record.sport_id)
|
||||||
@ -48,7 +61,9 @@ export const getRecordsBySports = (
|
|||||||
records: [],
|
records: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sportList[sport.translatedLabel].records.push(formatRecord(record, tz))
|
sportList[sport.translatedLabel].records.push(
|
||||||
|
formatRecord(record, tz, useImperialUnits)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return sportList
|
return sportList
|
||||||
}, {})
|
}, {})
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
} from '@/types/statistics'
|
} from '@/types/statistics'
|
||||||
import { incrementDate, getStartDate } from '@/utils/dates'
|
import { incrementDate, getStartDate } from '@/utils/dates'
|
||||||
import { sportColors } from '@/utils/sports'
|
import { sportColors } from '@/utils/sports'
|
||||||
|
import { convertStatsDistance } from '@/utils/units'
|
||||||
|
|
||||||
const dateFormats: Record<string, Record<string, string>> = {
|
const dateFormats: Record<string, Record<string, string>> = {
|
||||||
week: {
|
week: {
|
||||||
@ -94,12 +95,34 @@ export const getDatasets = (displayedSports: ISport[]): TStatisticsDatasets => {
|
|||||||
return datasets
|
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 = (
|
export const formatStats = (
|
||||||
params: IStatisticsDateParams,
|
params: IStatisticsDateParams,
|
||||||
weekStartingMonday: boolean,
|
weekStartingMonday: boolean,
|
||||||
sports: ISport[],
|
sports: ISport[],
|
||||||
displayedSportsId: number[],
|
displayedSportsId: number[],
|
||||||
apiStats: TStatisticsFromApi
|
apiStats: TStatisticsFromApi,
|
||||||
|
useImperialUnits: boolean
|
||||||
): IStatisticsChartData => {
|
): IStatisticsChartData => {
|
||||||
const dayKeys = getDateKeys(params, weekStartingMonday)
|
const dayKeys = getDateKeys(params, weekStartingMonday)
|
||||||
const dateFormat = dateFormats[params.duration]
|
const dateFormat = dateFormats[params.duration]
|
||||||
@ -123,7 +146,11 @@ export const formatStats = (
|
|||||||
apiStats !== {} &&
|
apiStats !== {} &&
|
||||||
date in apiStats &&
|
date in apiStats &&
|
||||||
sportsId[dataset.label] in apiStats[date]
|
sportsId[dataset.label] in apiStats[date]
|
||||||
? apiStats[date][sportsId[dataset.label]][datasetKey]
|
? convertStatsValue(
|
||||||
|
datasetKey,
|
||||||
|
apiStats[date][sportsId[dataset.label]][datasetKey],
|
||||||
|
useImperialUnits
|
||||||
|
)
|
||||||
: 0
|
: 0
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import { TStatisticsDatasetKeys } from '@/types/statistics'
|
import { TStatisticsDatasetKeys } from '@/types/statistics'
|
||||||
import { formatDuration } from '@/utils/duration'
|
import { formatDuration } from '@/utils/duration'
|
||||||
|
import { units } from '@/utils/units'
|
||||||
|
|
||||||
export const formatTooltipValue = (
|
export const formatTooltipValue = (
|
||||||
displayedData: TStatisticsDatasetKeys,
|
displayedData: TStatisticsDatasetKeys,
|
||||||
value: number,
|
value: number,
|
||||||
|
useImperialUnits: boolean,
|
||||||
formatWithUnits = true
|
formatWithUnits = true
|
||||||
): string => {
|
): string => {
|
||||||
|
const unitFrom = 'km'
|
||||||
|
const unitTo = useImperialUnits ? units[unitFrom].defaultTarget : unitFrom
|
||||||
switch (displayedData) {
|
switch (displayedData) {
|
||||||
case 'total_duration':
|
case 'total_duration':
|
||||||
return formatDuration(value, formatWithUnits)
|
return formatDuration(value, formatWithUnits)
|
||||||
case 'total_distance':
|
case 'total_distance':
|
||||||
return value.toFixed(2) + ' km'
|
return `${value.toFixed(2)} ${unitTo}`
|
||||||
case 'total_ascent':
|
case 'total_ascent':
|
||||||
case 'total_descent':
|
case 'total_descent':
|
||||||
return (value / 1000).toFixed(2) + ' km'
|
return `${(value / 1000).toFixed(2)} ${unitTo}`
|
||||||
default:
|
default:
|
||||||
return value.toString()
|
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,
|
TCoordinates,
|
||||||
TWorkoutDatasets,
|
TWorkoutDatasets,
|
||||||
} from '@/types/workouts'
|
} from '@/types/workouts'
|
||||||
|
import { convertStatsDistance } from '@/utils/units'
|
||||||
|
|
||||||
export const getDatasets = (
|
export const getDatasets = (
|
||||||
chartData: IWorkoutApiChartData[],
|
chartData: IWorkoutApiChartData[],
|
||||||
t: CallableFunction
|
t: CallableFunction,
|
||||||
|
useImperialUnits: boolean
|
||||||
): IWorkoutChartData => {
|
): IWorkoutChartData => {
|
||||||
const datasets: TWorkoutDatasets = {
|
const datasets: TWorkoutDatasets = {
|
||||||
speed: {
|
speed: {
|
||||||
@ -36,8 +38,12 @@ export const getDatasets = (
|
|||||||
chartData.map((data) => {
|
chartData.map((data) => {
|
||||||
distance_labels.push(data.distance)
|
distance_labels.push(data.distance)
|
||||||
duration_labels.push(data.duration)
|
duration_labels.push(data.duration)
|
||||||
datasets.speed.data.push(data.speed)
|
datasets.speed.data.push(
|
||||||
datasets.elevation.data.push(data.elevation)
|
convertStatsDistance('km', data.speed, useImperialUnits)
|
||||||
|
)
|
||||||
|
datasets.elevation.data.push(
|
||||||
|
convertStatsDistance('m', data.elevation, useImperialUnits)
|
||||||
|
)
|
||||||
coordinates.push({ latitude: data.latitude, longitude: data.longitude })
|
coordinates.push({ latitude: data.latitude, longitude: data.longitude })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<WorkoutSegments
|
<WorkoutSegments
|
||||||
v-if="!displaySegment && workoutData.workout.segments.length > 1"
|
v-if="!displaySegment && workoutData.workout.segments.length > 1"
|
||||||
:segments="workoutData.workout.segments"
|
:segments="workoutData.workout.segments"
|
||||||
|
:useImperialUnits="authUser.imperial_units"
|
||||||
/>
|
/>
|
||||||
<WorkoutNotes
|
<WorkoutNotes
|
||||||
v-if="!displaySegment"
|
v-if="!displaySegment"
|
||||||
|
@ -100,7 +100,113 @@ describe('formatRecord', () => {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatRecord(
|
formatRecord(
|
||||||
testParams.inputParams.record,
|
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
|
testParams.expected
|
||||||
)
|
)
|
||||||
@ -121,7 +227,8 @@ describe('formatRecord (invalid record type)', () => {
|
|||||||
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
workout_date: 'Sun, 07 Jul 2019 22:00:00 GMT',
|
||||||
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
workout_id: 'hvYBqYBRa7wwXpaStWR4V2',
|
||||||
},
|
},
|
||||||
'Europe/Paris'
|
'Europe/Paris',
|
||||||
|
false
|
||||||
)
|
)
|
||||||
).to.throw(
|
).to.throw(
|
||||||
'Invalid record type, expected: "AS", "FD", "LD", "MD", got: "M"'
|
'Invalid record type, expected: "AS", "FD", "LD", "MD", got: "M"'
|
||||||
@ -248,7 +355,138 @@ describe('getRecordsBySports', () => {
|
|||||||
getRecordsBySports(
|
getRecordsBySports(
|
||||||
testParams.input.records,
|
testParams.input.records,
|
||||||
translatedSports,
|
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
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -316,7 +316,7 @@ describe('formatStats', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [], inputStats),
|
formatStats(inputParams, false, sports, [], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -369,7 +369,7 @@ describe('formatStats', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [2], inputStats),
|
formatStats(inputParams, false, sports, [2], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -427,7 +427,7 @@ describe('formatStats', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [], inputStats),
|
formatStats(inputParams, false, sports, [], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -515,7 +515,7 @@ describe('formatStats', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [1], inputStats),
|
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -605,7 +605,7 @@ describe('formatStats (duration)', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [1], inputStats),
|
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -692,7 +692,7 @@ describe('formatStats (duration)', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [1], inputStats),
|
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -780,7 +780,7 @@ describe('formatStats (duration)', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
formatStats(inputParams, false, sports, [1], inputStats),
|
formatStats(inputParams, false, sports, [1], inputStats, false),
|
||||||
expected
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -868,7 +868,95 @@ describe('formatStats (duration)', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
assert.deepEqual(
|
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
|
expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -42,7 +42,56 @@ describe('formatTooltipValue', () => {
|
|||||||
assert.equal(
|
assert.equal(
|
||||||
formatTooltipValue(
|
formatTooltipValue(
|
||||||
testParams.inputDisplayedData,
|
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
|
testParams.expectedResult
|
||||||
)
|
)
|
||||||
@ -90,6 +139,7 @@ describe('formatTooltipValue (formatWithUnits = false)', () => {
|
|||||||
formatTooltipValue(
|
formatTooltipValue(
|
||||||
testParams.inputDisplayedData,
|
testParams.inputDisplayedData,
|
||||||
testParams.inputValue,
|
testParams.inputValue,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
testParams.expectedResult
|
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: {
|
inputParams: {
|
||||||
charData: [],
|
charData: [],
|
||||||
locale: 'fr',
|
locale: 'fr',
|
||||||
|
useImperialUnits: false,
|
||||||
},
|
},
|
||||||
expected: {
|
expected: {
|
||||||
distance_labels: [],
|
distance_labels: [],
|
||||||
@ -72,6 +73,7 @@ describe('getDatasets', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
useImperialUnits: false,
|
||||||
},
|
},
|
||||||
expected: {
|
expected: {
|
||||||
distance_labels: [0, 0, 0.01],
|
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) => {
|
testparams.map((testParams) => {
|
||||||
it(testParams.description, () => {
|
it(testParams.description, () => {
|
||||||
locale.value = testParams.inputParams.locale
|
locale.value = testParams.inputParams.locale
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
getDatasets(testParams.inputParams.charData, t),
|
getDatasets(
|
||||||
|
testParams.inputParams.charData,
|
||||||
|
t,
|
||||||
|
testParams.inputParams.useImperialUnits
|
||||||
|
),
|
||||||
testParams.expected
|
testParams.expected
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user