added workout viewing simple view

This commit is contained in:
Timo Boomers 2025-07-15 17:31:29 +02:00
parent d1029485d4
commit 5c0bb602f5
Signed by: xeovalyte
SSH Key Fingerprint: SHA256:GWI1hq+MNKR2UOcvk7n9tekASXT8vyazK7vDF9Xyciw
5 changed files with 171 additions and 7 deletions

View File

@ -1062,6 +1062,9 @@
outline-offset: 2px; outline-offset: 2px;
} }
} }
.flex-col {
flex-direction: column;
}
.flex-wrap { .flex-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -1082,6 +1085,9 @@
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); 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 {
overflow-y: auto; overflow-y: auto;
} }

View File

@ -20,3 +20,15 @@ pub struct WorkoutExercise {
pub reps: Option<i32>, pub reps: Option<i32>,
pub time: Option<i32>, pub time: Option<i32>,
} }
#[derive(Debug, FromRow)]
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,
}

View File

@ -2,13 +2,14 @@ use crate::{AppError, icons, layouts, util::AppState};
use axum::{Router, extract::State, routing::get}; use axum::{Router, extract::State, routing::get};
use maud::{Markup, html}; use maud::{Markup, html};
mod id;
mod new; mod new;
pub fn routes() -> Router<AppState> { pub fn routes() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(page)) .route("/", get(page))
.nest("/new", new::routes()) .nest("/new", new::routes())
// .nest("/{id}", id::routes()) .nest("/{id}", id::routes())
} }
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> { async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
@ -22,15 +23,15 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
let content = html! { let content = html! {
h1 { "Workouts" } h1 { "Workouts" }
a href="/workouts/new" { "new workout +" } a href="/workouts/new" { "new workout +" }
ul class="list" { div class="list" {
@for workout in workouts { @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 { div {
div class="font-bold" { (workout.name) } div class="font-bold" { (workout.name) }
div class="text-xs" { (workout.description) } 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]" { div class="size-[1.6em]" {
(icons::eye()) (icons::eye())
} }
@ -40,5 +41,5 @@ async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
} }
}; };
Ok(layouts::desktop(content, "Exercises")) Ok(layouts::desktop(content, "Workouts"))
} }

147
src/pages/workouts/id.rs Normal file
View File

@ -0,0 +1,147 @@
use crate::{
AppError, layouts,
models::{exercises::ExerciseVariant, workouts::WorkoutExerciseFull},
util::AppState,
};
use axum::{
Form, Router,
extract::{Path, State},
http::HeaderMap,
response::IntoResponse,
routing::get,
};
use maud::{Markup, html};
use serde::Deserialize;
use uuid::Uuid;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/", get(page).delete(delete).put(submit_edit))
.route("/edit", get(edit))
}
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?;
tracing::info!("{:?}", exercises);
let content = html! {
div {
div class="flex w-full" {
h1 { (workout.name) }
a href={"/workouts/" (workout.workout_id) "/edit" } class="ml-auto btn" { "Edit" }
}
p { (workout.description) }
h2 { "Exercises" }
div class="flex flex-col gap-y-3" {
@for exercise in exercises {
div class="bg-base-200 p-3 rounded" {
h3 { (exercise.name) }
p class="text-sm opacity-50" { (exercise.description) }
}
}
}
}
};
Ok(layouts::desktop(content, "Exercises"))
}
async fn delete(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
sqlx::query_as!(
crate::models::Exercise,
"DELETE FROM workouts WHERE workout_id = $1",
id
)
.execute(&state.pool)
.await?;
let mut headers = HeaderMap::new();
headers.insert("hx-location", "/workouts".parse().unwrap());
Ok((headers, html! {}))
}
async fn edit(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
let exercise = sqlx::query_as!(
crate::models::Exercise,
"SELECT exercise_id, name, description FROM exercises WHERE exercise_id = $1",
id
)
.fetch_one(&state.pool)
.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" }
input required="true" name="name" class="input" value={(exercise.name)} {}
}
fieldset class="fieldset" {
legend class="fieldset-legend" { "Description" }
textarea required="true" name="description" class="textarea" { (exercise.description) }
}
input type="submit" class="btn" value="save" { }
}
};
Ok(layouts::desktop(content, "Edit Exercises"))
}
#[derive(Deserialize, Debug)]
struct FormData {
name: String,
description: String,
}
async fn submit_edit(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Form(form): Form<FormData>,
) -> Result<impl IntoResponse, AppError> {
sqlx::query_as!(
crate::models::Exercise,
"UPDATE exercises SET name = $1, description = $2 WHERE exercise_id = $3",
form.name,
form.description,
id
)
.execute(&state.pool)
.await?;
let mut headers = HeaderMap::new();
let location = format!("/exercises/{}", id);
headers.insert("hx-location", location.parse().unwrap());
Ok((headers, html! {}))
}

View File

@ -224,8 +224,6 @@ async fn submit(
} }
} }
tracing::info!("{:?}", exercises);
let (workout_ids, exercise_ids, exercise_types, positions, sets, reps, times): ( let (workout_ids, exercise_ids, exercise_types, positions, sets, reps, times): (
Vec<_>, Vec<_>,
Vec<_>, Vec<_>,