feat: implement infinite scroll on /plants page
parent
dce23599ef
commit
fff85d4479
|
@ -1,5 +1,6 @@
|
||||||
*
|
*
|
||||||
|
|
||||||
|
!migrations
|
||||||
!Cargo.toml
|
!Cargo.toml
|
||||||
!Cargo.lock
|
!Cargo.lock
|
||||||
!src/**
|
!src/**
|
||||||
|
|
|
@ -355,6 +355,7 @@ dependencies = [
|
||||||
"mime",
|
"mime",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_urlencoded",
|
||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"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"] }
|
tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "set-header", "fs"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|
|
@ -72,8 +72,8 @@ pub fn initialize_db(path: impl AsRef<Path>, run_migrations: bool) -> Result<DbP
|
||||||
#[derive(Deserialize, Copy, Clone)]
|
#[derive(Deserialize, Copy, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Pagination {
|
pub struct Pagination {
|
||||||
page: u32,
|
pub page: u32,
|
||||||
per_page: u32,
|
pub per_page: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Pagination {
|
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 diesel::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Serialize, Queryable, Selectable)]
|
||||||
#[diesel(table_name = images)]
|
#[diesel(table_name = images)]
|
||||||
|
@ -30,6 +33,18 @@ pub struct ImageFilter {
|
||||||
pub plant_id: Option<i32>,
|
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 {
|
impl NewImage {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
plant_id: i32,
|
plant_id: i32,
|
||||||
|
|
|
@ -74,6 +74,9 @@ async fn get_images(
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("images", &images);
|
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(
|
Ok(Html(super::render_view(
|
||||||
&ctx.tera,
|
&ctx.tera,
|
||||||
"views/images.html",
|
"views/images.html",
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod error;
|
||||||
mod events;
|
mod events;
|
||||||
mod images;
|
mod images;
|
||||||
mod plants;
|
mod plants;
|
||||||
|
pub mod query;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
|
@ -9,7 +11,7 @@ use tera::Context;
|
||||||
|
|
||||||
use crate::db::{self, DbError, Event, Pagination, Plant};
|
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> {
|
pub fn app() -> axum::Router<crate::Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
@ -56,7 +58,7 @@ async fn get_plant_page(
|
||||||
|
|
||||||
async fn get_plants(
|
async fn get_plants(
|
||||||
State(ctx): State<crate::Context>,
|
State(ctx): State<crate::Context>,
|
||||||
Query(page): Query<Pagination>,
|
Query(mut page): Query<Pagination>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> super::Result<Html<String>> {
|
) -> super::Result<Html<String>> {
|
||||||
let plants = tokio::task::spawn_blocking(move || db::Plant::page(&ctx.pool, page))
|
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();
|
let mut context = Context::new();
|
||||||
context.insert("plants", &plants);
|
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(
|
Ok(Html(super::render_view(
|
||||||
&ctx.tera,
|
&ctx.tera,
|
||||||
"views/plants.html",
|
"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>
|
<h1>Plants</h1>
|
||||||
|
|
||||||
<ul>
|
<ul id="plants">
|
||||||
{% for plant in plants %}
|
{% for plant in plants %}
|
||||||
{{ comp_plant::li(plant=plant) }}
|
{{ comp_plant::li(plant=plant) }}
|
||||||
{% endfor %}
|
{% 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>
|
</ul>
|
||||||
|
|
Loading…
Reference in New Issue