This commit is contained in:
parent
d6555edc29
commit
00bb6f152d
8 changed files with 332 additions and 11 deletions
|
@ -9,7 +9,7 @@ use rocket::routes;
|
||||||
pub async fn index_page<'r>(ctx: RequestContext) -> impl Responder<'r, 'static> {
|
pub async fn index_page<'r>(ctx: RequestContext) -> impl Responder<'r, 'static> {
|
||||||
based::request::assets::DataResponse::new(
|
based::request::assets::DataResponse::new(
|
||||||
include_bytes!("../Cargo.toml").to_vec(),
|
include_bytes!("../Cargo.toml").to_vec(),
|
||||||
"text/toml",
|
"text/toml".to_string(),
|
||||||
Some(60 * 60 * 3),
|
Some(60 * 60 * 3),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,6 @@ impl User {
|
||||||
/// Change the password of a User
|
/// Change the password of a User
|
||||||
///
|
///
|
||||||
/// Returns a Result indicating whether the password change was successful or not
|
/// Returns a Result indicating whether the password change was successful or not
|
||||||
#[must_use]
|
|
||||||
pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> {
|
pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> {
|
||||||
if self.verify_pw(old) {
|
if self.verify_pw(old) {
|
||||||
sqlx::query("UPDATE users SET \"password\" = $1 WHERE username = $2;")
|
sqlx::query("UPDATE users SET \"password\" = $1 WHERE username = $2;")
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use maud::{PreEscaped, html};
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
use crate::request::{RequestContext, StringResponse};
|
use crate::request::{RequestContext, StringResponse};
|
||||||
|
|
||||||
use rocket::http::{ContentType, Status};
|
use rocket::http::{ContentType, Status};
|
||||||
|
|
160
src/page/search.rs
Normal file
160
src/page/search.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use maud::{PreEscaped, html};
|
||||||
|
|
||||||
|
use crate::request::{RequestContext, api::Pager};
|
||||||
|
|
||||||
|
/// Represents a search form with configurable options such as heading, placeholder, and CSS class.
|
||||||
|
pub struct Search {
|
||||||
|
post_url: String,
|
||||||
|
heading: Option<PreEscaped<String>>,
|
||||||
|
placeholder: Option<String>,
|
||||||
|
search_class: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Search {
|
||||||
|
/// Creates a new `Search` instance with the specified post URL.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `post_url` - The URL where the search form will send the POST request.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new `Search` instance with default settings.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(post_url: String) -> Self {
|
||||||
|
Self {
|
||||||
|
heading: None,
|
||||||
|
placeholder: None,
|
||||||
|
post_url,
|
||||||
|
search_class: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the placeholder text for the search input field.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `placeholder` - The placeholder text to display in the search input.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The updated `Search` instance.
|
||||||
|
#[must_use]
|
||||||
|
pub fn placeholder(mut self, placeholder: String) -> Self {
|
||||||
|
self.placeholder = Some(placeholder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the heading to be displayed above the search form.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `heading` - The heading element to display.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The updated `Search` instance.
|
||||||
|
#[must_use]
|
||||||
|
pub fn heading(mut self, heading: PreEscaped<String>) -> Self {
|
||||||
|
self.heading = Some(heading);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the CSS class for the search input field.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `class` - The CSS class to apply to the search input.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The updated `Search` instance.
|
||||||
|
#[must_use]
|
||||||
|
pub fn search_class(mut self, class: String) -> Self {
|
||||||
|
self.search_class = Some(class);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the HTML for search results based on the current page and query.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `pager` - The `Pager` instance to paginate results.
|
||||||
|
/// * `page` - The current page number.
|
||||||
|
/// * `query` - The search query string.
|
||||||
|
/// * `result_ui` - A function that transforms each result into HTML.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The HTML string containing the search results.
|
||||||
|
pub fn build_results<T>(
|
||||||
|
&self,
|
||||||
|
pager: Pager<T>,
|
||||||
|
page: i64,
|
||||||
|
query: &str,
|
||||||
|
result_ui: impl Fn(&T) -> PreEscaped<String>,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
let results = pager.page(page as u64);
|
||||||
|
let reslen = results.len();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
|
||||||
|
@for res in results {
|
||||||
|
(result_ui(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
@if reslen as u64 == pager.items_per_page {
|
||||||
|
div hx-get=(format!("{}?query={}&page={}", self.post_url, query, page+1))
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-swap="outerHTML" {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the full response based on the context (HTMX or full HTML response).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `ctx` - The request context, used to determine if HTMX is enabled.
|
||||||
|
/// * `results` - The `Pager` instance containing the search results.
|
||||||
|
/// * `page` - The current page number.
|
||||||
|
/// * `query` - The search query string.
|
||||||
|
/// * `result_ui` - A function that transforms each result into HTML.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The HTML string containing either the HTMX response or full page content.
|
||||||
|
pub fn build_response<T>(
|
||||||
|
&self,
|
||||||
|
ctx: &RequestContext,
|
||||||
|
results: Pager<T>,
|
||||||
|
page: i64,
|
||||||
|
query: &str,
|
||||||
|
result_ui: impl Fn(&T) -> PreEscaped<String>,
|
||||||
|
) -> PreEscaped<String> {
|
||||||
|
if ctx.is_htmx {
|
||||||
|
// Return HTMX Search elements
|
||||||
|
self.build_results(results, page, query, result_ui)
|
||||||
|
} else {
|
||||||
|
// Return full rendered site
|
||||||
|
let first_page = self.build_results(results, page, query, result_ui);
|
||||||
|
self.build(query, first_page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the full search form and first search page results.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `query` - The search query string.
|
||||||
|
/// * `first_page` - The HTML string containing the first page of search results.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The HTML string containing the entire search form and results UI.
|
||||||
|
#[must_use]
|
||||||
|
pub fn build(&self, query: &str, first_page: PreEscaped<String>) -> PreEscaped<String> {
|
||||||
|
let no_html = PreEscaped(String::new());
|
||||||
|
html! {
|
||||||
|
(self.heading.as_ref().unwrap_or_else(|| &no_html))
|
||||||
|
input type="search" name="query"
|
||||||
|
value=(query)
|
||||||
|
placeholder=(self.placeholder.as_ref().unwrap_or(&"Search...".to_string())) hx-get=(self.post_url)
|
||||||
|
hx-trigger="input changed delay:500ms, keyup[key=='Enter'], load"
|
||||||
|
hx-target="#search_results" hx-push-url="true"
|
||||||
|
class=(self.search_class.as_deref().unwrap_or_default()) {};
|
||||||
|
|
||||||
|
div id="search_results" {
|
||||||
|
@if !query.is_empty() {
|
||||||
|
(first_page)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,3 +83,168 @@ pub fn api_error(msg: &str) -> ApiError {
|
||||||
"error": msg
|
"error": msg
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `Pager` that manages paginated items, with the ability to handle incomplete data.
|
||||||
|
pub struct Pager<T> {
|
||||||
|
inner: Vec<T>,
|
||||||
|
pub items_per_page: u64,
|
||||||
|
complete_at: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Pager<T> {
|
||||||
|
/// Creates a new `Pager` instance with the given items and items per page.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `items` - A vector of items to paginate.
|
||||||
|
/// * `per_page` - Number of items per page.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new `Pager` instance.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(items: Vec<T>, per_page: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: items,
|
||||||
|
items_per_page: per_page,
|
||||||
|
complete_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Pager` instance for an incomplete dataset, starting from a specific page.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `items` - A vector of items to paginate.
|
||||||
|
/// * `per_page` - Number of items per page.
|
||||||
|
/// * `at_page` - The page where data starts, meaning there are no pages before it.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new `Pager` instance.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new_incomplete(items: Vec<T>, per_page: u64, at_page: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: items,
|
||||||
|
items_per_page: per_page,
|
||||||
|
complete_at: Some(at_page),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the offset for the given page number.
|
||||||
|
///
|
||||||
|
/// This method considers whether the pager has an incomplete dataset and adjusts the offset accordingly.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `page` - The page number for which the offset is calculated.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The calculated offset.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn offset(&self, page: u64) -> u64 {
|
||||||
|
if let Some(incomplete) = self.complete_at {
|
||||||
|
let page = page - incomplete;
|
||||||
|
return self.items_per_page * page;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items_per_page * (page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the items for a specific page.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `page` - The page number to retrieve.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if trying to access a page before the `complete_at` page in an incomplete pager.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A vector of items on the requested page.
|
||||||
|
#[must_use]
|
||||||
|
pub fn page(&self, page: u64) -> Vec<&T> {
|
||||||
|
if let Some(incomplete) = self.complete_at {
|
||||||
|
assert!(
|
||||||
|
page >= incomplete,
|
||||||
|
"Tried to access illegal page on incomplete Pager"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.skip(self.offset(page).try_into().unwrap())
|
||||||
|
.take(self.items_per_page.try_into().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `GeneratedPager` is a paginated generator that fetches items dynamically from a generator function.
|
||||||
|
pub struct GeneratedPager<T, G, I>
|
||||||
|
where
|
||||||
|
G: Fn(I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
||||||
|
{
|
||||||
|
generator: G,
|
||||||
|
pub items_per_page: u64,
|
||||||
|
_marker: std::marker::PhantomData<(T, I)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, G, I> GeneratedPager<T, G, I>
|
||||||
|
where
|
||||||
|
G: Fn(I, u64, u64) -> futures::future::BoxFuture<'static, Vec<T>>,
|
||||||
|
{
|
||||||
|
/// Creates a new `GeneratedPager` instance with the provided generator and pagination settings.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `generator` - A function that generates a page of items based on the input and pagination settings.
|
||||||
|
/// * `items_per_page` - Number of items per page.
|
||||||
|
///
|
||||||
|
/// # Generator
|
||||||
|
/// The generator function should take the following arguments:
|
||||||
|
/// * `input` - Generic input to the generator
|
||||||
|
/// * `offset` - Offset value
|
||||||
|
/// * `limit` - Limit value (items per page)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A new `GeneratedPager` instance.
|
||||||
|
pub const fn new(generator: G, items_per_page: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
generator,
|
||||||
|
items_per_page,
|
||||||
|
_marker: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the offset for the given page number.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `page` - The page number for which the offset is calculated.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The calculated offset.
|
||||||
|
pub const fn offset(&self, page: u64) -> u64 {
|
||||||
|
self.items_per_page * (page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronously retrieves the items for a specific page from the generator.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `page` - The page number to retrieve.
|
||||||
|
/// * `input` - The input that is passed to the generator.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A vector of items on the requested page.
|
||||||
|
pub async fn page(&self, page: u64, input: I) -> Vec<T> {
|
||||||
|
let offset = self.offset(page);
|
||||||
|
(self.generator)(input, offset, self.items_per_page).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the `GeneratedPager` into a regular `Pager` for a given page of items.
|
||||||
|
///
|
||||||
|
/// This method allows you to use a `GeneratedPager` like a regular `Pager`, with a pre-generated dataset.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `page` - The page number to retrieve.
|
||||||
|
/// * `input` - The input to pass to the generator function.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `Pager` instance containing the requested page of items.
|
||||||
|
pub async fn pager(&self, page: u64, input: I) -> Pager<T> {
|
||||||
|
let content = self.page(page, input).await;
|
||||||
|
Pager::new_incomplete(content, self.items_per_page, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub struct DataResponse {
|
||||||
|
|
||||||
impl DataResponse {
|
impl DataResponse {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(data: Vec<u8>, content_type: String, cache_duration: Option<u64>) -> Self {
|
pub const fn new(data: Vec<u8>, content_type: String, cache_duration: Option<u64>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
content_type,
|
content_type,
|
||||||
|
|
|
@ -4,6 +4,7 @@ use rocket::{
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Represents contextual information about an HTTP request.
|
/// Represents contextual information about an HTTP request.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct RequestContext {
|
pub struct RequestContext {
|
||||||
/// A flag indicating if the request is an HTMX request.
|
/// A flag indicating if the request is an HTMX request.
|
||||||
///
|
///
|
||||||
|
@ -21,9 +22,3 @@ impl<'r> FromRequest<'r> for RequestContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RequestContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { is_htmx: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ pub fn respond_json(json: &serde_json::Value) -> StringResponse {
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A `StringResponse` with status `200 OK`, content type `text/html`, and the HTML content as the body.
|
/// A `StringResponse` with status `200 OK`, content type `text/html`, and the HTML content as the body.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn respond_html(html: String) -> StringResponse {
|
pub const fn respond_html(html: String) -> StringResponse {
|
||||||
(Status::Ok, (ContentType::HTML, html))
|
(Status::Ok, (ContentType::HTML, html))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ pub fn respond_html(html: String) -> StringResponse {
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A `StringResponse` with status `200 OK`, content type `text/javascript`, and the JS content as the body.
|
/// A `StringResponse` with status `200 OK`, content type `text/javascript`, and the JS content as the body.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn respond_script(script: String) -> StringResponse {
|
pub const fn respond_script(script: String) -> StringResponse {
|
||||||
(Status::Ok, (ContentType::JavaScript, script))
|
(Status::Ok, (ContentType::JavaScript, script))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue