use axum::{ extract::{Query, Request, State}, response::{IntoResponse, Redirect, Response}, }; use maud::{PreEscaped, html}; use serde::Deserialize; use crate::{ MnemoState, error::CompositeError, quotes::Quote, users::{User, auth::UserAuthenticate}, web::{ components::{nav::nav, quote::quote}, icons, pages::base, }, }; pub mod add; pub mod id; #[derive(Deserialize)] pub struct PageQuery { page: Option, s: Option, } pub async fn page( State(state): State, Query(query): Query, req: Request, ) -> Result { let mut conn = state.pool.acquire().await?; let u = match User::authenticate(&mut *conn, req.headers()).await? { Some(u) => u, None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), }; let page = query.page.unwrap_or(1).max(1); let per_page = 10; let offset = (page - 1) * per_page; let search = query.s.as_deref().unwrap_or(""); let quotes = match search { "" => Quote::get_chronological_offset(&mut *conn, offset, per_page).await?, _ => Quote::get_by_search_query(&mut *conn, search, offset, per_page).await?, }; let total_quotes = match search { "" => Quote::total_count(&mut *conn).await?, _ => Quote::search_query_count(&mut *conn, search).await?, }; let total_pages = (total_quotes as f64 / per_page as f64).ceil() as i64; let s_qs = if search.is_empty() { String::new() } else { format!("&s={}", search) }; Ok(base( "Quotes | Mnemosyne", html!( (nav(&mut conn, Some(&u), req.uri().path()).await) div class="max-w-4xl mx-auto px-2" { div class="my-4 flex justify-between" { p class="flex items-center gap-2" { span class="text-neutral-500" {(PreEscaped(icons::SCROLL_TEXT))} span class="text-2xl font-semibold font-lora" {"Quotes"} } a href="/quotes/add" class="group border rounded flex items-center gap-1 px-2 border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-400/5 hover:bg-neutral-400/10" { span class="text-neutral-300 group-hover:text-neutral-200" {(PreEscaped(icons::PLUS))} span class="text-neutral-300 group-hover:text-neutral-200" {"Add quote"} } } form method="get" action="/quotes" { input type="text" name="s" class="border w-full border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-950/50 p-2 rounded" placeholder="Search quotes..." value={(search)}; } div class="my-2 w-full" { p class="ml-auto w-fit text-neutral-500 text-sm flex items-center" { span class="scale-[.6]" {(PreEscaped(icons::CALENDAR_ARROW_DOWN))} "Chronological" } } div class="flex flex-col gap-4 mb-8" { @for q in "es { a href=(format!("/quotes/{}", q.id)) class="group/a" {(quote(q))} } div class="flex justify-between items-center mt-4 text-neutral-400" { @if page > 1 { a href=(format!("/quotes?page={}{}", (page - 1).max(1), s_qs)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Previous" } } @else { div {} } span { "Page " (page) " of " (total_pages.max(1)) } @if page < total_pages { a href=(format!("/quotes?page={}{}", page + 1, s_qs)) class="px-4 py-2 border border-neutral-200/25 hover:border-neutral-200/45 bg-neutral-200/5 hover:bg-neutral-200/15 rounded" { "Next" } } @else { div {} } } } } ), ) .into_response()) }