Added basic exercise addition and deletion
This commit is contained in:
15
src/database.rs
Normal file
15
src/database.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
|
||||
pub async fn connect_database() -> PgPool {
|
||||
let db_connection_str =
|
||||
std::env::var("DATABASE_URL").expect("Could not find DATABASE_URL environment variable");
|
||||
|
||||
PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(3))
|
||||
.connect(&db_connection_str)
|
||||
.await
|
||||
.expect("Could not connect to database")
|
||||
}
|
3
src/icons.rs
Normal file
3
src/icons.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod eye;
|
||||
|
||||
pub use eye::eye;
|
9
src/icons/eye.rs
Normal file
9
src/icons/eye.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub fn eye() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 256 256" {
|
||||
path fill="currentColor" d="M247.31 124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57 61.26 162.88 48 128 48S61.43 61.26 36.34 86.35C17.51 105.18 9 124 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208s66.57-13.26 91.66-38.34c18.83-18.83 27.3-37.61 27.65-38.4a8 8 0 0 0 0-6.5M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.5 133.5 0 0 1 25 128a133.3 133.3 0 0 1 23.07-30.75C70.33 75.19 97.22 64 128 64s57.67 11.19 79.93 33.25A133.5 133.5 0 0 1 231.05 128c-7.21 13.46-38.62 64-103.05 64m0-112a48 48 0 1 0 48 48a48.05 48.05 0 0 0-48-48m0 80a32 32 0 1 1 32-32a32 32 0 0 1-32 32" {}
|
||||
}
|
||||
}
|
||||
}
|
25
src/main.rs
25
src/main.rs
@@ -2,8 +2,16 @@ use axum::Router;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::util::AppState;
|
||||
|
||||
mod database;
|
||||
mod icons;
|
||||
mod layouts;
|
||||
mod models;
|
||||
mod pages;
|
||||
mod util;
|
||||
|
||||
pub use util::error::AppError;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -15,10 +23,25 @@ async fn main() {
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
dotenvy::dotenv().expect("Could not initialize dotenvy");
|
||||
|
||||
// Connect to databse
|
||||
let pool = database::connect_database().await;
|
||||
|
||||
// Migrate
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("Database migrations failed");
|
||||
|
||||
// Initialize global app state
|
||||
let app_state = AppState { pool };
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.nest_service("/assets", ServeDir::new("assets"))
|
||||
.merge(pages::routes());
|
||||
.merge(pages::routes())
|
||||
.with_state(app_state);
|
||||
|
||||
// run it
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||
|
9
src/models.rs
Normal file
9
src/models.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod categories;
|
||||
pub mod exercises;
|
||||
pub mod muscle_groups;
|
||||
pub mod workouts;
|
||||
|
||||
pub use categories::Category;
|
||||
pub use exercises::Exercise;
|
||||
pub use muscle_groups::MuscleGroup;
|
||||
pub use workouts::Workout;
|
8
src/models/categories.rs
Normal file
8
src/models/categories.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use sqlx::{PgPool, prelude::FromRow};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct Category {
|
||||
pub category_id: String,
|
||||
pub name: String,
|
||||
}
|
23
src/models/exercises.rs
Normal file
23
src/models/exercises.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use sqlx::{PgPool, prelude::FromRow};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct Exercise {
|
||||
pub exercise_id: Uuid,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Exercise {
|
||||
pub async fn from_id(id: Uuid, conn: &PgPool) -> Result<Self, sqlx::Error> {
|
||||
let exercise = sqlx::query_as!(
|
||||
Exercise,
|
||||
"SELECT exercise_id, name, description FROM exercises WHERE exercise_id = $1",
|
||||
id
|
||||
)
|
||||
.fetch_one(conn)
|
||||
.await?;
|
||||
|
||||
Ok(exercise)
|
||||
}
|
||||
}
|
7
src/models/muscle_groups.rs
Normal file
7
src/models/muscle_groups.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use sqlx::{PgPool, prelude::FromRow};
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct MuscleGroup {
|
||||
pub muscle_group_id: String,
|
||||
pub name: String,
|
||||
}
|
8
src/models/muscles.rs
Normal file
8
src/models/muscles.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use sqlx::{PgPool, prelude::FromRow};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct Muscle {
|
||||
pub muscle_id: i16,
|
||||
pub name: String,
|
||||
}
|
9
src/models/workouts.rs
Normal file
9
src/models/workouts.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use sqlx::{PgPool, prelude::FromRow};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct Workout {
|
||||
exercise_id: Uuid,
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
use axum::Router;
|
||||
|
||||
use crate::util::AppState;
|
||||
|
||||
mod exercises;
|
||||
mod index;
|
||||
|
||||
pub fn routes() -> Router {
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.merge(index::routes())
|
||||
.nest("/exercises", exercises::routes())
|
||||
|
@@ -1,20 +1,45 @@
|
||||
use crate::layouts;
|
||||
use axum::{Router, routing::get};
|
||||
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 {
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(page))
|
||||
.nest("/new", new::routes())
|
||||
.nest("/{id}", id::routes())
|
||||
}
|
||||
|
||||
async fn page() -> Markup {
|
||||
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
let exercises = sqlx::query_as!(
|
||||
crate::models::Exercise,
|
||||
"SELECT exercise_id, name, description FROM exercises"
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 { "Exercises" }
|
||||
a href="/exercises/new" { "new exercise +" }
|
||||
ul class="list" {
|
||||
@for exercise in exercises {
|
||||
li hx-get={ "/exercises/" (exercise.exercise_id) } hx-target="body" hx-push-url="true" 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="size-[1.6em]" {
|
||||
(icons::eye())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
layouts::desktop(content, "Exercises")
|
||||
Ok(layouts::desktop(content, "Exercises"))
|
||||
}
|
||||
|
50
src/pages/exercises/id.rs
Normal file
50
src/pages/exercises/id.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{AppError, layouts, util::AppState};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
http::HeaderMap,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", get(page).delete(delete))
|
||||
}
|
||||
|
||||
async fn page(State(state): State<AppState>, Path(id): Path<Uuid>) -> Result<Markup, 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 { (exercise.name) }
|
||||
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" }
|
||||
};
|
||||
|
||||
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 exercises WHERE exercise_id = $1",
|
||||
id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("hx-location", "/exercises".parse().unwrap());
|
||||
|
||||
Ok((headers, html! {}))
|
||||
}
|
@@ -1,49 +1,56 @@
|
||||
use crate::layouts;
|
||||
use axum::{Router, routing::get};
|
||||
use crate::{AppError, layouts, util::AppState};
|
||||
use axum::{Form, Router, extract::State, http::StatusCode, routing::get};
|
||||
use maud::{Markup, html};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new().route("/", get(page))
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", get(page).post(submit))
|
||||
}
|
||||
|
||||
async fn page() -> Markup {
|
||||
async fn page(State(state): State<AppState>) -> Result<Markup, AppError> {
|
||||
let muscles = sqlx::query_as!(crate::models::MuscleGroup, "SELECT * FROM muscle_groups")
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let categories = sqlx::query_as!(crate::models::Category, "SELECT * FROM categories")
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let content = html! {
|
||||
h1 class="mb-5" { "New Exercise" }
|
||||
|
||||
forum class="space-y-1" {
|
||||
form hx-post="/exercises/new" class="space-y-1" {
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Name" }
|
||||
input class="input" {}
|
||||
input required="true" name="name" class="input" {}
|
||||
}
|
||||
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Description" }
|
||||
textarea class="textarea" {}
|
||||
textarea required="true" name="description" class="textarea" {}
|
||||
}
|
||||
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Muscle Group" }
|
||||
|
||||
label class="label" {
|
||||
input type="checkbox" checked="checked" class="checkbox" {}
|
||||
"Chest"
|
||||
}
|
||||
label class="label" {
|
||||
input type="checkbox" checked="checked" class="checkbox" {}
|
||||
"Body"
|
||||
@for muscle in muscles {
|
||||
@let name = muscle.name;
|
||||
label class="label" {
|
||||
input type="checkbox" class="checkbox" {}
|
||||
(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset class="fieldset" {
|
||||
legend class="fieldset-legend" { "Equipment" }
|
||||
legend class="fieldset-legend" { "Category" }
|
||||
|
||||
label class="label" {
|
||||
input type="checkbox" checked="checked" class="checkbox" {}
|
||||
"Weigted plates"
|
||||
}
|
||||
label class="label" {
|
||||
input type="checkbox" checked="checked" class="checkbox" {}
|
||||
"Jump Rope"
|
||||
@for category in categories {
|
||||
@let name = category.name;
|
||||
label class="label" {
|
||||
input type="checkbox" class="checkbox" {}
|
||||
(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,5 +58,33 @@ async fn page() -> Markup {
|
||||
}
|
||||
};
|
||||
|
||||
layouts::desktop(content, "New Exercise")
|
||||
Ok(layouts::desktop(content, "New Exercise"))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct FormData {
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
async fn submit(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<FormData>,
|
||||
) -> Result<Markup, AppError> {
|
||||
let exercise_id = uuid::Uuid::new_v4();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO exercises (exercise_id, name, description) VALUES ($1, $2, $3)",
|
||||
exercise_id,
|
||||
form.name,
|
||||
form.description
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(html! {
|
||||
p {
|
||||
"New exercise has been created!"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
use crate::layouts;
|
||||
use crate::{layouts, util::AppState};
|
||||
use axum::{Router, routing::get};
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", get(page))
|
||||
}
|
||||
|
||||
|
8
src/util.rs
Normal file
8
src/util.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub pool: PgPool,
|
||||
}
|
27
src/util/error.rs
Normal file
27
src/util/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub enum AppError {
|
||||
DatabaseError(sqlx::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match self {
|
||||
AppError::DatabaseError(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Error with database".to_owned(),
|
||||
),
|
||||
};
|
||||
|
||||
(status, message).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for AppError {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
Self::DatabaseError(value)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user