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