feat: implement typed List template page for plants
parent
1b22c8d118
commit
4e92c02e63
|
@ -20,5 +20,5 @@ async fn post_event(
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("event", &event);
|
context.insert("event", &event);
|
||||||
Ok(Html(Update::EventLi.render(&ctx.tera, &context)?))
|
Ok(Html(Update::EventLi.render_ctx(&ctx.tera, &context)?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,9 @@ async fn get_images(
|
||||||
context.insert("is_final_page", &is_final_page);
|
context.insert("is_final_page", &is_final_page);
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
View::Images.headers(&headers).render(&ctx.tera, &context)?,
|
View::Images
|
||||||
|
.headers(&headers)
|
||||||
|
.render_ctx(&ctx.tera, &context)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,9 @@ pub async fn render_home(ctx: crate::Context, headers: &HeaderMap) -> Result<Htm
|
||||||
context.insert("plants", &plants);
|
context.insert("plants", &plants);
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
View::Index.headers(headers).render(&ctx.tera, &context)?,
|
View::Index
|
||||||
|
.headers(headers)
|
||||||
|
.render_ctx(&ctx.tera, &context)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,9 @@ pub fn render_login(ctx: crate::Context, headers: &HeaderMap) -> Result<Html<Str
|
||||||
let context = Context::new();
|
let context = Context::new();
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
View::Login.headers(headers).render(&ctx.tera, &context)?,
|
View::Login
|
||||||
|
.headers(headers)
|
||||||
|
.render_ctx(&ctx.tera, &context)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ use tera::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{self, DbError, Event, Pagination, Plant},
|
db::{self, DbError, Event, Pagination, Plant},
|
||||||
template::{Template, Update, View},
|
template::{List, ListItem, Template, Update, View},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{error::AppError, query::ToQuery};
|
use super::error::AppError;
|
||||||
|
|
||||||
pub fn app() -> axum::Router<crate::Context> {
|
pub fn app() -> axum::Router<crate::Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
@ -47,7 +47,9 @@ async fn get_plant_page(
|
||||||
context.insert("event_types", &db::EVENT_TYPES);
|
context.insert("event_types", &db::EVENT_TYPES);
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
View::Plant.headers(&headers).render(&ctx.tera, &context)?,
|
View::Plant
|
||||||
|
.headers(&headers)
|
||||||
|
.render_ctx(&ctx.tera, &context)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
None => Err(AppError::NotFound),
|
None => Err(AppError::NotFound),
|
||||||
|
@ -56,28 +58,17 @@ async fn get_plant_page(
|
||||||
|
|
||||||
async fn get_plants(
|
async fn get_plants(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
Query(mut page): Query<Pagination>,
|
Query(page): Query<Pagination>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> super::Result<Html<String>> {
|
) -> super::Result<Html<String>> {
|
||||||
let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page))
|
let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page))
|
||||||
.await
|
.await
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
let mut context = Context::new();
|
let list = List::Ul
|
||||||
context.insert("plants", &plants);
|
.items("Plants", ListItem::Plant, plants)
|
||||||
|
.page("/plants", page, None);
|
||||||
let is_final_page = plants.len() < page.per_page.try_into().unwrap();
|
Ok(Html(View::other(list).headers(&headers).render(&ctx.tera)?))
|
||||||
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)?,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_plant(
|
async fn post_plant(
|
||||||
|
@ -90,5 +81,5 @@ async fn post_plant(
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("plant", &plant);
|
context.insert("plant", &plant);
|
||||||
Ok(Html(Update::PlantLi.render(&ctx.tera, &context)?))
|
Ok(Html(Update::PlantLi.render_ctx(&ctx.tera, &context)?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,3 +18,9 @@ impl Query {
|
||||||
pub trait ToQuery {
|
pub trait ToQuery {
|
||||||
fn to_query(self) -> Query;
|
fn to_query(self) -> Query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToQuery for Query {
|
||||||
|
fn to_query(self) -> Query {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<T: Template, I: Serialize>(
|
||||||
|
self,
|
||||||
|
heading: &'static str,
|
||||||
|
item_tmpl: T,
|
||||||
|
items: Vec<I>,
|
||||||
|
) -> ListWrapper<T, I> {
|
||||||
|
ListWrapper {
|
||||||
|
list: self,
|
||||||
|
heading,
|
||||||
|
item_tmpl,
|
||||||
|
items,
|
||||||
|
next_page_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ListWrapper<T: Template, I: Serialize> {
|
||||||
|
list: List,
|
||||||
|
heading: &'static str,
|
||||||
|
item_tmpl: T,
|
||||||
|
items: Vec<I>,
|
||||||
|
next_page_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Template, I: Serialize> ListWrapper<T, I> {
|
||||||
|
pub fn page(mut self, url: &'static str, mut page: Pagination, query: Option<Query>) -> 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<T: Template, I: Serialize> Template for ListWrapper<T, I> {
|
||||||
|
fn template(&self) -> &'static str {
|
||||||
|
self.list.template()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
|
||||||
|
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::<tera::Result<Vec<String>>>()?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
|
mod list;
|
||||||
mod update;
|
mod update;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
|
pub use list::{List, ListItem};
|
||||||
pub use update::Update;
|
pub use update::Update;
|
||||||
pub use view::View;
|
pub use view::View;
|
||||||
|
|
||||||
|
@ -9,7 +11,15 @@ pub trait Template {
|
||||||
fn template(&self) -> &'static str;
|
fn template(&self) -> &'static str;
|
||||||
|
|
||||||
/// Render the template with the given context
|
/// Render the template with the given context
|
||||||
fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
||||||
tera.render(self.template(), ctx)
|
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<String> {
|
||||||
|
self.render_ctx(tera, &tera::Context::new())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
use axum::http::{HeaderMap, HeaderValue};
|
use axum::http::{HeaderMap, HeaderValue};
|
||||||
|
use tera::Context;
|
||||||
|
|
||||||
use super::Template;
|
use super::Template;
|
||||||
|
|
||||||
const HX_REQUEST_HEADER: &str = "HX-Request";
|
const HX_REQUEST_HEADER: &str = "HX-Request";
|
||||||
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
|
const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum View {
|
pub enum View {
|
||||||
Plant,
|
Plant,
|
||||||
Plants,
|
|
||||||
Images,
|
Images,
|
||||||
Index,
|
Index,
|
||||||
Login,
|
Login,
|
||||||
|
Other(Box<dyn Template>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View {
|
impl View {
|
||||||
|
pub fn other(tmpl: impl Template + 'static) -> Self {
|
||||||
|
View::Other(Box::new(tmpl))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn headers(self, headers: &HeaderMap) -> ViewWrapper {
|
pub fn headers(self, headers: &HeaderMap) -> ViewWrapper {
|
||||||
let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
|
let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
|
||||||
let is_hist_restore_req = headers
|
let is_hist_restore_req = headers
|
||||||
|
@ -35,10 +39,24 @@ impl Template for View {
|
||||||
fn template(&self) -> &'static str {
|
fn template(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
View::Plant => "views/plant.html",
|
View::Plant => "views/plant.html",
|
||||||
View::Plants => "views/plants.html",
|
|
||||||
View::Index => "views/index.html",
|
View::Index => "views/index.html",
|
||||||
View::Images => "views/images.html",
|
View::Images => "views/images.html",
|
||||||
View::Login => "views/login.html",
|
View::Login => "views/login.html",
|
||||||
|
View::Other(tmpl) => tmpl.template(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
|
||||||
|
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<String> {
|
||||||
|
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,
|
include_base: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template for ViewWrapper {
|
impl ViewWrapper {
|
||||||
fn template(&self) -> &'static str {
|
fn wrap(&self, tera: &tera::Tera, view: String) -> tera::Result<String> {
|
||||||
self.view.template()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
|
||||||
let view = self.view.render(tera, ctx)?;
|
|
||||||
|
|
||||||
if self.include_base {
|
if self.include_base {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("view", &view);
|
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<String> {
|
||||||
|
let view = self.view.render(tera)?;
|
||||||
|
self.wrap(tera, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
|
||||||
|
let view = self.view.render_ctx(tera, ctx)?;
|
||||||
|
self.wrap(tera, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% import "components/plant.html" as comp_plant %}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{{ comp_plant::name(plant=item) }}
|
||||||
|
</li>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<h1>{{ heading }}</h1>
|
||||||
|
|
||||||
|
<ul id="items">
|
||||||
|
{% for item in items %}
|
||||||
|
{{ item | safe }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if next_page_url %}
|
||||||
|
<div
|
||||||
|
aria-busy="true"
|
||||||
|
hx-get="{{ next_page_url }}"
|
||||||
|
hx-select="#items > *"
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
Loading…
Reference in New Issue