feat: getting the hang of htmx

image-uploads
Jef Roosens 2024-12-29 13:28:49 +01:00
parent f96bf4d193
commit 4c84c944d4
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
12 changed files with 153 additions and 46 deletions

View File

@ -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"

View File

@ -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)),
}
}

View File

@ -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();

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
{% include "partials/plant_info.html" %}
{% endblock content %}

View File

@ -1,5 +0,0 @@
<ul id="plants">
{% for plant in plants %}
<li>{{ plant.name }} ({{ plant.species }})</li>
{% endfor %}
</ul>