Server: Expose profile search on discover page
This commit is contained in:
parent
aee79ec311
commit
a9166f3984
|
@ -8,6 +8,11 @@ pub(crate) enum PageSource {
|
|||
OlderThan(Uuid),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PageNum {
|
||||
pub(crate) page: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Page {
|
||||
pub(crate) items: Vec<Uuid>,
|
||||
|
@ -16,12 +21,25 @@ pub(crate) struct Page {
|
|||
pub(crate) reset: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SearchPage {
|
||||
pub(crate) items: Vec<Uuid>,
|
||||
pub(crate) next: Option<usize>,
|
||||
pub(crate) prev: Option<usize>,
|
||||
pub(crate) term: String,
|
||||
}
|
||||
|
||||
pub trait Pagination {
|
||||
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a>;
|
||||
fn from_min<'a>(&'a mut self, min: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a>;
|
||||
fn from_start<'a>(&'a mut self) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a>;
|
||||
}
|
||||
|
||||
pub trait SearchPagination {
|
||||
fn from_term<'a>(&'a mut self, term: &'a str)
|
||||
-> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a>;
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub(crate) fn from_pagination(
|
||||
mut pagination: impl Pagination,
|
||||
|
@ -83,3 +101,47 @@ impl Page {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchPage {
|
||||
pub(crate) fn from_pagination(
|
||||
mut pagination: impl SearchPagination,
|
||||
per_page: usize,
|
||||
term: String,
|
||||
page: Option<PageNum>,
|
||||
) -> Self {
|
||||
let mut items: Vec<Uuid>;
|
||||
let mut next = None;
|
||||
let mut prev = None;
|
||||
|
||||
match page {
|
||||
Some(PageNum { page }) => {
|
||||
items = pagination
|
||||
.from_term(&term)
|
||||
.skip(page * per_page)
|
||||
.take(per_page + 1)
|
||||
.collect();
|
||||
if page > 0 {
|
||||
prev = Some(page - 1);
|
||||
}
|
||||
if items.len() == per_page + 1 {
|
||||
items.pop();
|
||||
next = Some(page + 1);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
items = pagination.from_term(&term).take(per_page + 1).collect();
|
||||
if items.len() == per_page + 1 {
|
||||
items.pop();
|
||||
next = Some(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchPage {
|
||||
items,
|
||||
next,
|
||||
prev,
|
||||
term,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ use crate::{
|
|||
extensions::ProfileExt,
|
||||
middleware::UserProfile,
|
||||
nav::NavState,
|
||||
pagination::{Page, PageSource, Pagination},
|
||||
pagination::{Page, PageNum, PageSource, Pagination, SearchPage, SearchPagination},
|
||||
views::ProfileView,
|
||||
State,
|
||||
};
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
||||
use hyaenidae_profiles::store::{File, Profile};
|
||||
use hyaenidae_toolkit::Button;
|
||||
use hyaenidae_toolkit::{Button, TextInput};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -23,23 +23,43 @@ async fn profile_list(
|
|||
req: HttpRequest,
|
||||
profile: UserProfile,
|
||||
page: Option<web::Query<ProfilePage>>,
|
||||
search: Option<web::Query<Search>>,
|
||||
page_num: Option<web::Query<PageNumQuery>>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let page = page.map(|query| query.into_inner().into());
|
||||
let page_num = page_num.map(|query| query.into_inner().into());
|
||||
|
||||
let state = ProfileListState::build(req.path(), profile.0.id(), page, &state).await?;
|
||||
let state = if let Some(search) = search {
|
||||
ProfileListState::search(
|
||||
req.path().to_owned(),
|
||||
profile.0.id(),
|
||||
search.into_inner().term,
|
||||
page_num,
|
||||
&state,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
ProfileListState::build(req.path().to_owned(), profile.0.id(), page, &state).await?
|
||||
};
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::profiles::discover(cursor, &state, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PageKind {
|
||||
Linear(Page),
|
||||
Search(SearchPage),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfileListState {
|
||||
path: String,
|
||||
cache: ProfileCache,
|
||||
profiles: Page,
|
||||
profiles: PageKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||||
|
@ -49,6 +69,16 @@ enum ProfilePage {
|
|||
Min { min: Uuid },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||||
struct PageNumQuery {
|
||||
page: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct Search {
|
||||
term: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProfileCache {
|
||||
profiles: HashMap<Uuid, Profile>,
|
||||
|
@ -64,9 +94,31 @@ struct ProfilePager<'b> {
|
|||
viewer: Uuid,
|
||||
}
|
||||
|
||||
impl PageKind {
|
||||
fn items(&self) -> &[Uuid] {
|
||||
match self {
|
||||
PageKind::Linear(page) => &page.items,
|
||||
PageKind::Search(page) => &page.items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileListState {
|
||||
pub(crate) fn search_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub(crate) fn search_input(&self) -> TextInput {
|
||||
let input = TextInput::new("term").title("Handle").placeholder("Handle");
|
||||
|
||||
match &self.profiles {
|
||||
PageKind::Search(search) => input.value(&search.term),
|
||||
_ => input,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn profiles<'a>(&'a self) -> impl Iterator<Item = ProfileView<'a>> + 'a {
|
||||
self.profiles.items.iter().filter_map(move |profile_id| {
|
||||
self.profiles.items().iter().filter_map(move |profile_id| {
|
||||
let profile = self.cache.profiles.get(profile_id)?;
|
||||
let icon = profile
|
||||
.icon()
|
||||
|
@ -115,31 +167,92 @@ impl ProfileListState {
|
|||
}
|
||||
|
||||
pub(crate) fn has_nav(&self) -> bool {
|
||||
self.profiles.next.is_some() || self.profiles.prev.is_some()
|
||||
match &self.profiles {
|
||||
PageKind::Linear(page) => page.next.is_some() || page.prev.is_some(),
|
||||
PageKind::Search(page) => page.next.is_some() || page.prev.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn nav(&self) -> Vec<Button> {
|
||||
let mut nav = vec![];
|
||||
|
||||
if let Some(prev) = self.profiles.prev {
|
||||
nav.push(Button::secondary("Previous").href(&format!("{}?min={}", self.path, prev)));
|
||||
match &self.profiles {
|
||||
PageKind::Linear(page) => {
|
||||
if let Some(prev) = page.prev {
|
||||
nav.push(
|
||||
Button::secondary("Previous").href(&format!("{}?min={}", self.path, prev)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(next) = self.profiles.next {
|
||||
nav.push(Button::secondary("Next").href(&format!("{}?max={}", self.path, next)));
|
||||
if let Some(next) = page.next {
|
||||
nav.push(
|
||||
Button::secondary("Next").href(&format!("{}?max={}", self.path, next)),
|
||||
);
|
||||
}
|
||||
}
|
||||
PageKind::Search(page) => {
|
||||
if let Some(prev) = page.prev {
|
||||
nav.push(
|
||||
Button::secondary("Previous")
|
||||
.href(&format!("{}?term={}&page={}", self.path, page.term, prev)),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(next) = page.next {
|
||||
nav.push(
|
||||
Button::secondary("Next")
|
||||
.href(&format!("{}?term={}&page={}", self.path, page.term, next)),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
nav
|
||||
}
|
||||
|
||||
async fn search(
|
||||
path: String,
|
||||
viewer: Uuid,
|
||||
term: String,
|
||||
page: Option<PageNum>,
|
||||
state: &State,
|
||||
) -> Result<Self, Error> {
|
||||
let store = state.profiles.clone();
|
||||
|
||||
let state = web::block(move || {
|
||||
let mut cache = ProfileCache::new();
|
||||
|
||||
let profiles = SearchPage::from_pagination(
|
||||
ProfilePager {
|
||||
cache: &mut cache,
|
||||
store: &store,
|
||||
viewer,
|
||||
},
|
||||
PER_PAGE,
|
||||
term,
|
||||
page,
|
||||
);
|
||||
|
||||
let state = ProfileListState {
|
||||
path,
|
||||
cache,
|
||||
profiles: PageKind::Search(profiles),
|
||||
};
|
||||
|
||||
Ok(state) as Result<_, Error>
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
async fn build(
|
||||
path: &str,
|
||||
path: String,
|
||||
viewer: Uuid,
|
||||
source: Option<PageSource>,
|
||||
state: &State,
|
||||
) -> Result<Self, Error> {
|
||||
let store = state.profiles.clone();
|
||||
let path = path.to_owned();
|
||||
|
||||
let state = web::block(move || {
|
||||
let mut cache = ProfileCache::new();
|
||||
|
@ -157,7 +270,7 @@ impl ProfileListState {
|
|||
let state = ProfileListState {
|
||||
path,
|
||||
cache,
|
||||
profiles,
|
||||
profiles: PageKind::Linear(profiles),
|
||||
};
|
||||
|
||||
Ok(state) as Result<Self, Error>
|
||||
|
@ -291,6 +404,21 @@ impl<'b> Pagination for ProfilePager<'b> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'b> SearchPagination for ProfilePager<'b> {
|
||||
fn from_term<'a>(
|
||||
&'a mut self,
|
||||
term: &'a str,
|
||||
) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
|
||||
Box::new(
|
||||
self.store
|
||||
.store
|
||||
.profiles
|
||||
.search(term)
|
||||
.filter_map(move |profile_id| self.filter_profile(profile_id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProfilePage> for PageSource {
|
||||
fn from(page: ProfilePage) -> Self {
|
||||
match page {
|
||||
|
@ -299,3 +427,9 @@ impl From<ProfilePage> for PageSource {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PageNumQuery> for PageNum {
|
||||
fn from(page: PageNumQuery) -> Self {
|
||||
PageNum { page: page.page }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
@use crate::nav::NavState;
|
||||
@use crate::profile_list::ProfileListState;
|
||||
@use crate::templates::layouts::home;
|
||||
@use hyaenidae_toolkit::templates::button_group;
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body}, Card};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
@use hyaenidae_toolkit::templates::profile;
|
||||
@use hyaenidae_toolkit::templates::text_input;
|
||||
|
||||
@(state: &ProfileListState, nav_state: &NavState)
|
||||
|
||||
@:home("Discover Users", "Find users on Hyaenidae", nav_state, {}, {
|
||||
@:card(&Card::full_width().dark(nav_state.dark()), {
|
||||
<form method="GET" action="@state.search_path()">
|
||||
@:card_title({ Search })
|
||||
@:card_body({
|
||||
@:text_input(&state.search_input().dark(nav_state.dark()))
|
||||
})
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
Button::primary("Search"),
|
||||
])
|
||||
})
|
||||
</form>
|
||||
})
|
||||
@for pview in state.profiles() {
|
||||
@:card(&Card::full_width().dark(nav_state.dark()), {
|
||||
@:profile(&pview.heading().dark(nav_state.dark()))
|
||||
|
|
Loading…
Reference in a new issue