Server: Expose profile search on discover page

This commit is contained in:
asonix 2021-01-25 20:38:05 -06:00
parent aee79ec311
commit a9166f3984
3 changed files with 227 additions and 17 deletions

View file

@ -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,
}
}
}

View file

@ -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 }
}
}

View file

@ -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()))