feat: implement infinite scroll on /plants page
							parent
							
								
									dce23599ef
								
							
						
					
					
						commit
						fff85d4479
					
				|  | @ -1,5 +1,6 @@ | |||
| * | ||||
| 
 | ||||
| !migrations | ||||
| !Cargo.toml | ||||
| !Cargo.lock | ||||
| !src/** | ||||
|  |  | |||
|  | @ -355,6 +355,7 @@ dependencies = [ | |||
|  "mime", | ||||
|  "rand", | ||||
|  "serde", | ||||
|  "serde_urlencoded", | ||||
|  "tera", | ||||
|  "tokio", | ||||
|  "tokio-util", | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -72,8 +72,8 @@ pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbP | |||
| #[derive(Deserialize, Copy, Clone)] | ||||
| #[serde(default)] | ||||
| pub struct Pagination { | ||||
|     page: u32, | ||||
|     per_page: u32, | ||||
|     pub page: u32, | ||||
|     pub per_page: u32, | ||||
| } | ||||
| 
 | ||||
| impl Default for Pagination { | ||||
|  | @ -84,3 +84,12 @@ impl Default for Pagination { | |||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl crate::server::query::ToQuery for Pagination { | ||||
|     fn to_query(self) -> 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()), | ||||
|         ]) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<i32>, | ||||
| } | ||||
| 
 | ||||
| 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, | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ mod error; | |||
| mod events; | ||||
| mod images; | ||||
| mod plants; | ||||
| pub mod query; | ||||
| 
 | ||||
| use std::path::Path; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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<crate::Context> { | ||||
|     Router::new() | ||||
|  | @ -56,7 +58,7 @@ 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)) | ||||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  | @ -2,8 +2,18 @@ | |||
| 
 | ||||
| <h1>Plants</h1> | ||||
| 
 | ||||
| <ul> | ||||
| <ul id="plants"> | ||||
| {% for plant in plants %} | ||||
|     {{ comp_plant::li(plant=plant) }} | ||||
| {% endfor %} | ||||
| {% if not is_final_page %} | ||||
| <div  | ||||
|     aria-busy="true" | ||||
|     hx-get="/plants?{{ query }}" | ||||
|     hx-select="ul > *" | ||||
|     hx-trigger="revealed" | ||||
|     hx-target="this" | ||||
|     hx-swap="outerHTML"> | ||||
| </div> | ||||
| {% endif %} | ||||
| </ul> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue