Compare commits
	
		
			No commits in common. "b17c2e7df661418fe25a3e6451216f9a6a7bb2eb" and "1b22c8d118baf81fc87688f7ee3402512ac1f179" have entirely different histories. 
		
	
	
		
			b17c2e7df6
			...
			1b22c8d118
		
	
		| 
						 | 
				
			
			@ -20,5 +20,5 @@ async fn post_event(
 | 
			
		|||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("event", &event);
 | 
			
		||||
    Ok(Html(Update::EventLi.render_ctx(&ctx.tera, &context)?))
 | 
			
		||||
    Ok(Html(Update::EventLi.render(&ctx.tera, &context)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ use std::{io::BufWriter, path::PathBuf};
 | 
			
		|||
use super::error::AppError;
 | 
			
		||||
use crate::{
 | 
			
		||||
    db::{self, Image, NewImage, Pagination},
 | 
			
		||||
    template::{List, ListCard, Template, View},
 | 
			
		||||
    template::{Template, View},
 | 
			
		||||
    IMG_DIR,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -72,11 +72,15 @@ async fn get_images(
 | 
			
		|||
            .await
 | 
			
		||||
            .unwrap()?;
 | 
			
		||||
 | 
			
		||||
    let list = List::Card
 | 
			
		||||
        .items("Images", ListCard::Image, images)
 | 
			
		||||
        .page("/images", page)
 | 
			
		||||
        .query(filter);
 | 
			
		||||
    Ok(Html(View::other(list).headers(&headers).render(&ctx.tera)?))
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("images", &images);
 | 
			
		||||
 | 
			
		||||
    let is_final_page = images.len() < page.per_page.try_into().unwrap();
 | 
			
		||||
    context.insert("is_final_page", &is_final_page);
 | 
			
		||||
 | 
			
		||||
    Ok(Html(
 | 
			
		||||
        View::Images.headers(&headers).render(&ctx.tera, &context)?,
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_image_original(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,9 +60,7 @@ pub async fn render_home(ctx: crate::Context, headers: &HeaderMap) -> Result<Htm
 | 
			
		|||
    context.insert("plants", &plants);
 | 
			
		||||
 | 
			
		||||
    Ok(Html(
 | 
			
		||||
        View::Index
 | 
			
		||||
            .headers(headers)
 | 
			
		||||
            .render_ctx(&ctx.tera, &context)?,
 | 
			
		||||
        View::Index.headers(headers).render(&ctx.tera, &context)?,
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,9 +68,7 @@ pub fn render_login(ctx: crate::Context, headers: &HeaderMap) -> Result<Html<Str
 | 
			
		|||
    let context = Context::new();
 | 
			
		||||
 | 
			
		||||
    Ok(Html(
 | 
			
		||||
        View::Login
 | 
			
		||||
            .headers(headers)
 | 
			
		||||
            .render_ctx(&ctx.tera, &context)?,
 | 
			
		||||
        View::Login.headers(headers).render(&ctx.tera, &context)?,
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,10 @@ use tera::Context;
 | 
			
		|||
 | 
			
		||||
use crate::{
 | 
			
		||||
    db::{self, DbError, Event, Pagination, Plant},
 | 
			
		||||
    template::{List, ListItem, Template, Update, View},
 | 
			
		||||
    template::{Template, Update, View},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::error::AppError;
 | 
			
		||||
use super::{error::AppError, query::ToQuery};
 | 
			
		||||
 | 
			
		||||
pub fn app() -> axum::Router<crate::Context> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
| 
						 | 
				
			
			@ -47,9 +47,7 @@ async fn get_plant_page(
 | 
			
		|||
            context.insert("event_types", &db::EVENT_TYPES);
 | 
			
		||||
 | 
			
		||||
            Ok(Html(
 | 
			
		||||
                View::Plant
 | 
			
		||||
                    .headers(&headers)
 | 
			
		||||
                    .render_ctx(&ctx.tera, &context)?,
 | 
			
		||||
                View::Plant.headers(&headers).render(&ctx.tera, &context)?,
 | 
			
		||||
            ))
 | 
			
		||||
        }
 | 
			
		||||
        None => Err(AppError::NotFound),
 | 
			
		||||
| 
						 | 
				
			
			@ -58,17 +56,28 @@ async fn get_plant_page(
 | 
			
		|||
 | 
			
		||||
async fn get_plants(
 | 
			
		||||
    State(ctx): State<crate::Context>,
 | 
			
		||||
    Query(page): Query<Pagination>,
 | 
			
		||||
    Query(mut page): Query<Pagination>,
 | 
			
		||||
    headers: HeaderMap,
 | 
			
		||||
) -> super::Result<Html<String>> {
 | 
			
		||||
    let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page))
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap()?;
 | 
			
		||||
 | 
			
		||||
    let list = List::Ul
 | 
			
		||||
        .items("Plants", ListItem::Plant, plants)
 | 
			
		||||
        .page("/plants", page);
 | 
			
		||||
    Ok(Html(View::other(list).headers(&headers).render(&ctx.tera)?))
 | 
			
		||||
    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)?,
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn post_plant(
 | 
			
		||||
| 
						 | 
				
			
			@ -81,5 +90,5 @@ async fn post_plant(
 | 
			
		|||
 | 
			
		||||
    let mut context = Context::new();
 | 
			
		||||
    context.insert("plant", &plant);
 | 
			
		||||
    Ok(Html(Update::PlantLi.render_ctx(&ctx.tera, &context)?))
 | 
			
		||||
    Ok(Html(Update::PlantLi.render(&ctx.tera, &context)?))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Clone)]
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
pub struct Query(pub Vec<(String, String)>);
 | 
			
		||||
 | 
			
		||||
impl Query {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,9 +18,3 @@ impl Query {
 | 
			
		|||
pub trait ToQuery {
 | 
			
		||||
    fn to_query(self) -> Query;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ToQuery for Query {
 | 
			
		||||
    fn to_query(self) -> Query {
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,135 +0,0 @@
 | 
			
		|||
use serde::Serialize;
 | 
			
		||||
use tera::Context;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    db::Pagination,
 | 
			
		||||
    server::query::{Query, ToQuery},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::Template;
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Copy)]
 | 
			
		||||
pub enum List {
 | 
			
		||||
    Ul,
 | 
			
		||||
    Card,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum ListItem {
 | 
			
		||||
    Plant,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum ListCard {
 | 
			
		||||
    Image,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Template for List {
 | 
			
		||||
    fn template(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            List::Ul => "list/ul.html",
 | 
			
		||||
            List::Card => "list/card.html",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Template for ListItem {
 | 
			
		||||
    fn template(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Plant => "li/plant.html",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Template for ListCard {
 | 
			
		||||
    fn template(&self) -> &'static str {
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Image => "card/image.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: None,
 | 
			
		||||
            query: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ListWrapper<T: Template, I: Serialize> {
 | 
			
		||||
    list: List,
 | 
			
		||||
    heading: &'static str,
 | 
			
		||||
    item_tmpl: T,
 | 
			
		||||
    items: Vec<I>,
 | 
			
		||||
    next_page: Option<(&'static str, Pagination)>,
 | 
			
		||||
    query: Option<Query>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T: Template, I: Serialize> ListWrapper<T, I> {
 | 
			
		||||
    pub fn page(mut self, url: &'static str, mut page: Pagination) -> Self {
 | 
			
		||||
        if self.items.len() as u32 == page.per_page {
 | 
			
		||||
            page.page += 1;
 | 
			
		||||
 | 
			
		||||
            self.next_page = Some((url, page));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn query(mut self, query: impl ToQuery) -> Self {
 | 
			
		||||
        if let Some(old_query) = self.query {
 | 
			
		||||
            self.query = Some(old_query.join(query));
 | 
			
		||||
        } else {
 | 
			
		||||
            self.query = Some(query.to_query());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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((url, page)) = &self.next_page {
 | 
			
		||||
            let query = if let Some(query) = &self.query {
 | 
			
		||||
                query.clone().join(page.clone())
 | 
			
		||||
            } else {
 | 
			
		||||
                page.to_query()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let full_url = format!("{url}?{}", query.encode());
 | 
			
		||||
            ctx.insert("next_page_url", &full_url);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.render_ctx(tera, &ctx)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
mod list;
 | 
			
		||||
mod update;
 | 
			
		||||
mod view;
 | 
			
		||||
 | 
			
		||||
pub use list::{List, ListCard, ListItem};
 | 
			
		||||
pub use update::Update;
 | 
			
		||||
pub use view::View;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -11,15 +9,7 @@ pub trait Template {
 | 
			
		|||
    fn template(&self) -> &'static str;
 | 
			
		||||
 | 
			
		||||
    /// Render the template with the given context
 | 
			
		||||
    fn render_ctx(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
 | 
			
		||||
    fn render(&self, tera: &tera::Tera, ctx: &tera::Context) -> tera::Result<String> {
 | 
			
		||||
        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,24 +1,20 @@
 | 
			
		|||
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<dyn Template>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
| 
						 | 
				
			
			@ -39,24 +35,10 @@ 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<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),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -66,8 +48,14 @@ pub struct ViewWrapper {
 | 
			
		|||
    include_base: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ViewWrapper {
 | 
			
		||||
    fn wrap(&self, tera: &tera::Tera, view: String) -> tera::Result<String> {
 | 
			
		||||
impl Template for ViewWrapper {
 | 
			
		||||
    fn template(&self) -> &'static str {
 | 
			
		||||
        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 {
 | 
			
		||||
            let mut ctx = tera::Context::new();
 | 
			
		||||
            ctx.insert("view", &view);
 | 
			
		||||
| 
						 | 
				
			
			@ -78,19 +66,3 @@ impl 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
{% import "components/plant.html" as comp_plant %}
 | 
			
		||||
<article>
 | 
			
		||||
    <p>Date taken: {{ item.0.date_taken }}</p>
 | 
			
		||||
    <p>Note: {{ item.0.note }}</p>
 | 
			
		||||
    <p>Plant: {{ comp_plant::name(plant=item.1) }}</p>
 | 
			
		||||
    <a href="/images/{{ item.0.id }}/original" target="_blank">
 | 
			
		||||
        <img src="/images/{{ item.0.id }}/thumb">
 | 
			
		||||
    </a>
 | 
			
		||||
</article>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
{% import "components/plant.html" as comp_plant %}
 | 
			
		||||
 | 
			
		||||
<li>
 | 
			
		||||
    {{ comp_plant::name(plant=item) }} 
 | 
			
		||||
</li>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
<h1>{{ heading }}</h1>
 | 
			
		||||
 | 
			
		||||
<div 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 %}
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
<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