From fff85d447996703f2df9ce697554b2a784bdbc99 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 25 Jan 2025 11:08:53 +0100 Subject: [PATCH] feat: implement infinite scroll on /plants page --- .dockerignore | 1 + Cargo.lock | 1 + Cargo.toml | 1 + src/db/mod.rs | 13 +++++++++++-- src/db/models/image.rs | 17 ++++++++++++++++- src/server/images.rs | 3 +++ src/server/mod.rs | 1 + src/server/plants.rs | 15 +++++++++++++-- src/server/query.rs | 20 ++++++++++++++++++++ templates/views/plants.html | 12 +++++++++++- 10 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/server/query.rs diff --git a/.dockerignore b/.dockerignore index 38f1a0f..4e0f2bb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ * +!migrations !Cargo.toml !Cargo.lock !src/** diff --git a/Cargo.lock b/Cargo.lock index 85ae352..d97bcfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,7 @@ dependencies = [ "mime", "rand", "serde", + "serde_urlencoded", "tera", "tokio", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index eb5d537..b4766cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ tower = { version = "0.5.2", features = ["util"] } tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "set-header", "fs"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" +serde_urlencoded = "0.7.1" [profile.release] strip = true diff --git a/src/db/mod.rs b/src/db/mod.rs index 0081462..17c2bfa 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -72,8 +72,8 @@ pub fn initialize_db(path: impl AsRef, run_migrations: bool) -> Result crate::server::query::Query { + crate::server::query::Query(vec![ + ("page".to_string(), self.page.to_string()), + ("per_page".to_string(), self.per_page.to_string()), + ]) + } +} diff --git a/src/db/models/image.rs b/src/db/models/image.rs index 539537f..bab983d 100644 --- a/src/db/models/image.rs +++ b/src/db/models/image.rs @@ -2,7 +2,10 @@ use chrono::NaiveDate; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use crate::db::{schema::*, DbPool, DbResult, Pagination, Plant}; +use crate::{ + db::{schema::*, DbPool, DbResult, Pagination, Plant}, + server::query::{Query, ToQuery}, +}; #[derive(Serialize, Queryable, Selectable)] #[diesel(table_name = images)] @@ -30,6 +33,18 @@ pub struct ImageFilter { pub plant_id: Option, } +impl ToQuery for ImageFilter { + fn to_query(self) -> crate::server::query::Query { + let mut out = vec![]; + + if let Some(plant_id) = self.plant_id { + out.push(("plant_id".to_string(), plant_id.to_string())); + } + + Query(out) + } +} + impl NewImage { pub fn new( plant_id: i32, diff --git a/src/server/images.rs b/src/server/images.rs index 15e8894..0ea87f1 100644 --- a/src/server/images.rs +++ b/src/server/images.rs @@ -74,6 +74,9 @@ async fn get_images( 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(super::render_view( &ctx.tera, "views/images.html", diff --git a/src/server/mod.rs b/src/server/mod.rs index 8d3a472..1af8eed 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -3,6 +3,7 @@ mod error; mod events; mod images; mod plants; +pub mod query; use std::path::Path; diff --git a/src/server/plants.rs b/src/server/plants.rs index 4e7df3a..4fae1bd 100644 --- a/src/server/plants.rs +++ b/src/server/plants.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use axum::{ extract::{Path, Query, State}, http::HeaderMap, @@ -9,7 +11,7 @@ use tera::Context; use crate::db::{self, DbError, Event, Pagination, Plant}; -use super::error::AppError; +use super::{error::AppError, query::ToQuery}; pub fn app() -> axum::Router { Router::new() @@ -56,7 +58,7 @@ async fn get_plant_page( async fn get_plants( State(ctx): State, - Query(page): Query, + Query(mut page): Query, headers: HeaderMap, ) -> super::Result> { let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page)) @@ -66,6 +68,15 @@ async fn get_plants( 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(super::render_view( &ctx.tera, "views/plants.html", diff --git a/src/server/query.rs b/src/server/query.rs new file mode 100644 index 0000000..5f66853 --- /dev/null +++ b/src/server/query.rs @@ -0,0 +1,20 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct Query(pub Vec<(String, String)>); + +impl Query { + pub fn join(mut self, other: impl ToQuery) -> Self { + self.0.extend(other.to_query().0); + + self + } + + pub fn encode(self) -> String { + serde_urlencoded::to_string(self).unwrap() + } +} + +pub trait ToQuery { + fn to_query(self) -> Query; +} diff --git a/templates/views/plants.html b/templates/views/plants.html index 22d4aa6..fc7893d 100644 --- a/templates/views/plants.html +++ b/templates/views/plants.html @@ -2,8 +2,18 @@

Plants

-
    +
      {% for plant in plants %} {{ comp_plant::li(plant=plant) }} {% endfor %} +{% if not is_final_page %} +
      +
      +{% endif %}