Compare commits
21 Commits
d1029485d4
...
master
Author | SHA1 | Date | |
---|---|---|---|
f82c31392d
|
|||
8cdb335fa1
|
|||
6c04f21a20
|
|||
31df9a24aa
|
|||
7f200edd72
|
|||
bbd51275c8
|
|||
5ba6b1bf16
|
|||
226e7114d4
|
|||
7ef71125cc
|
|||
7638592c03
|
|||
e60f11e9a8
|
|||
d94b67e689
|
|||
52673a9aa5
|
|||
779a62d587
|
|||
5887e083d9
|
|||
91c4026baa
|
|||
87fcf6d1dd
|
|||
3db46d0130
|
|||
ad98db7f7c
|
|||
39006435b2
|
|||
5c0bb602f5
|
37
.gitea/workflows/ci.yml
Normal file
37
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Cargo Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
push:
|
||||
name: Push to container registry
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: catthehacker/ubuntu:rust-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup update stable && rustup default stable
|
||||
|
||||
- run: cargo install sqlx-cli && cargo sqlx prepare --check
|
||||
|
||||
- name: Login in to the container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.xeovalyte.dev
|
||||
username: xeovalyte
|
||||
password: ${{ secrets.RUNNER_TOKEN }}
|
||||
|
||||
- name: Build an publish
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: gitea.xeovalyte.dev/xeovalyte/workout:latest
|
26
.sqlx/query-002d293c501005c025256289cb7ef4f89f767c53ad62a544b68cb7e8f6750c96.json
generated
Normal file
26
.sqlx/query-002d293c501005c025256289cb7ef4f89f767c53ad62a544b68cb7e8f6750c96.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM categories",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "category_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "002d293c501005c025256289cb7ef4f89f767c53ad62a544b68cb7e8f6750c96"
|
||||
}
|
34
.sqlx/query-421d1864f2062f99355f474195c36a05dae84949eb65fed12eb615859d774bda.json
generated
Normal file
34
.sqlx/query-421d1864f2062f99355f474195c36a05dae84949eb65fed12eb615859d774bda.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT exercise_id, name, description FROM exercises WHERE name ILIKE $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exercise_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "421d1864f2062f99355f474195c36a05dae84949eb65fed12eb615859d774bda"
|
||||
}
|
75
.sqlx/query-4f4994330ab5b680ddca03e962ce2a0a15bfbc27f28e30788422c5aa20e6c965.json
generated
Normal file
75
.sqlx/query-4f4994330ab5b680ddca03e962ce2a0a15bfbc27f28e30788422c5aa20e6c965.json
generated
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT exercises.exercise_id, exercise_type as \"exercise_type:ExerciseVariant\", sets, reps, time, name, description, position\n FROM workout_exercises\n JOIN exercises ON workout_exercises.exercise_id = exercises.exercise_id\n WHERE workout_id = $1\n ORDER BY position\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exercise_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "exercise_type:ExerciseVariant",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "exercise_variant",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"time",
|
||||
"number",
|
||||
"failure"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "sets",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "reps",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "time",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "position",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4f4994330ab5b680ddca03e962ce2a0a15bfbc27f28e30788422c5aa20e6c965"
|
||||
}
|
14
.sqlx/query-65160034413e2aed60354f87649e8d707b2f28c8d378d8fd47b0a33222a4396b.json
generated
Normal file
14
.sqlx/query-65160034413e2aed60354f87649e8d707b2f28c8d378d8fd47b0a33222a4396b.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM exercises WHERE exercise_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "65160034413e2aed60354f87649e8d707b2f28c8d378d8fd47b0a33222a4396b"
|
||||
}
|
15
.sqlx/query-73476d5fa346379f89b0deef86cd60615564d3ca4c2e505c20f775d7433bc643.json
generated
Normal file
15
.sqlx/query-73476d5fa346379f89b0deef86cd60615564d3ca4c2e505c20f775d7433bc643.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO exercise_categories (exercise_id, category_id) SELECT * FROM UNNEST($1::uuid[], $2::varchar[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"VarcharArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "73476d5fa346379f89b0deef86cd60615564d3ca4c2e505c20f775d7433bc643"
|
||||
}
|
16
.sqlx/query-746236a3e38a5e49d4d2b65d7b26ffbc1092cebaf07a0840aa4350d94c0c5585.json
generated
Normal file
16
.sqlx/query-746236a3e38a5e49d4d2b65d7b26ffbc1092cebaf07a0840aa4350d94c0c5585.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE exercises SET name = $1, description = $2 WHERE exercise_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "746236a3e38a5e49d4d2b65d7b26ffbc1092cebaf07a0840aa4350d94c0c5585"
|
||||
}
|
26
.sqlx/query-99dd58daa888b66b5e75f3603583d82e6aa665668dd45e2e40d7ccc4846c5ef5.json
generated
Normal file
26
.sqlx/query-99dd58daa888b66b5e75f3603583d82e6aa665668dd45e2e40d7ccc4846c5ef5.json
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM muscles",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "muscle_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "99dd58daa888b66b5e75f3603583d82e6aa665668dd45e2e40d7ccc4846c5ef5"
|
||||
}
|
34
.sqlx/query-9f989d04f5f8aa4b916987ba1b62fb3d1996dc025aba53b4b28997e6f49db61e.json
generated
Normal file
34
.sqlx/query-9f989d04f5f8aa4b916987ba1b62fb3d1996dc025aba53b4b28997e6f49db61e.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workout_id, name, description FROM workouts WHERE workout_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workout_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9f989d04f5f8aa4b916987ba1b62fb3d1996dc025aba53b4b28997e6f49db61e"
|
||||
}
|
16
.sqlx/query-b10aa422afe7a01589a6f090b0a2cab616fd28c793c91f2046864e7575b0d242.json
generated
Normal file
16
.sqlx/query-b10aa422afe7a01589a6f090b0a2cab616fd28c793c91f2046864e7575b0d242.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO exercises (exercise_id, name, description) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b10aa422afe7a01589a6f090b0a2cab616fd28c793c91f2046864e7575b0d242"
|
||||
}
|
32
.sqlx/query-b8d583d3a2ca1b196acc9a27cb7d8f1746d7b2c0063e8770a068ec2444ca8b8b.json
generated
Normal file
32
.sqlx/query-b8d583d3a2ca1b196acc9a27cb7d8f1746d7b2c0063e8770a068ec2444ca8b8b.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT exercise_id, name, description FROM exercises",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exercise_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b8d583d3a2ca1b196acc9a27cb7d8f1746d7b2c0063e8770a068ec2444ca8b8b"
|
||||
}
|
38
.sqlx/query-cd1cd6869f9be3b1bc95c8b273451e29f26fd153bb6041a5040e2eaa285d51c4.json
generated
Normal file
38
.sqlx/query-cd1cd6869f9be3b1bc95c8b273451e29f26fd153bb6041a5040e2eaa285d51c4.json
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO workout_exercises (workout_id, exercise_id, exercise_type, position, sets, reps, time)\n SELECT * FROM UNNEST($1::uuid[], $2::uuid[], $3::exercise_variant[], $4::int[], $5::int[], $6::int[], $7::int[])\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"UuidArray",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "exercise_variant[]",
|
||||
"kind": {
|
||||
"Array": {
|
||||
"Custom": {
|
||||
"name": "exercise_variant",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"time",
|
||||
"number",
|
||||
"failure"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int4Array",
|
||||
"Int4Array",
|
||||
"Int4Array",
|
||||
"Int4Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd1cd6869f9be3b1bc95c8b273451e29f26fd153bb6041a5040e2eaa285d51c4"
|
||||
}
|
16
.sqlx/query-cefe6cff888e999bbe55688bcf1c8c1657013fef519791de1274ab83fd8377a9.json
generated
Normal file
16
.sqlx/query-cefe6cff888e999bbe55688bcf1c8c1657013fef519791de1274ab83fd8377a9.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workouts (workout_id, name, description) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cefe6cff888e999bbe55688bcf1c8c1657013fef519791de1274ab83fd8377a9"
|
||||
}
|
32
.sqlx/query-d7c9cb014b26cd70eaf742fe04d79e71271b311ad1418fa8eea59263431e22ab.json
generated
Normal file
32
.sqlx/query-d7c9cb014b26cd70eaf742fe04d79e71271b311ad1418fa8eea59263431e22ab.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workout_id, name, description FROM workouts",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workout_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d7c9cb014b26cd70eaf742fe04d79e71271b311ad1418fa8eea59263431e22ab"
|
||||
}
|
34
.sqlx/query-dbf66e8b66a946d0327dd71562d540f24245b576798814c366770800bc8a1616.json
generated
Normal file
34
.sqlx/query-dbf66e8b66a946d0327dd71562d540f24245b576798814c366770800bc8a1616.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT exercise_id, name, description FROM exercises WHERE exercise_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exercise_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dbf66e8b66a946d0327dd71562d540f24245b576798814c366770800bc8a1616"
|
||||
}
|
15
.sqlx/query-dfb1728ec2f194bc898deab2bbddcd9cfa11edb60a6d1625534eb6388bcf948b.json
generated
Normal file
15
.sqlx/query-dfb1728ec2f194bc898deab2bbddcd9cfa11edb60a6d1625534eb6388bcf948b.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO exercise_muscles (exercise_id, muscle_id) SELECT * FROM UNNEST($1::uuid[], $2::varchar[])",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"UuidArray",
|
||||
"VarcharArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dfb1728ec2f194bc898deab2bbddcd9cfa11edb60a6d1625534eb6388bcf948b"
|
||||
}
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM rust:1.88-bullseye as builder
|
||||
WORKDIR /usr/src/myapp
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /usr/local/cargo/bin/workout /usr/local/bin/workout
|
||||
COPY assets /app/assets
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["workout"]
|
||||
|
@@ -13,12 +13,16 @@
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--text-2xl: 1.5rem;
|
||||
--text-2xl--line-height: calc(2 / 1.5);
|
||||
--text-3xl: 1.875rem;
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--text-4xl: 2.25rem;
|
||||
--text-4xl--line-height: calc(2.5 / 2.25);
|
||||
--text-6xl: 3.75rem;
|
||||
--text-6xl--line-height: 1;
|
||||
--font-weight-bold: 700;
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
@@ -371,6 +375,74 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.dock {
|
||||
position: fixed;
|
||||
right: calc(0.25rem * 0);
|
||||
bottom: calc(0.25rem * 0);
|
||||
left: calc(0.25rem * 0);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
background-color: var(--color-base-100);
|
||||
padding: calc(0.25rem * 2);
|
||||
color: currentColor;
|
||||
border-top: 0.5px solid var(--color-base-content);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
border-top: 0.5px solid color-mix(in oklab, var(--color-base-content) 5%, #0000);
|
||||
}
|
||||
height: 4rem;
|
||||
height: calc(4rem + env(safe-area-inset-bottom));
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
> * {
|
||||
position: relative;
|
||||
margin-bottom: calc(0.25rem * 2);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: calc(0.25rem * 32);
|
||||
flex-shrink: 1;
|
||||
flex-basis: 100%;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
border-radius: var(--radius-box);
|
||||
background-color: transparent;
|
||||
transition: opacity 0.2s ease-out;
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
opacity: 80%;
|
||||
}
|
||||
}
|
||||
&[aria-disabled="true"], &[disabled] {
|
||||
&, &:hover {
|
||||
pointer-events: none;
|
||||
color: var(--color-base-content);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
|
||||
}
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
.dock-label {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: calc(0.25rem * 1);
|
||||
width: calc(0.25rem * 6);
|
||||
border-radius: calc(infinity * 1px);
|
||||
background-color: transparent;
|
||||
bottom: 0.2rem;
|
||||
border-top: 3px solid transparent;
|
||||
transition: background-color 0.1s ease-out, text-color 0.1s ease-out, width 0.1s ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
:where(&) {
|
||||
width: unset;
|
||||
@@ -959,6 +1031,15 @@
|
||||
.mt-3 {
|
||||
margin-top: calc(var(--spacing) * 3);
|
||||
}
|
||||
.mt-5 {
|
||||
margin-top: calc(var(--spacing) * 5);
|
||||
}
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
.mr-1 {
|
||||
margin-right: calc(var(--spacing) * 1);
|
||||
}
|
||||
.fieldset-legend {
|
||||
margin-bottom: calc(0.25rem * -1);
|
||||
display: flex;
|
||||
@@ -1015,10 +1096,21 @@
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
.size-5 {
|
||||
width: calc(var(--spacing) * 5);
|
||||
height: calc(var(--spacing) * 5);
|
||||
}
|
||||
.size-\[1\.5em\] {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
.size-\[1\.6em\] {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
}
|
||||
.h-48 {
|
||||
height: calc(var(--spacing) * 48);
|
||||
}
|
||||
.h-\[1\.2em\] {
|
||||
height: 1.2em;
|
||||
}
|
||||
@@ -1037,6 +1129,9 @@
|
||||
.w-56 {
|
||||
width: calc(var(--spacing) * 56);
|
||||
}
|
||||
.w-\[1\.6em\] {
|
||||
width: 1.6em;
|
||||
}
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1062,12 +1157,24 @@
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.gap-3 {
|
||||
gap: calc(var(--spacing) * 3);
|
||||
}
|
||||
.space-y-1 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
@@ -1082,6 +1189,9 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-y-3 {
|
||||
row-gap: calc(var(--spacing) * 3);
|
||||
}
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -1094,20 +1204,35 @@
|
||||
.p-3 {
|
||||
padding: calc(var(--spacing) * 3);
|
||||
}
|
||||
.p-5 {
|
||||
padding: calc(var(--spacing) * 5);
|
||||
}
|
||||
.py-2 {
|
||||
padding-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
.py-10 {
|
||||
padding-block: calc(var(--spacing) * 10);
|
||||
}
|
||||
.pt-3 {
|
||||
padding-top: calc(var(--spacing) * 3);
|
||||
}
|
||||
.pb-6 {
|
||||
padding-bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.pl-10 {
|
||||
padding-left: calc(var(--spacing) * 10);
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.text-2xl {
|
||||
font-size: var(--text-2xl);
|
||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||
}
|
||||
.text-4xl {
|
||||
font-size: var(--text-4xl);
|
||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
||||
}
|
||||
.text-6xl {
|
||||
font-size: var(--text-6xl);
|
||||
line-height: var(--tw-leading, var(--text-6xl--line-height));
|
||||
}
|
||||
.text-sm {
|
||||
font-size: var(--text-sm);
|
||||
@@ -1125,6 +1250,9 @@
|
||||
--tw-font-weight: var(--font-weight-bold);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.opacity-50 {
|
||||
opacity: 50%;
|
||||
}
|
||||
@@ -1158,6 +1286,10 @@
|
||||
--btn-color: var(--color-error);
|
||||
--btn-fg: var(--color-error-content);
|
||||
}
|
||||
.btn-success {
|
||||
--btn-color: var(--color-success);
|
||||
--btn-fg: var(--color-success-content);
|
||||
}
|
||||
.hover\:cursor-pointer {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
@@ -1165,6 +1297,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.max-md\:hidden {
|
||||
@media (width < 48rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.sm\:modal-middle {
|
||||
@media (width >= 40rem) {
|
||||
place-items: center;
|
||||
@@ -1182,19 +1319,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: var(--tw-leading, var(--text-3xl--line-height));
|
||||
.md\:hidden {
|
||||
@media (width >= 48rem) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||
}
|
||||
.desc {
|
||||
opacity: 80%;
|
||||
}
|
||||
@layer base {
|
||||
:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
|
||||
color-scheme: light;
|
||||
|
63
assets/js/play.js
Normal file
63
assets/js/play.js
Normal file
@@ -0,0 +1,63 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('exercisePlayer', () => {
|
||||
return {
|
||||
timer: null,
|
||||
counter: 0,
|
||||
timeLeft: 0,
|
||||
currentPosition: 0,
|
||||
exercises: [],
|
||||
isPaused: false,
|
||||
init() {
|
||||
this.exercises = window.exerciseData;
|
||||
},
|
||||
start() {
|
||||
if (!this.isPaused) {
|
||||
this.timeLeft = this.exercises[this.currentPosition].time;
|
||||
this.counter = 0;
|
||||
}
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
if (this.timeLeft === 0) {
|
||||
this.next()
|
||||
};
|
||||
|
||||
this.counter++;
|
||||
this.timeLeft--;
|
||||
}, 1000);
|
||||
},
|
||||
destroy() {
|
||||
// Detach the handler, avoiding memory and side-effect leakage
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
if (!this.isPaused) {
|
||||
this.counter = 0;
|
||||
this.timeLeft = 0;
|
||||
}
|
||||
},
|
||||
next() {
|
||||
this.destroy()
|
||||
this.currentPosition++;
|
||||
|
||||
if (this.exercises[this.currentPosition].exercise_type === "Time") {
|
||||
this.start()
|
||||
}
|
||||
},
|
||||
previous() {
|
||||
this.destroy();
|
||||
this.currentPosition--;
|
||||
},
|
||||
|
||||
pause() {
|
||||
if (this.isPaused) {
|
||||
this.start();
|
||||
this.isPaused = false;
|
||||
} else {
|
||||
this.isPaused = true;
|
||||
this.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
42
src/icons.rs
42
src/icons.rs
@@ -2,4 +2,46 @@ mod eye;
|
||||
mod search;
|
||||
|
||||
pub use eye::eye;
|
||||
use maud::{Markup, html};
|
||||
pub use search::search;
|
||||
|
||||
pub fn arrow_left() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||
path fill="currentColor" d="M224 128a8 8 0 0 1-8 8H59.31l58.35 58.34a8 8 0 0 1-11.32 11.32l-72-72a8 8 0 0 1 0-11.32l72-72a8 8 0 0 1 11.32 11.32L59.31 120H216a8 8 0 0 1 8 8" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn house() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||
path
|
||||
fill="currentColor"
|
||||
d="m219.31 108.68l-80-80a16 16 0 0 0-22.62 0l-80 80A15.87 15.87 0 0 0 32 120v96a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-56h32v56a8 8 0 0 0 8 8h64a8 8 0 0 0 8-8v-96a15.87 15.87 0 0 0-4.69-11.32M208 208h-48v-56a8 8 0 0 0-8-8h-48a8 8 0 0 0-8 8v56H48v-88l80-80l80 80Z"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn barbell() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||
path
|
||||
fill="currentColor"
|
||||
d="M248 120h-8V88a16 16 0 0 0-16-16h-16v-8a16 16 0 0 0-16-16h-24a16 16 0 0 0-16 16v56h-48V64a16 16 0 0 0-16-16H64a16 16 0 0 0-16 16v8H32a16 16 0 0 0-16 16v32H8a8 8 0 0 0 0 16h8v32a16 16 0 0 0 16 16h16v8a16 16 0 0 0 16 16h24a16 16 0 0 0 16-16v-56h48v56a16 16 0 0 0 16 16h24a16 16 0 0 0 16-16v-8h16a16 16 0 0 0 16-16v-32h8a8 8 0 0 0 0-16M32 168V88h16v80Zm56 24H64V64h24zm104 0h-24V64h24zm32-24h-16V88h16Z"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn person_simple_run() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||
path
|
||||
fill="currentColor"
|
||||
d="M152 88a32 32 0 1 0-32-32a32 32 0 0 0 32 32m0-48a16 16 0 1 1-16 16a16 16 0 0 1 16-16m67.31 100.68c-.61.28-7.49 3.28-19.67 3.28c-13.85 0-34.55-3.88-60.69-20a169.3 169.3 0 0 1-15.41 32.34a104.3 104.3 0 0 1 31.31 15.81C173.92 186.65 184 207.35 184 232a8 8 0 0 1-16 0c0-41.7-34.69-56.71-54.14-61.85c-.55.7-1.12 1.41-1.69 2.1c-19.64 23.8-44.25 36.18-71.63 36.18a92 92 0 0 1-9.34-.43a8 8 0 0 1 1.6-16c25.92 2.58 48.47-7.49 67-30c12.49-15.14 21-33.61 25.25-47c-38.92-22.65-63.78-3.37-64.05-3.16a8 8 0 1 1-10-12.48c1.5-1.2 37.22-29 89.51 6.57c45.47 30.91 71.93 20.31 72.18 20.19a8 8 0 1 1 6.63 14.56Z"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,22 @@
|
||||
use maud::{Markup, html};
|
||||
|
||||
use crate::icons;
|
||||
|
||||
use super::empty;
|
||||
|
||||
pub fn desktop_minimal(content: Markup, name: &str) -> Markup {
|
||||
let content = html! {
|
||||
div class="w-full h-screen flex" {
|
||||
div class="w-56" {
|
||||
div class="max-md:hidden w-56" {
|
||||
(sidebar())
|
||||
}
|
||||
div class="w-full overflow-y-auto" {
|
||||
(content)
|
||||
div class="w-full h-full flex flex-col" {
|
||||
div class="overflow-y-auto" {
|
||||
(content)
|
||||
}
|
||||
div class="md:hidden mt-auto" {
|
||||
(dock())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -17,10 +24,13 @@ pub fn desktop_minimal(content: Markup, name: &str) -> Markup {
|
||||
empty(content, name)
|
||||
}
|
||||
|
||||
pub fn desktop(content: Markup, name: &str) -> Markup {
|
||||
pub fn desktop(content: Markup, name: &str, back_button: bool) -> Markup {
|
||||
let content = html! {
|
||||
div class="pl-10 py-10 w-full max-w-2xl" {
|
||||
(content)
|
||||
div class="flex justify-center" {
|
||||
div class="p-5 w-full max-w-2xl" {
|
||||
(header(name, back_button))
|
||||
(content)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,15 +45,50 @@ fn sidebar() -> Markup {
|
||||
h2 { "Timo's Workouts" }
|
||||
}
|
||||
a href="/" {
|
||||
div class="size-5" { (icons::house()) }
|
||||
"Overview"
|
||||
}
|
||||
a href="/workouts" {
|
||||
div class="size-5" { (icons::barbell()) }
|
||||
"Workouts"
|
||||
}
|
||||
a href="/exercises" {
|
||||
div class="size-5" { (icons::person_simple_run()) }
|
||||
"Exercises"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dock() -> Markup {
|
||||
html! {
|
||||
div class="dock" {
|
||||
a href="/" {
|
||||
div class="size-[1.5em]" { (icons::house()) }
|
||||
span class="dock-label" { "Overview" }
|
||||
}
|
||||
a href="/workouts" {
|
||||
div class="size-[1.5em]" { (icons::barbell()) }
|
||||
span class="dock-label" { "Workouts" }
|
||||
}
|
||||
a href="/exercises" {
|
||||
div class="size-[1.5em]" { (icons::person_simple_run()) }
|
||||
span class="dock-label" { "Exercises" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header(title: &str, back_button: bool) -> Markup {
|
||||
html! {
|
||||
div class="flex items-center mb-5" {
|
||||
@if back_button {
|
||||
button onclick="history.back()" class="w-[1.6em] mr-1" {
|
||||
(icons::arrow_left())
|
||||
}
|
||||
}
|
||||
h1 class="text-2xl" { (title) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ pub fn empty(content: Markup, name: &str) -> Markup {
|
||||
head {
|
||||
script src="/assets/lib/htmx.min.js" { }
|
||||
script defer src="/assets/lib/alpine.min.js" { }
|
||||
script src="/assets/js/play.js" {}
|
||||
link rel="stylesheet" href="/assets/css/main.css" { }
|
||||
title {
|
||||
(name) " - Timo's Workouts"
|
||||
|
@@ -24,7 +24,7 @@ async fn main() {
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
dotenvy::dotenv().expect("Could not initialize dotenvy");
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
// Connect to databse
|
||||
let pool = database::connect_database().await;
|
||||
@@ -45,9 +45,7 @@ async fn main() {
|
||||
.with_state(app_state);
|
||||
|
||||
// run it
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||
.await
|
||||
.unwrap();
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
println!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ pub struct ExerciseFull {
|
||||
pub muscles: Vec<Muscle>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, serde::Deserialize, Debug)]
|
||||
#[derive(sqlx::Type, serde::Deserialize, serde::Serialize, Debug)]
|
||||
#[sqlx(type_name = "exercise_variant", rename_all = "lowercase")]
|
||||
pub enum ExerciseVariant {
|
||||
Time,
|
||||
|
@@ -20,3 +20,15 @@ pub struct WorkoutExercise {
|
||||
pub reps: Option<i32>,
|
||||
pub time: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, serde::Serialize)]
|
||||
pub struct WorkoutExerciseFull {
|
||||
pub exercise_id: Uuid,
|
||||
pub exercise_type: ExerciseVariant,
|
||||
pub position: i32,
|
||||
pub sets: Option<i32>,
|
||||
pub reps: Option<i32>,
|
||||
pub time: Option<i32>,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
@@ -21,17 +21,17 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 { "Exercises" }
|
||||
a href="/exercises/new" { "new exercise +" }
|
||||
ul class="list" {
|
||||
|
||||
div class="list" {
|
||||
@for exercise in exercises {
|
||||
li hx-get={ "/exercises/" (exercise.exercise_id) } hx-target="body" hx-push-url="true" class="list-row" {
|
||||
a href={"/exercises/" (exercise.exercise_id)} class="list-row" {
|
||||
div {}
|
||||
div {
|
||||
div class="font-bold" { (exercise.name) }
|
||||
div class="text-xs" { (exercise.description) }
|
||||
}
|
||||
a class="btn btn-square btn-ghost" {
|
||||
div class="btn btn-square btn-ghost" {
|
||||
div class="size-[1.6em]" {
|
||||
(icons::eye())
|
||||
}
|
||||
@@ -41,5 +41,5 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "Exercises"))
|
||||
Ok(layouts::desktop(content, "Browse Exercises", false))
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::{AppError, layouts, util::AppState};
|
||||
use crate::{AppError, layouts, models::exercises::ExerciseFull, util::AppState};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::{Path, State},
|
||||
@@ -26,17 +26,16 @@ async fn page(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Mar
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
div {
|
||||
div class="flex w-full" {
|
||||
h1 { (exercise.name) }
|
||||
a href={"/exercises/" (exercise.exercise_id) "/edit" } class="ml-auto btn" { "Edit" }
|
||||
}
|
||||
p { (exercise.description) }
|
||||
button hx-delete={ "/exercises/" (exercise.exercise_id) } hx-confirm="Are you sure that you want to delete this exercise?" class="btn btn-error" { "Delete Exercise" }
|
||||
}
|
||||
h2 { (exercise.name) }
|
||||
p class="desc" { (exercise.description) }
|
||||
|
||||
a href={"/exercises/" (id) "/edit"} class="ml-auto btn" { "Edit" }
|
||||
|
||||
p { (exercise.description) }
|
||||
button hx-delete={"/exercises/" (id)} hx-confirm="Are you sure that you want to delete this exercise?" class="btn btn-error" { "Delete Exercise" }
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "Exercises"))
|
||||
Ok(layouts::desktop(content, "View Exercise", true))
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
@@ -70,8 +69,6 @@ async fn edit(
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 class="mb-5" { "Edit Exercise" }
|
||||
|
||||
form hx-put={"/exercises/" (exercise.exercise_id)} class="space-y-1" {
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Name" }
|
||||
@@ -87,7 +84,7 @@ async fn edit(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "Edit Exercises"))
|
||||
Ok(layouts::desktop(content, "Edit", true))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@@ -28,8 +28,6 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
.collect();
|
||||
|
||||
let content = html! {
|
||||
h1 class="mb-5" { "New Exercise" }
|
||||
|
||||
form hx-post="/exercises/new" class="space-y-1" {
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Name" }
|
||||
@@ -49,7 +47,7 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "New Exercise"))
|
||||
Ok(layouts::desktop(content, "New Exercise", true))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@@ -9,5 +9,5 @@ pub fn routes() -> Router<AppState> {
|
||||
async fn page() -> Markup {
|
||||
let content = html! {};
|
||||
|
||||
layouts::desktop(content, "Home")
|
||||
layouts::desktop(content, "Home", false)
|
||||
}
|
||||
|
@@ -2,13 +2,14 @@ use crate::{AppError, icons, layouts, util::AppState};
|
||||
use axum::{Router, extract::State, routing::get};
|
||||
use maud::{Markup, html};
|
||||
|
||||
mod id;
|
||||
mod new;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(page))
|
||||
.nest("/new", new::routes())
|
||||
// .nest("/{id}", id::routes())
|
||||
.nest("/{id}", id::routes())
|
||||
}
|
||||
|
||||
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
@@ -20,17 +21,16 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 { "Workouts" }
|
||||
a href="/workouts/new" { "new workout +" }
|
||||
ul class="list" {
|
||||
div class="list" {
|
||||
@for workout in workouts {
|
||||
li hx-get={ "/workouts/" (workout.workout_id) } hx-target="body" hx-push-url="true" class="list-row" {
|
||||
a href={ "/workouts/" (workout.workout_id) } class="list-row" {
|
||||
div {}
|
||||
div {
|
||||
div class="font-bold" { (workout.name) }
|
||||
div class="text-xs" { (workout.description) }
|
||||
}
|
||||
a class="btn btn-square btn-ghost" {
|
||||
div class="btn btn-square btn-ghost" {
|
||||
div class="size-[1.6em]" {
|
||||
(icons::eye())
|
||||
}
|
||||
@@ -40,5 +40,5 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "Exercises"))
|
||||
Ok(layouts::desktop(content, "Browse Workouts", false))
|
||||
}
|
||||
|
90
src/pages/workouts/id.rs
Normal file
90
src/pages/workouts/id.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::{
|
||||
AppError, layouts,
|
||||
models::{exercises::ExerciseVariant, workouts::WorkoutExerciseFull},
|
||||
util::AppState,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod play;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(page))
|
||||
.nest("/play", play::routes())
|
||||
}
|
||||
|
||||
async fn page(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Markup, AppError> {
|
||||
let workout = sqlx::query_as!(
|
||||
crate::models::Workout,
|
||||
"SELECT workout_id, name, description FROM workouts WHERE workout_id = $1",
|
||||
id
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
let exercises = sqlx::query_as!(
|
||||
WorkoutExerciseFull,
|
||||
r#"
|
||||
SELECT exercises.exercise_id, exercise_type as "exercise_type:ExerciseVariant", sets, reps, time, name, description, position
|
||||
FROM workout_exercises
|
||||
JOIN exercises ON workout_exercises.exercise_id = exercises.exercise_id
|
||||
WHERE workout_id = $1
|
||||
ORDER BY position
|
||||
"#,
|
||||
&id,
|
||||
).fetch_all(&state.pool).await?;
|
||||
|
||||
let content = html! {
|
||||
div {
|
||||
div class="mb-5" {
|
||||
h2 { (workout.name) }
|
||||
p class="desc" { (workout.description) }
|
||||
}
|
||||
|
||||
h3 { "Exercises" }
|
||||
|
||||
div class="flex flex-col gap-y-3" {
|
||||
@for exercise in exercises {
|
||||
(display_exercise(exercise))
|
||||
}
|
||||
}
|
||||
|
||||
a href={"/workouts/" (id) "/play"} class="btn btn-success" { "Start Workout" }
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "View Workout", true))
|
||||
}
|
||||
|
||||
fn display_exercise(exercise: WorkoutExerciseFull) -> Markup {
|
||||
html! {
|
||||
div class="bg-base-200 p-3 rounded flex" {
|
||||
div {
|
||||
h3 { (exercise.name) }
|
||||
p class="text-sm opacity-50" { (exercise.description) }
|
||||
}
|
||||
div class="ml-auto flex gap-2 items-center" {
|
||||
@match exercise.exercise_type {
|
||||
ExerciseVariant::Time => {
|
||||
div { (exercise.time.unwrap_or(-1))"s" }
|
||||
},
|
||||
ExerciseVariant::Number => {
|
||||
div {
|
||||
div class="whitespace-nowrap text-right" { (exercise.sets.unwrap_or(-1)) " sets" }
|
||||
div class="whitespace-nowrap text-right" { (exercise.reps.unwrap_or(-1)) " reps" }
|
||||
}
|
||||
},
|
||||
ExerciseVariant::Failure => {
|
||||
"Failure"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
79
src/pages/workouts/id/play.rs
Normal file
79
src/pages/workouts/id/play.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::{
|
||||
AppError, layouts,
|
||||
models::{exercises::ExerciseVariant, workouts::WorkoutExerciseFull},
|
||||
util::AppState,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", get(page))
|
||||
}
|
||||
|
||||
async fn page(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Markup, AppError> {
|
||||
let exercises = sqlx::query_as!(
|
||||
WorkoutExerciseFull,
|
||||
r#"
|
||||
SELECT exercises.exercise_id, exercise_type as "exercise_type:ExerciseVariant", sets, reps, time, name, description, position
|
||||
FROM workout_exercises
|
||||
JOIN exercises ON workout_exercises.exercise_id = exercises.exercise_id
|
||||
WHERE workout_id = $1
|
||||
ORDER BY position
|
||||
"#,
|
||||
&id,
|
||||
).fetch_all(&state.pool).await?;
|
||||
|
||||
let exercises_json = serde_json::to_string(&exercises).unwrap();
|
||||
|
||||
let content = html! {
|
||||
script {
|
||||
"window.exerciseData = " (PreEscaped(exercises_json)) ";"
|
||||
}
|
||||
div x-data="exercisePlayer" {
|
||||
@for exercise in exercises {
|
||||
(display_exercise(exercise))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "Working out", true))
|
||||
}
|
||||
|
||||
fn display_exercise(exercise: WorkoutExerciseFull) -> Markup {
|
||||
html! {
|
||||
div x-bind:class={"{ 'hidden': currentPosition !="(exercise.position)" }"} {
|
||||
div class="flex justify-center items-center h-48" {
|
||||
@match exercise.exercise_type {
|
||||
ExerciseVariant::Time => div {
|
||||
div {
|
||||
span class="font-bold text-6xl" x-text="timeLeft" {}
|
||||
span class="" { " / " }
|
||||
span class="" {(exercise.time.unwrap_or(-1))}
|
||||
}
|
||||
},
|
||||
ExerciseVariant::Number => div {
|
||||
div { span class="font-bold text-6xl" {(exercise.sets.unwrap_or(-1))} " sets" }
|
||||
div { span class="font-bold text-6xl" {(exercise.reps.unwrap_or(-1))} " reps" }
|
||||
},
|
||||
ExerciseVariant::Failure => div {
|
||||
div { span class="font-bold text-6xl" { "Failure" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="flex justify-center gap-3" {
|
||||
button class="btn" x-on:click="previous()" { "Previous" }
|
||||
button class="btn" disabled[exercise.time.is_none()] x-on:click="pause()" { "Pause" }
|
||||
button class="btn" x-on:click="next()" { "Next" }
|
||||
}
|
||||
div class="mt-5" {
|
||||
h2 class="text-center font-bold text-4xl" { (exercise.name) }
|
||||
p class="text-center desc" { (exercise.description) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -29,8 +29,6 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 class="mb-5" { "New Workout" }
|
||||
|
||||
div x-data="{ exercises: [] }" {
|
||||
form hx-post="/workouts/new" class="space-y-1" {
|
||||
fieldset class="fieldset" {
|
||||
@@ -132,7 +130,7 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
|
||||
};
|
||||
|
||||
Ok(layouts::desktop(content, "New Exercise"))
|
||||
Ok(layouts::desktop(content, "New Workout", true))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -224,8 +222,6 @@ async fn submit(
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("{:?}", exercises);
|
||||
|
||||
let (workout_ids, exercise_ids, exercise_types, positions, sets, reps, times): (
|
||||
Vec<_>,
|
||||
Vec<_>,
|
||||
|
12
tailwind.css
12
tailwind.css
@@ -39,14 +39,14 @@
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-3xl
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-2xl
|
||||
@apply text-xl
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-xl
|
||||
@apply text-lg
|
||||
}
|
||||
|
||||
.desc {
|
||||
@apply opacity-80
|
||||
}
|
||||
|
Reference in New Issue
Block a user