Skip to content

Commit

Permalink
Add other types of workouts without location
Browse files Browse the repository at this point in the history
Eg. add weight and repetition based workouts.

Addresses #145, #140, #134, #55

Signed-off-by: Jo Vandeginste <[email protected]>
  • Loading branch information
jovandeginste committed Jun 17, 2024
1 parent 11a7bc6 commit 9e4c8cd
Show file tree
Hide file tree
Showing 19 changed files with 431 additions and 90 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ build-dist: clean-dist
cp -R ./node_modules/@fortawesome/fontawesome-free/ ./assets/dist/fontawesome/
cp -v ./node_modules/apexcharts/dist/apexcharts.min.js ./assets/dist/
cp -v ./node_modules/apexcharts/dist/apexcharts.css ./assets/dist/
cp -v ./node_modules/htmx.org/dist/htmx.min.js ./assets/dist/


watch-tw:
Expand Down
4 changes: 4 additions & 0 deletions assets/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -2824,6 +2824,10 @@ table {
content: "\f00d";
}

.\[a-zA-Z\:\\-\\\.\] {
a-z-a--z: \-\.;
}

@media (min-width: 640px) {
.sm\:mt-0 {
margin-top: 0px;
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"apexcharts": "^3.48.0",
"fullcalendar": "^6.1.11",
"htmx.org": "^1.9.12",
"leaflet": "^1.9.4",
"shareon": "^2.5.0",
"sorttable": "^1.0.2",
Expand Down
1 change: 1 addition & 0 deletions pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func (a *App) Configure() error {
if err := a.ConfigureDatabase(); err != nil {
return err
}

if err := a.ConfigureGeocoder(); err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions pkg/app/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ func (a *App) secureRoutes(e *echo.Group) *echo.Group {
workoutsGroup.POST("/:id/delete", a.workoutsDeleteHandler).Name = "workout-delete"
workoutsGroup.POST("/:id/refresh", a.workoutsRefreshHandler).Name = "workout-refresh"
workoutsGroup.GET("/add", a.workoutsAddHandler).Name = "workout-add"
workoutsGroup.GET("/form", a.workoutsFormHandler).Name = "workout-form"

equipmentGroup := secureGroup.Group("/equipment")
equipmentGroup.GET("", a.equipmentHandler).Name = "equipment"
Expand Down
69 changes: 69 additions & 0 deletions pkg/app/workouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ import (
"mime/multipart"
"net/http"
"strings"
"time"

"github.com/jovandeginste/workout-tracker/pkg/database"
"github.com/labstack/echo/v4"
)

const (
htmlDateFormat = "2006-01-02T15:04"
htmlDurationFormat = "15:04"
)

func uploadedFile(file *multipart.FileHeader) ([]byte, error) {
src, err := file.Open()
if err != nil {
Expand All @@ -26,7 +32,70 @@ func uploadedFile(file *multipart.FileHeader) ([]byte, error) {
return content, nil
}

type ManualWorkout struct {
Name string `form:"name"`
Date string `form:"date"`
Duration string `form:"duration"`
Repetitions int `form:"repetitions"`
Weight float64 `form:"weight"`
Notes string `form:"notes"`
Type database.WorkoutType `form:"type"`
}

func (m *ManualWorkout) ToDate() time.Time {
d, err := time.Parse(htmlDateFormat, m.Date)
if err != nil {
return time.Time{}
}

return d
}

func (m *ManualWorkout) ToDuration() time.Duration {
d, err := time.Parse(htmlDurationFormat, m.Duration)
if err != nil {
return 0
}

return time.Duration(d.Hour())*time.Hour + time.Duration(d.Minute())*time.Minute
}

func (a *App) addWorkout(c echo.Context) error {
if strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), echo.MIMEMultipartForm) {
return a.addWorkoutFromFile(c)
}

d := &ManualWorkout{}

if err := c.Bind(d); err != nil {
return a.redirectWithError(c, "/workouts", err)
}

dDate := d.ToDate()
dDuration := d.ToDuration()

w := database.Workout{
Name: d.Name,
Notes: d.Notes,
Date: &dDate,
Type: d.Type,
User: a.getCurrentUser(c),
UserID: a.getCurrentUser(c).ID,
Data: &database.MapData{
TotalDuration: dDuration,
TotalRepetitions: d.Repetitions,
TotalWeight: d.Weight,
},
}

if err := w.Save(a.db); err != nil {
return a.redirectWithError(c, "/workouts", err)
}

return c.Redirect(http.StatusFound, a.echo.Reverse("workouts"))
}

func (a *App) addWorkoutFromFile(c echo.Context) error {
form, err := c.MultipartForm()
if err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions pkg/app/workouts_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func (a *App) workoutsAddHandler(c echo.Context) error {
return c.Render(http.StatusOK, "workouts_add.html", data)
}

func (a *App) workoutsFormHandler(c echo.Context) error {
t := database.WorkoutType(c.FormValue("type"))
return c.Render(http.StatusOK, "workout_form.html", t)
}

func (a *App) workoutsDeleteHandler(c echo.Context) error { //nolint:dupl
workout, err := a.getWorkout(c)
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions pkg/database/equipment.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,17 @@ func (e *Equipment) GetTotals() (WorkoutTotals, error) {
if w.Type.IsDuration() {
rs.Duration += w.Duration()
}

if w.Type.IsRepetition() {
rs.Repetitions += w.Repetitions()
}
}

return rs, nil
}

type WorkoutTotals struct {
Distance float64
Duration time.Duration
Distance float64
Duration time.Duration
Repetitions int
}
10 changes: 10 additions & 0 deletions pkg/database/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type UserPreferredUnits struct {
SpeedRaw string `form:"speed" json:"speed"` // The user's preferred speed unit
DistanceRaw string `form:"distance" json:"distance"` // The user's preferred distance unit
ElevationRaw string `form:"elevation" json:"elevation"` // The user's preferred elevation unit
WeightRaw string `form:"weight" json:"weight"` // The user's preferred weight unit
}

func (u UserPreferredUnits) Tempo() string {
Expand All @@ -50,6 +51,15 @@ func (u UserPreferredUnits) Elevation() string {
}
}

func (u UserPreferredUnits) Weight() string {
switch u.WeightRaw {
case "lbs":
return "lbs"
default:
return "kg"
}
}

func (u UserPreferredUnits) Distance() string {
switch u.DistanceRaw {
case "mi":
Expand Down
103 changes: 86 additions & 17 deletions pkg/database/workout_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,109 @@ const (
// We need to add each of these types to the "messages.html" partial view.
// Then it gets picked up by the i18n system, added to the list of translatable
// strings, etc.
WorkoutTypeAutoDetect WorkoutType = "auto"
WorkoutTypeRunning WorkoutType = "running"
WorkoutTypeCycling WorkoutType = "cycling"
WorkoutTypeWalking WorkoutType = "walking"
WorkoutTypeSkiing WorkoutType = "skiing"
WorkoutTypeSnowboarding WorkoutType = "snowboarding"
WorkoutTypeSwimming WorkoutType = "swimming"
WorkoutTypeKayaking WorkoutType = "kayaking"
WorkoutTypeGolfing WorkoutType = "golfing"
WorkoutTypeHiking WorkoutType = "hiking"
WorkoutTypeAutoDetect WorkoutType = "auto"
WorkoutTypeRunning WorkoutType = "running"
WorkoutTypeCycling WorkoutType = "cycling"
WorkoutTypeWalking WorkoutType = "walking"
WorkoutTypeSkiing WorkoutType = "skiing"
WorkoutTypeSnowboarding WorkoutType = "snowboarding"
WorkoutTypeSwimming WorkoutType = "swimming"
WorkoutTypeKayaking WorkoutType = "kayaking"
WorkoutTypeGolfing WorkoutType = "golfing"
WorkoutTypeHiking WorkoutType = "hiking"
WorkoutTypePushups WorkoutType = "push-ups"
WorkoutTypeWeightLifting WorkoutType = "weight lifting"
)

func WorkoutTypes() []WorkoutType {
return []WorkoutType{WorkoutTypeRunning, WorkoutTypeCycling, WorkoutTypeWalking, WorkoutTypeSkiing, WorkoutTypeSnowboarding, WorkoutTypeSwimming, WorkoutTypeKayaking, WorkoutTypeGolfing, WorkoutTypeHiking}
type WorkoutTypeConfiguration struct {
Location bool
Distance bool
Repetition bool
Weight bool
}

var workoutTypeConfigs = map[WorkoutType]WorkoutTypeConfiguration{
WorkoutTypeRunning: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeCycling: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeWalking: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeSkiing: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeSnowboarding: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeSwimming: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeKayaking: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeGolfing: {Location: true, Distance: true, Repetition: false, Weight: false},
WorkoutTypeHiking: {Location: true, Distance: true, Repetition: false, Weight: false},

WorkoutTypePushups: {Location: false, Distance: false, Repetition: true, Weight: false},
WorkoutTypeWeightLifting: {Location: false, Distance: false, Repetition: true, Weight: true},
}

func DurationWorkoutTypes() []WorkoutType {
return []WorkoutType{WorkoutTypeRunning, WorkoutTypeCycling, WorkoutTypeWalking, WorkoutTypeSkiing, WorkoutTypeSnowboarding, WorkoutTypeSwimming, WorkoutTypeKayaking, WorkoutTypeGolfing, WorkoutTypeHiking}
func WorkoutTypes() []WorkoutType {
keys := []WorkoutType{}

for k := range workoutTypeConfigs {
keys = append(keys, k)
}

slices.Sort(keys)

return keys
}

func DistanceWorkoutTypes() []WorkoutType {
return []WorkoutType{WorkoutTypeRunning, WorkoutTypeCycling, WorkoutTypeWalking, WorkoutTypeSkiing, WorkoutTypeSnowboarding, WorkoutTypeSwimming, WorkoutTypeKayaking, WorkoutTypeGolfing, WorkoutTypeHiking}
keys := []WorkoutType{}

for k, c := range workoutTypeConfigs {
if !c.Distance {
continue
}

keys = append(keys, k)
}

slices.Sort(keys)

return keys
}

func LocationWorkoutTypes() []WorkoutType {
keys := []WorkoutType{}

for k, c := range workoutTypeConfigs {
if !c.Location {
continue
}

keys = append(keys, k)
}

slices.Sort(keys)

return keys
}

func (wt WorkoutType) String() string {
return string(wt)
}

func (wt WorkoutType) IsDistance() bool {
return slices.Contains(DistanceWorkoutTypes(), wt)
return workoutTypeConfigs[wt].Distance
}

func (wt WorkoutType) IsRepetition() bool {
return workoutTypeConfigs[wt].Repetition
}

func (wt WorkoutType) IsDuration() bool {
return slices.Contains(DurationWorkoutTypes(), wt)
_, ok := workoutTypeConfigs[wt]
return ok
}

func (wt WorkoutType) IsWeight() bool {
return workoutTypeConfigs[wt].Weight
}

func (wt WorkoutType) IsLocation() bool {
return workoutTypeConfigs[wt].Location
}

func AsWorkoutType(s string) WorkoutType {
Expand Down
8 changes: 8 additions & 0 deletions pkg/database/workouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ type GPXData struct {
Filename string // The filename of the file
}

func (w *Workout) Repetitions() int {
if w.Data == nil {
return 0
}

return w.Data.TotalRepetitions
}

func (w *Workout) Duration() time.Duration {
if w.Data == nil {
return 0
Expand Down
30 changes: 16 additions & 14 deletions pkg/database/workouts_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@ func correctAltitude(creator string, lat, long, alt float64) float64 {

type MapData struct {
gorm.Model
WorkoutID uint `gorm:"not null;uniqueIndex"` // The workout this data belongs to
Creator string // The tool that created this workout
Name string // The name of the workout
Center MapCenter `gorm:"serializer:json"` // The center of the workout (in coordinates)
Address *geo.Address `gorm:"serializer:json"` // The address of the workout
TotalDistance float64 // The total distance of the workout
TotalDuration time.Duration // The total duration of the workout
MaxSpeed float64 // The maximum speed of the workout
PauseDuration time.Duration // The total pause duration of the workout
MinElevation float64 // The minimum elevation of the workout
MaxElevation float64 // The maximum elevation of the workout
TotalUp float64 // The total distance up of the workout
TotalDown float64 // The total distance down of the workout
Details *MapDataDetails `json:",omitempty"` // The details of the workout
WorkoutID uint `gorm:"not null;uniqueIndex"` // The workout this data belongs to
Creator string // The tool that created this workout
Name string // The name of the workout
Center MapCenter `gorm:"serializer:json"` // The center of the workout (in coordinates)
Address *geo.Address `gorm:"serializer:json"` // The address of the workout
TotalDistance float64 // The total distance of the workout
TotalDuration time.Duration // The total duration of the workout
MaxSpeed float64 // The maximum speed of the workout
PauseDuration time.Duration // The total pause duration of the workout
MinElevation float64 // The minimum elevation of the workout
MaxElevation float64 // The maximum elevation of the workout
TotalUp float64 // The total distance up of the workout
TotalDown float64 // The total distance down of the workout
Details *MapDataDetails `json:",omitempty"` // The details of the workout
TotalRepetitions int // The number of repetitions of the workout
TotalWeight float64 // The weight of the workout

Points []MapPoint `gorm:"serializer:json" json:"-"` // To be removed
}
Expand Down
5 changes: 5 additions & 0 deletions views/equipment/equipment_show.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ <h3 class="grow justify-start {{ IconFor `totals` }}">
<th>{{ i18n "Total duration" }}</th>
<td>{{ .Duration | HumanDuration }}</td>
</tr>
<tr>
<td class="{{ IconFor `repetitions` }}"></td>
<th>{{ i18n "Total repetitions" }}</th>
<td>{{ .Repetitions }}</td>
</tr>
</tbody>
</table>
{{ end }}
Expand Down
Loading

0 comments on commit 9e4c8cd

Please sign in to comment.