diff --git a/Cargo.toml b/Cargo.toml index 34b1f9a..838a997 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,6 @@ r2d2_sqlite = "0.25.0" serde = { version = "1.0.217", features = ["derive"] } tera = "1.20.0" tokio = { version = "1.42.0", features = ["full"] } -tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip"] } +tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "set-header"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" diff --git a/src/db.rs b/src/db.rs index 1c71ad1..fa1f3cc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -95,3 +95,14 @@ pub fn insert_plant(pool: &DbPool, plant: &NewPlant) -> Result { })?, ) } + +pub fn get_plant(pool: &DbPool, id: u32) -> Result, DbError> { + let conn = pool.get()?; + + let mut stmt = conn.prepare("select * from plants where id = $1")?; + match stmt.query_row((id,), |row| Plant::from_row(row)) { + Ok(plant) => Ok(Some(plant)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(err) => Err(DbError::Db(err)), + } +} diff --git a/src/main.rs b/src/main.rs index c972af7..b09ca2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,9 +83,21 @@ fn load_templates() -> Tera { let mut tera = Tera::default(); tera.add_raw_templates(vec![ + ("base.html", include_str!("templates/base.html")), ("index.html", include_str!("templates/index.html")), - ("plants_ul.html", include_str!("templates/plants_ul.html")), - ("plant_li.html", include_str!("templates/plant_li.html")), + ( + "partials/plants_ul.html", + include_str!("templates/partials/plants_ul.html"), + ), + ( + "partials/plant_li.html", + include_str!("templates/partials/plant_li.html"), + ), + ("plant_page.html", include_str!("templates/plant_page.html")), + ( + "partials/plant_info.html", + include_str!("templates/partials/plant_info.html"), + ), ]) .unwrap(); diff --git a/src/server/mod.rs b/src/server/mod.rs index f81f786..0956014 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,23 +1,41 @@ +mod plants; + use axum::{ extract::State, + http::{header::VARY, HeaderMap, HeaderValue}, response::Html, - routing::{get, post}, - Form, Router, + routing::get, + Router, }; use tera::Context; +use tower_http::set_header::SetResponseHeaderLayer; use crate::db; +const HX_REQUEST_HEADER: &str = "HX-Request"; + +pub fn is_htmx_req(headers: &HeaderMap) -> bool { + headers.get(HX_REQUEST_HEADER).is_some() +} + pub fn app(ctx: crate::Context) -> axum::Router { let mut router = Router::new() .route("/", get(get_index)) - .route("/plants", post(post_plants)); + .with_state(ctx.clone()) + .nest("/plants", plants::app(ctx.clone())); for (name, content) in crate::STATIC_FILES { router = router.route(&format!("/static/{}", name), get(content)) } - router.with_state(ctx) + // Routes return either partial or full pages depending on whether the request is done using + // HTMX or just as a plain HTTP request. Adding the Vary header ensures caches don't mix + // partial and full responses. + // https://htmx.org/docs/#caching + router.layer(SetResponseHeaderLayer::appending( + VARY, + HeaderValue::from_static(HX_REQUEST_HEADER), + )) } async fn get_index(State(ctx): State) -> Html { @@ -30,17 +48,3 @@ async fn get_index(State(ctx): State) -> Html { context.insert("plants", &plants); Html(ctx.tera.render("index.html", &context).unwrap()) } - -async fn post_plants( - State(ctx): State, - Form(plant): Form, -) -> Html { - let plant = tokio::task::spawn_blocking(move || db::insert_plant(&ctx.pool, &plant)) - .await - .unwrap() - .unwrap(); - - let mut context = Context::new(); - context.insert("plant", &plant); - Html(ctx.tera.render("plant_li.html", &context).unwrap()) -} diff --git a/src/server/plants.rs b/src/server/plants.rs new file mode 100644 index 0000000..b80428a --- /dev/null +++ b/src/server/plants.rs @@ -0,0 +1,55 @@ +use axum::{ + extract::{Path, State}, + http::HeaderMap, + response::Html, + routing::{get, post}, + Form, Router, +}; +use tera::Context; + +use crate::db; + +use super::is_htmx_req; + +pub fn app(ctx: crate::Context) -> axum::Router { + Router::new() + .route("/:id", get(get_plant_page)) + .route("/", post(post_plant)) + .with_state(ctx) +} + +async fn get_plant_page( + State(ctx): State, + headers: HeaderMap, + Path(plant_id): Path, +) -> Html { + let plant = tokio::task::spawn_blocking(move || db::get_plant(&ctx.pool, plant_id)) + .await + .unwrap() + .unwrap(); + + let mut context = Context::new(); + context.insert("plant", &plant); + + let tmpl = if is_htmx_req(&headers) { + "partials/plant_info.html" + } else { + "plant_page.html" + }; + + Html(ctx.tera.render(tmpl, &context).unwrap()) +} + +async fn post_plant( + State(ctx): State, + Form(plant): Form, +) -> Html { + let plant = tokio::task::spawn_blocking(move || db::insert_plant(&ctx.pool, &plant)) + .await + .unwrap() + .unwrap(); + + let mut context = Context::new(); + context.insert("plant", &plant); + Html(ctx.tera.render("partials/plant_li.html", &context).unwrap()) +} diff --git a/src/templates/base.html b/src/templates/base.html new file mode 100644 index 0000000..4831d61 --- /dev/null +++ b/src/templates/base.html @@ -0,0 +1,12 @@ + + + + + + +
+ {% block content %} + {% endblock content %} +
+ + diff --git a/src/templates/index.html b/src/templates/index.html index b3a9274..03724f8 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -1,20 +1,19 @@ - - - - - -

Calathea

-

Plants

- {% include "plants_ul.html" %} -

Add new plant

-
- -
- -
- -
- -
- - +{% extends "base.html" %} + +{% block content %} + +

Calathea

+

Plants

+{% include "partials/plants_ul.html" %} +

Add new plant

+
+ +
+ +
+ +
+ +
+ +{% endblock content %} diff --git a/src/templates/partials/plant_info.html b/src/templates/partials/plant_info.html new file mode 100644 index 0000000..3b1d59a --- /dev/null +++ b/src/templates/partials/plant_info.html @@ -0,0 +1,6 @@ +

Calathea

+

{{ plant.name }}

+

Species

+

{{ plant.species }}

+

Description

+

{{ plant.description }}

diff --git a/src/templates/plant_li.html b/src/templates/partials/plant_li.html similarity index 100% rename from src/templates/plant_li.html rename to src/templates/partials/plant_li.html diff --git a/src/templates/partials/plants_ul.html b/src/templates/partials/plants_ul.html new file mode 100644 index 0000000..251ad9b --- /dev/null +++ b/src/templates/partials/plants_ul.html @@ -0,0 +1,7 @@ +
    + {% for plant in plants %} +
  • + {{ plant.name }} ({{ plant.species }}) +
  • + {% endfor %} +
diff --git a/src/templates/plant_page.html b/src/templates/plant_page.html new file mode 100644 index 0000000..0fb41ef --- /dev/null +++ b/src/templates/plant_page.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block content %} + +{% include "partials/plant_info.html" %} + +{% endblock content %} diff --git a/src/templates/plants_ul.html b/src/templates/plants_ul.html deleted file mode 100644 index 952defc..0000000 --- a/src/templates/plants_ul.html +++ /dev/null @@ -1,5 +0,0 @@ -
    - {% for plant in plants %} -
  • {{ plant.name }} ({{ plant.species }})
  • - {% endfor %} -