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"] } | ||||
| 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" | ||||
|  |  | |||
							
								
								
									
										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(); | ||||
| 
 | ||||
|     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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<crate::Context>) -> Html<String> { | ||||
|  | @ -30,17 +48,3 @@ async fn get_index(State(ctx): State<crate::Context>) -> Html<String> { | |||
|     context.insert("plants", &plants); | ||||
|     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> | ||||
|     <head> | ||||
|         <script src="/static/htmx_2.0.4.min.js" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script> | ||||
|     </head> | ||||
|     <body> | ||||
|         <h1>Calathea</h1> | ||||
|         <h2>Plants</h2> | ||||
|         {% include "plants_ul.html" %} | ||||
|         <h3>Add new plant</h3> | ||||
|         <form hx-post="/plants" hx-target="#plants" hx-swap="beforeend"> | ||||
|             <label for="name">Name:</label> | ||||
|             <input type="text" id="name" name="name"></br> | ||||
|             <label for="species">Species:</label> | ||||
|             <input type="text" id="species" name="species"></br> | ||||
|             <label for="description">Description:</label> | ||||
|             <textarea id="description" name="description" rows=4></textarea></br> | ||||
|             <input type="submit"> | ||||
|         </form> | ||||
|     </body> | ||||
| </html> | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
| <h1>Calathea</h1> | ||||
| <h2>Plants</h2> | ||||
| {% include "partials/plants_ul.html" %} | ||||
| <h3>Add new plant</h3> | ||||
| <form hx-post="/plants" hx-target="#plants" hx-swap="beforeend"> | ||||
|     <label for="name">Name:</label> | ||||
|     <input type="text" id="name" name="name"></br> | ||||
|     <label for="species">Species:</label> | ||||
|     <input type="text" id="species" name="species"></br> | ||||
|     <label for="description">Description:</label> | ||||
|     <textarea id="description" name="description" rows=4></textarea></br> | ||||
|     <input type="submit"> | ||||
| </form> | ||||
| 
 | ||||
| {% endblock content %} | ||||
|  |  | |||
|  | @ -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