Skip to content

Commit

Permalink
Workout related features (#389)
Browse files Browse the repository at this point in the history
* build(backend): patch sea-orm

* Revert "build(backend): patch sea-orm"

This reverts commit 6f4c788.

* fix(frontend): increase size of timer

* try(backend): use new timezone handling for cron

* Revert "try(backend): use new timezone handling for cron"

This reverts commit bbb8bdd.

* fix(ts-utils): add timeago locale correctly

* feat(frontend): store previous sets

* feat(frontend): display previous sets for exercises

* fix(frontend): incorrect set being displayed

* fix(frontend): also hide pace

* feat(frontend): proper width for exercises

* fix(frontend): padding for exercise display

* Revert "Revert "try(backend): use new timezone handling for cron""

This reverts commit d7ec08b.

* try(backend): use new timezone settings

* Revert "try(backend): use new timezone settings"

This reverts commit 4c09f55.

* fix(frontend): separate btn to open timer

* fix(frontend): remove floating btn

* feat(frontend): re-order btn

* fix(frontend): weird offset issue

* fix(frontend): general changes

* fix(frontend): make re-ordering smoother

* fix(frontend): incorrect exercises turning up

* Revert "Revert "try(backend): use new timezone settings""

This reverts commit 1f12599.

* build(backend): do not patch apalis

* feat(backend): use new apalis stuff

* docs(configuration): add info about `TZ` and defaults

* build(backend): bump version

* fix(frontend): apply correct height

* fix(frontend): change size of additional info

* feat(backend): config param for genre display

* feat(frontend): allow changing num genres display

* fix(frontend): display only as many genres as configured

* style(backend): order of cron jobs

* chore(frontend): general changes

* style(frontend): remove useless braces around str JSX expr

This had been triggering my OCD for a long time.

* fix(frontend): btn positions

* fix(frontend): hide re-order btn when lte 2 exercises

* fix(frontend): show re-order btn when necessary

* fix(frontend): hoist hooks to page variables

* fix(frontend): useless bold text

* feat(frontend): new entry for rest timer

* feat(frontend): allow enabling rest timer

* feat(frontend): allow setting rest timer duration

* feat(frontend): display rest timer duration

* feat(frontend): start timer when exercise finished

* build(ts): upgrade deps

* build(rs): upgrade deps

* style(frontend): add supression comments

* fix(frontend): allow entering stats

* fix(frontend): set default value for previous set

* fix(backend): do not search with attributes

* fix(frontend): change size of filters

* fix(backend): reverse join with user

* feat(backend): return less details about exercises

* fix(backend): sort by name when nothing specified

* feat(backend): allow sorting by num times performed

* feat(frontend): allow sorting on list page

* fix(backend): sort by name

* feat(backend): return num times performed

* feat(backend): return number of times exercise was performed

* feat(frontend): display num times exercise was performed

* refactor(backend): do not return useless info from exercise list

* refactor(backend): change name of structs

* chore(frontend): adapt to new gql schema

* feat(backend): deploy download exercises job

* feat(backend): allow sorting exercises by `last_performed`

* fix(backend): order by name asc when selected

Earlier, it used to return exercises in the reverse
order.

* feat(frontend): display additional exercise data

* feat(frontend): display muscles for exercise

* ci(fly): set max machines to 1

* fix(frontend): change size of font

* feat(frontend): display exercise details

* fix(frontend): add radius for btns

* refactor(frontend): do not use useless fn

* fix(frontend): add correct description for specifics

* fix(frontend): change order of inputs

* feat(backend): store images and videos for workouts

* fix(frontend): types for new assets

* fix(frontend): open docs in new page

* fix(frontend): use correct element

* build(backend): update `schematic` dep

* feat(backend): start using `schematic`

* Revert "feat(backend): start using `schematic`"

This reverts commit 3c216df.

* feat(frontend): basic webcam stuff

* fix(frontend): add image and video assets

* feat(frontend): allow taking images

* refactor(frontend): extract fn to upload file to s3

* feat(frontend): allow uploading images for exercise

* feat(frontend): display uploaded images

* fix(frontend): incorrect capture handling

* fix(frontend): fetch the correct image

* feat(frontend): allow removing images

* Revert "Revert "feat(backend): start using `schematic`""

This reverts commit 6003de1.

* Revert "Revert "Revert "feat(backend): start using `schematic`"""

This reverts commit 50d2259.

* chore(backend): remove `specta`

* refactor(backend): remove generic field

* build(backend): bump schematic

* chore(frontend): display num images uploaded

* feat(backend): resolver to delete an s3 object

* feat(frontend): delete image from s3 when removed

* fix(frontend): allow file upload only when enabled

* chore(backend): change names of s3 related queries/mutations

* chore(frontend): adapt to new gql schema

* feat(backend): fn to generate gql representation of workout

* feat(backend): resolver to get workout details

* chore(frontend): adapt to new gql schema

* feat(backend): partial model for workouts

* chore(graphql): fetch details about workout

* feat(backend): resolver to get workouts for a user

* chore(graphql): query to get user workouts

* feat(frontend): add route for workouts

* fix(graphql): fetch the user workout id

* feat(frontend): basic workout list display

* feat(frontend): add pagination for workout list

* fix(frontend): react to pagination changes

* feat(frontend): display workouts in accordion

* fix(frontend): workouts should always have a name

* feat(backend): make workout name not null

* chore(graphql): make workout name not null

* feat(*): allow disabling workouts

* fix(frontend): display number of workouts

* fix(frontend): show notif if no workout name

* refactor(frontend): centralize `localStorage` keys

* refactor(frontend): change names of keys

* chore(frontend): extract more localstorage keys to constants

* build(backend): upgrade deps

* build(frontend): upgrade deps

* chore(backend): add fixme comment

* fix(frontend): display workout date

* chore(graphql): change data type for decimal

* feat(frontend): display workout summary

* chore(graphql,frontend): change data type of decimal

* feat(frontend): focusing on input selects the entire text

* fix(frontend): re-ordering exercises would delete sets

* refactor(frontend): fn to get stats text

* feat(backend): generate export schema with schematic

* fix(frontend): display exercise details in a better way

* build(backend): upgrade apalis deps

* feat(backend): save exercise lot in summary

* fix(frontend): layout issues

* feat(frontend): display workout summary

* fix(frontend): change font sizes and wording

* chore(frontend): move workouts page to dedicated folder

* chore(graphql): change order of features

* feat(frontend): btn to view workout details

* chore(frontend): delete useless 404 page

This page would never be displayed since 404 errors
are handled by Axum backend server.

* style(frontend): order of imports

* fix(frontend): use better keys for exercises

* fix(frontend): do not total workouts

* feat(frontend): make workout page better

* fix(frontend): change exercise display

* chore(backend): add info when necessary

* build(backend): bump version

* fix(frontend): display muscles in a better way

* style(backend): import style

* feat(frontend): basic workout details page

* feat(backend): store exercise lot for processed exercise

* feat(frontend): show exercises done in workout

* feat(frontend): display basic exercise stats

* feat(frontend): display exercise personal bests

* feat(frontend): display notes and assets for workout
  • Loading branch information
IgnisDa authored Oct 11, 2023
1 parent 48c6ea0 commit 70f7d5f
Show file tree
Hide file tree
Showing 86 changed files with 2,964 additions and 1,487 deletions.
449 changes: 269 additions & 180 deletions Cargo.lock

Large diffs are not rendered by default.

34 changes: 16 additions & 18 deletions apps/backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "ryot"
version = "2.21.2"
version = "2.22.0"
edition = "2021"
repository = "https://github.com/IgnisDa/ryot"
license = "GPL-V3"

[dependencies]
anyhow = "1.0.75"
apalis = { version = "0.4.4", features = [
apalis = { version = "0.4.5", features = [
"cron",
"extensions",
"limit",
Expand All @@ -22,17 +22,18 @@ async-graphql = { version = "6.0.7", features = [
] }
async-graphql-axum = "6.0.7"
async-trait = "0.1.73"
aws-sdk-s3 = "0.31.2"
aws-sdk-s3 = "0.33.0"
axum = { version = "0.6.20", features = ["macros", "multipart"] }
axum-extra = { version = "0.8.0", default-features = false, features = [
"cookie",
] }
boilermates = "0.3.0"
chrono = "0.4.31"
chrono-tz = "0.8.3"
convert_case = "0.6.0"
const-str = "0.5.6"
cookie = "0.17.0"
csv = "1.2.2"
cookie = "0.18.0"
csv = "1.3.0"
derive_more = { version = "1.0.0-beta.3", features = [
"add",
"sum",
Expand All @@ -56,20 +57,23 @@ mime_guess = "2.0.4"
nanoid = "0.4.0"
quick-xml = { version = "0.30.0", features = ["serde", "serialize"] }
rand = "0.8.5"
regex = "1.9.6"
regex = "1.10.0"
retainer = "0.3.0"
rs-utils = { path = "../../libs/rs-utils" }
rust-embed = "8.0.0"
rust_decimal = "1.32.0"
rust_decimal_macros = "1.32.0"
rust_iso3166 = "0.1.10"
schematic = { version = "0.11.8", features = [
rust_iso3166 = "0.1.11"
schematic = { version = "0.12.2", features = [
"config",
"json",
"schema",
"toml",
"typescript",
"type_chrono",
"type_rust_decimal",
"url",
"yaml",
"valid_url",
], default-features = false }
scraper = "0.17.1"
sea-orm = { version = "0.12.3", features = [
Expand All @@ -85,27 +89,21 @@ sea-orm = { version = "0.12.3", features = [
] }
sea-orm-migration = "0.12.3"
sea-query = "0.30.2"
semver = "1.0.19"
semver = "1.0.20"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
serde_with = { version = "3.3.0", features = ["chrono_0_4"] }
serde-xml-rs = "0.6.0"
slug = "0.1.4"
sqlx = "0.7.2"
sonyflake = "0.2.0"
specta = { version = "2.0.0-rc.3", features = [
"typescript",
"chrono",
"rust_decimal",
"export",
] }
strum = { version = "0.25.0", features = ["derive"] }
surf = { version = "2.3.2", features = [
"h1-client-rustls",
], default-features = false }
surf-governor = "0.2.0"
surf-retry = "0.3.1"
tokio = { version = "1.32.0", features = ["full"] }
surf-retry = "0.3.2"
tokio = { version = "1.33.0", features = ["full"] }
tower-http = { version = "0.4.4", features = ["catch-panic", "cors", "trace"] }
tracing = { version = "0.1.37", features = ["attributes"] }
tracing-appender = "0.2.2"
Expand Down
10 changes: 5 additions & 5 deletions apps/backend/src/background.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{sync::Arc, time::Instant};

use apalis::prelude::{Job, JobContext, JobError};
use sea_orm::prelude::DateTimeUtc;
use chrono::DateTime;
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use strum::Display;

Expand All @@ -16,11 +17,10 @@ use crate::{

// Cron Jobs

#[derive(Debug, Deserialize, Serialize)]
pub struct ScheduledJob(DateTimeUtc);
pub struct ScheduledJob(DateTime<Tz>);

impl From<DateTimeUtc> for ScheduledJob {
fn from(value: DateTimeUtc) -> Self {
impl From<DateTime<Tz>> for ScheduledJob {
fn from(value: DateTime<Tz>) -> Self {
Self(value)
}
}
Expand Down
33 changes: 31 additions & 2 deletions apps/backend/src/entities/exercise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
use std::sync::Arc;

use async_graphql::SimpleObject;
use sea_orm::entity::prelude::*;
use sea_orm::{entity::prelude::*, FromQueryResult};
use serde::{Deserialize, Serialize};

use crate::{
file_storage::FileStorageService,
migrator::{ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic},
migrator::{
ExerciseEquipment, ExerciseForce, ExerciseLevel, ExerciseLot, ExerciseMechanic,
ExerciseMuscle,
},
models::fitness::{ExerciseAttributes, ExerciseMuscles},
utils::get_stored_asset,
};
Expand Down Expand Up @@ -47,6 +50,32 @@ impl Model {
}
}

#[derive(Clone, Debug, Deserialize, SimpleObject, FromQueryResult)]
pub struct ExerciseListItem {
pub id: i32,
pub lot: ExerciseLot,
pub name: String,
#[graphql(skip)]
pub attributes: ExerciseAttributes,
pub num_times_performed: Option<i32>,
pub muscle: Option<ExerciseMuscle>,
pub image: Option<String>,
#[graphql(skip)]
pub muscles: ExerciseMuscles,
}

impl ExerciseListItem {
pub async fn graphql_repr(self, file_storage_service: &Arc<FileStorageService>) -> Self {
let mut converted_exercise = self.clone();
if let Some(img) = self.attributes.internal_images.first() {
converted_exercise.image =
Some(get_stored_asset(img.clone(), file_storage_service).await);
}
converted_exercise.muscle = self.muscles.0.first().cloned();
converted_exercise
}
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::user_to_exercise::Entity")]
Expand Down
5 changes: 2 additions & 3 deletions apps/backend/src/entities/user_measurement.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1
use async_graphql::{InputObject, SimpleObject};
use schematic::Schematic;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use specta::Type;

use crate::models::fitness::UserMeasurementStats;

Expand All @@ -20,10 +20,9 @@ use crate::models::fitness::UserMeasurementStats;
Deserialize,
SimpleObject,
InputObject,
Type,
Schematic,
)]
#[graphql(name = "UserMeasurement", input_name = "UserMeasurementInput")]
#[specta(rename = "ExportUserMeasurementItem")]
#[sea_orm(table_name = "user_measurement")]
pub struct Model {
/// The date and time this measurement was made.
Expand Down
35 changes: 32 additions & 3 deletions apps/backend/src/entities/workout.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,54 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.1
use std::sync::Arc;

use async_graphql::SimpleObject;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

use crate::models::fitness::{WorkoutInformation, WorkoutSummary};
use crate::{
file_storage::FileStorageService,
models::fitness::{WorkoutInformation, WorkoutSummary},
};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "workout")]
#[graphql(name = "Workout")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub processed: bool,
pub start_time: DateTimeUtc,
pub end_time: DateTimeUtc,
#[graphql(skip)]
pub user_id: i32,
pub summary: WorkoutSummary,
pub information: WorkoutInformation,
pub name: Option<String>,
pub name: String,
pub comment: Option<String>,
}

impl Model {
pub async fn graphql_repr(self, file_storage_service: &Arc<FileStorageService>) -> Self {
let mut cnv_workout = self.clone();
for image in cnv_workout.information.assets.images.iter_mut() {
*image = file_storage_service.get_presigned_url(image.clone()).await;
}
for video in cnv_workout.information.assets.videos.iter_mut() {
*video = file_storage_service.get_presigned_url(video.clone()).await;
}
for exercise in cnv_workout.information.exercises.iter_mut() {
for image in exercise.assets.images.iter_mut() {
*image = file_storage_service.get_presigned_url(image.clone()).await;
}
for video in exercise.assets.videos.iter_mut() {
*video = file_storage_service.get_presigned_url(video.clone()).await;
}
}
cnv_workout
}
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
Expand Down
10 changes: 10 additions & 0 deletions apps/backend/src/file_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ impl FileStorageService {
.to_string()
}

pub async fn delete_object(&self, key: String) -> bool {
self.s3_client
.delete_object()
.bucket(&self.bucket_name)
.key(key)
.send()
.await
.is_ok()
}

pub async fn get_presigned_put_url(&self, filename: String) -> (String, String) {
let key = format!("uploads/{}-{}", Uuid::new_v4(), filename);
let url = self
Expand Down
42 changes: 26 additions & 16 deletions apps/backend/src/fitness/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ use crate::{
},
migrator::ExerciseLot,
models::fitness::{
ExerciseBestSetRecord, ProcessedExercise, SetLot, SetStatistic, TotalMeasurement,
EntityAssets, ExerciseBestSetRecord, ProcessedExercise, SetLot,
UserToExerciseBestSetExtraInformation, UserToExerciseExtraInformation,
UserToExerciseHistoryExtraInformation, WorkoutInformation, WorkoutSetPersonalBest,
WorkoutSetRecord, WorkoutSummary, WorkoutSummaryExercise,
WorkoutSetRecord, WorkoutSetStatistic, WorkoutSummary, WorkoutSummaryExercise,
WorkoutTotalMeasurement,
},
users::{UserExercisePreferences, UserUnitSystem},
};
Expand Down Expand Up @@ -66,7 +67,7 @@ fn get_index_of_highest_pb(

#[derive(Clone, Debug, Deserialize, Serialize, InputObject)]
pub struct UserWorkoutSetRecord {
pub statistic: SetStatistic,
pub statistic: WorkoutSetStatistic,
pub lot: SetLot,
}

Expand Down Expand Up @@ -94,16 +95,18 @@ pub struct UserExerciseInput {
pub sets: Vec<UserWorkoutSetRecord>,
pub notes: Vec<String>,
pub rest_time: Option<u16>,
pub assets: EntityAssets,
}

#[derive(Clone, Debug, Deserialize, Serialize, InputObject)]
pub struct UserWorkoutInput {
pub name: Option<String>,
pub name: String,
pub comment: Option<String>,
pub start_time: DateTimeUtc,
pub end_time: DateTimeUtc,
pub exercises: Vec<UserExerciseInput>,
pub supersets: Vec<Vec<u16>>,
pub assets: EntityAssets,
}

impl UserWorkoutInput {
Expand All @@ -123,7 +126,7 @@ impl UserWorkoutInput {
.await?
.ok_or_else(|| anyhow!("No exercise found!"))?;
let mut sets = vec![];
let mut total = TotalMeasurement::default();
let mut total = WorkoutTotalMeasurement::default();
let association = UserToExercise::find()
.filter(user_to_exercise::Column::UserId.eq(user_id))
.filter(user_to_exercise::Column::ExerciseId.eq(ex.exercise_id))
Expand All @@ -144,7 +147,7 @@ impl UserWorkoutInput {
last_updated_on: ActiveValue::Set(Utc::now()),
extra_information: ActiveValue::Set(UserToExerciseExtraInformation {
history: vec![history_item],
lifetime_stats: TotalMeasurement::default(),
lifetime_stats: WorkoutTotalMeasurement::default(),
personal_bests: vec![],
}),
};
Expand Down Expand Up @@ -239,14 +242,19 @@ impl UserWorkoutInput {
association_extra_information.personal_bests = personal_bests;
association.extra_information = ActiveValue::Set(association_extra_information);
association.update(db).await?;
exercises.push(ProcessedExercise {
exercise_id: ex.exercise_id,
exercise_name: db_ex.name,
sets,
notes: ex.notes,
rest_time: ex.rest_time,
total,
});
exercises.push((
db_ex.lot,
ProcessedExercise {
exercise_id: ex.exercise_id,
exercise_name: db_ex.name,
exercise_lot: db_ex.lot,
sets,
notes: ex.notes,
rest_time: ex.rest_time,
assets: ex.assets,
total,
},
));
}
let summary_total = workout_totals.into_iter().sum();
let model = workout::Model {
Expand All @@ -261,16 +269,18 @@ impl UserWorkoutInput {
total: summary_total,
exercises: exercises
.iter()
.map(|e| WorkoutSummaryExercise {
.map(|(lot, e)| WorkoutSummaryExercise {
num_sets: e.sets.len(),
name: e.exercise_name.clone(),
lot: lot.clone(),
best_set: e.sets[get_best_set_index(&e.sets).unwrap()].clone(),
})
.collect(),
},
information: WorkoutInformation {
supersets: self.supersets,
exercises,
assets: self.assets,
exercises: exercises.into_iter().map(|(_, ex)| ex).collect(),
},
};
let insert: workout::ActiveModel = model.into();
Expand Down
Loading

0 comments on commit 70f7d5f

Please sign in to comment.