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