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