diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e117cd --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Calathea + +## Templates and rendering + +Calathea uses the [Tera](https://keats.github.io/tera/) templating engine. To +combine Tera and HTMX, the template directory has the following structure: + +* `components` contains components used to build views. Components are defined + as Tera [macros](https://keats.github.io/tera/docs/#macros). +* `views` contains view templates that make use of the components. +* `updates` are templates returned from e.g. POST requests. Tbey usually + directly wrap a component, e.g. a plant `li` item. +* `base.html` defines the skeleton for each page. Each view is rendered with + this as its surrounding HTML. + +When a page is requested from the server, it inspects the `HX-Request` and +`HX-History-Restore-Request` headers to determine whether a full page should be +returned or only the inner view. Using this, the server cn support direct +routing to pages and caching properly while still supporting partial rendering +using HTMX. diff --git a/src/main.rs b/src/main.rs index da92aa0..beba981 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ async fn main() { let pool = r2d2::Pool::new(manager).unwrap(); db::run_migrations(&pool, &MIGRATIONS).unwrap(); - let tera = load_templates(); + let tera = Tera::new("templates/**/*").unwrap(); let ctx = Context { pool, tera: Arc::new(tera), @@ -48,40 +48,3 @@ async fn main() { .await .unwrap(); } - -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")), - ( - "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"), - ), - ( - "partials/comment_li.html", - include_str!("templates/partials/comment_li.html"), - ), - ( - "partials/event_li.html", - include_str!("templates/partials/event_li.html"), - ), - ( - "macros/event.html", - include_str!("templates/macros/event.html"), - ), - ]) - .unwrap(); - - tera -} diff --git a/src/server/comments.rs b/src/server/comments.rs index c5f7f9d..fb2510a 100644 --- a/src/server/comments.rs +++ b/src/server/comments.rs @@ -17,5 +17,5 @@ async fn post_comment( let mut context = Context::new(); context.insert("comment", &comment); - Ok(Html(ctx.tera.render("partials/comment_li.html", &context)?)) + Ok(Html(ctx.tera.render("updates/comment_li.html", &context)?)) } diff --git a/src/server/events.rs b/src/server/events.rs index ed916f5..ba34300 100644 --- a/src/server/events.rs +++ b/src/server/events.rs @@ -17,5 +17,5 @@ async fn post_event( let mut context = Context::new(); context.insert("event", &event); - Ok(Html(ctx.tera.render("partials/event_li.html", &context)?)) + Ok(Html(ctx.tera.render("updates/event_li.html", &context)?)) } diff --git a/src/server/mod.rs b/src/server/mod.rs index 3879144..16c31de 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -20,14 +20,32 @@ pub type Result = std::result::Result; const HX_REQUEST_HEADER: &str = "HX-Request"; const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request"; -pub fn render_partial(headers: &HeaderMap) -> bool { +pub fn should_render_full(headers: &HeaderMap) -> bool { let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some(); let is_hist_restore_req = headers .get(HX_HISTORY_RESTORE_HEADER) .map(|val| val == HeaderValue::from_static("true")) .unwrap_or(false); - is_htmx_req && !is_hist_restore_req + !is_htmx_req || is_hist_restore_req +} + +pub fn render_view( + tera: &tera::Tera, + view: &str, + ctx: &tera::Context, + headers: &HeaderMap, +) -> tera::Result { + let view = tera.render(view, ctx)?; + + if should_render_full(headers) { + let mut ctx = tera::Context::new(); + ctx.insert("view", &view); + + tera.render("base.html", &ctx) + } else { + Ok(view) + } } pub fn app(ctx: crate::Context) -> axum::Router { @@ -52,12 +70,17 @@ pub fn app(ctx: crate::Context) -> axum::Router { )) } -async fn get_index(State(ctx): State) -> Result> { +async fn get_index(State(ctx): State, headers: HeaderMap) -> Result> { let plants = tokio::task::spawn_blocking(move || Plant::all(&ctx.pool)) .await .unwrap()?; let mut context = Context::new(); context.insert("plants", &plants); - Ok(Html(ctx.tera.render("index.html", &context)?)) + Ok(Html(render_view( + &ctx.tera, + "views/index.html", + &context, + &headers, + )?)) } diff --git a/src/server/plants.rs b/src/server/plants.rs index f736cc7..99515de 100644 --- a/src/server/plants.rs +++ b/src/server/plants.rs @@ -9,7 +9,7 @@ use tera::Context; use crate::db::{self, DbError, Plant}; -use super::{error::AppError, render_partial}; +use super::error::AppError; pub fn app() -> axum::Router { Router::new() @@ -45,13 +45,12 @@ async fn get_plant_page( context.insert("events", &events); context.insert("event_types", &db::EVENT_TYPES); - let tmpl = if render_partial(&headers) { - "partials/plant_info.html" - } else { - "plant_page.html" - }; - - Ok(Html(ctx.tera.render(tmpl, &context)?)) + Ok(Html(super::render_view( + &ctx.tera, + "views/plant.html", + &context, + &headers, + )?)) } None => Err(AppError::NotFound), } @@ -67,5 +66,5 @@ async fn post_plant( let mut context = Context::new(); context.insert("plant", &plant); - Ok(Html(ctx.tera.render("partials/plant_li.html", &context)?)) + Ok(Html(ctx.tera.render("updates/plant_li.html", &context)?)) } diff --git a/src/templates/index.html b/src/templates/index.html deleted file mode 100644 index 03724f8..0000000 --- a/src/templates/index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -

Calathea

-

Plants

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

Add new plant

-
- -
- -
- -
- -
- -{% endblock content %} diff --git a/src/templates/partials/comment_li.html b/src/templates/partials/comment_li.html deleted file mode 100644 index af278d6..0000000 --- a/src/templates/partials/comment_li.html +++ /dev/null @@ -1 +0,0 @@ -
  • {{ comment.comment }}
  • diff --git a/src/templates/partials/event_li.html b/src/templates/partials/event_li.html deleted file mode 100644 index e72babc..0000000 --- a/src/templates/partials/event_li.html +++ /dev/null @@ -1,2 +0,0 @@ -{% import "macros/event.html" as macros_event %} -{{ macros_event::li(event=event) }} diff --git a/src/templates/partials/plant_info.html b/src/templates/partials/plant_info.html deleted file mode 100644 index 7f19f26..0000000 --- a/src/templates/partials/plant_info.html +++ /dev/null @@ -1,31 +0,0 @@ -{% import "macros/event.html" as macros_event %} - -

    Calathea

    -

    {{ plant.name }}

    -

    Species

    -

    {{ plant.species }}

    -

    Description

    -

    {{ plant.description }}

    -

    Events

    -
    -
      - {% for event in events %} - {{ macros_event::li(event=event) }} - {% endfor %} -
    - {{ macros_event::form(plant_id=plant.id) }} -
    -

    Comments

    -
      -{% for comment in comments %} -
    • - {{ comment.comment }} -
    • -{% endfor %} -
    -
    - - -
    - -
    diff --git a/src/templates/partials/plant_li.html b/src/templates/partials/plant_li.html deleted file mode 100644 index 6ddb100..0000000 --- a/src/templates/partials/plant_li.html +++ /dev/null @@ -1 +0,0 @@ -
  • {{ plant.name }} ({{ plant.species }})
  • diff --git a/src/templates/partials/plants_ul.html b/src/templates/partials/plants_ul.html deleted file mode 100644 index 251ad9b..0000000 --- a/src/templates/partials/plants_ul.html +++ /dev/null @@ -1,7 +0,0 @@ -
      - {% for plant in plants %} -
    • - {{ plant.name }} ({{ plant.species }}) -
    • - {% endfor %} -
    diff --git a/src/templates/plant_page.html b/src/templates/plant_page.html deleted file mode 100644 index cf2133a..0000000 --- a/src/templates/plant_page.html +++ /dev/null @@ -1,8 +0,0 @@ -{% import "macros/event.html" as macros_event %} - -{% extends "base.html" %} -{% block content %} - -{% include "partials/plant_info.html" %} - -{% endblock content %} diff --git a/src/templates/base.html b/templates/base.html similarity index 61% rename from src/templates/base.html rename to templates/base.html index 4831d61..e117f06 100644 --- a/src/templates/base.html +++ b/templates/base.html @@ -4,9 +4,11 @@ -
    - {% block content %} - {% endblock content %} -
    +
    +

    Calathea

    +
    + {{ view | safe }} +
    +
    diff --git a/templates/components/comment.html b/templates/components/comment.html new file mode 100644 index 0000000..1b83844 --- /dev/null +++ b/templates/components/comment.html @@ -0,0 +1,22 @@ +{% macro li(comment) %} +
  • {{ comment.comment }}
  • +{% endmacro li %} + +{% macro list(comments) %} +
    +
      + {% for comment in comments %} + {{ self::li(comment=comment) }} + {% endfor %} +
    +
    +{% endmacro list %} + +{% macro form(plant_id, target="#comments > ul") %} +
    + + +
    + +
    +{% endmacro form %} diff --git a/src/templates/macros/event.html b/templates/components/event.html similarity index 78% rename from src/templates/macros/event.html rename to templates/components/event.html index a188c67..4c8f470 100644 --- a/src/templates/macros/event.html +++ b/templates/components/event.html @@ -8,7 +8,17 @@ {% endmacro li %} -{% macro form(plant_id, target="#events_ul") %} +{% macro list(events) %} +
    +
      + {% for event in events %} + {{ self::li(event=event) }} + {% endfor %} +
    +
    +{% endmacro ul %} + +{% macro form(plant_id, target="#events > ul") %}
    diff --git a/templates/components/plant.html b/templates/components/plant.html new file mode 100644 index 0000000..32432b0 --- /dev/null +++ b/templates/components/plant.html @@ -0,0 +1,37 @@ +{% macro info(plant) %} +
    +

    {{ plant.name }}

    +

    Species

    +

    {{ plant.species }}

    +

    Description

    +

    {{ plant.description }}

    +
    +{% endmacro info %} + +{% macro li(plant) %} +
  • + {{ plant.name }} ({{ plant.species }}) +
  • +{% endmacro li %} + +{% macro list(plants) %} +
    +
      + {% for plant in plants %} + {{ self::li(plant=plant) }} + {% endfor %} +
    +
    +{% endmacro %} + +{% macro form(target="#plants > ul") %} + + +
    + +
    + +
    + +
    +{% endmacro %} diff --git a/templates/updates/comment_li.html b/templates/updates/comment_li.html new file mode 100644 index 0000000..c8d15af --- /dev/null +++ b/templates/updates/comment_li.html @@ -0,0 +1,2 @@ +{% import "components/comment.html" as comp_comment %} +{{ comp_comment::li(comment=comment) }} diff --git a/templates/updates/event_li.html b/templates/updates/event_li.html new file mode 100644 index 0000000..1395f6f --- /dev/null +++ b/templates/updates/event_li.html @@ -0,0 +1,2 @@ +{% import "components/event.html" as comp_event %} +{{ comp_event::li(event=event) }} diff --git a/templates/updates/plant_li.html b/templates/updates/plant_li.html new file mode 100644 index 0000000..92e6288 --- /dev/null +++ b/templates/updates/plant_li.html @@ -0,0 +1,2 @@ +{% import "components/plant.html" as comp_plant %} +{{ comp_plant::li(plant=plant) }} diff --git a/templates/views/index.html b/templates/views/index.html new file mode 100644 index 0000000..c2d60f6 --- /dev/null +++ b/templates/views/index.html @@ -0,0 +1,6 @@ +{% import "components/plant.html" as comp_plant %} + +

    Plants

    +{{ comp_plant::list(plants=plants) }} +

    Add new plant

    +{{ comp_plant::form() }} diff --git a/templates/views/plant.html b/templates/views/plant.html new file mode 100644 index 0000000..2968584 --- /dev/null +++ b/templates/views/plant.html @@ -0,0 +1,11 @@ +{% import "components/event.html" as comp_event %} +{% import "components/plant.html" as comp_plant %} +{% import "components/comment.html" as comp_comment %} + +{{ comp_plant::info(plant=plant) }} +

    Events

    +{{ comp_event::list(events=events) }} +{{ comp_event::form(plant_id=plant.id) }} +

    Comments

    +{{ comp_comment::list(comments=comments) }} +{{ comp_comment::form(plant_id=plant.id) }}