diff --git a/src/server/events.rs b/src/server/events.rs index d23d539..d93cda2 100644 --- a/src/server/events.rs +++ b/src/server/events.rs @@ -20,5 +20,5 @@ async fn post_event( let mut context = Context::new(); context.insert("event", &event); - Ok(Html(Update::EventLi.render(&ctx.tera, &context)?)) + Ok(Html(Update::EventLi.render_ctx(&ctx.tera, &context)?)) } diff --git a/src/server/images.rs b/src/server/images.rs index bd9203b..6aff978 100644 --- a/src/server/images.rs +++ b/src/server/images.rs @@ -79,7 +79,9 @@ async fn get_images( context.insert("is_final_page", &is_final_page); Ok(Html( - View::Images.headers(&headers).render(&ctx.tera, &context)?, + View::Images + .headers(&headers) + .render_ctx(&ctx.tera, &context)?, )) } diff --git a/src/server/mod.rs b/src/server/mod.rs index bb99d8e..bd7adc9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -60,7 +60,9 @@ pub async fn render_home(ctx: crate::Context, headers: &HeaderMap) -> Result Result axum::Router { Router::new() @@ -47,7 +47,9 @@ async fn get_plant_page( context.insert("event_types", &db::EVENT_TYPES); Ok(Html( - View::Plant.headers(&headers).render(&ctx.tera, &context)?, + View::Plant + .headers(&headers) + .render_ctx(&ctx.tera, &context)?, )) } None => Err(AppError::NotFound), @@ -56,28 +58,17 @@ async fn get_plant_page( async fn get_plants( State(ctx): State, - Query(mut page): Query, + Query(page): Query, headers: HeaderMap, ) -> super::Result> { let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page)) .await .unwrap()?; - let mut context = Context::new(); - context.insert("plants", &plants); - - let is_final_page = plants.len() < page.per_page.try_into().unwrap(); - context.insert("is_final_page", &is_final_page); - - if !is_final_page { - page.page += 1; - - context.insert("query", &page.to_query().encode()); - } - - Ok(Html( - View::Plants.headers(&headers).render(&ctx.tera, &context)?, - )) + let list = List::Ul + .items("Plants", ListItem::Plant, plants) + .page("/plants", page, None); + Ok(Html(View::other(list).headers(&headers).render(&ctx.tera)?)) } async fn post_plant( @@ -90,5 +81,5 @@ async fn post_plant( let mut context = Context::new(); context.insert("plant", &plant); - Ok(Html(Update::PlantLi.render(&ctx.tera, &context)?)) + Ok(Html(Update::PlantLi.render_ctx(&ctx.tera, &context)?)) } diff --git a/src/server/query.rs b/src/server/query.rs index 5f66853..2f94ec7 100644 --- a/src/server/query.rs +++ b/src/server/query.rs @@ -18,3 +18,9 @@ impl Query { pub trait ToQuery { fn to_query(self) -> Query; } + +impl ToQuery for Query { + fn to_query(self) -> Query { + self + } +} diff --git a/src/template/list.rs b/src/template/list.rs new file mode 100644 index 0000000..5da2df3 --- /dev/null +++ b/src/template/list.rs @@ -0,0 +1,108 @@ +use serde::Serialize; +use tera::Context; + +use crate::{ + db::Pagination, + server::query::{Query, ToQuery}, +}; + +use super::Template; + +#[derive(Clone, Copy)] +pub enum List { + Ul, +} + +pub enum ListItem { + Plant, +} + +impl Template for List { + fn template(&self) -> &'static str { + match self { + List::Ul => "list/ul.html", + } + } +} + +impl Template for ListItem { + fn template(&self) -> &'static str { + match self { + Self::Plant => "li/plant.html", + } + } +} + +impl List { + pub fn items( + self, + heading: &'static str, + item_tmpl: T, + items: Vec, + ) -> ListWrapper { + ListWrapper { + list: self, + heading, + item_tmpl, + items, + next_page_url: None, + } + } +} + +pub struct ListWrapper { + list: List, + heading: &'static str, + item_tmpl: T, + items: Vec, + next_page_url: Option, +} + +impl ListWrapper { + pub fn page(mut self, url: &'static str, mut page: Pagination, query: Option) -> Self { + if self.items.len() as u32 == page.per_page { + page.page += 1; + + let query = if let Some(query) = query { + query.join(page) + } else { + page.to_query() + }; + + self.next_page_url = Some(format!("{url}?{}", query.encode())); + } + + self + } +} + +impl Template for ListWrapper { + fn template(&self) -> &'static str { + self.list.template() + } + + fn render(&self, tera: &tera::Tera) -> tera::Result { + let mut ctx = Context::new(); + + // Render each item separately + let items = self + .items + .iter() + .map(|item| { + ctx.insert("item", &item); + + tera.render(self.item_tmpl.template(), &ctx) + }) + .collect::>>()?; + + ctx.remove("item"); + ctx.insert("heading", &self.heading); + ctx.insert("items", &items); + + if let Some(next_page_url) = &self.next_page_url { + ctx.insert("next_page_url", &next_page_url); + } + + self.render_ctx(tera, &ctx) + } +} diff --git a/src/template/mod.rs b/src/template/mod.rs index 757dbcc..868a44a 100644 --- a/src/template/mod.rs +++ b/src/template/mod.rs @@ -1,6 +1,8 @@ +mod list; mod update; mod view; +pub use list::{List, ListItem}; pub use update::Update; pub use view::View; @@ -9,7 +11,15 @@ pub trait Template { fn template(&self) -> &'static str; /// Render the template with the given context - fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result { + fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result { tera.render(self.template(), ctx) } + + /// Render the template without providing a context. This is useful for types that manage their + /// own context. + /// + /// By default, this simply renders the template with an empty context. + fn render(&self, tera: &tera::Tera) -> tera::Result { + self.render_ctx(tera, &tera::Context::new()) + } } diff --git a/src/template/view.rs b/src/template/view.rs index 6d06da2..f591ad7 100644 --- a/src/template/view.rs +++ b/src/template/view.rs @@ -1,20 +1,24 @@ use axum::http::{HeaderMap, HeaderValue}; +use tera::Context; use super::Template; const HX_REQUEST_HEADER: &str = "HX-Request"; const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request"; -#[derive(Clone, Copy)] pub enum View { Plant, - Plants, Images, Index, Login, + Other(Box), } impl View { + pub fn other(tmpl: impl Template + 'static) -> Self { + View::Other(Box::new(tmpl)) + } + pub fn headers(self, headers: &HeaderMap) -> ViewWrapper { let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some(); let is_hist_restore_req = headers @@ -35,10 +39,24 @@ impl Template for View { fn template(&self) -> &'static str { match self { View::Plant => "views/plant.html", - View::Plants => "views/plants.html", View::Index => "views/index.html", View::Images => "views/images.html", View::Login => "views/login.html", + View::Other(tmpl) => tmpl.template(), + } + } + + fn render(&self, tera: &tera::Tera) -> tera::Result { + match self { + View::Other(tmpl) => tmpl.render(tera), + _ => tera.render(self.template(), &Context::new()), + } + } + + fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result { + match self { + View::Other(tmpl) => tmpl.render_ctx(tera, ctx), + _ => tera.render(self.template(), ctx), } } } @@ -48,14 +66,8 @@ pub struct ViewWrapper { include_base: bool, } -impl Template for ViewWrapper { - fn template(&self) -> &'static str { - self.view.template() - } - - fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result { - let view = self.view.render(tera, ctx)?; - +impl ViewWrapper { + fn wrap(&self, tera: &tera::Tera, view: String) -> tera::Result { if self.include_base { let mut ctx = tera::Context::new(); ctx.insert("view", &view); @@ -66,3 +78,19 @@ impl Template for ViewWrapper { } } } + +impl Template for ViewWrapper { + fn template(&self) -> &'static str { + self.view.template() + } + + fn render(&self, tera: &tera::Tera) -> tera::Result { + let view = self.view.render(tera)?; + self.wrap(tera, view) + } + + fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result { + let view = self.view.render_ctx(tera, ctx)?; + self.wrap(tera, view) + } +} diff --git a/templates/li/plant.html b/templates/li/plant.html new file mode 100644 index 0000000..e299da0 --- /dev/null +++ b/templates/li/plant.html @@ -0,0 +1,5 @@ +{% import "components/plant.html" as comp_plant %} + +
  • + {{ comp_plant::name(plant=item) }} +
  • diff --git a/templates/list/ul.html b/templates/list/ul.html new file mode 100644 index 0000000..61cb7ab --- /dev/null +++ b/templates/list/ul.html @@ -0,0 +1,18 @@ +

    {{ heading }}

    + +
      +{% for item in items %} + {{ item | safe }} +{% endfor %} + +{% if next_page_url %} +
      +
      +{% endif %} +