feat: shifts
Oh boy what a mess this was lmao, I definitely should have done this is more atomic commits But it could also be argued that very little of this is standalone
This commit is contained in:
parent
1515294500
commit
96dd76e463
18 changed files with 1860 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.env
|
||||
|
|
1606
Cargo.lock
generated
1606
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -4,6 +4,10 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "cm_lib"
|
||||
path = "src/lib/mod.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.20"
|
||||
|
@ -14,6 +18,12 @@ tracing = "0.1"
|
|||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tower-http = { version = "0.4.4", features = ["full"] }
|
||||
ructe.workspace = true
|
||||
tokio-stream = "0.1.14"
|
||||
futures-util = "0.3.28"
|
||||
diesel = { version = "2.1.0", features = ["chrono"] }
|
||||
diesel-async = { version = "0.3.1", features = ["mysql", "deadpool"] }
|
||||
dotenvy = "0.15"
|
||||
chrono = "0.4.31"
|
||||
|
||||
[build-dependencies]
|
||||
ructe.workspace = true
|
||||
|
|
9
diesel.toml
Normal file
9
diesel.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/lib/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
|
@ -0,0 +1,24 @@
|
|||
# This DC is just to get a dev mysql db, nothing else.
|
||||
# Use root/example as user/password credentials
|
||||
version: '3.1'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: mysql:8.0.34
|
||||
restart: always
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
MYSQL_HOST: "%"
|
||||
MYSQL_USER: clubmanager
|
||||
MYSQL_PASSWORD: clubmanager
|
||||
MYSQL_DATABASE: clubmanager
|
||||
MYSQL_ROOT_PASSWORD: admin
|
||||
TZ: America/Los_Angeles
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
4
example.env
Normal file
4
example.env
Normal file
|
@ -0,0 +1,4 @@
|
|||
# This defines the mysql db url that clubmanager will use
|
||||
# The default shown here is what the docker-compose.yml will generate
|
||||
# Please don't use it for prod
|
||||
DATABASE_URL=mysql://clubmanager:clubmanager@127.0.0.1/clubmanager
|
2
migrations/2023-09-24-104018_create_shifts/down.sql
Normal file
2
migrations/2023-09-24-104018_create_shifts/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE shifts
|
6
migrations/2023-09-24-104018_create_shifts/up.sql
Normal file
6
migrations/2023-09-24-104018_create_shifts/up.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE shifts
|
||||
(
|
||||
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
start DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
end DATETIME DEFAULT NULL
|
||||
)
|
5
mprocs.yaml
Normal file
5
mprocs.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
procs:
|
||||
server:
|
||||
shell: "cargo watch -x run"
|
||||
docker:
|
||||
shell: "docker compose up"
|
68
src/api.rs
Normal file
68
src/api.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use axum::{extract::State, response::IntoResponse, routing::post};
|
||||
use cm_lib::models::Shift;
|
||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
|
||||
use diesel_async::{scoped_futures::ScopedFutureExt, AsyncConnection, RunQueryDsl};
|
||||
|
||||
use crate::axum_ructe::render;
|
||||
use crate::AppState;
|
||||
|
||||
pub(crate) fn router() -> axum::Router<AppState> {
|
||||
axum::Router::new()
|
||||
.route("/shifts/open", post(open_shift))
|
||||
.route("/shifts/close", post(close_shift))
|
||||
}
|
||||
|
||||
async fn open_shift(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let shift = {
|
||||
let mut conn = state.connection.get().await.unwrap();
|
||||
conn.transaction(|conn| {
|
||||
use cm_lib::schema::shifts::dsl::*;
|
||||
|
||||
async move {
|
||||
diesel::insert_into(shifts)
|
||||
.default_values()
|
||||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
shifts
|
||||
.order(id.desc())
|
||||
.select(Shift::as_select())
|
||||
.first(conn)
|
||||
.await
|
||||
}
|
||||
.scope_boxed()
|
||||
})
|
||||
.await
|
||||
.optional()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
render!(crate::templates::home_html, shift)
|
||||
}
|
||||
|
||||
async fn close_shift(State(state): State<AppState>) -> impl IntoResponse {
|
||||
{
|
||||
let mut conn = state.connection.get().await.unwrap();
|
||||
conn.transaction(|conn| {
|
||||
use cm_lib::schema::shifts::dsl::*;
|
||||
|
||||
async move {
|
||||
let open_shift = shifts
|
||||
.filter(end.is_null())
|
||||
.select(Shift::as_select())
|
||||
.first(conn)
|
||||
.await?;
|
||||
|
||||
diesel::update(shifts.filter(id.eq(open_shift.id)))
|
||||
.set(end.eq(Some(chrono::Utc::now().naive_local())))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
.scope_boxed()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
render!(crate::templates::home_html, None)
|
||||
}
|
2
src/lib/mod.rs
Normal file
2
src/lib/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod models;
|
||||
pub mod schema;
|
10
src/lib/models.rs
Normal file
10
src/lib/models.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use diesel::prelude::*;
|
||||
|
||||
#[derive(Queryable, Selectable, Debug)]
|
||||
#[diesel(table_name = crate::schema::shifts)]
|
||||
#[diesel(check_for_backend(diesel::mysql::Mysql))]
|
||||
pub struct Shift {
|
||||
pub id: u32,
|
||||
pub start: chrono::NaiveDateTime,
|
||||
pub end: Option<chrono::NaiveDateTime>,
|
||||
}
|
9
src/lib/schema.rs
Normal file
9
src/lib/schema.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
shifts (id) {
|
||||
id -> Unsigned<Integer>,
|
||||
start -> Datetime,
|
||||
end -> Nullable<Datetime>,
|
||||
}
|
||||
}
|
87
src/main.rs
87
src/main.rs
|
@ -1,19 +1,54 @@
|
|||
mod api;
|
||||
mod axum_ructe;
|
||||
|
||||
use axum_ructe::render;
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use cm_lib::models::Shift;
|
||||
use diesel::result::OptionalExtension;
|
||||
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
|
||||
use diesel_async::{
|
||||
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
|
||||
AsyncMysqlConnection, RunQueryDsl,
|
||||
};
|
||||
use dotenvy::dotenv;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
||||
async fn establish_connection() -> Pool<AsyncMysqlConnection> {
|
||||
dotenv().ok();
|
||||
|
||||
let database_url = std::env::var("DATABASE_URL").expect("You must set DATABASE_URL");
|
||||
let config = AsyncDieselConnectionManager::<AsyncMysqlConnection>::new(database_url);
|
||||
|
||||
Pool::builder(config)
|
||||
.build()
|
||||
.expect("Error making connection pool")
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppState {
|
||||
#[allow(dead_code)]
|
||||
connection: Pool<AsyncMysqlConnection>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn init() -> Self {
|
||||
Self {
|
||||
connection: establish_connection().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// initialize tracing
|
||||
|
@ -26,10 +61,38 @@ async fn main() {
|
|||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let state = AppState::init().await;
|
||||
|
||||
// let shift = {
|
||||
// let mut conn = state.connection.get().await.unwrap();
|
||||
// conn.transaction(|conn| {
|
||||
// use cm_lib::schema::shifts;
|
||||
//
|
||||
// async move {
|
||||
// diesel::insert_into(shifts::table)
|
||||
// .default_values()
|
||||
// .execute(conn)
|
||||
// .await?;
|
||||
//
|
||||
// shifts::table
|
||||
// .order(shifts::id.desc())
|
||||
// .select(Shift::as_select())
|
||||
// .first(conn)
|
||||
// .await
|
||||
// }
|
||||
// .scope_boxed()
|
||||
// })
|
||||
// .await
|
||||
// };
|
||||
|
||||
// tracing::debug!("{shift:?}");
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.nest("/api", api::router())
|
||||
.route("/", get(root))
|
||||
.route("/users", post(create_user));
|
||||
.route("/users", post(create_user))
|
||||
.with_state(state);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
@ -42,8 +105,26 @@ async fn main() {
|
|||
}
|
||||
|
||||
// basic handler that responds with a static string
|
||||
async fn root() -> impl IntoResponse {
|
||||
render!(templates::home_html)
|
||||
async fn root(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let mut conn = state.connection.get().await.unwrap();
|
||||
|
||||
let open_shift: Option<Shift> = {
|
||||
use cm_lib::schema::shifts::dsl::*;
|
||||
|
||||
shifts
|
||||
.filter(end.is_null())
|
||||
.select(Shift::as_select())
|
||||
.first(&mut conn)
|
||||
.await
|
||||
.optional()
|
||||
.expect("Query failed")
|
||||
};
|
||||
|
||||
tracing::debug!("{open_shift:?}");
|
||||
|
||||
// let is_open_shift = open_shift.is_some();
|
||||
|
||||
render!(templates::home_html, open_shift)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
|
|
|
@ -19,15 +19,14 @@
|
|||
<!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> -->
|
||||
|
||||
<script src="https://unpkg.com/htmx.org@@1.9.5"></script>
|
||||
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
|
||||
|
||||
<!-- <link rel="manifest" href="site.webmanifest"> -->
|
||||
<meta name="theme-color" content="#fafafa">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
@:body()
|
||||
</main>
|
||||
@:body()
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
5
templates/components/close_shift_button.rs.html
Normal file
5
templates/components/close_shift_button.rs.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
@()
|
||||
|
||||
<button hx-post="/api/shifts/close" hx-swap="outerHTML" type="button">
|
||||
Close Shift
|
||||
</button>
|
5
templates/components/open_shift_button.rs.html
Normal file
5
templates/components/open_shift_button.rs.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
@()
|
||||
|
||||
<button hx-post="/api/shifts/open" hx-swap="outerHTML" type="button">
|
||||
Open Shift
|
||||
</button>
|
|
@ -1,9 +1,20 @@
|
|||
@use super::base_html;
|
||||
@use super::components::close_shift_button_html;
|
||||
@use super::components::open_shift_button_html;
|
||||
@use cm_lib::models::Shift;
|
||||
|
||||
@()
|
||||
@(open_shift: Option<Shift>)
|
||||
|
||||
@:base_html({
|
||||
|
||||
<h1>Welcome to clubmanager!</h1>
|
||||
<main>
|
||||
<div>
|
||||
@if open_shift.is_none() {
|
||||
@:open_shift_button_html()
|
||||
} else {
|
||||
@:close_shift_button_html()
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue