Refactor
@@ -3,44 +3,25 @@
go 1.24 require ( - github.com/BurntSushi/toml v1.4.0 github.com/glebarez/sqlite v1.11.0 github.com/joho/godotenv v1.5.1 - github.com/nicksnyder/go-i18n/v2 v2.5.1 - golang.org/x/text v0.23.0 - gorm.io/gorm v1.25.12 + gorm.io/gorm v1.26.1 ) require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/a-h/templ v0.3.857 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/tools v0.24.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect -) - -tool ( - github.com/a-h/templ/cmd/templ - github.com/nicksnyder/go-i18n/v2/goi18n + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + modernc.org/libc v1.65.8 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.37.1 // indirect )
@@ -1,44 +1,11 @@
-github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg= -github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M= -github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A= -github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=@@ -47,75 +14,48 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= -github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= -github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= -github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= -gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= -modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q= +modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= -modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= +modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
@@ -2,298 +2,277 @@ package api
import ( "encoding/json" - "errors" "net/http" + "strconv" "github.com/birabittoh/go-lift/src/database" "gorm.io/gorm" ) -type WorkoutStats struct { - TotalWorkouts int64 `json:"totalWorkouts"` - TotalMinutes int `json:"totalMinutes"` - TotalExercises int64 `json:"totalExercises"` - MostFrequentExercise *struct { - Name string `json:"name"` - Count int `json:"count"` - } `json:"mostFrequentExercise,omitempty"` - MostFrequentRoutine *struct { - Name string `json:"name"` - Count int `json:"count"` - } `json:"mostFrequentRoutine,omitempty"` - RecentWorkouts []database.RecordRoutine `json:"recentWorkouts"` -} - // User handlers func getUserHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid user ID") + return + } + var user database.User if err := db.First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if err == gorm.ErrRecordNotFound { jsonError(w, http.StatusNotFound, "User not found") return } - jsonError(w, http.StatusInternalServerError, "Failed to fetch user: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Database error") return } + jsonResponse(w, http.StatusOK, user) } } func updateUserHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid user ID") + return + } + var user database.User if err := db.First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { + if err == gorm.ErrRecordNotFound { jsonError(w, http.StatusNotFound, "User not found") return } - jsonError(w, http.StatusInternalServerError, "Failed to fetch user: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Database error") return } - if err := json.NewDecoder(r.Body).Decode(&user); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + var updateData database.User + if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid JSON") return } + + // Update specific fields + user.Name = updateData.Name + user.IsFemale = updateData.IsFemale + user.Height = updateData.Height + user.Weight = updateData.Weight + user.BirthDate = updateData.BirthDate + if err := db.Save(&user).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to update user: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Failed to update user") return } + jsonResponse(w, http.StatusOK, user) } } -// Routines handlers -func getRoutinesHandler(db *gorm.DB) http.HandlerFunc { +// Exercise handlers (read-only) +func getExercisesHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var routines []database.Routine - result := db. - Preload("RoutineItems"). - Preload("RoutineItems.Exercises"). - Preload("RoutineItems.Supersets"). - Preload("RoutineItems.Supersets.PrimaryExercise"). - Preload("RoutineItems.Supersets.SecondaryExercise"). - Find(&routines) - if result.Error != nil { - jsonError(w, http.StatusInternalServerError, "Failed to fetch routines: "+result.Error.Error()) - return - } - jsonResponse(w, http.StatusOK, routines) - } -} + var exercises []database.Exercise + query := db.Preload("PrimaryMuscles").Preload("SecondaryMuscles") -func getRoutineHandler(db *gorm.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - var routine database.Routine - result := db. - Preload("RoutineItems.Exercises"). - Preload("RoutineItems.Supersets"). - Preload("RoutineItems.Supersets.PrimaryExercise"). - Preload("RoutineItems.Supersets.SecondaryExercise"). - First(&routine, id) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Routine not found") - return - } - jsonError(w, http.StatusInternalServerError, "Failed to fetch routine: "+result.Error.Error()) - return + // Optional filtering + if category := r.URL.Query().Get("category"); category != "" { + query = query.Where("category = ?", category) } - jsonResponse(w, http.StatusOK, routine) - } -} - -func createRoutineHandler(db *gorm.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var routine database.Routine - if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) - return + if level := r.URL.Query().Get("level"); level != "" { + query = query.Where("level = ?", level) } - if err := db.Create(&routine).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to create routine: "+err.Error()) + if err := query.Find(&exercises).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Database error") return } - // Reload with associations - db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, routine.ID) - - jsonResponse(w, http.StatusCreated, routine) + jsonResponse(w, http.StatusOK, exercises) } } -func updateRoutineHandler(db *gorm.DB) http.HandlerFunc { +func getExerciseHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - var routine database.Routine + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid exercise ID") + return + } - // Check if exists - if err := db.First(&routine, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Routine not found") + var exercise database.Exercise + if err := db.Preload("PrimaryMuscles").Preload("SecondaryMuscles").First(&exercise, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + jsonError(w, http.StatusNotFound, "Exercise not found") return } - jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Database error") return } - // Parse update data - if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) - return - } + jsonResponse(w, http.StatusOK, exercise) + } +} - // Save with associations - if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&routine).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to update routine: "+err.Error()) +// Muscle handlers (read-only) +func getMusclesHandler(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var muscles []database.Muscle + if err := db.Find(&muscles).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Database error") return } - // Reload complete data - db.Preload("Exercises").Preload("Supersets").Preload("Supersets.Sets").First(&routine, id) - jsonResponse(w, http.StatusOK, routine) + jsonResponse(w, http.StatusOK, muscles) } } -func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc { +// Routine handlers +func getRoutinesHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - if err := db.Delete(&database.Routine{}, id).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to delete routine: "+err.Error()) + var routines []database.Routine + if err := db.Find(&routines).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Database error") return } - jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted successfully"}) + + jsonResponse(w, http.StatusOK, routines) } } -// Exercises handlers -func getExercisesHandler(db *gorm.DB) http.HandlerFunc { +func getRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var exercises []database.Exercise - if err := db.Find(&exercises).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to fetch exercises: "+err.Error()) + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid routine ID") return } - jsonResponse(w, http.StatusOK, exercises) - } -} -func getExerciseHandler(db *gorm.DB) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - var exercise database.Exercise - if err := db.First(&exercise, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Exercise not found") + var routine database.Routine + if err := db.Preload("Items.ExerciseItems.Exercise.PrimaryMuscles"). + Preload("Items.ExerciseItems.Exercise.SecondaryMuscles"). + Preload("Items.ExerciseItems.Sets"). + Order("Items.order_index, Items.ExerciseItems.order_index, Items.ExerciseItems.Sets.order_index"). + First(&routine, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + jsonError(w, http.StatusNotFound, "Routine not found") return } - jsonError(w, http.StatusInternalServerError, "Failed to fetch exercise: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Database error") return } - jsonResponse(w, http.StatusOK, exercise) + + jsonResponse(w, http.StatusOK, routine) } } -func createExerciseHandler(db *gorm.DB) http.HandlerFunc { +func createRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var exercise database.Exercise - if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + var routine database.Routine + if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid JSON") return } - if err := db.Create(&exercise).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to create exercise: "+err.Error()) - return - } - - jsonResponse(w, http.StatusCreated, exercise) - } -} - -func upsertExercisesHandler(dbStruct *database.Database) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := dbStruct.UpdateExercises(); err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to update exercises: "+err.Error()) + if err := db.Create(&routine).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to create routine") return } - jsonResponse(w, http.StatusOK, map[string]string{"message": "Exercises updated successfully"}) + jsonResponse(w, http.StatusCreated, routine) } } -func updateExerciseHandler(db *gorm.DB) http.HandlerFunc { +func updateRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - - // Verify exercise exists - var exercise database.Exercise - if err := db.First(&exercise, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Exercise not found") - return - } - jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid routine ID") return } - // Parse update data - if err := json.NewDecoder(r.Body).Decode(&exercise); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + var routine database.Routine + if err := json.NewDecoder(r.Body).Decode(&routine); err != nil { + jsonError(w, http.StatusBadRequest, "Invalid JSON") return } - if err := db.Save(&exercise).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to update exercise: "+err.Error()) + routine.ID = uint(id) + if err := db.Save(&routine).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to update routine") return } - jsonResponse(w, http.StatusOK, exercise) + jsonResponse(w, http.StatusOK, routine) } } -func deleteExerciseHandler(db *gorm.DB) http.HandlerFunc { +func deleteRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - if err := db.Delete(&database.Exercise{}, id).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to delete exercise: "+err.Error()) + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid routine ID") return } - jsonResponse(w, http.StatusOK, map[string]string{"message": "Exercise deleted successfully"}) + + if err := db.Delete(&database.Routine{}, id).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to delete routine") + return + } + + jsonResponse(w, http.StatusOK, map[string]string{"message": "Routine deleted"}) } } -// RecordRoutines handlers +// Record routine handlers (workout sessions) func getRecordRoutinesHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var records []database.RecordRoutine - result := db.Preload("RecordExercises").Preload("Routine.Exercises").Preload("Routine.Supersets").Find(&records) - if result.Error != nil { - jsonError(w, http.StatusInternalServerError, "Failed to fetch record routines: "+result.Error.Error()) + query := db.Preload("Routine").Order("created_at DESC") + + // Optional limit for recent workouts + if limit := r.URL.Query().Get("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil { + query = query.Limit(l) + } + } + + if err := query.Find(&records).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Database error") return } + jsonResponse(w, http.StatusOK, records) } } func getRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid record ID") + return + } + var record database.RecordRoutine - result := db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Record routine not found") + if err := db.Preload("Routine"). + Preload("RecordItems.RoutineItem"). + Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.PrimaryMuscles"). + Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.SecondaryMuscles"). + Preload("RecordItems.RecordExerciseItems.RecordSets.Set"). + Order("RecordItems.order_index, RecordItems.RecordExerciseItems.order_index, RecordItems.RecordExerciseItems.RecordSets.order_index"). + First(&record, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + jsonError(w, http.StatusNotFound, "Record not found") return } - jsonError(w, http.StatusInternalServerError, "Failed to fetch record routine: "+result.Error.Error()) + jsonError(w, http.StatusInternalServerError, "Database error") return } + jsonResponse(w, http.StatusOK, record) } }@@ -302,17 +281,14 @@ func createRecordRoutineHandler(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { var record database.RecordRoutine if err := json.NewDecoder(r.Body).Decode(&record); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + jsonError(w, http.StatusBadRequest, "Invalid JSON") return } if err := db.Create(&record).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to create record routine: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Failed to create record") return } - - // Reload with associations - db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, record.ID) jsonResponse(w, http.StatusCreated, record) }@@ -320,121 +296,115 @@ }
func updateRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - var record database.RecordRoutine - - // Check if exists - if err := db.First(&record, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - jsonError(w, http.StatusNotFound, "Record routine not found") - return - } - jsonError(w, http.StatusInternalServerError, "Database error: "+err.Error()) + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid record ID") return } - // Parse update data + var record database.RecordRoutine if err := json.NewDecoder(r.Body).Decode(&record); err != nil { - jsonError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) + jsonError(w, http.StatusBadRequest, "Invalid JSON") return } - // Save with associations - if err := db.Session(&gorm.Session{FullSaveAssociations: true}).Save(&record).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to update record routine: "+err.Error()) + record.ID = uint(id) + if err := db.Save(&record).Error; err != nil { + jsonError(w, http.StatusInternalServerError, "Failed to update record") return } - // Reload complete data - db.Preload("Routine").Preload("Routine.Exercises").Preload("Routine.Supersets").First(&record, id) jsonResponse(w, http.StatusOK, record) } } func deleteRecordRoutineHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") + id, err := getIDFromPath(r) + if err != nil { + jsonError(w, http.StatusBadRequest, "Invalid record ID") + return + } + if err := db.Delete(&database.RecordRoutine{}, id).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to delete record routine: "+err.Error()) + jsonError(w, http.StatusInternalServerError, "Failed to delete record") return } - jsonResponse(w, http.StatusOK, map[string]string{"message": "Record routine deleted successfully"}) + + jsonResponse(w, http.StatusOK, map[string]string{"message": "Record deleted"}) } } +// Stats handler func getStatsHandler(db *gorm.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { stats := WorkoutStats{} - // Get total workouts - if err := db.Model(&database.RecordRoutine{}).Count(&stats.TotalWorkouts).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to count workouts: "+err.Error()) - return - } + // Total workouts + db.Model(&database.RecordRoutine{}).Count(&stats.TotalWorkouts) - // Get total minutes - stats.TotalMinutes = 0 + // Total minutes (sum of all workout durations) + var totalSeconds uint + db.Model(&database.RecordRoutine{}).Select("COALESCE(SUM(duration), 0)").Scan(&totalSeconds) + stats.TotalMinutes = int(totalSeconds / 60) - // Get total exercises - if err := db.Model(&database.RecordExercise{}).Count(&stats.TotalExercises).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to count exercises: "+err.Error()) - return - } + // Total exercises completed + db.Model(&database.RecordExerciseItem{}).Count(&stats.TotalExercises) - // Get most frequent exercise - var mostFrequentExercise struct { - Name string `gorm:"column:name"` - Count int `gorm:"column:count"` + // Most frequent exercise + var exerciseStats struct { + ExerciseName string `json:"exercise_name"` + Count int `json:"count"` } - exerciseQuery := db.Model(&database.RecordExercise{}). - Select("exercises.name, COUNT(*) as count"). - Joins("JOIN exercises ON record_exercises.exercise_id = exercises.id"). - Group("exercises.name"). - Order("count DESC"). - Limit(1) + db.Raw(` + SELECT e.name as exercise_name, COUNT(*) as count + FROM record_exercise_items rei + JOIN exercise_items ei ON rei.exercise_item_id = ei.id + JOIN exercises e ON ei.exercise_id = e.id + GROUP BY e.id, e.name + ORDER BY count DESC + LIMIT 1 + `).Scan(&exerciseStats) - if err := exerciseQuery.Scan(&mostFrequentExercise).Error; err == nil && mostFrequentExercise.Name != "" { + if exerciseStats.Count > 0 { stats.MostFrequentExercise = &struct { Name string `json:"name"` Count int `json:"count"` }{ - Name: mostFrequentExercise.Name, - Count: mostFrequentExercise.Count, + Name: exerciseStats.ExerciseName, + Count: exerciseStats.Count, } } - // Get most frequent routine - var mostFrequentRoutine struct { - Name string `gorm:"column:name"` - Count int `gorm:"column:count"` + // Most frequent routine + var routineStats struct { + RoutineName string `json:"routine_name"` + Count int `json:"count"` } - routineQuery := db.Model(&database.RecordRoutine{}). - Select("routines.name, COUNT(*) as count"). - Joins("JOIN routines ON record_routines.routine_id = routines.id"). - Group("routines.name"). - Order("count DESC"). - Limit(1) + db.Raw(` + SELECT r.name as routine_name, COUNT(*) as count + FROM record_routines rr + JOIN routines r ON rr.routine_id = r.id + GROUP BY r.id, r.name + ORDER BY count DESC + LIMIT 1 + `).Scan(&routineStats) - if err := routineQuery.Scan(&mostFrequentRoutine).Error; err == nil && mostFrequentRoutine.Name != "" { + if routineStats.Count > 0 { stats.MostFrequentRoutine = &struct { Name string `json:"name"` Count int `json:"count"` }{ - Name: mostFrequentRoutine.Name, - Count: mostFrequentRoutine.Count, + Name: routineStats.RoutineName, + Count: routineStats.Count, } } - // Get recent workouts (last 5) - if err := db. - Preload("RecordRoutineItems"). - Preload("Routine"). + // Recent workouts (last 5) + db.Preload("Routine"). Order("created_at DESC"). Limit(5). - Find(&stats.RecentWorkouts).Error; err != nil { - jsonError(w, http.StatusInternalServerError, "Failed to fetch recent workouts: "+err.Error()) - return - } + Find(&stats.RecentWorkouts) jsonResponse(w, http.StatusOK, stats) }
@@ -8,7 +8,7 @@
"github.com/birabittoh/go-lift/src/database" ) -const uiDir = "ui/dist" +const uiDir = "ui" var fileServer = http.FileServer(http.Dir(uiDir))@@ -25,6 +25,13 @@ // Profile routes
mux.HandleFunc("GET /api/users/{id}", getUserHandler(db)) mux.HandleFunc("PUT /api/users/{id}", updateUserHandler(db)) + // Exercises routes (read-only) + mux.HandleFunc("GET /api/exercises", getExercisesHandler(db)) + mux.HandleFunc("GET /api/exercises/{id}", getExerciseHandler(db)) + + // Muscles routes (read-only) + mux.HandleFunc("GET /api/muscles", getMusclesHandler(db)) + // Routines routes mux.HandleFunc("GET /api/routines", getRoutinesHandler(db)) mux.HandleFunc("GET /api/routines/{id}", getRoutineHandler(db))@@ -32,39 +39,43 @@ mux.HandleFunc("POST /api/routines", createRoutineHandler(db))
mux.HandleFunc("PUT /api/routines/{id}", updateRoutineHandler(db)) mux.HandleFunc("DELETE /api/routines/{id}", deleteRoutineHandler(db)) - // Exercises routes - mux.HandleFunc("GET /api/exercises", getExercisesHandler(db)) - mux.HandleFunc("GET /api/exercises/{id}", getExerciseHandler(db)) - mux.HandleFunc("POST /api/exercises", createExerciseHandler(db)) - mux.HandleFunc("POST /api/exercises/update", upsertExercisesHandler(dbStruct)) - mux.HandleFunc("PUT /api/exercises/{id}", updateExerciseHandler(db)) - mux.HandleFunc("DELETE /api/exercises/{id}", deleteExerciseHandler(db)) - - // RecordRoutines routes - mux.HandleFunc("GET /api/recordroutines", getRecordRoutinesHandler(db)) - mux.HandleFunc("GET /api/recordroutines/{id}", getRecordRoutineHandler(db)) - mux.HandleFunc("POST /api/recordroutines", createRecordRoutineHandler(db)) - mux.HandleFunc("PUT /api/recordroutines/{id}", updateRecordRoutineHandler(db)) - mux.HandleFunc("DELETE /api/recordroutines/{id}", deleteRecordRoutineHandler(db)) + // Record routines routes (workout sessions) + mux.HandleFunc("GET /api/records", getRecordRoutinesHandler(db)) + mux.HandleFunc("GET /api/records/{id}", getRecordRoutineHandler(db)) + mux.HandleFunc("POST /api/records", createRecordRoutineHandler(db)) + mux.HandleFunc("PUT /api/records/{id}", updateRecordRoutineHandler(db)) + mux.HandleFunc("DELETE /api/records/{id}", deleteRecordRoutineHandler(db)) // Stats routes mux.HandleFunc("GET /api/stats", getStatsHandler(db)) // Static UI route mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Check if the file exists at the requested path requestedFile := filepath.Join(uiDir, r.URL.Path) _, err := os.Stat(requestedFile) - // If file exists or it's the root, serve it directly if err == nil || r.URL.Path == "/" { fileServer.ServeHTTP(w, r) return } - // For file not found, serve index.html for SPA routing http.ServeFile(w, r, filepath.Join(uiDir, "index.html")) }) return mux } + +type WorkoutStats struct { + TotalWorkouts int64 `json:"totalWorkouts"` + TotalMinutes int `json:"totalMinutes"` + TotalExercises int64 `json:"totalExercises"` + MostFrequentExercise *struct { + Name string `json:"name"` + Count int `json:"count"` + } `json:"mostFrequentExercise,omitempty"` + MostFrequentRoutine *struct { + Name string `json:"name"` + Count int `json:"count"` + } `json:"mostFrequentRoutine,omitempty"` + RecentWorkouts []database.RecordRoutine `json:"recentWorkouts"` +}
@@ -3,6 +3,7 @@
import ( "encoding/json" "net/http" + "strconv" "strings" )@@ -15,3 +16,8 @@
func jsonError(w http.ResponseWriter, status int, messages ...string) { jsonResponse(w, status, map[string]string{"error": strings.Join(messages, " ")}) } + +func getIDFromPath(r *http.Request) (uint, error) { + id, err := strconv.Atoi(r.PathValue("id")) + return uint(id), err +}
@@ -2,18 +2,9 @@ package database
import ( "log" - "time" ) -var defaultUserList = []User{ - { - Name: "Admin", - IsFemale: false, - Height: 180, - Weight: 75, - BirthDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), - }, -} +var defaultUserList = []User{{Name: "Admin"}} // CheckInitialData ensures that all necessary initial data is in the database func (db *Database) CheckInitialData() (err error) {
@@ -18,216 +18,152 @@
type User struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:50" json:"name"` - IsFemale bool `gorm:"default:false" json:"isFemale"` - Height float64 `json:"height"` // In cm - Weight float64 `json:"weight"` // In kg - BirthDate time.Time `json:"birthDate"` + IsFemale bool `json:"isFemale"` + Height *float64 `json:"height"` // In cm + Weight *float64 `json:"weight"` // In kg + BirthDate *time.Time `json:"birthDate"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` } type Exercise struct { - ID uint `gorm:"primaryKey;autoIncrement"` - Name string `gorm:"not null;uniqueIndex"` - Level string `gorm:"size:50;not null"` - Category string `gorm:"size:50;not null"` - Force *string `gorm:"size:50"` - Mechanic *string `gorm:"size:50"` - Equipment *string `gorm:"size:50"` - Instructions *string + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Name string `gorm:"not null;uniqueIndex" json:"name"` + Level string `gorm:"size:50;not null" json:"level"` + Category string `gorm:"size:50;not null" json:"category"` + Force *string `gorm:"size:50" json:"force"` + Mechanic *string `gorm:"size:50" json:"mechanic"` + Equipment *string `gorm:"size:50" json:"equipment"` + Instructions *string `json:"instructions"` - PrimaryMuscles []Muscle `gorm:"many2many:exercise_primary_muscles;constraint:OnDelete:CASCADE"` - SecondaryMuscles []Muscle `gorm:"many2many:exercise_secondary_muscles;constraint:OnDelete:CASCADE"` + PrimaryMuscles []Muscle `gorm:"many2many:exercise_primary_muscles;constraint:OnDelete:CASCADE" json:"primaryMuscles"` + SecondaryMuscles []Muscle `gorm:"many2many:exercise_secondary_muscles;constraint:OnDelete:CASCADE" json:"secondaryMuscles"` - CreatedAt time.Time - UpdatedAt time.Time + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type Muscle struct { - ID uint `gorm:"primaryKey"` - Name string `gorm:"uniqueIndex;size:50;not null"` -} + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"uniqueIndex;size:50;not null" json:"name"` -type Set struct { - ID uint `gorm:"primaryKey" json:"id"` - ExerciseID uint `gorm:"index" json:"exerciseId"` - Reps int `json:"reps"` - Weight float64 `json:"weight"` - Duration int `json:"duration"` // In seconds, for timed exercises - OrderIndex int `gorm:"not null" json:"orderIndex"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` - - Exercise Exercise `json:"-"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } -// SuperSet to handle two exercises with single rest time -type SuperSet struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:100" json:"name"` - PrimaryExerciseID uint `gorm:"index" json:"primaryExerciseId"` - SecondaryExerciseID uint `gorm:"index" json:"secondaryExerciseId"` - RestTime int `gorm:"default:0" json:"restTime"` // In seconds - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` +// Routine represents a workout routine blueprint +type Routine struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Description string `gorm:"size:500" json:"description"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - PrimaryExercise Exercise `json:"primaryExercise"` - SecondaryExercise Exercise `json:"secondaryExercise"` + Items []RoutineItem `json:"items"` } -// RoutineItem represents either an Exercise or a SuperSet in a Routine +// RoutineItem can be either a single exercise or a superset type RoutineItem struct { - ID uint `gorm:"primaryKey" json:"id"` - RoutineID uint `gorm:"index" json:"routineId"` - ExerciseID *uint `gorm:"index" json:"exerciseId"` - SuperSetID *uint `gorm:"index" json:"superSetId"` - RestTime int `gorm:"default:0" json:"restTime"` // In seconds - OrderIndex int `gorm:"not null" json:"orderIndex"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` + ID uint `gorm:"primaryKey" json:"id"` + RoutineID uint `gorm:"index;not null" json:"routineId"` + Type string `gorm:"size:20;not null" json:"type"` // "exercise" or "superset" + RestTime int `gorm:"default:0" json:"restTime"` // In seconds + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - Routine Routine `json:"-"` - SuperSet *SuperSet `json:"superSet,omitempty"` - Exercise *Exercise `json:"exercise,omitempty"` + Routine Routine `json:"-"` + ExerciseItems []ExerciseItem `json:"exerciseItems,omitempty"` // For both single exercises and superset items } -type Routine struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:100;not null" json:"name"` - Description string `gorm:"size:500" json:"description"` - //UserID uint `gorm:"index" json:"userId"` - //IsPublic bool `gorm:"default:false" json:"isPublic"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` +// ExerciseItem represents an exercise within a routine item (could be standalone or part of superset) +type ExerciseItem struct { + ID uint `gorm:"primaryKey" json:"id"` + RoutineItemID uint `gorm:"index;not null" json:"routineItemId"` + ExerciseID uint `gorm:"index;not null" json:"exerciseId"` + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - //User User `json:"-"` - RoutineItems []RoutineItem `json:"routineItems,omitempty"` + RoutineItem RoutineItem `json:"-"` + Exercise Exercise `json:"exercise"` + Sets []Set `json:"sets"` } -/* -type User struct { - ID uint `gorm:"primaryKey" json:"id"` - Username string `gorm:"size:50;not null;uniqueIndex" json:"username"` - Email string `gorm:"size:100;not null;uniqueIndex" json:"email"` - Password string `gorm:"size:100;not null" json:"-"` - Name string `gorm:"size:100" json:"name"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` +// Set represents a planned set within an exercise +type Set struct { + ID uint `gorm:"primaryKey" json:"id"` + ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"` + Reps int `json:"reps"` + Weight float64 `json:"weight"` + Duration int `json:"duration"` // In seconds + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - Exercises []Exercise `json:"exercises,omitempty"` - Routines []Routine `json:"routines,omitempty"` - RecordRoutines []RecordRoutine `json:"recordRoutines,omitempty"` + ExerciseItem ExerciseItem `json:"-"` } -*/ + +// ===== RECORD MODELS (for actual workout completion) ===== +// RecordRoutine records a completed workout session type RecordRoutine struct { - ID uint `gorm:"primaryKey" json:"id"` - //UserID uint `gorm:"index" json:"userId"` - RoutineID uint `gorm:"index" json:"routineId"` - StartedAt time.Time `gorm:"not null" json:"startedAt"` - EndedAt *time.Time `json:"endedAt"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` + ID uint `gorm:"primaryKey" json:"id"` + RoutineID uint `gorm:"index;not null" json:"routineId"` + Duration *uint `json:"duration"` // In seconds + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - //User User `json:"-"` - Routine Routine `json:"routine"` - RecordRoutineItems []RecordRoutineItem `json:"recordRoutineItems,omitempty"` + Routine Routine `json:"routine"` + RecordItems []RecordItem `json:"recordItems"` } -// RecordRoutineItem represents either a RecordExercise or a RecordSuperSet in a completed routine -type RecordRoutineItem struct { - ID uint `gorm:"primaryKey" json:"id"` - RecordRoutineID uint `gorm:"index" json:"recordRoutineId"` - RecordExerciseID *uint `gorm:"index" json:"recordExerciseId"` - RecordSuperSetID *uint `gorm:"index" json:"recordSuperSetId"` - ActualRestTime int `json:"actualRestTime"` // In seconds - OrderIndex int `gorm:"not null" json:"orderIndex"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` +// RecordItem records completion of a routine item (exercise or superset) +type RecordItem struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordRoutineID uint `gorm:"index;not null" json:"recordRoutineId"` + RoutineItemID uint `gorm:"index;not null" json:"routineItemId"` + Duration *uint `json:"duration"` // In seconds + ActualRestTime *int `json:"actualRestTime"` // In seconds + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - RecordRoutine RecordRoutine `json:"-"` - RecordSuperSet *RecordSuperSet `json:"recordSuperSet,omitempty"` - RecordExercise *RecordExercise `json:"recordExercise,omitempty"` + RecordRoutine RecordRoutine `json:"-"` + RoutineItem RoutineItem `json:"routineItem"` + RecordExerciseItems []RecordExerciseItem `json:"recordExerciseItems"` } -// RecordSuperSet records a completed superset -type RecordSuperSet struct { - ID uint `gorm:"primaryKey" json:"id"` - RecordRoutineID uint `gorm:"index" json:"recordRoutineId"` - SuperSetID uint `gorm:"index" json:"superSetId"` - StartedAt time.Time `gorm:"not null" json:"startedAt"` - EndedAt time.Time `gorm:"not null" json:"endedAt"` - ActualRestTime int `json:"actualRestTime"` // In seconds - OrderIndex int `gorm:"not null" json:"orderIndex"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` +// RecordExerciseItem records completion of an exercise within a routine item +type RecordExerciseItem struct { + ID uint `gorm:"primaryKey" json:"id"` + RecordItemID uint `gorm:"index;not null" json:"recordItemId"` + ExerciseItemID uint `gorm:"index;not null" json:"exerciseItemId"` + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` - RecordRoutine RecordRoutine `json:"-"` - SuperSet SuperSet `json:"superSet"` + RecordItem RecordItem `json:"-"` + ExerciseItem ExerciseItem `json:"exerciseItem"` + RecordSets []RecordSet `json:"recordSets"` } -type RecordExercise struct { - ID uint `gorm:"primaryKey" json:"id"` - RecordRoutineID uint `gorm:"index" json:"recordRoutineId"` - RecordRoutine RecordRoutine `json:"-"` - ExerciseID uint `gorm:"index" json:"exerciseId"` - Exercise Exercise `json:"exercise"` - StartedAt time.Time `gorm:"not null" json:"startedAt"` - EndedAt time.Time `gorm:"not null" json:"endedAt"` - ActualRestTime int `json:"actualRestTime"` // In seconds - RecordSets []RecordSet `json:"recordSets,omitempty"` - OrderIndex int `gorm:"not null" json:"orderIndex"` - RecordSuperSetID *uint `gorm:"index" json:"recordSuperSetId"` - RecordSuperSet *RecordSuperSet `json:"-"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` -} - +// RecordSet records completion of an actual set type RecordSet struct { - ID uint `gorm:"primaryKey" json:"id"` - RecordExerciseID uint `gorm:"index" json:"recordExerciseId"` - SetID uint `gorm:"index" json:"setId"` - ActualReps int `json:"actualReps"` - ActualWeight float64 `json:"actualWeight"` - ActualDuration int `json:"actualDuration"` // In seconds - CompletedAt time.Time `gorm:"not null" json:"completedAt"` - OrderIndex int `gorm:"not null" json:"orderIndex"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` - - RecordExercise RecordExercise `json:"-"` - Set Set `json:"set"` -} - -type Localization struct { - ID uint `gorm:"primaryKey" json:"id"` - LanguageID uint `gorm:"not null;uniqueIndex:idxLangKeyword" json:"languageId"` - Keyword string `gorm:"size:255;not null;uniqueIndex:idxLangKeyword" json:"keyword"` - Text string `gorm:"size:1000;not null" json:"text"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` -} + ID uint `gorm:"primaryKey" json:"id"` + RecordExerciseItemID uint `gorm:"index;not null" json:"recordExerciseItemId"` + SetID uint `gorm:"index;not null" json:"setId"` + ActualReps int `json:"actualReps"` + ActualWeight float64 `json:"actualWeight"` + ActualDuration int `json:"actualDuration"` // In seconds + CompletedAt time.Time `gorm:"not null" json:"completedAt"` + OrderIndex int `gorm:"not null" json:"orderIndex"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` -type Language struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` - Code string `gorm:"size:8;not null;uniqueIndex" json:"code"` - Flag string `gorm:"size:50" json:"flag"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` + RecordExerciseItem RecordExerciseItem `json:"-"` + Set Set `json:"set"` } // InitializeDB creates and initializes the SQLite database with all models@@ -252,7 +188,7 @@ Colorful: true,
}, ) - dialector := sqlite.Open(dbPath + "?Pragma=foreignKeys(1)") + dialector := sqlite.Open(dbPath + "?_pragma=foreign_keys(1)") config := &gorm.Config{Logger: newLogger} // Open connection to the database@@ -272,21 +208,19 @@ sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) - // Auto migrate the models + // Auto migrate the models in correct order err = conn.AutoMigrate( - Muscle{}, - Exercise{}, - Set{}, - SuperSet{}, - RoutineItem{}, - Routine{}, - User{}, - RecordRoutine{}, - RecordExercise{}, - RecordSuperSet{}, - RecordSet{}, - Localization{}, - Language{}, + &User{}, + &Muscle{}, + &Exercise{}, + &Routine{}, + &RoutineItem{}, + &ExerciseItem{}, + &Set{}, + &RecordRoutine{}, + &RecordItem{}, + &RecordExerciseItem{}, + &RecordSet{}, ) if err != nil { return@@ -300,6 +234,38 @@ if err != nil {
return nil, err } - log.Println("Database initialized successfully") return db, nil } + +// Helper methods for creating and querying routines + +// CreateRoutineWithData creates a routine with all nested data +func (db *Database) CreateRoutineWithData(routine *Routine) error { + return db.Create(routine).Error +} + +// GetRoutineWithItems retrieves a routine with all its nested data +func (db *Database) GetRoutineWithItems(routineID uint) (*Routine, error) { + var routine Routine + err := db.Preload("Items.ExerciseItems.Exercise.PrimaryMuscles"). + Preload("Items.ExerciseItems.Exercise.SecondaryMuscles"). + Preload("Items.ExerciseItems.Sets"). + Order("Items.order_index, Items.ExerciseItems.order_index, Items.ExerciseItems.Sets.order_index"). + First(&routine, routineID).Error + + return &routine, err +} + +// GetRecordRoutineWithData retrieves a completed workout with all nested data +func (db *Database) GetRecordRoutineWithData(recordID uint) (*RecordRoutine, error) { + var record RecordRoutine + err := db.Preload("Routine"). + Preload("RecordItems.RoutineItem"). + Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.PrimaryMuscles"). + Preload("RecordItems.RecordExerciseItems.ExerciseItem.Exercise.SecondaryMuscles"). + Preload("RecordItems.RecordExerciseItems.RecordSets.Set"). + Order("RecordItems.order_index, RecordItems.RecordExerciseItems.order_index, RecordItems.RecordExerciseItems.RecordSets.order_index"). + First(&record, recordID).Error + + return &record, err +}
@@ -2640,6 +2640,19 @@ "engines": {
"node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",@@ -2784,13 +2797,13 @@ "dev": true,
"license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert"@@ -3127,19 +3140,6 @@ "funding": {
"url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",@@ -3330,19 +3330,6 @@ },
"yaml": { "optional": true } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/which": {
@@ -1,1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
@@ -1,6 +1,6 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; import type { User } from '../types/models'; -import { ProfileService } from '../services/api'; +import { userService } from '../services/api'; interface AppContextType { user: User | null;@@ -39,7 +39,7 @@ useEffect(() => {
const loadProfile = async () => { try { setIsLoading(true); - const profile = await ProfileService.get(); + const profile = await userService.get(1); setUser(profile); setError(null); } catch (err) {@@ -58,7 +58,7 @@
const updateUser = async (profile: User) => { try { setIsLoading(true); - const updatedProfile = await ProfileService.update(profile); + const updatedProfile = await userService.update(1, profile); setUser(updatedProfile); setError(null); } catch (err) {
@@ -13,13 +13,12 @@ } from 'react-icons/fa';
import type { Routine, Exercise, - RecordRoutine, - RecordExercise, + RecordRoutine, RecordSet, Set, - RecordRoutineItem + RecordItem } from '../types/models'; -import { RoutineService, WorkoutService } from '../services/api'; +import { routineService } from '../services/api'; interface SetForWorkout { id?: number;@@ -76,7 +75,7 @@ useEffect(() => {
const fetchRoutines = async () => { try { setIsLoading(true); - const data = await RoutineService.getAll(); + const data = await routineService.getAll(); setRoutines(data); // Check if a routine was pre-selected (from workouts page)@@ -109,8 +108,8 @@ // Initialize workout exercises from routine items
const exercises: ExerciseForWorkout[] = []; // Process routine items into exercises for the workout - routine.routineItems.forEach(item => { - if (item.exercise && item.exerciseId) { + routine.items.forEach(item => { + if (item.exerciseItems && item.exerciseId) { // This is a regular exercise item const exercise = item.exercise;@@ -315,7 +314,7 @@ exercise: ex.exercise
})); // Create RecordRoutineItems from recordExercises - const recordRoutineItems: RecordRoutineItem[] = recordExercises.map((ex, index) => ({ + const recordItems: RecordItem[] = recordExercises.map((ex, index) => ({ recordRoutineId: 0, // Will be filled in by backend recordExerciseId: undefined, // Will be filled in after recordExercise is created recordSuperSetId: null,@@ -327,10 +326,9 @@ }));
const workoutRecord: RecordRoutine = { routineId: selectedRoutine.id!, - startedAt: startTime, - endedAt: endTime || now, + duration: elapsedSeconds, routine: selectedRoutine, - recordRoutineItems: recordRoutineItems + recordItems: recordItems }; await WorkoutService.create(workoutRecord);
@@ -145,7 +145,7 @@ name="weight"
min="20" max="300" step="1" - value={formData.weight} + value={formData.weight ?? 0} onChange={handleInputChange} disabled={!isEditing} required@@ -158,10 +158,10 @@ <input
type="number" id="height" name="height" - min="30" + min="0" max="300" step="1" - value={formData.height} + value={formData.height ?? 0} onChange={handleInputChange} disabled={!isEditing} required@@ -174,7 +174,7 @@ <input
type="date" id="birthDate" name="birthDate" - value={parseBirthDate(formData.birthDate)} + value={parseBirthDate(formData.birthDate ?? '')} onChange={handleInputChange} disabled={!isEditing} required
@@ -1,229 +1,142 @@
-import type { - Exercise, - Routine, - RecordRoutine, - User, - Equipment, - MuscleGroup, - Set, - SuperSet, - RecordExercise, - WorkoutStats -} from '../types/models'; +import type { User, Exercise, Muscle, Routine, RecordRoutine, WorkoutStats } from '../types/models'; + +const API_BASE = '/api'; + +class BaseService<T> { + protected endpoint: string; + constructor(endpoint: string) { + this.endpoint = endpoint; + } + + protected async request<R>(path: string, options?: RequestInit): Promise<R> { + const response = await fetch(`${API_BASE}${path}`, { + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } + + return response.json(); + } + + async getAll(): Promise<T[]> { + return this.request<T[]>(this.endpoint); + } -// Base API URL - should be configurable via environment variables in a real app -const API_BASE_URL = '/api'; + async get(id: number): Promise<T> { + return this.request<T>(`${this.endpoint}/${id}`); + } -// Generic fetch with error handling -async function fetchApi<T>( - endpoint: string, - options: RequestInit = {} -): Promise<T> { - const url = `${API_BASE_URL}${endpoint}`; - - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, - ...options, - }); + async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> { + return this.request<T>(this.endpoint, { + method: 'POST', + body: JSON.stringify(data), + }); + } - if (!response.ok) { - const error = await response.text(); - throw new Error(error || `API request failed with status ${response.status}`); + async update(id: number, data: Partial<T>): Promise<T> { + return this.request<T>(`${this.endpoint}/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); } - return response.json(); + async delete(id: number): Promise<void> { + await this.request<void>(`${this.endpoint}/${id}`, { + method: 'DELETE', + }); + } } -// Equipment API services -export const EquipmentService = { - getAll: () => fetchApi<Equipment[]>('/equipment'), - - getById: (id: number) => fetchApi<Equipment>(`/equipment/${id}`), - - create: (equipment: Equipment) => fetchApi<Equipment>('/equipment', { - method: 'POST', - body: JSON.stringify(equipment), - }), - - update: (id: number, equipment: Equipment) => fetchApi<Equipment>(`/equipment/${id}`, { - method: 'PUT', - body: JSON.stringify(equipment), - }), - - delete: (id: number) => fetchApi<void>(`/equipment/${id}`, { - method: 'DELETE', - }), -}; +class UserService extends BaseService<User> { + constructor() { + super('/users'); + } + + // Override get to default to user ID 1 + async get(id: number = 1): Promise<User> { + return super.get(id); + } +} + +class ExerciseService extends BaseService<Exercise> { + constructor() { + super('/exercises'); + } +} + +class MuscleService extends BaseService<Muscle> { + constructor() { + super('/muscles'); + } +} + +class RoutineService extends BaseService<Routine> { + constructor() { + super('/routines'); + } +} + +class RecordService extends BaseService<RecordRoutine> { + constructor() { + super('/records'); + } +} -// MuscleGroup API services -export const MuscleGroupService = { - getAll: () => fetchApi<MuscleGroup[]>('/musclegroups'), - - getById: (id: number) => fetchApi<MuscleGroup>(`/musclegroups/${id}`), - - create: (muscleGroup: MuscleGroup) => fetchApi<MuscleGroup>('/musclegroups', { - method: 'POST', - body: JSON.stringify(muscleGroup), - }), - - update: (id: number, muscleGroup: MuscleGroup) => fetchApi<MuscleGroup>(`/musclegroups/${id}`, { - method: 'PUT', - body: JSON.stringify(muscleGroup), - }), - - delete: (id: number) => fetchApi<void>(`/musclegroups/${id}`, { - method: 'DELETE', - }), -}; +class StatsService { + protected async request<R>(path: string, options?: RequestInit): Promise<R> { + const response = await fetch(`${API_BASE}${path}`, { + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); -// Exercise API services -export const ExerciseService = { - getAll: () => fetchApi<Exercise[]>('/exercises'), - - getById: (id: number) => fetchApi<Exercise>(`/exercises/${id}`), - - create: (exercise: Exercise) => fetchApi<Exercise>('/exercises', { - method: 'POST', - body: JSON.stringify(exercise), - }), - - update: (id: number, exercise: Exercise) => fetchApi<Exercise>(`/exercises/${id}`, { - method: 'PUT', - body: JSON.stringify(exercise), - }), - - delete: (id: number) => fetchApi<void>(`/exercises/${id}`, { - method: 'DELETE', - }), -}; + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } -// Set API services -export const SetService = { - getAll: (exerciseId: number) => fetchApi<Set[]>(`/exercises/${exerciseId}/sets`), - - getById: (id: number) => fetchApi<Set>(`/sets/${id}`), - - create: (set: Set) => fetchApi<Set>('/sets', { - method: 'POST', - body: JSON.stringify(set), - }), - - update: (id: number, set: Set) => fetchApi<Set>(`/sets/${id}`, { - method: 'PUT', - body: JSON.stringify(set), - }), - - delete: (id: number) => fetchApi<void>(`/sets/${id}`, { - method: 'DELETE', - }), -}; + return response.json(); + } -// SuperSet API services -export const SuperSetService = { - getAll: () => fetchApi<SuperSet[]>('/supersets'), - - getById: (id: number) => fetchApi<SuperSet>(`/supersets/${id}`), - - create: (superSet: SuperSet) => fetchApi<SuperSet>('/supersets', { - method: 'POST', - body: JSON.stringify(superSet), - }), - - update: (id: number, superSet: SuperSet) => fetchApi<SuperSet>(`/supersets/${id}`, { - method: 'PUT', - body: JSON.stringify(superSet), - }), - - delete: (id: number) => fetchApi<void>(`/supersets/${id}`, { - method: 'DELETE', - }), -}; + async get(): Promise<WorkoutStats> { + return this.request<WorkoutStats>('/stats'); + } +} -// Routine API services -export const RoutineService = { - getAll: () => fetchApi<Routine[]>('/routines'), - - getById: (id: number) => fetchApi<Routine>(`/routines/${id}`), - - create: (routine: Routine) => fetchApi<Routine>('/routines', { - method: 'POST', - body: JSON.stringify(routine), - }), - - update: (id: number, routine: Routine) => fetchApi<Routine>(`/routines/${id}`, { - method: 'PUT', - body: JSON.stringify(routine), - }), - - delete: (id: number) => fetchApi<void>(`/routines/${id}`, { - method: 'DELETE', - }), -}; +class HealthService { + protected async request<R>(path: string, options?: RequestInit): Promise<R> { + const response = await fetch(`${API_BASE}${path}`, { + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + ...options, + }); -// RecordRoutine (Workout) API services -export const WorkoutService = { - getAll: () => fetchApi<RecordRoutine[]>('/recordroutines'), - - getById: (id: number) => fetchApi<RecordRoutine>(`/recordroutines/${id}`), - - create: (workout: RecordRoutine) => fetchApi<RecordRoutine>('/recordroutines', { - method: 'POST', - body: JSON.stringify(workout), - }), - - update: (id: number, workout: RecordRoutine) => fetchApi<RecordRoutine>(`/recordroutines/${id}`, { - method: 'PUT', - body: JSON.stringify(workout), - }), - - delete: (id: number) => fetchApi<void>(`/recordroutines/${id}`, { - method: 'DELETE', - }), - - // Additional method to get workout statistics for the home page - getStats: () => fetchApi<WorkoutStats>('/stats'), -}; + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } -// RecordExercise API services -export const RecordExerciseService = { - getAll: (recordRoutineId: number) => fetchApi<RecordExercise[]>(`/recordroutines/${recordRoutineId}/exercises`), - - getById: (id: number) => fetchApi<RecordExercise>(`/recordexercises/${id}`), - - create: (recordExercise: RecordExercise) => fetchApi<RecordExercise>('/recordexercises', { - method: 'POST', - body: JSON.stringify(recordExercise), - }), - - update: (id: number, recordExercise: RecordExercise) => fetchApi<RecordExercise>(`/recordexercises/${id}`, { - method: 'PUT', - body: JSON.stringify(recordExercise), - }), - - delete: (id: number) => fetchApi<void>(`/recordexercises/${id}`, { - method: 'DELETE', - }), -}; + return response.json(); + } -// User profile service -export const ProfileService = { - get: async () => { - const user = await fetchApi<User>('/users/1'); - user.birthDate = new Date(user.birthDate).toISOString(); - return user; - }, - - update: async (profile: User) => { - profile.birthDate = new Date(profile.birthDate).toISOString(); - profile.isFemale = profile.isFemale === 'true'; - profile.weight = +profile.weight; - profile.height = +profile.height; + async ping(): Promise<{ message: string }> { + return this.request<{ message: string }>('/ping'); + } +} - return await fetchApi<User>('/users/1', { - method: 'PUT', - body: JSON.stringify(profile), - })}, -}; +// Export service instances +export const userService = new UserService(); +export const exerciseService = new ExerciseService(); +export const muscleService = new MuscleService(); +export const routineService = new RoutineService(); +export const recordService = new RecordService(); +export const statsService = new StatsService(); +export const healthService = new HealthService();
@@ -1,211 +1,126 @@
-// Models for Go-Lift app that match the backend models - -// Equipment model -export interface Equipment { +export interface User { id?: number; name: string; - description: string; + isFemale: boolean; + height?: number; // In cm + weight?: number; // In kg + birthDate?: string; createdAt?: string; updatedAt?: string; - deletedAt?: string | null; - exercises?: Exercise[]; // Many-to-many relationship } -// Muscle Group model -export interface MuscleGroup { - id?: number; +export interface Muscle { + id: number; name: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - exercises?: Exercise[]; // Many-to-many relationship + createdAt: string; + updatedAt: string; } -// ExerciseMuscleGroup join table -export interface ExerciseMuscleGroup { - id?: number; - exerciseId: number; - muscleGroupId: number; - exercise?: Exercise; - muscleGroup?: MuscleGroup; +export interface Exercise { + id: number; + name: string; + level: string; + category: string; + force?: string; + mechanic?: string; + equipment?: string; + instructions?: string; + primaryMuscles: Muscle[]; + secondaryMuscles: Muscle[]; + createdAt: string; + updatedAt: string; } -// Set definition for an exercise export interface Set { - id?: number; - exerciseId: number; + id: number; + exerciseItemId: number; reps: number; weight: number; - duration: number; // In seconds, for timed exercises + duration: number; // In seconds orderIndex: number; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - exercise?: Exercise; // Excluded in JSON via json:"-" but useful for frontend -} - -// Exercise definition -export interface Exercise { - id?: number; - name: string; - description: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - equipment: Equipment[]; - muscleGroups: MuscleGroup[]; - sets?: Set[]; + createdAt: string; + updatedAt: string; } -// SuperSet to handle two exercises with single rest time -export interface SuperSet { - id?: number; - name: string; - primaryExerciseId: number; - secondaryExerciseId: number; - restTime: number; // In seconds - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - primaryExercise: Exercise; - secondaryExercise: Exercise; +export interface ExerciseItem { + id: number; + routineItemId: number; + exerciseId: number; + orderIndex: number; + createdAt: string; + updatedAt: string; + exercise: Exercise; + sets: Set[]; } -// RoutineItem represents either an Exercise or a SuperSet in a Routine export interface RoutineItem { - id?: number; + id: number; routineId: number; - exerciseId?: number | null; - superSetId?: number | null; + type: string; // "exercise" or "superset" restTime: number; // In seconds orderIndex: number; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - superSet?: SuperSet | null; - exercise?: Exercise | null; + createdAt: string; + updatedAt: string; + exerciseItems: ExerciseItem[]; } -// A collection of exercises and/or supersets that make up a workout routine export interface Routine { - id?: number; + id: number; name: string; description: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - routineItems: RoutineItem[]; + createdAt: string; + updatedAt: string; + items: RoutineItem[]; } -// RecordRoutine represents a completed workout session -export interface RecordRoutine { - id?: number; - routineId: number; - startedAt: string; - endedAt?: string | null; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - routine: Routine; - recordRoutineItems: RecordRoutineItem[]; -} - -// RecordRoutineItem represents either a RecordExercise or a RecordSuperSet in a completed routine -export interface RecordRoutineItem { - id?: number; - recordRoutineId: number; - recordExerciseId?: number | null; - recordSuperSetId?: number | null; - actualRestTime: number; // In seconds - orderIndex: number; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - recordSuperSet?: RecordSuperSet | null; - recordExercise?: RecordExercise | null; -} - -// RecordSuperSet records a completed superset -export interface RecordSuperSet { - id?: number; - recordRoutineId: number; - superSetId: number; - startedAt: string; - endedAt: string; - actualRestTime: number; // In seconds - orderIndex: number; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - superSet: SuperSet; -} - -// RecordExercise tracks a completed exercise -export interface RecordExercise { - id?: number; - recordRoutineId: number; - exerciseId: number; - startedAt: string; - endedAt: string; - actualRestTime: number; // In seconds - orderIndex: number; - recordSuperSetId?: number | null; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; - exercise: Exercise; - recordSets: RecordSet[]; -} - -// RecordSet tracks an individual completed set export interface RecordSet { - id?: number; - recordExerciseId: number; + id: number; + recordExerciseItemId: number; setId: number; actualReps: number; actualWeight: number; actualDuration: number; // In seconds completedAt: string; orderIndex: number; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; + createdAt: string; + updatedAt: string; set: Set; } -// Additional models for localization -export interface Localization { - id?: number; - languageId: number; - keyword: string; - text: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; +export interface RecordExerciseItem { + id: number; + recordItemId: number; + exerciseItemId: number; + orderIndex: number; + createdAt: string; + updatedAt: string; + exerciseItem: ExerciseItem; + recordSets: RecordSet[]; } -export interface Language { - id?: number; - name: string; - code: string; - flag: string; - createdAt?: string; - updatedAt?: string; - deletedAt?: string | null; +export interface RecordItem { + id: number; + recordRoutineId: number; + routineItemId: number; + duration?: number; // In seconds + actualRestTime?: number; // In seconds + orderIndex: number; + createdAt: string; + updatedAt: string; + routineItem: RoutineItem; + recordExerciseItems: RecordExerciseItem[]; } -export interface User { - id?: number; - name: string; - isFemale: string | boolean; - weight: string | number; // in kg - height: string | number; // in cm - birthDate: string; // ISO format date string - createdAt?: string; - updatedAt?: string; +export interface RecordRoutine { + id: number; + routineId: number; + duration?: number; // In seconds + createdAt: string; + updatedAt: string; + routine: Routine; + recordItems: RecordItem[]; } -// Stats for the home page export interface WorkoutStats { totalWorkouts: number; totalMinutes: number;@@ -220,27 +135,3 @@ count: number;
}; recentWorkouts: RecordRoutine[]; } - -// Some simpler interfaces for UI use when full objects are too complex -export interface ExerciseSimple { - id?: number; - name: string; - muscleGroups: string[]; - equipment: string[]; -} - -export interface SetForUI { - id?: number; - reps: number; - weight: number; - duration?: number; - completed: boolean; -} - -export interface ExerciseForWorkout { - id?: number; - exerciseId: number; - exercise?: Exercise; - sets: SetForUI[]; - notes?: string; -}