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),
|
OlderThan(Uuid),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct PageNum {
|
||||||
|
pub(crate) page: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Page {
|
pub(crate) struct Page {
|
||||||
pub(crate) items: Vec<Uuid>,
|
pub(crate) items: Vec<Uuid>,
|
||||||
|
@ -16,12 +21,25 @@ pub(crate) struct Page {
|
||||||
pub(crate) reset: bool,
|
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 {
|
pub trait Pagination {
|
||||||
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a>;
|
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_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>;
|
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 {
|
impl Page {
|
||||||
pub(crate) fn from_pagination(
|
pub(crate) fn from_pagination(
|
||||||
mut pagination: impl 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,
|
extensions::ProfileExt,
|
||||||
middleware::UserProfile,
|
middleware::UserProfile,
|
||||||
nav::NavState,
|
nav::NavState,
|
||||||
pagination::{Page, PageSource, Pagination},
|
pagination::{Page, PageNum, PageSource, Pagination, SearchPage, SearchPagination},
|
||||||
views::ProfileView,
|
views::ProfileView,
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
||||||
use hyaenidae_profiles::store::{File, Profile};
|
use hyaenidae_profiles::store::{File, Profile};
|
||||||
use hyaenidae_toolkit::Button;
|
use hyaenidae_toolkit::{Button, TextInput};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -23,23 +23,43 @@ async fn profile_list(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
profile: UserProfile,
|
profile: UserProfile,
|
||||||
page: Option<web::Query<ProfilePage>>,
|
page: Option<web::Query<ProfilePage>>,
|
||||||
|
search: Option<web::Query<Search>>,
|
||||||
|
page_num: Option<web::Query<PageNumQuery>>,
|
||||||
nav_state: NavState,
|
nav_state: NavState,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let page = page.map(|query| query.into_inner().into());
|
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::rendered(HttpResponse::Ok(), |cursor| {
|
||||||
crate::templates::profiles::discover(cursor, &state, &nav_state)
|
crate::templates::profiles::discover(cursor, &state, &nav_state)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum PageKind {
|
||||||
|
Linear(Page),
|
||||||
|
Search(SearchPage),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ProfileListState {
|
pub struct ProfileListState {
|
||||||
path: String,
|
path: String,
|
||||||
cache: ProfileCache,
|
cache: ProfileCache,
|
||||||
profiles: Page,
|
profiles: PageKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||||||
|
@ -49,6 +69,16 @@ enum ProfilePage {
|
||||||
Min { min: Uuid },
|
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)]
|
#[derive(Debug, Default)]
|
||||||
struct ProfileCache {
|
struct ProfileCache {
|
||||||
profiles: HashMap<Uuid, Profile>,
|
profiles: HashMap<Uuid, Profile>,
|
||||||
|
@ -64,9 +94,31 @@ struct ProfilePager<'b> {
|
||||||
viewer: Uuid,
|
viewer: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PageKind {
|
||||||
|
fn items(&self) -> &[Uuid] {
|
||||||
|
match self {
|
||||||
|
PageKind::Linear(page) => &page.items,
|
||||||
|
PageKind::Search(page) => &page.items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProfileListState {
|
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 {
|
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 profile = self.cache.profiles.get(profile_id)?;
|
||||||
let icon = profile
|
let icon = profile
|
||||||
.icon()
|
.icon()
|
||||||
|
@ -115,31 +167,92 @@ impl ProfileListState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_nav(&self) -> bool {
|
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> {
|
pub(crate) fn nav(&self) -> Vec<Button> {
|
||||||
let mut nav = vec![];
|
let mut nav = vec![];
|
||||||
|
|
||||||
if let Some(prev) = self.profiles.prev {
|
match &self.profiles {
|
||||||
nav.push(Button::secondary("Previous").href(&format!("{}?min={}", self.path, prev)));
|
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 {
|
if let Some(next) = page.next {
|
||||||
nav.push(Button::secondary("Next").href(&format!("{}?max={}", self.path, 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
|
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(
|
async fn build(
|
||||||
path: &str,
|
path: String,
|
||||||
viewer: Uuid,
|
viewer: Uuid,
|
||||||
source: Option<PageSource>,
|
source: Option<PageSource>,
|
||||||
state: &State,
|
state: &State,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let store = state.profiles.clone();
|
let store = state.profiles.clone();
|
||||||
let path = path.to_owned();
|
|
||||||
|
|
||||||
let state = web::block(move || {
|
let state = web::block(move || {
|
||||||
let mut cache = ProfileCache::new();
|
let mut cache = ProfileCache::new();
|
||||||
|
@ -157,7 +270,7 @@ impl ProfileListState {
|
||||||
let state = ProfileListState {
|
let state = ProfileListState {
|
||||||
path,
|
path,
|
||||||
cache,
|
cache,
|
||||||
profiles,
|
profiles: PageKind::Linear(profiles),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(state) as Result<Self, Error>
|
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 {
|
impl From<ProfilePage> for PageSource {
|
||||||
fn from(page: ProfilePage) -> Self {
|
fn from(page: ProfilePage) -> Self {
|
||||||
match page {
|
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::nav::NavState;
|
||||||
@use crate::profile_list::ProfileListState;
|
@use crate::profile_list::ProfileListState;
|
||||||
@use crate::templates::layouts::home;
|
@use crate::templates::layouts::home;
|
||||||
@use hyaenidae_toolkit::templates::button_group;
|
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||||
@use hyaenidae_toolkit::{templates::{card, card_body}, Card};
|
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||||
@use hyaenidae_toolkit::templates::profile;
|
@use hyaenidae_toolkit::templates::profile;
|
||||||
|
@use hyaenidae_toolkit::templates::text_input;
|
||||||
|
|
||||||
@(state: &ProfileListState, nav_state: &NavState)
|
@(state: &ProfileListState, nav_state: &NavState)
|
||||||
|
|
||||||
@:home("Discover Users", "Find users on Hyaenidae", nav_state, {}, {
|
@: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() {
|
@for pview in state.profiles() {
|
||||||
@:card(&Card::full_width().dark(nav_state.dark()), {
|
@:card(&Card::full_width().dark(nav_state.dark()), {
|
||||||
@:profile(&pview.heading().dark(nav_state.dark()))
|
@:profile(&pview.heading().dark(nav_state.dark()))
|
||||||
|
|
Loading…
Reference in a new issue