feat: getting the hang of htmx
							parent
							
								
									f96bf4d193
								
							
						
					
					
						commit
						4c84c944d4
					
				| 
						 | 
					@ -11,6 +11,6 @@ r2d2_sqlite = "0.25.0"
 | 
				
			||||||
serde = { version = "1.0.217", features = ["derive"] }
 | 
					serde = { version = "1.0.217", features = ["derive"] }
 | 
				
			||||||
tera = "1.20.0"
 | 
					tera = "1.20.0"
 | 
				
			||||||
tokio = { version = "1.42.0", features = ["full"] }
 | 
					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 = "0.1.41"
 | 
				
			||||||
tracing-subscriber = "0.3.19"
 | 
					tracing-subscriber = "0.3.19"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/db.rs
								
								
								
								
							
							
						
						
									
										11
									
								
								src/db.rs
								
								
								
								
							| 
						 | 
					@ -95,3 +95,14 @@ pub fn insert_plant(pool: &DbPool, plant: &NewPlant) -> Result<Plant, DbError> {
 | 
				
			||||||
        })?,
 | 
					        })?,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn get_plant(pool: &DbPool, id: u32) -> Result<Option<Plant>, 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)),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										16
									
								
								src/main.rs
								
								
								
								
							| 
						 | 
					@ -83,9 +83,21 @@ fn load_templates() -> Tera {
 | 
				
			||||||
    let mut tera = Tera::default();
 | 
					    let mut tera = Tera::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tera.add_raw_templates(vec![
 | 
					    tera.add_raw_templates(vec![
 | 
				
			||||||
 | 
					        ("base.html", include_str!("templates/base.html")),
 | 
				
			||||||
        ("index.html", include_str!("templates/index.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();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,41 @@
 | 
				
			||||||
 | 
					mod plants;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use axum::{
 | 
					use axum::{
 | 
				
			||||||
    extract::State,
 | 
					    extract::State,
 | 
				
			||||||
 | 
					    http::{header::VARY, HeaderMap, HeaderValue},
 | 
				
			||||||
    response::Html,
 | 
					    response::Html,
 | 
				
			||||||
    routing::{get, post},
 | 
					    routing::get,
 | 
				
			||||||
    Form, Router,
 | 
					    Router,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use tera::Context;
 | 
					use tera::Context;
 | 
				
			||||||
 | 
					use tower_http::set_header::SetResponseHeaderLayer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::db;
 | 
					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 {
 | 
					pub fn app(ctx: crate::Context) -> axum::Router {
 | 
				
			||||||
    let mut router = Router::new()
 | 
					    let mut router = Router::new()
 | 
				
			||||||
        .route("/", get(get_index))
 | 
					        .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 {
 | 
					    for (name, content) in crate::STATIC_FILES {
 | 
				
			||||||
        router = router.route(&format!("/static/{}", name), get(content))
 | 
					        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<crate::Context>) -> Html<String> {
 | 
					async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
 | 
				
			||||||
| 
						 | 
					@ -30,17 +48,3 @@ async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
 | 
				
			||||||
    context.insert("plants", &plants);
 | 
					    context.insert("plants", &plants);
 | 
				
			||||||
    Html(ctx.tera.render("index.html", &context).unwrap())
 | 
					    Html(ctx.tera.render("index.html", &context).unwrap())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn post_plants(
 | 
					 | 
				
			||||||
    State(ctx): State<crate::Context>,
 | 
					 | 
				
			||||||
    Form(plant): Form<db::NewPlant>,
 | 
					 | 
				
			||||||
) -> Html<String> {
 | 
					 | 
				
			||||||
    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())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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<crate::Context>,
 | 
				
			||||||
 | 
					    headers: HeaderMap,
 | 
				
			||||||
 | 
					    Path(plant_id): Path<u32>,
 | 
				
			||||||
 | 
					) -> Html<String> {
 | 
				
			||||||
 | 
					    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<crate::Context>,
 | 
				
			||||||
 | 
					    Form(plant): Form<db::NewPlant>,
 | 
				
			||||||
 | 
					) -> Html<String> {
 | 
				
			||||||
 | 
					    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())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <script src="/static/htmx_2.0.4.min.js" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        <div id="content">
 | 
				
			||||||
 | 
					            {% block content %}
 | 
				
			||||||
 | 
					            {% endblock content %}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,19 @@
 | 
				
			||||||
<html>
 | 
					{% extends "base.html" %}
 | 
				
			||||||
    <head>
 | 
					
 | 
				
			||||||
        <script src="/static/htmx_2.0.4.min.js" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
 | 
					{% block content %}
 | 
				
			||||||
    </head>
 | 
					
 | 
				
			||||||
    <body>
 | 
					<h1>Calathea</h1>
 | 
				
			||||||
        <h1>Calathea</h1>
 | 
					<h2>Plants</h2>
 | 
				
			||||||
        <h2>Plants</h2>
 | 
					{% include "partials/plants_ul.html" %}
 | 
				
			||||||
        {% include "plants_ul.html" %}
 | 
					<h3>Add new plant</h3>
 | 
				
			||||||
        <h3>Add new plant</h3>
 | 
					<form hx-post="/plants" hx-target="#plants" hx-swap="beforeend">
 | 
				
			||||||
        <form hx-post="/plants" hx-target="#plants" hx-swap="beforeend">
 | 
					    <label for="name">Name:</label>
 | 
				
			||||||
            <label for="name">Name:</label>
 | 
					    <input type="text" id="name" name="name"></br>
 | 
				
			||||||
            <input type="text" id="name" name="name"></br>
 | 
					    <label for="species">Species:</label>
 | 
				
			||||||
            <label for="species">Species:</label>
 | 
					    <input type="text" id="species" name="species"></br>
 | 
				
			||||||
            <input type="text" id="species" name="species"></br>
 | 
					    <label for="description">Description:</label>
 | 
				
			||||||
            <label for="description">Description:</label>
 | 
					    <textarea id="description" name="description" rows=4></textarea></br>
 | 
				
			||||||
            <textarea id="description" name="description" rows=4></textarea></br>
 | 
					    <input type="submit">
 | 
				
			||||||
            <input type="submit">
 | 
					</form>
 | 
				
			||||||
        </form>
 | 
					
 | 
				
			||||||
    </body>
 | 
					{% endblock content %}
 | 
				
			||||||
</html>
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					<h1>Calathea</h1>
 | 
				
			||||||
 | 
					<h2>{{ plant.name }}</h2>
 | 
				
			||||||
 | 
					<h3>Species</h3>
 | 
				
			||||||
 | 
					<p>{{ plant.species }}</p>
 | 
				
			||||||
 | 
					<h3>Description</h3>
 | 
				
			||||||
 | 
					<p>{{ plant.description }}</p>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					<ul id="plants">
 | 
				
			||||||
 | 
					    {% for plant in plants %}
 | 
				
			||||||
 | 
					    <li>
 | 
				
			||||||
 | 
					        <a hx-get="/plants/{{ plant.id }}" hx-target="#content" hx-push-url="true">{{ plant.name }}</a> ({{ plant.species }})
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					</ul>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					{% extends "base.html" %}
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% include "partials/plant_info.html" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% endblock content %}
 | 
				
			||||||
| 
						 | 
					@ -1,5 +0,0 @@
 | 
				
			||||||
<ul id="plants">
 | 
					 | 
				
			||||||
    {% for plant in plants %}
 | 
					 | 
				
			||||||
    <li>{{ plant.name }} ({{ plant.species }})</li>
 | 
					 | 
				
			||||||
    {% endfor %}
 | 
					 | 
				
			||||||
</ul>
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue