mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 07:47:58 -05:00
Merge branch 'pr/199' into pre-release
This commit is contained in:
8
backend/app/activities/activity_streams/constants.py
Normal file
8
backend/app/activities/activity_streams/constants.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Stream type constants for activity streams
|
||||
STREAM_TYPE_HR = 1
|
||||
STREAM_TYPE_POWER = 2
|
||||
STREAM_TYPE_CADENCE = 3
|
||||
STREAM_TYPE_ELEVATION = 4
|
||||
STREAM_TYPE_SPEED = 5
|
||||
STREAM_TYPE_PACE = 6
|
||||
STREAM_TYPE_MAP = 7
|
||||
@@ -1,6 +1,9 @@
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import numpy as np
|
||||
import datetime
|
||||
|
||||
import activities.activity_streams.constants as activity_streams_constants
|
||||
import activities.activity_streams.schema as activity_streams_schema
|
||||
import activities.activity_streams.models as activity_streams_models
|
||||
|
||||
@@ -10,14 +13,16 @@ import activities.activity.models as activities_models
|
||||
|
||||
import server_settings.crud as server_settings_crud
|
||||
|
||||
import users.user.crud as users_crud
|
||||
|
||||
import core.logger as core_logger
|
||||
|
||||
|
||||
def get_activity_streams(activity_id: int, token_user_id: int, db: Session):
|
||||
def get_activity_streams(
|
||||
activity_id: int, token_user_id: int, db: Session
|
||||
) -> list[activity_streams_schema.ActivityStreams] | None:
|
||||
try:
|
||||
activity = activity_crud.get_activity_by_id(
|
||||
activity_id, db
|
||||
)
|
||||
activity = activity_crud.get_activity_by_id(activity_id, db)
|
||||
|
||||
if not activity:
|
||||
# If the activity does not exist, return None
|
||||
@@ -42,20 +47,52 @@ def get_activity_streams(activity_id: int, token_user_id: int, db: Session):
|
||||
|
||||
if not user_is_owner:
|
||||
activity_streams = [
|
||||
stream for stream in activity_streams
|
||||
stream
|
||||
for stream in activity_streams
|
||||
if not (
|
||||
(activity.hide_hr and stream.stream_type == 1) or
|
||||
(activity.hide_power and stream.stream_type == 2) or
|
||||
(activity.hide_cadence and stream.stream_type == 3) or
|
||||
(activity.hide_elevation and stream.stream_type == 4) or
|
||||
(activity.hide_speed and stream.stream_type == 5) or
|
||||
(activity.hide_pace and stream.stream_type == 6) or
|
||||
(activity.hide_map and stream.stream_type == 7)
|
||||
(
|
||||
activity.hide_hr
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_HR
|
||||
)
|
||||
or (
|
||||
activity.hide_power
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_POWER
|
||||
)
|
||||
or (
|
||||
activity.hide_cadence
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_CADENCE
|
||||
)
|
||||
or (
|
||||
activity.hide_elevation
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_ELEVATION
|
||||
)
|
||||
or (
|
||||
activity.hide_speed
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_SPEED
|
||||
)
|
||||
or (
|
||||
activity.hide_pace
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_PACE
|
||||
)
|
||||
or (
|
||||
activity.hide_map
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_MAP
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
# Return the activity streams
|
||||
return activity_streams
|
||||
return [
|
||||
transform_activity_streams(stream, activity, db)
|
||||
for stream in activity_streams
|
||||
]
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -76,10 +113,8 @@ def get_public_activity_streams(activity_id: int, db: Session):
|
||||
# Return None if public sharable links are disabled
|
||||
if not server_settings or not server_settings.public_shareable_links:
|
||||
return None
|
||||
|
||||
activity = activity_crud.get_activity_by_id_if_is_public(
|
||||
activity_id, db
|
||||
)
|
||||
|
||||
activity = activity_crud.get_activity_by_id_if_is_public(activity_id, db)
|
||||
|
||||
if not activity:
|
||||
# If the activity does not exist, return None
|
||||
@@ -104,22 +139,52 @@ def get_public_activity_streams(activity_id: int, db: Session):
|
||||
# Check if there are activity streams, if not return None
|
||||
if not activity_streams:
|
||||
return None
|
||||
|
||||
|
||||
activity_streams = [
|
||||
stream for stream in activity_streams
|
||||
stream
|
||||
for stream in activity_streams
|
||||
if not (
|
||||
(activity.hide_hr and stream.stream_type == 1) or
|
||||
(activity.hide_power and stream.stream_type == 2) or
|
||||
(activity.hide_cadence and stream.stream_type == 3) or
|
||||
(activity.hide_elevation and stream.stream_type == 4) or
|
||||
(activity.hide_speed and stream.stream_type == 5) or
|
||||
(activity.hide_pace and stream.stream_type == 6) or
|
||||
(activity.hide_map and stream.stream_type == 7)
|
||||
(
|
||||
activity.hide_hr
|
||||
and stream.stream_type == activity_streams_constants.STREAM_TYPE_HR
|
||||
)
|
||||
or (
|
||||
activity.hide_power
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_POWER
|
||||
)
|
||||
or (
|
||||
activity.hide_cadence
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_CADENCE
|
||||
)
|
||||
or (
|
||||
activity.hide_elevation
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_ELEVATION
|
||||
)
|
||||
or (
|
||||
activity.hide_speed
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_SPEED
|
||||
)
|
||||
or (
|
||||
activity.hide_pace
|
||||
and stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_PACE
|
||||
)
|
||||
or (
|
||||
activity.hide_map
|
||||
and stream.stream_type == activity_streams_constants.STREAM_TYPE_MAP
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
# Return the activity streams
|
||||
return activity_streams
|
||||
return [
|
||||
transform_activity_streams(stream, activity, db)
|
||||
for stream in activity_streams
|
||||
]
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -132,16 +197,16 @@ def get_public_activity_streams(activity_id: int, db: Session):
|
||||
) from err
|
||||
|
||||
|
||||
def get_activity_stream_by_type(activity_id: int, stream_type: int, token_user_id: int, db: Session):
|
||||
def get_activity_stream_by_type(
|
||||
activity_id: int, stream_type: int, token_user_id: int, db: Session
|
||||
):
|
||||
try:
|
||||
activity = activity_crud.get_activity_by_id(
|
||||
activity_id, db
|
||||
)
|
||||
activity = activity_crud.get_activity_by_id(activity_id, db)
|
||||
|
||||
if not activity:
|
||||
# If the activity does not exist, return None
|
||||
return None
|
||||
|
||||
|
||||
# Get the activity stream from the database
|
||||
activity_stream = (
|
||||
db.query(activity_streams_models.ActivityStreams)
|
||||
@@ -155,29 +220,57 @@ def get_activity_stream_by_type(activity_id: int, stream_type: int, token_user_i
|
||||
# Check if there are activity stream if not return None
|
||||
if not activity_stream:
|
||||
return None
|
||||
|
||||
|
||||
user_is_owner = True
|
||||
if token_user_id != activity.user_id:
|
||||
user_is_owner = False
|
||||
|
||||
if not user_is_owner:
|
||||
if activity.hide_hr and activity_stream.stream_type == 1:
|
||||
if (
|
||||
activity.hide_hr
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_HR
|
||||
):
|
||||
return None
|
||||
if activity.hide_power and activity_stream.stream_type == 2:
|
||||
if (
|
||||
activity.hide_power
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_POWER
|
||||
):
|
||||
return None
|
||||
if activity.hide_cadence and activity_stream.stream_type == 3:
|
||||
if (
|
||||
activity.hide_cadence
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_CADENCE
|
||||
):
|
||||
return None
|
||||
if activity.hide_elevation and activity_stream.stream_type == 4:
|
||||
if (
|
||||
activity.hide_elevation
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_ELEVATION
|
||||
):
|
||||
return None
|
||||
if activity.hide_speed and activity_stream.stream_type == 5:
|
||||
if (
|
||||
activity.hide_speed
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_SPEED
|
||||
):
|
||||
return None
|
||||
if activity.hide_pace and activity_stream.stream_type == 6:
|
||||
if (
|
||||
activity.hide_pace
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_PACE
|
||||
):
|
||||
return None
|
||||
if activity.hide_map and activity_stream.stream_type == 7:
|
||||
if (
|
||||
activity.hide_map
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_MAP
|
||||
):
|
||||
return None
|
||||
|
||||
# Return the activity stream
|
||||
return activity_stream
|
||||
return transform_activity_streams(activity_stream, activity, db)
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
@@ -190,6 +283,99 @@ def get_activity_stream_by_type(activity_id: int, stream_type: int, token_user_i
|
||||
) from err
|
||||
|
||||
|
||||
def transform_activity_streams(activity_stream, activity, db):
|
||||
"""
|
||||
Transforms an activity stream based on its stream type.
|
||||
If the stream type of the given activity_stream is heart rate (HR), this function delegates
|
||||
the transformation to the `transform_activity_streams_hr` function. Otherwise, it returns
|
||||
the activity_stream unchanged.
|
||||
Args:
|
||||
activity_stream: The activity stream object to be transformed.
|
||||
activity: The activity object associated with the stream.
|
||||
db: The database session or connection object.
|
||||
Returns:
|
||||
The transformed activity stream if the stream type is HR, otherwise the original activity_stream.
|
||||
"""
|
||||
if activity_stream.stream_type == activity_streams_constants.STREAM_TYPE_HR:
|
||||
return transform_activity_streams_hr(activity_stream, activity, db)
|
||||
|
||||
return activity_stream
|
||||
|
||||
|
||||
def transform_activity_streams_hr(activity_stream, activity, db):
|
||||
"""
|
||||
Transforms an activity stream by calculating the percentage of time spent in each heart rate zone based on user details.
|
||||
Args:
|
||||
activity_stream: The activity stream object containing waypoints with heart rate data.
|
||||
activity: The activity object associated with the stream, used to retrieve the user ID.
|
||||
db: The database session or connection used to fetch user details.
|
||||
Returns:
|
||||
The activity stream object with an added 'hr_zone_percentages' attribute, which contains the percentage of time spent in each heart rate zone and their respective HR boundaries. If waypoints or user details are missing, returns the original activity stream unchanged.
|
||||
Notes:
|
||||
- Heart rate zones are calculated using the formula: max_heart_rate = 220 - age.
|
||||
- The function expects waypoints to be a list of dicts with an "hr" key.
|
||||
- If no valid heart rate data is present, the activity stream is returned as is.
|
||||
"""
|
||||
# Check if the activity stream has waypoints
|
||||
waypoints = activity_stream.stream_waypoints
|
||||
if not waypoints or not isinstance(waypoints, list):
|
||||
# If there are no waypoints, return the activity stream as is
|
||||
return activity_stream
|
||||
|
||||
# Get the user details to calculate heart rate zones
|
||||
detail_user = users_crud.get_user_by_id(activity.user_id, db)
|
||||
if not detail_user or not detail_user.birthdate:
|
||||
# If user details are not available or birthdate is missing, return the activity stream as is
|
||||
return activity_stream
|
||||
|
||||
# Calculate the maximum heart rate based on the user's birthdate
|
||||
year = int(detail_user.birthdate.split("-")[0])
|
||||
current_year = datetime.datetime.now().year
|
||||
max_heart_rate = 220 - (current_year - year)
|
||||
|
||||
# Calculate heart rate zones based on the maximum heart rate
|
||||
zone_1 = max_heart_rate * 0.5
|
||||
zone_2 = max_heart_rate * 0.6
|
||||
zone_3 = max_heart_rate * 0.7
|
||||
zone_4 = max_heart_rate * 0.8
|
||||
|
||||
# Extract heart rate values from waypoints
|
||||
hr_values = np.array([wp.get("hr") for wp in waypoints if wp.get("hr") is not None])
|
||||
|
||||
# If there are no valid heart rate values, return the activity stream as is
|
||||
total = len(hr_values)
|
||||
if total == 0:
|
||||
return activity_stream
|
||||
|
||||
# Calculate the percentage of time spent in each heart rate zone
|
||||
zone_counts = [
|
||||
np.sum(hr_values < zone_1),
|
||||
np.sum((hr_values >= zone_1) & (hr_values < zone_2)),
|
||||
np.sum((hr_values >= zone_2) & (hr_values < zone_3)),
|
||||
np.sum((hr_values >= zone_3) & (hr_values < zone_4)),
|
||||
np.sum(hr_values >= zone_4),
|
||||
]
|
||||
zone_percentages = [round((count / total) * 100, 2) for count in zone_counts]
|
||||
|
||||
# Calculate zone HR boundaries for display
|
||||
zone_hr = {
|
||||
"zone_1": f"< {int(zone_1)}",
|
||||
"zone_2": f"{int(zone_1)} - {int(zone_2) - 1}",
|
||||
"zone_3": f"{int(zone_2)} - {int(zone_3) - 1}",
|
||||
"zone_4": f"{int(zone_3)} - {int(zone_4) - 1}",
|
||||
"zone_5": f">= {int(zone_4)}",
|
||||
}
|
||||
activity_stream.hr_zone_percentages = {
|
||||
"zone_1": {"percent": zone_percentages[0], "hr": zone_hr["zone_1"]},
|
||||
"zone_2": {"percent": zone_percentages[1], "hr": zone_hr["zone_2"]},
|
||||
"zone_3": {"percent": zone_percentages[2], "hr": zone_hr["zone_3"]},
|
||||
"zone_4": {"percent": zone_percentages[3], "hr": zone_hr["zone_4"]},
|
||||
"zone_5": {"percent": zone_percentages[4], "hr": zone_hr["zone_5"]},
|
||||
}
|
||||
|
||||
return activity_stream
|
||||
|
||||
|
||||
def get_public_activity_stream_by_type(activity_id: int, stream_type: int, db: Session):
|
||||
try:
|
||||
# Check if public sharable links are enabled in server settings
|
||||
@@ -198,10 +384,8 @@ def get_public_activity_stream_by_type(activity_id: int, stream_type: int, db: S
|
||||
# Return None if public sharable links are disabled
|
||||
if not server_settings or not server_settings.public_shareable_links:
|
||||
return None
|
||||
|
||||
activity = activity_crud.get_activity_by_id_if_is_public(
|
||||
activity_id, db
|
||||
)
|
||||
|
||||
activity = activity_crud.get_activity_by_id_if_is_public(activity_id, db)
|
||||
|
||||
if not activity:
|
||||
# If the activity does not exist, return None
|
||||
@@ -227,24 +411,51 @@ def get_public_activity_stream_by_type(activity_id: int, stream_type: int, db: S
|
||||
# Check if there is an activity stream; if not, return None
|
||||
if not activity_stream:
|
||||
return None
|
||||
|
||||
if activity.hide_hr and activity_stream.stream_type == 1:
|
||||
|
||||
if (
|
||||
activity.hide_hr
|
||||
and activity_stream.stream_type == activity_streams_constants.STREAM_TYPE_HR
|
||||
):
|
||||
return None
|
||||
if activity.hide_power and activity_stream.stream_type == 2:
|
||||
if (
|
||||
activity.hide_power
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_POWER
|
||||
):
|
||||
return None
|
||||
if activity.hide_cadence and activity_stream.stream_type == 3:
|
||||
if (
|
||||
activity.hide_cadence
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_CADENCE
|
||||
):
|
||||
return None
|
||||
if activity.hide_elevation and activity_stream.stream_type == 4:
|
||||
if (
|
||||
activity.hide_elevation
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_ELEVATION
|
||||
):
|
||||
return None
|
||||
if activity.hide_speed and activity_stream.stream_type == 5:
|
||||
if (
|
||||
activity.hide_speed
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_SPEED
|
||||
):
|
||||
return None
|
||||
if activity.hide_pace and activity_stream.stream_type == 6:
|
||||
if (
|
||||
activity.hide_pace
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_PACE
|
||||
):
|
||||
return None
|
||||
if activity.hide_map and activity_stream.stream_type == 7:
|
||||
if (
|
||||
activity.hide_map
|
||||
and activity_stream.stream_type
|
||||
== activity_streams_constants.STREAM_TYPE_MAP
|
||||
):
|
||||
return None
|
||||
|
||||
# Return the activity stream
|
||||
return activity_stream
|
||||
return transform_activity_streams(activity_stream, activity, db)
|
||||
except Exception as err:
|
||||
# Log the exception
|
||||
core_logger.print_to_log(
|
||||
|
||||
@@ -3,11 +3,29 @@ from typing import List
|
||||
|
||||
|
||||
class ActivityStreams(BaseModel):
|
||||
"""
|
||||
Represents a stream of activity data associated with an activity.
|
||||
|
||||
Attributes:
|
||||
id (int | None): Unique identifier for the activity stream (optional).
|
||||
activity_id (int): Identifier of the related activity.
|
||||
stream_type (int): Type of the stream (e.g., GPS, heart rate, etc.).
|
||||
stream_waypoints (List[dict]): List of waypoints or data points in the stream.
|
||||
strava_activity_stream_id (int | None): Identifier for the corresponding Strava activity stream (optional).
|
||||
hr_zone_percentages (dict | None): Heart rate zone percentages for the activity (optional).
|
||||
"""
|
||||
id: int | None = None
|
||||
activity_id: int
|
||||
stream_type: int
|
||||
stream_waypoints: List[dict]
|
||||
strava_activity_stream_id: int | None = None
|
||||
hr_zone_percentages: dict | None = None
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Pydantic configuration class enabling ORM mode for model serialization.
|
||||
|
||||
Attributes:
|
||||
orm_mode (bool): When set to True, allows Pydantic models to read data from ORM objects.
|
||||
"""
|
||||
orm_mode = True
|
||||
@@ -646,8 +646,12 @@ def fetch_and_process_activity_laps(
|
||||
"total_elapsed_time": lap.elapsed_time,
|
||||
"total_timer_time": lap.moving_time,
|
||||
"total_distance": lap.distance,
|
||||
"avg_heart_rate": round(lap.average_heartrate),
|
||||
"max_heart_rate": round(lap.max_heartrate),
|
||||
"avg_heart_rate": (
|
||||
round(lap.average_heartrate) if lap.average_heartrate else None
|
||||
),
|
||||
"max_heart_rate": (
|
||||
round(lap.max_heartrate) if lap.max_heartrate else None
|
||||
),
|
||||
"avg_cadence": round(cad_avg) if cad_stream else None,
|
||||
"max_cadence": round(cad_max) if cad_stream else None,
|
||||
"avg_power": round(power_avg) if power_stream else None,
|
||||
|
||||
10
frontend/app/package-lock.json
generated
10
frontend/app/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.6",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"flag-icons": "^7.2.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"luxon": "^3.5.0",
|
||||
@@ -3885,6 +3886,15 @@
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-plugin-datalabels": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
|
||||
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"chart.js": "^4.4.6",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"flag-icons": "^7.2.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"luxon": "^3.5.0",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div if="activity" class="fw-lighter">
|
||||
<!-- laps -->
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units" v-if="activityActivityLaps && activityActivityLaps.length > 0"/>
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units"
|
||||
v-if="activityActivityLaps && activityActivityLaps.length > 0" />
|
||||
|
||||
<!-- Pace values -->
|
||||
<div v-if="pacePresent">
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitlePace") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'pace'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'pace'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="formattedPace">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgPace") }}
|
||||
@@ -40,14 +42,19 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleSpeed") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'vel'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'vel'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_speed">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgSpeed") }}
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="activity.average_speed && Number(units) === 1"><b>{{ formatAverageSpeedMetric(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsKmH") }}</b></span>
|
||||
<span v-else-if="activity.average_speed && Number(units) === 2"><b>{{ formatAverageSpeedImperial(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsMph") }}</b></span>
|
||||
<span v-if="activity.average_speed && Number(units) === 1"><b>{{
|
||||
formatAverageSpeedMetric(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsKmH")
|
||||
}}</b></span>
|
||||
<span v-else-if="activity.average_speed && Number(units) === 2"><b>{{
|
||||
formatAverageSpeedImperial(activity.average_speed) }}{{ ' ' + $t("generalItems.unitsMph")
|
||||
}}</b></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.max_speed">
|
||||
@@ -55,8 +62,12 @@
|
||||
{{ $t("activityBellowMPillsComponent.labelMaxSpeed") }}
|
||||
</span>
|
||||
<span>
|
||||
<span v-if="activity.max_speed && Number(units) === 1"><b>{{ formatAverageSpeedMetric(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsKmH") }}</b></span>
|
||||
<span v-else-if="activity.max_speed && Number(units) === 2"><b>{{ formatAverageSpeedImperial(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsMph") }}</b></span>
|
||||
<span v-if="activity.max_speed && Number(units) === 1"><b>{{
|
||||
formatAverageSpeedMetric(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsKmH")
|
||||
}}</b></span>
|
||||
<span v-else-if="activity.max_speed && Number(units) === 2"><b>{{
|
||||
formatAverageSpeedImperial(activity.max_speed) }}{{ ' ' + $t("generalItems.unitsMph")
|
||||
}}</b></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.total_elapsed_time">
|
||||
@@ -82,7 +93,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleHeartRate") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'hr'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'hr'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_hr">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgHeartRate") }}
|
||||
@@ -99,6 +111,11 @@
|
||||
<b>{{ activity.max_hr }}{{ ' ' + $t("generalItems.unitsBpm") }}</b>
|
||||
</span>
|
||||
</div>
|
||||
<BarChartComponent v-if="Object.values(hrZones).length > 0 && hrPresent"
|
||||
:labels="getHrBarChartData(hrZones, t).labels" :values="getHrBarChartData(hrZones, t).values"
|
||||
:barColors="getHrBarChartData(hrZones, t).barColors"
|
||||
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
|
||||
:title="$t('activityMandAbovePillsComponent.labelHRZones')" />
|
||||
<hr>
|
||||
</div>
|
||||
<!-- Power values -->
|
||||
@@ -106,7 +123,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitlePower") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'power'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'power'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_power">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgPower") }}
|
||||
@@ -141,7 +159,8 @@
|
||||
<span class="fw-normal" v-else>
|
||||
{{ $t("activityBellowMPillsComponent.subTitleStrokeRate") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'cad'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'cad'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.average_cad">
|
||||
<span v-if="!activityTypeIsSwimming(activity)">
|
||||
{{ $t("activityBellowMPillsComponent.labelAvgCadence") }}
|
||||
@@ -171,7 +190,8 @@
|
||||
<span class="fw-normal">
|
||||
{{ $t("activityBellowMPillsComponent.subTitleElevation") }}
|
||||
</span>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'ele'" :activityStreams="activityActivityStreams" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="'ele'"
|
||||
:activityStreams="activityActivityStreams" />
|
||||
<div class="d-flex justify-content-between mt-3" v-if="activity.elevation_gain">
|
||||
<span>
|
||||
{{ $t("activityBellowMPillsComponent.labelElevationGain") }}
|
||||
@@ -197,8 +217,11 @@
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<!-- sets -->
|
||||
<ActivityWorkoutStepsComponent :activity="activity" :activityActivityWorkoutSteps="activityActivityWorkoutSteps" :units="units" :activityActivityExerciseTitles="activityActivityExerciseTitles" :activityActivitySets="activityActivitySets" v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0"/>
|
||||
<!-- sets -->
|
||||
<ActivityWorkoutStepsComponent :activity="activity" :activityActivityWorkoutSteps="activityActivityWorkoutSteps"
|
||||
:units="units" :activityActivityExerciseTitles="activityActivityExerciseTitles"
|
||||
:activityActivitySets="activityActivitySets"
|
||||
v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -209,50 +232,51 @@ import { useI18n } from "vue-i18n";
|
||||
import ActivityLapsComponent from "@/components/Activities/ActivityLapsComponent.vue";
|
||||
import ActivityStreamsLineChartComponent from "@/components/Activities/ActivityStreamsLineChartComponent.vue";
|
||||
import ActivityWorkoutStepsComponent from "@/components/Activities/ActivityWorkoutStepsComponent.vue";
|
||||
import BarChartComponent from '@/components/GeneralComponents/BarChartComponent.vue';
|
||||
// Import Notivue push
|
||||
import { push } from "notivue";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
// Import the utils
|
||||
import { getHrBarChartData } from "@/utils/chartUtils";
|
||||
import { formatPaceMetric, formatPaceImperial, formatPaceSwimMetric, formatPaceSwimImperial, formatAverageSpeedMetric, formatAverageSpeedImperial, activityTypeIsSwimming } from "@/utils/activityUtils";
|
||||
import { formatSecondsToMinutes } from "@/utils/dateTimeUtils";
|
||||
import {
|
||||
metersToFeet,
|
||||
metersToFeet,
|
||||
} from "@/utils/unitsUtils";
|
||||
|
||||
// Define props
|
||||
const props = defineProps({
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activityActivityLaps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityWorkoutSteps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityStreams: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
activityActivityExerciseTitles: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivitySets: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activityActivityLaps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityWorkoutSteps: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivityStreams: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
activityActivityExerciseTitles: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
activityActivitySets: {
|
||||
type: [Object, null],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Setup composables and reactive data
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
const hrPresent = ref(false);
|
||||
const powerPresent = ref(false);
|
||||
const elePresent = ref(false);
|
||||
@@ -260,74 +284,85 @@ const cadPresent = ref(false);
|
||||
const velPresent = ref(false);
|
||||
const pacePresent = ref(false);
|
||||
const formattedPace = ref(null);
|
||||
const hrZones = ref({
|
||||
zone_1: {},
|
||||
zone_2: {},
|
||||
zone_3: {},
|
||||
zone_4: {},
|
||||
zone_5: {},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
// Check if the activity has the streams
|
||||
for (let i = 0; i < props.activityActivityStreams.length; i++) {
|
||||
if (props.activityActivityStreams[i].stream_type === 1) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 2) {
|
||||
powerPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 3) {
|
||||
cadPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 4) {
|
||||
elePresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (
|
||||
props.activity.activity_type === 4 ||
|
||||
props.activity.activity_type === 5 ||
|
||||
props.activity.activity_type === 6 ||
|
||||
props.activity.activity_type === 7 ||
|
||||
props.activity.activity_type === 27
|
||||
) {
|
||||
velPresent.value = true;
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (
|
||||
props.activity.activity_type !== 4 &&
|
||||
props.activity.activity_type !== 5 &&
|
||||
props.activity.activity_type !== 6 &&
|
||||
props.activity.activity_type !== 7 &&
|
||||
props.activity.activity_type !== 27
|
||||
) {
|
||||
pacePresent.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
push.error(
|
||||
`${t("activityMandAbovePillsComponent.errorMessageProcessingActivityStreams")} - ${error}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
// Check if the activity has the streams
|
||||
for (let i = 0; i < props.activityActivityStreams.length; i++) {
|
||||
if (props.activityActivityStreams[i].stream_type === 1) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 2) {
|
||||
powerPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 3) {
|
||||
cadPresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 4) {
|
||||
elePresent.value = true;
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (
|
||||
props.activity.activity_type === 4 ||
|
||||
props.activity.activity_type === 5 ||
|
||||
props.activity.activity_type === 6 ||
|
||||
props.activity.activity_type === 7 ||
|
||||
props.activity.activity_type === 27
|
||||
) {
|
||||
velPresent.value = true;
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (
|
||||
props.activity.activity_type !== 4 &&
|
||||
props.activity.activity_type !== 5 &&
|
||||
props.activity.activity_type !== 6 &&
|
||||
props.activity.activity_type !== 7 &&
|
||||
props.activity.activity_type !== 27
|
||||
) {
|
||||
pacePresent.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
hrZones.value = props.activityActivityStreams.find(stream => stream.hr_zone_percentages).hr_zone_percentages || {};
|
||||
if (Object.keys(hrZones.value).length > 0) {
|
||||
hrPresent.value = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there is an error, set the error message and show the error alert.
|
||||
push.error(
|
||||
`${t("activityMandAbovePillsComponent.errorMessageProcessingActivityStreams")} - ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
activityTypeIsSwimming(props.activity) ||
|
||||
props.activity.activity_type === 13
|
||||
) {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceSwimMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceSwimImperial(props.activity.pace));
|
||||
}
|
||||
} else {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceImperial(props.activity.pace));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
}
|
||||
try {
|
||||
if (
|
||||
activityTypeIsSwimming(props.activity) ||
|
||||
props.activity.activity_type === 13
|
||||
) {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceSwimMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceSwimImperial(props.activity.pace));
|
||||
}
|
||||
} else {
|
||||
if (Number(props.units) === 1) {
|
||||
formattedPace.value = computed(() => formatPaceMetric(props.activity.pace));
|
||||
} else {
|
||||
formattedPace.value = computed(() => formatPaceImperial(props.activity.pace));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -1,59 +1,88 @@
|
||||
<template>
|
||||
<ul class="nav nav-pills mb-3 mt-3 justify-content-center" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation" v-if="graphItems && graphItems.length > 0">
|
||||
<button class="nav-link link-body-emphasis" :class="{ active: graphItems || graphItems.length > 0 }" id="pills-graphs-tab" data-bs-toggle="pill" data-bs-target="#pills-graphs" type="button" role="tab" aria-controls="pills-graphs" :aria-selected="graphItems && graphItems.length > 0 ? true : false">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillGraphs") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation" v-if="activityActivityLaps && activityActivityLaps.length > 0">
|
||||
<button class="nav-link link-body-emphasis" :class="{ active: !graphItems || graphItems.length === 0 }" id="pills-laps-tab" data-bs-toggle="pill" data-bs-target="#pills-laps" type="button" role="tab" aria-controls="pills-laps" :aria-selected="!graphItems || graphItems.length === 0 ? 'true' : 'false'">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillLaps") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation" v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0">
|
||||
<button class="nav-link link-body-emphasis" id="pills-workout-steps-tab" data-bs-toggle="pill" data-bs-target="#pills-workout-steps" type="button" role="tab" aria-controls="pills-workout-steps" aria-selected="false">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillWorkoutSets") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav nav-pills mb-3 mt-3 justify-content-center" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation" v-if="graphItems && graphItems.length > 0">
|
||||
<button class="nav-link link-body-emphasis" :class="{ active: graphItems || graphItems.length > 0 }"
|
||||
id="pills-graphs-tab" data-bs-toggle="pill" data-bs-target="#pills-graphs" type="button" role="tab"
|
||||
aria-controls="pills-graphs" :aria-selected="graphItems && graphItems.length > 0 ? true : false">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillGraphs") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation" v-if="activityActivityLaps && activityActivityLaps.length > 0">
|
||||
<button class="nav-link link-body-emphasis" :class="{ active: !graphItems || graphItems.length === 0 }"
|
||||
id="pills-laps-tab" data-bs-toggle="pill" data-bs-target="#pills-laps" type="button" role="tab"
|
||||
aria-controls="pills-laps" :aria-selected="!graphItems || graphItems.length === 0 ? 'true' : 'false'">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillLaps") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation"
|
||||
v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0">
|
||||
<button class="nav-link link-body-emphasis" id="pills-workout-steps-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-workout-steps" type="button" role="tab" aria-controls="pills-workout-steps"
|
||||
aria-selected="false">
|
||||
{{ $t("activityMandAbovePillsComponent.labelPillWorkoutSets") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<div class="tab-pane fade show" :class="{ active: graphItems || graphItems.length > 0 }" id="pills-graphs" role="tabpanel" aria-labelledby="pills-graphs-tab" tabindex="0" v-if="graphItems && graphItems.length > 0">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<p>{{ $t("activityMandAbovePillsComponent.labelGraph") }}</p>
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="sidebarLineGraph">
|
||||
<li class="nav-item" v-for="item in graphItems" :key="item.type">
|
||||
<a href="javascript:void(0);" class="nav-link text-secondary"
|
||||
:class="{ 'active text-white': graphSelection === item.type }"
|
||||
@click="selectGraph(item.type)">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="mt-2">{{ $t("activityMandAbovePillsComponent.labelDownsampling") }}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div if="activity">
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'hr' && hrPresent"/>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'power' && powerPresent"/>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'cad' && cadPresent"/>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'ele' && elePresent"/>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'vel' && velPresent"/>
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection" :activityStreams="activityActivityStreams" v-if="graphSelection === 'pace' && pacePresent"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" :class="{ 'show active': !graphItems || graphItems.length === 0 }" id="pills-laps" role="tabpanel" aria-labelledby="pills-laps-tab" tabindex="1" v-if="activityActivityLaps && activityActivityLaps.length > 0">
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units" />
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-workout-steps" role="tabpanel" aria-labelledby="pills-workout-steps-tab" tabindex="2" v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0">
|
||||
<ActivityWorkoutStepsComponent :activity="activity" :activityActivityWorkoutSteps="activityActivityWorkoutSteps" :units="units" :activityActivityExerciseTitles="activityActivityExerciseTitles" :activityActivitySets="activityActivitySets" />
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<div class="tab-pane fade show" :class="{ active: graphItems || graphItems.length > 0 }" id="pills-graphs"
|
||||
role="tabpanel" aria-labelledby="pills-graphs-tab" tabindex="0" v-if="graphItems && graphItems.length > 0">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<p>{{ $t("activityMandAbovePillsComponent.labelGraph") }}</p>
|
||||
<ul class="nav nav-pills flex-column mb-auto" id="sidebarLineGraph">
|
||||
<li class="nav-item" v-for="item in graphItems" :key="item.type">
|
||||
<a href="javascript:void(0);" class="nav-link text-secondary"
|
||||
:class="{ 'active text-white': graphSelection === item.type }"
|
||||
@click="selectGraph(item.type)">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="mt-2">{{ $t("activityMandAbovePillsComponent.labelDownsampling") }}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div if="activity">
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams" v-if="graphSelection === 'hr' && hrPresent" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams"
|
||||
v-if="graphSelection === 'power' && powerPresent" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams" v-if="graphSelection === 'cad' && cadPresent" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams" v-if="graphSelection === 'ele' && elePresent" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams" v-if="graphSelection === 'vel' && velPresent" />
|
||||
<ActivityStreamsLineChartComponent :activity="activity" :graphSelection="graphSelection"
|
||||
:activityStreams="activityActivityStreams"
|
||||
v-if="graphSelection === 'pace' && pacePresent" />
|
||||
<BarChartComponent
|
||||
v-if="Object.values(hrZones).length > 0 && graphSelection === 'hrZones' && hrPresent"
|
||||
:labels="getHrBarChartData(hrZones, t).labels"
|
||||
:values="getHrBarChartData(hrZones, t).values"
|
||||
:barColors="getHrBarChartData(hrZones, t).barColors"
|
||||
:datalabelsFormatter="(value) => `${Math.round(value)}%`"
|
||||
:title="$t('activityMandAbovePillsComponent.labelHRZones')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" :class="{ 'show active': !graphItems || graphItems.length === 0 }" id="pills-laps"
|
||||
role="tabpanel" aria-labelledby="pills-laps-tab" tabindex="1"
|
||||
v-if="activityActivityLaps && activityActivityLaps.length > 0">
|
||||
<ActivityLapsComponent :activity="activity" :activityActivityLaps="activityActivityLaps" :units="units" />
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-workout-steps" role="tabpanel" aria-labelledby="pills-workout-steps-tab"
|
||||
tabindex="2" v-if="activityActivityWorkoutSteps && activityActivityWorkoutSteps.length > 0">
|
||||
<ActivityWorkoutStepsComponent :activity="activity"
|
||||
:activityActivityWorkoutSteps="activityActivityWorkoutSteps" :units="units"
|
||||
:activityActivityExerciseTitles="activityActivityExerciseTitles"
|
||||
:activityActivitySets="activityActivitySets" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -63,10 +92,12 @@ import { useI18n } from "vue-i18n";
|
||||
import ActivityLapsComponent from "@/components/Activities/ActivityLapsComponent.vue";
|
||||
import ActivityStreamsLineChartComponent from "@/components/Activities/ActivityStreamsLineChartComponent.vue";
|
||||
import ActivityWorkoutStepsComponent from "@/components/Activities/ActivityWorkoutStepsComponent.vue";
|
||||
import BarChartComponent from '@/components/GeneralComponents/BarChartComponent.vue';
|
||||
import { activityTypeIsSwimming } from "@/utils/activityUtils";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
// Import Notivue push
|
||||
import { push } from "notivue";
|
||||
// Import the utils
|
||||
import { getHrBarChartData } from "@/utils/chartUtils";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -102,7 +133,6 @@ const props = defineProps({
|
||||
|
||||
// Composables
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Reactive state
|
||||
const graphSelection = ref("hr");
|
||||
@@ -113,6 +143,13 @@ const elePresent = ref(false);
|
||||
const cadPresent = ref(false);
|
||||
const velPresent = ref(false);
|
||||
const pacePresent = ref(false);
|
||||
const hrZones = ref({
|
||||
zone_1: {},
|
||||
zone_2: {},
|
||||
zone_3: {},
|
||||
zone_4: {},
|
||||
zone_5: {},
|
||||
});
|
||||
|
||||
// Methods
|
||||
function selectGraph(type) {
|
||||
@@ -124,16 +161,16 @@ onMounted(async () => {
|
||||
try {
|
||||
if (props.activityActivityStreams && props.activityActivityStreams.length > 0) {
|
||||
// Check if the activity has the streams
|
||||
for (let i = 0; i < props.activityActivityStreams.length; i++) {
|
||||
if (props.activityActivityStreams[i].stream_type === 1) {
|
||||
for (const element of props.activityActivityStreams) {
|
||||
if (element.stream_type === 1) {
|
||||
hrPresent.value = true;
|
||||
graphItems.value.push({ type: "hr", label: `${t("activityMandAbovePillsComponent.labelGraphHR")}` });
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 2) {
|
||||
if (element.stream_type === 2) {
|
||||
powerPresent.value = true;
|
||||
graphItems.value.push({ type: "power", label: `${t("activityMandAbovePillsComponent.labelGraphPower")}` });
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 3) {
|
||||
if (element.stream_type === 3) {
|
||||
cadPresent.value = true;
|
||||
// Label as "Stroke Rate" over "Cadence" for swimming activities
|
||||
if (activityTypeIsSwimming(props.activity)) {
|
||||
@@ -142,14 +179,14 @@ onMounted(async () => {
|
||||
graphItems.value.push({ type: "cad", label: `${t("activityMandAbovePillsComponent.labelGraphCadence")}` });
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 4) {
|
||||
if (element.stream_type === 4) {
|
||||
// Do not show elevation for swimming activities
|
||||
if (!activityTypeIsSwimming(props.activity)) {
|
||||
elePresent.value = true;
|
||||
graphItems.value.push({ type: "ele", label: `${t("activityMandAbovePillsComponent.labelGraphElevation")}` });
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (element.stream_type === 5) {
|
||||
velPresent.value = true;
|
||||
if (
|
||||
props.activity.activity_type === 4 ||
|
||||
@@ -161,7 +198,7 @@ onMounted(async () => {
|
||||
graphItems.value.push({ type: "vel", label: `${t("activityMandAbovePillsComponent.labelGraphVelocity")}` });
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (element.stream_type === 6) {
|
||||
pacePresent.value = true;
|
||||
if (
|
||||
props.activity.activity_type !== 4 &&
|
||||
@@ -174,6 +211,11 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
hrZones.value = props.activityActivityStreams.find(stream => stream.hr_zone_percentages).hr_zone_percentages || {};
|
||||
if (Object.keys(hrZones.value).length > 0) {
|
||||
hrPresent.value = true;
|
||||
graphItems.value.push({ type: "hrZones", label: `${t("activityMandAbovePillsComponent.labelHRZones")}` });
|
||||
}
|
||||
}
|
||||
if (graphItems.value.length > 0) {
|
||||
graphSelection.value = graphItems.value[0].type;
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
<div class="d-flex justify-content-between">
|
||||
<!-- user name and photo zone -->
|
||||
<div class="d-flex align-items-center">
|
||||
<UserAvatarComponent :user="userActivity" :width=55 :height=55 />
|
||||
<UserAvatarComponent :user="userActivity" :width=55 :height=55 />
|
||||
<div class="ms-3 me-3">
|
||||
<div class="fw-bold">
|
||||
<router-link :to="{ name: 'activity', params: { id: activity.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="source === 'home'">
|
||||
{{ activity.name}}
|
||||
<router-link :to="{ name: 'activity', params: { id: activity.id } }"
|
||||
class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
v-if="source === 'home'">
|
||||
{{ activity.name }}
|
||||
</router-link>
|
||||
<span v-if="userActivity">
|
||||
<router-link :to="{ name: 'user', params: { id: userActivity.id }}" class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover" v-if="source === 'activity'">
|
||||
{{ userActivity.name}}
|
||||
<router-link :to="{ name: 'user', params: { id: userActivity.id } }"
|
||||
class="link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
v-if="source === 'activity'">
|
||||
{{ userActivity.name }}
|
||||
</router-link>
|
||||
</span>
|
||||
<span v-else>
|
||||
@@ -24,52 +28,68 @@
|
||||
<h6>
|
||||
<!-- Display the visibility of the activity -->
|
||||
<span v-if="activity.visibility == 0">
|
||||
<font-awesome-icon :icon="['fas', 'globe']"/> {{ $t("activitySummaryComponent.visibilityPublic") }}
|
||||
<font-awesome-icon :icon="['fas', 'globe']" /> {{
|
||||
$t("activitySummaryComponent.visibilityPublic") }}
|
||||
</span>
|
||||
<span v-if="activity.visibility == 1">
|
||||
<font-awesome-icon :icon="['fas', 'users']" v-if="activity.visibility == 1" /> {{ $t("activitySummaryComponent.visibilityFollowers") }}
|
||||
<font-awesome-icon :icon="['fas', 'users']" v-if="activity.visibility == 1" /> {{
|
||||
$t("activitySummaryComponent.visibilityFollowers") }}
|
||||
</span>
|
||||
<span v-if="activity.visibility == 2">
|
||||
<font-awesome-icon :icon="['fas', 'lock']" v-if="activity.visibility == 2" /> {{ $t("activitySummaryComponent.visibilityPrivate") }}
|
||||
<font-awesome-icon :icon="['fas', 'lock']" v-if="activity.visibility == 2" /> {{
|
||||
$t("activitySummaryComponent.visibilityPrivate") }}
|
||||
</span>
|
||||
<span> - </span>
|
||||
|
||||
<!-- Display the activity type -->
|
||||
<span>
|
||||
<font-awesome-icon class="me-1" :icon="getIcon(activity.activity_type)" />
|
||||
<span v-if="activity.activity_type === 3 || activity.activity_type === 7">{{ $t("activitySummaryComponent.labelVirtual") }}</span>
|
||||
<span v-if="activity.activity_type === 3 || activity.activity_type === 7">{{
|
||||
$t("activitySummaryComponent.labelVirtual") }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Display the date and time -->
|
||||
<span v-if="activity.start_time">{{ formatDateMed(activity.start_time) }} @ {{ formatTime(activity.start_time) }}</span>
|
||||
<span v-if="activity.start_time">
|
||||
{{ formatDateMed(activity.start_time) }} @ {{ formatTime(activity.start_time) }}
|
||||
</span>
|
||||
<!-- Conditionally display city and country -->
|
||||
<span v-if="activity.town || activity.city || activity.country">
|
||||
-
|
||||
-
|
||||
<span>{{ formatLocation(activity) }}</span>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-flex" v-if="activity.user_id == authStore.user.id">
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" :href="`https://www.strava.com/activities/${activity.strava_activity_id}`" role="button" v-if="activity.strava_activity_id">
|
||||
<a class="btn btn-link btn-lg link-body-emphasis"
|
||||
:href="`https://www.strava.com/activities/${activity.strava_activity_id}`" role="button"
|
||||
v-if="activity.strava_activity_id">
|
||||
<font-awesome-icon :icon="['fab', 'fa-strava']" />
|
||||
</a>
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" :href="`https://connect.garmin.com/modern/activity/${activity.garminconnect_activity_id}`" role="button" v-if="activity.garminconnect_activity_id">
|
||||
<img src="/src/assets/garminconnect/Garmin_Connect_app_1024x1024-02.png" alt="Garmin Connect logo" height="22" />
|
||||
<a class="btn btn-link btn-lg link-body-emphasis"
|
||||
:href="`https://connect.garmin.com/modern/activity/${activity.garminconnect_activity_id}`"
|
||||
role="button" v-if="activity.garminconnect_activity_id">
|
||||
<img src="/src/assets/garminconnect/Garmin_Connect_app_1024x1024-02.png" alt="Garmin Connect logo"
|
||||
height="22" />
|
||||
</a>
|
||||
<div>
|
||||
<button class="btn btn-link btn-lg link-body-emphasis" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button class="btn btn-link btn-lg link-body-emphasis" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<font-awesome-icon :icon="['fas', 'fa-ellipsis-vertical']" />
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-if="source === 'activity'">
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editActivityModal">
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#editActivityModal">
|
||||
{{ $t("activitySummaryComponent.buttonEditActivity") }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="source === 'activity'"><hr class="dropdown-divider"></li>
|
||||
<li v-if="source === 'activity'">
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteActivityModal">
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#deleteActivityModal">
|
||||
{{ $t("activitySummaryComponent.buttonDeleteActivity") }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -79,10 +99,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal edit activity -->
|
||||
<EditActivityModalComponent :activity="activity" @activityEditedFields="updateActivityFieldsOnEdit"/>
|
||||
<EditActivityModalComponent :activity="activity" @activityEditedFields="updateActivityFieldsOnEdit" />
|
||||
|
||||
<!-- Modal delete activity -->
|
||||
<ModalComponent modalId="deleteActivityModal" :title="t('activitySummaryComponent.buttonDeleteActivity')" :body="`${t('activitySummaryComponent.modalDeleteBody1')}<b>${activity.name}</b>?<br>${t('activitySummaryComponent.modalDeleteBody2')}`" :actionButtonType="`danger`" :actionButtonText="t('activitySummaryComponent.buttonDeleteActivity')" @submitAction="submitDeleteActivity"/>
|
||||
<ModalComponent modalId="deleteActivityModal" :title="t('activitySummaryComponent.buttonDeleteActivity')"
|
||||
:body="`${t('activitySummaryComponent.modalDeleteBody1')}<b>${activity.name}</b>?<br>${t('activitySummaryComponent.modalDeleteBody2')}`"
|
||||
:actionButtonType="`danger`" :actionButtonText="t('activitySummaryComponent.buttonDeleteActivity')"
|
||||
@submitAction="submitDeleteActivity" />
|
||||
|
||||
<!-- Activity title -->
|
||||
<h1 class="mt-3" v-if="source === 'activity'">
|
||||
@@ -95,7 +118,8 @@
|
||||
<!-- Activity summary -->
|
||||
<div class="row mt-3 align-items-center text-start">
|
||||
<!-- distance -->
|
||||
<div class="col" v-if="activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<div class="col"
|
||||
v-if="activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityDistance") }}
|
||||
</span>
|
||||
@@ -120,15 +144,17 @@
|
||||
</div>
|
||||
<div class="col border-start border-opacity-50">
|
||||
<!-- elevation -->
|
||||
<div v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 8 && activity.activity_type != 9 && activity.activity_type != 10 && activity.activity_type != 13 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<div
|
||||
v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 8 && activity.activity_type != 9 && activity.activity_type != 10 && activity.activity_type != 13 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityElevationGain") }}
|
||||
{{ $t("activitySummaryComponent.activityEleGain") }}
|
||||
</span>
|
||||
<br>
|
||||
<span>{{ formatElevation(activity.elevation_gain, authStore.user.units) }}</span>
|
||||
</div>
|
||||
<!-- pace -->
|
||||
<div v-else-if="activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<div
|
||||
v-else-if="activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityPace") }}
|
||||
</span>
|
||||
@@ -144,10 +170,12 @@
|
||||
<span>{{ formatHr(activity.average_hr) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex mt-3" v-if="source === 'activity' && activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
</div>
|
||||
<div class="row d-flex mt-3"
|
||||
v-if="source === 'activity' && activity.activity_type != 10 && activity.activity_type != 14 && activity.activity_type != 18 && activity.activity_type != 19 && activity.activity_type != 20 && activity.activity_type != 21 && activity.activity_type != 22 && activity.activity_type != 23 && activity.activity_type != 24 && activity.activity_type != 25 && activity.activity_type != 26">
|
||||
<!-- avg_power running and cycling activities-->
|
||||
<div class="col" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 || activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6 || activity.activity_type == 7 || activity.activity_type == 27">
|
||||
<div class="col"
|
||||
v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3 || activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6 || activity.activity_type == 7 || activity.activity_type == 27">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityAvgPower") }}
|
||||
</span>
|
||||
@@ -155,7 +183,8 @@
|
||||
<span>{{ formatPower(activity.average_power) }}</span>
|
||||
</div>
|
||||
<!-- avg_hr not running and cycling activities-->
|
||||
<div class="col" v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 4 && activity.activity_type != 5 && activity.activity_type != 6 && activity.activity_type != 7 && activity.activity_type != 27">
|
||||
<div class="col"
|
||||
v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 4 && activity.activity_type != 5 && activity.activity_type != 6 && activity.activity_type != 7 && activity.activity_type != 27">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityAvgHR") }}
|
||||
</span>
|
||||
@@ -163,7 +192,8 @@
|
||||
<span>{{ formatHr(activity.average_hr) }}</span>
|
||||
</div>
|
||||
<!-- max_hr not running and cycling activities-->
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 4 && activity.activity_type != 5 && activity.activity_type != 6 && activity.activity_type != 7 && activity.activity_type != 27">
|
||||
<div class="col border-start border-opacity-50"
|
||||
v-if="activity.activity_type != 1 && activity.activity_type != 2 && activity.activity_type != 3 && activity.activity_type != 4 && activity.activity_type != 5 && activity.activity_type != 6 && activity.activity_type != 7 && activity.activity_type != 27">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityMaxHR") }}
|
||||
</span>
|
||||
@@ -171,13 +201,15 @@
|
||||
<span>{{ formatHr(activity.max_hr) }}</span>
|
||||
</div>
|
||||
<!-- ele gain running activities -->
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
|
||||
<div class="col border-start border-opacity-50"
|
||||
v-if="activity.activity_type == 1 || activity.activity_type == 2 || activity.activity_type == 3">
|
||||
<span class="fw-lighter">{{ $t("activitySummaryComponent.activityEleGain") }}</span>
|
||||
<br>
|
||||
<span>{{ formatElevation(activity.elevation_gain, authStore.user.units) }}</span>
|
||||
</div>
|
||||
<!-- avg_speed cycling activities -->
|
||||
<div class="col border-start border-opacity-50" v-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6 || activity.activity_type == 7 || activity.activity_type == 27">
|
||||
<div class="col border-start border-opacity-50"
|
||||
v-if="activity.activity_type == 4 || activity.activity_type == 5 || activity.activity_type == 6 || activity.activity_type == 7 || activity.activity_type == 27">
|
||||
<span class="fw-lighter">
|
||||
{{ $t("activitySummaryComponent.activityAvgSpeed") }}
|
||||
</span>
|
||||
@@ -210,37 +242,38 @@ import LoadingComponent from "@/components/GeneralComponents/LoadingComponent.vu
|
||||
import UserAvatarComponent from "@/components/Users/UserAvatarComponent.vue";
|
||||
import EditActivityModalComponent from "@/components/Activities/Modals/EditActivityModalComponent.vue";
|
||||
import ModalComponent from "@/components/Modals/ModalComponent.vue";
|
||||
import BarChartComponent from '@/components/GeneralComponents/BarChartComponent.vue';
|
||||
// Importing the services
|
||||
import { users } from "@/services/usersService";
|
||||
import { activities } from "@/services/activitiesService";
|
||||
// Importing the utils
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatPace,
|
||||
formatHr,
|
||||
formatCalories,
|
||||
getIcon,
|
||||
formatLocation,
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatPace,
|
||||
formatHr,
|
||||
formatCalories,
|
||||
getIcon,
|
||||
formatLocation,
|
||||
formatAverageSpeed,
|
||||
formatPower,
|
||||
} from "@/utils/activityUtils";
|
||||
import {
|
||||
formatDateMed,
|
||||
formatTime,
|
||||
formatDateMed,
|
||||
formatTime,
|
||||
formatSecondsToMinutes,
|
||||
} from "@/utils/dateTimeUtils";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
activity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
units: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
@@ -262,7 +295,7 @@ const userActivity = ref(null);
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
try {
|
||||
try {
|
||||
if (authStore.isAuthenticated) {
|
||||
userActivity.value = await users.getUserById(props.activity.user_id);
|
||||
} else {
|
||||
@@ -270,17 +303,17 @@ onMounted(async () => {
|
||||
userActivity.value = await users.getPublicUserById(props.activity.user_id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorFetchingUserById")} - ${error}`);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Methods
|
||||
async function submitDeleteActivity() {
|
||||
try {
|
||||
userActivity.value = await activities.deleteActivity(props.activity.id);
|
||||
try {
|
||||
userActivity.value = await activities.deleteActivity(props.activity.id);
|
||||
if (props.source === 'activity') {
|
||||
return router.push({
|
||||
path: "/",
|
||||
@@ -288,13 +321,13 @@ async function submitDeleteActivity() {
|
||||
});
|
||||
}
|
||||
emit("activityDeleted", props.activity.id);
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorDeletingActivity")} - ${error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t("activitySummaryComponent.errorDeletingActivity")} - ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateActivityFieldsOnEdit(data) {
|
||||
// Emit the activityEditedFields event to the parent component
|
||||
emit("activityEditedFields", data);
|
||||
// Emit the activityEditedFields event to the parent component
|
||||
emit("activityEditedFields", data);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="barChartCanvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
const props = defineProps({
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
barColors: {
|
||||
type: Array,
|
||||
default: () => ['#1e90ff', '#28a745', '#ffc107', '#fd7e14', '#dc3545']
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
datalabelsFormatter: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const barChartCanvas = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
function renderChart() {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
chartInstance = new Chart(barChartCanvas.value, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: props.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: props.title,
|
||||
data: props.values,
|
||||
backgroundColor: props.barColors,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: {
|
||||
display: props.title,
|
||||
text: props.title
|
||||
},
|
||||
tooltip: { enabled: false },
|
||||
datalabels: {
|
||||
backgroundColor: function(context) {
|
||||
return "black";
|
||||
},
|
||||
borderRadius: 4,
|
||||
color: 'white',
|
||||
align: 'end', // Align datalabels to the end of the bar
|
||||
anchor: 'end', // Anchor datalabels to the end of the bar
|
||||
formatter: props.datalabelsFormatter || undefined,
|
||||
padding: 6
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { stepSize: 10 }
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [ChartDataLabels]
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderChart();
|
||||
});
|
||||
|
||||
watch(() => [props.labels, props.values, props.barColors, props.title], renderChart, { deep: true });
|
||||
</script>
|
||||
@@ -4,11 +4,13 @@
|
||||
"labelPillWorkoutSets": "Sets",
|
||||
"labelGraph": "Activity data graphs",
|
||||
"labelGraphHR": "Heart rate",
|
||||
"labelHRZones": "Heart rate zones",
|
||||
"labelGraphPower": "Power",
|
||||
"labelGraphCadence": "Cadence",
|
||||
"labelGraphElevation": "Elevation",
|
||||
"labelGraphVelocity": "Speed",
|
||||
"labelGraphPace": "Pace",
|
||||
"labelGraphHRZone": "Zone",
|
||||
"labelDownsampling": "Data downsampled to ~200 points",
|
||||
"errorMessageProcessingActivityStreams": "Error processing activity streams",
|
||||
"labelGraphStrokeRate": "Stroke rate"
|
||||
|
||||
@@ -10,14 +10,12 @@
|
||||
"labelVirtual": "(Virtual) ",
|
||||
"activityDistance": "Distance",
|
||||
"activityTime": "Time",
|
||||
"activityElevationGain": "Elevation Gain",
|
||||
"activityPace": "Pace",
|
||||
"activityAvgPower": "Avg Power",
|
||||
"activityAvgSpeed": "Avg Speed",
|
||||
"activityAvgHR": "Avg HR",
|
||||
"activityMaxHR": "Max HR",
|
||||
"activityEleGain": "Elevation gain",
|
||||
"activityEleLoss": "Elevation loss",
|
||||
"activityAvgPower": "Avg power",
|
||||
"activityAvgSpeed": "Avg speed",
|
||||
"activityEleGain": "Ele gain",
|
||||
"activityEleLoss": "Ele loss",
|
||||
"activityCalories": "Calories",
|
||||
"activityNoData": "No data",
|
||||
"errorFetchingUserById": "Error fetching user by id",
|
||||
|
||||
21
frontend/app/src/utils/chartUtils.js
Normal file
21
frontend/app/src/utils/chartUtils.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export function getZoneColor(index) {
|
||||
// Example colors for 5 HR zones
|
||||
const colors = [
|
||||
'#1e90ff', // Zone 1: blue
|
||||
'#28a745', // Zone 2: green
|
||||
'#ffc107', // Zone 3: yellow
|
||||
'#fd7e14', // Zone 4: orange
|
||||
'#dc3545', // Zone 5: red
|
||||
];
|
||||
return colors[index] || '#000';
|
||||
}
|
||||
|
||||
export function getHrBarChartData(hrZones, t) {
|
||||
const zones = Object.values(hrZones);
|
||||
return {
|
||||
labels: zones.map((z, i) => `${t('activityMandAbovePillsComponent.labelGraphHRZone')} ${i + 1} (${z.hr || ''})`),
|
||||
// values: zones.map(z => `${z.percent ?? 0}%`),
|
||||
values: zones.map(z => z.percent ?? 0),
|
||||
barColors: zones.map((_, i) => getZoneColor(i)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user