Finished search logic

This commit is contained in:
Franklin 2023-03-20 16:37:16 -04:00
parent 3b801c59fe
commit 12095f4141
11 changed files with 283 additions and 46 deletions

10
Cargo.lock generated
View File

@ -212,6 +212,14 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "err"
version = "0.1.1"
source = "git+https://git.franklinblanco.dev/franklinblanco/err.git#d814091e7367d101197c35e2f7e56a744ce4296b"
dependencies = [
"serde",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -698,6 +706,7 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
name = "jl-frontend"
version = "0.1.0"
dependencies = [
"err",
"jl-types",
"js-sys",
"log",
@ -726,7 +735,6 @@ dependencies = [
"serde",
"serde_json",
"uuid",
"yew",
]
[[package]]

View File

@ -27,6 +27,8 @@ uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.88"
err = { git = "https://git.franklinblanco.dev/franklinblanco/err.git" }
# Core
jl-types = { path = "../jl-types", features = ["yew", "wasm"] }
jl-types = { path = "../jl-types", features = ["wasm"] }

View File

@ -7,17 +7,26 @@ Divide the Details page into 3 main sections:
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: center;
padding: 0px 30px;
background-color: black;
margin-top: 30px;
width: 100%;
align-items: start;
padding: 0px 15px;
margin-top: 60px;
}
.details-head {
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: center;
width: 100%;
}
.details-head-image-frame{
.details-head-image-frame {
display: flex;
height: 300px;
width: 100%;
background-color: gray;
}
.details-head-title {
font-size: 25px;
}

View File

@ -57,6 +57,18 @@
background-color:rgba(0, 0, 0, 0.1);
}
.project-search-button {
margin-top: 20px;
height: 50px;
background-color: #252631;
color: white;
border: 0px;
border-radius: 5px;
font-size: large;
font-family: Source Sans Pro;
font-weight: lighter;
}
/* Results */
.project-search-results-container {

View File

@ -0,0 +1,25 @@
use std::collections::HashSet;
use jl_types::{dto::{filters::Filter, listing::Listing}, domain::project::Project};
use reqwest::Method;
use uuid::Uuid;
use super::base::perform_request_without_client;
const BASE_URL: &str = "http://localhost:8095/";
pub async fn get_all_cities() -> Result<HashSet<String>, err::Error> {
perform_request_without_client::<String, HashSet<String>>(BASE_URL.into(), Method::GET, "read/locations".into(), None, 200, Vec::new(), None).await
}
pub async fn get_all_districts_in_city(city: &String) -> Result<HashSet<String>, err::Error> {
perform_request_without_client::<String, HashSet<String>>(BASE_URL.into(), Method::GET, format!("read/locations/{city}"), None, 200, Vec::new(), None).await
}
pub async fn get_all_projects_with_filters_paged(page: &i64, filters: Vec<Filter>) -> Result<Vec<Project>, err::Error> {
perform_request_without_client::<String, Vec<Project>>(BASE_URL.into(), Method::GET, format!("read/projects/{page}"), None, 200, Vec::new(), Some(filters.into_iter().map(|filter| filter.to_param()).collect())).await
}
pub async fn get_project_listing(project_id: &Uuid) -> Result<Listing, err::Error> {
perform_request_without_client::<String, Listing>(BASE_URL.into(), Method::GET, format!("read/project/{project_id}"), None, 200, Vec::new(), None).await
}

View File

@ -0,0 +1,63 @@
use err::{Error, MessageResource};
use reqwest::Client;
use serde::{Serialize, de::DeserializeOwned};
/// This function is mainly for when you don't have a client in your application and just want to get it over with.
/// This shouldn't be used as it takes more resource consumption than the above method.
pub async fn perform_request_without_client<B: Serialize, R: DeserializeOwned>(
base_url: String,
method: reqwest::Method,
path: String,
body: Option<B>,
expected_status_code: u16,
headers: Vec<(String, String)>,
params: Option<Vec<(String, String)>>
) -> Result<R, Error> {
let client = Client::new();
let mut req_incomplete =
client.request(method, format!("{url}{path}", url = base_url, path = path));
for header in headers {
req_incomplete = req_incomplete.header(&header.0, &header.1);
}
if let Some(parameters) = params {
req_incomplete = req_incomplete.query(&parameters)
}
let req_complete = match body {
Some(b) => req_incomplete.json(&b),
None => req_incomplete.header("content-length", 0),
};
println!("{:?}", req_complete);
match req_complete.send().await {
// Error handling here
Ok(res) => {
// Request sent correctly
match res.status().as_u16() == expected_status_code {
true => {
match res.json::<R>().await {
Ok(resp_dto) => Ok(resp_dto), // Return correctly deserialized obj
Err(err) => Err(Error::Serde(MessageResource::from(err))),
}
}
false => {
//If status code is any other than expected
Err(Error::UnexpectedStatusCode(
expected_status_code,
res.status().as_u16(),
match res.json::<Vec<MessageResource>>().await {
Ok(messages) => messages,
Err(e) => vec![MessageResource::from(e)],
},
))
}
}
}
Err(e) => {
// Request couldn't be sent
Err(Error::Network(MessageResource::from(e)))
}
}
}

View File

@ -37,14 +37,14 @@ pub fn navigation_bar() -> Html {
{NAVBAR_COL_LANDING}
</div>
<div onclick={move |_| cloned_navigator_2.push(&Route::Search { property_state: ProjectState::Finished })} class={"navbar-item"}>
{NAVBAR_COL_PROYECTOS_ACABADOS}
</div>
<div onclick={move |_| cloned_navigator_3.push(&Route::Search { property_state: ProjectState::InConstruction })} class={"navbar-item"}>
<div onclick={move |_| cloned_navigator_2.push(&Route::Search { project_state: ProjectState::InConstruction })} class={"navbar-item"}>
{NAVBAR_COL_PROYECTOS_EN_CONSTRUCCION}
</div>
<div onclick={move |_| cloned_navigator_3.push(&Route::Search { project_state: ProjectState::Finished })} class={"navbar-item"}>
{NAVBAR_COL_PROYECTOS_ACABADOS}
</div>
<div onclick={move |_| cloned_navigator_4.push(&Route::Contact)} class={"navbar-item"}>
{NAVBAR_COL_CONTACTO}
</div>

View File

@ -1,4 +1,4 @@
use uuid::Uuid;
use jl_types::domain::{project::Project, media::Media};
use yew::prelude::*;
use yew_router::prelude::use_navigator;
@ -6,15 +6,24 @@ use crate::routes::main_router::Route;
#[function_component(ProjectCard)]
pub fn project_card() -> Html {
pub fn project_card(props: &ProjectCardProps) -> Html {
let navigator = use_navigator().unwrap();
let project_id = props.project.id.clone();
let project_view_cb = Callback::from(move |_|{
navigator.push(&Route::Details { property_id: Uuid::default() });
navigator.push(&Route::Details { project_id });
});
let cover_image_url;
if let Some(first_media) = props.project.media.media_list.get(0) {
cover_image_url = match first_media {
Media::Photo(url) => url.clone(),
Media::Video(_) => String::new(),
}
} else {
cover_image_url = String::new()
}
html!{
<div class={"project-search-result-card"} onclick={project_view_cb}>
<img src={"https://refa.com.do/uploads/posiv.jpg"} alt={"project image"} class={"project-search-result-card-picture"}/>
<img src={cover_image_url} alt={"project image"} class={"project-search-result-card-picture"}/>
<div class={"project-search-result-card-title"}>
{"Suites by refa Piantini"}
@ -41,4 +50,9 @@ pub fn project_card() -> Html {
</div>
</div>
}
}
#[derive(Properties, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProjectCardProps {
pub project: Project
}

View File

@ -1,21 +1,31 @@
use uuid::Uuid;
use yew::prelude::*;
use crate::components::nav_bar::NavigationBar;
#[function_component(DetailsPage)]
pub fn details_page() -> Html {
pub fn details_page(_props: &DetailsPageProps) -> Html {
html!{
<>
<NavigationBar/>
<div class={"page-container"}>
<div class={"details-container"}>
<div class={"details-head"}>
<div class={""}>
<div class={"details-head-image-frame"}>
</div>
<div class={"details-head-title"}>{"Suites by refa piantini"}</div>
<div class={"details-head-price"}>{"RD$123,130.00"}</div>
<div>{"Andres Julio Aybar #39"}</div>
<div>{"Descripción"}</div>
</div>
</div>
</div>
</>
}
}
#[derive(Properties, PartialEq)]
pub struct DetailsPageProps {
pub project_id: Uuid
}

View File

@ -1,29 +1,63 @@
use jl_types::domain::{project_state::ProjectState, project_type::ProjectType, project_condition::ProjectCondition};
use jl_types::{domain::{project_state::ProjectState, project_type::ProjectType, project_condition::ProjectCondition, project::Project}, dto::filters::Filter};
use log::info;
use yew::prelude::*;
use yew_utils::{components::drop_down::{DropDownProps, DropDown}, vdom::comp_with};
use jl_types::domain::option_wrapper::OptionWrapper;
use crate::components::{nav_bar::NavigationBar, project_card::ProjectCard};
use crate::{components::{nav_bar::NavigationBar, project_card::ProjectCard}, api::backend::{get_all_cities, get_all_districts_in_city, get_all_projects_with_filters_paged}};
#[function_component(SearchPage)]
pub fn search_page(_props: &SearchPageProperties) -> Html {
pub fn search_page(props: &SearchPageProperties) -> Html {
// let force_update_trigger = use_force_update();
let cities_handle = use_state(|| Vec::from([OptionWrapper::new(None)]));
let districts_handle = use_state(|| Vec::from([OptionWrapper::new(None)]));
let search_results_handle: UseStateHandle<Vec<Project>> = use_state(|| Vec::new());
let page_counter: UseStateHandle<i64> = use_state(|| 1);
let mut filters = Vec::new();
if props.project_state.eq(&ProjectState::Finished) {
filters.push(Filter::Finished);
}
// All code to execute on first render and never again
use_state(|| {
let cities_handle = cities_handle.clone();
let search_results_handle = search_results_handle.clone();
let page_counter = page_counter.clone();
wasm_bindgen_futures::spawn_local(async move {
match get_all_cities().await {
Ok(cities) => {
let mut cities: Vec<OptionWrapper<String>> = cities.into_iter().map(|location| OptionWrapper::new(Some(location))).collect();
cities.insert(0, OptionWrapper::new(None));
cities_handle.set(cities);
},
Err(error) => info!("Error in loading cities: {error}")
};
match get_all_projects_with_filters_paged(&(*page_counter), filters).await {
Ok(projects) => {
search_results_handle.set(projects)
},
Err(error) => info!("Error in loading projects: {error}"),
};
});
});
// Dropdown
let project_type_filter: UseStateHandle<OptionWrapper<ProjectType>> = use_state(|| OptionWrapper::new(None));
// Dropdown
let project_condition_filter: UseStateHandle<OptionWrapper<ProjectCondition>> = use_state(|| OptionWrapper::new(None));
// Dropdown
let project_state_filter: UseStateHandle<OptionWrapper<ProjectState>> = use_state(|| OptionWrapper::new(None));
// let project_state_filter: UseStateHandle<OptionWrapper<ProjectState>> = use_state(|| OptionWrapper::new(Some(props.project_state.clone())));
// Dropdown
let project_city_filter: UseStateHandle<OptionWrapper<String>> = use_state(|| OptionWrapper::new(None));
// Dropdown
let project_district_filter: UseStateHandle<OptionWrapper<String>> = use_state(|| OptionWrapper::new(None));
//TODO: Think about price filtering
// TextField
/*// TextField
let _project_min_price_filter: UseStateHandle<OptionWrapper<f64>> = use_state(|| OptionWrapper::new(None));
// TextField
let _project_max_price_filter: UseStateHandle<OptionWrapper<f64>> = use_state(|| OptionWrapper::new(None));
let _project_max_price_filter: UseStateHandle<OptionWrapper<f64>> = use_state(|| OptionWrapper::new(None));*/
let project_type_drop_down = comp_with::<DropDown<OptionWrapper<ProjectType>>>(DropDownProps {
@ -39,9 +73,11 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
class_css: Some("project-search-filter-item".into())
});
/*
// TODO: Fix ProjectState tostring printing the db insertable
let project_state_drop_down = comp_with::<DropDown<OptionWrapper<ProjectState>>>(DropDownProps {
initial: OptionWrapper::new(None),
options: vec![OptionWrapper::new(None), OptionWrapper::new(Some(ProjectState::InConstruction)), OptionWrapper::new(Some(ProjectState::Finished)) ],
initial: (*project_state_filter).clone(),
options: vec![OptionWrapper::new(Some(ProjectState::InConstruction)), OptionWrapper::new(Some(ProjectState::Finished)) ],
selection_changed: {
let cloned_project_state_filter = project_state_filter.clone();
Callback::from(move |project_state: OptionWrapper<ProjectState>| {
@ -50,7 +86,7 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
}
)},
class_css: Some("project-search-filter-item".into())
});
});*/
let project_condition_drop_down = comp_with::<DropDown<OptionWrapper<ProjectCondition>>>(DropDownProps {
initial: OptionWrapper::new(None),
@ -67,12 +103,25 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
let project_city_drop_down = comp_with::<DropDown<OptionWrapper<String>>>(DropDownProps {
initial: OptionWrapper::new(None),
options: vec![OptionWrapper::new(None), OptionWrapper::new(Some("Santo Domingo".into())), OptionWrapper::new(Some("Punta Cana".into())) ],
options: (*cities_handle).clone(),
selection_changed: {
let cloned_project_city_filter = project_city_filter.clone();
let districts_handle = districts_handle.clone();
Callback::from(move |project_city: OptionWrapper<String>| {
let districts_handle = districts_handle.clone();
info!("{}", project_city.to_string());
cloned_project_city_filter.set(project_city)
cloned_project_city_filter.set(project_city.clone());
wasm_bindgen_futures::spawn_local(async move {
match get_all_districts_in_city(&project_city.to_string()).await {
Ok(districts) => {
let mut districts_vec: Vec<OptionWrapper<String>> = districts.into_iter().map(|district| OptionWrapper::new(Some(district))).collect();
districts_vec.insert(0, OptionWrapper::new(None));
districts_handle.set(districts_vec);
},
Err(error) => info!("Error in dropdown callback: {}", error),
};
});
}
)},
class_css: Some("project-search-filter-item".into())
@ -81,7 +130,7 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
//TODO: District dropdown should only show districts in city, otherwise show nothing or disabled
let project_district_drop_down = comp_with::<DropDown<OptionWrapper<String>>>(DropDownProps {
initial: OptionWrapper::new(None),
options: vec![OptionWrapper::new(None), OptionWrapper::new(Some("Evaristo Morales".into())), OptionWrapper::new(Some("Cap Cana".into())) ],
options: (*districts_handle).clone(),
selection_changed: {
let cloned_project_district_filter = project_district_filter.clone();
Callback::from(move |project_district: OptionWrapper<String>| {
@ -92,6 +141,50 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
class_css: Some("project-search-filter-item".into())
});
let search_onclick = {
let search_results_handle = search_results_handle.clone();
let page_counter = page_counter.clone();
let project_type_filter = project_type_filter.clone();
let project_condition_filter = project_condition_filter.clone();
let project_city_filter = project_city_filter.clone();
let project_district_filter = project_district_filter.clone();
let props = props.clone();
Callback::from(move |_| {
let mut filters = Vec::new();
if props.project_state.eq(&ProjectState::Finished) {
filters.push(Filter::Finished);
}
match &(*project_type_filter).option {
Some(project_type) => filters.push(Filter::ByProjectType(project_type.clone())),
None => {},
};
match &(*project_condition_filter).option {
Some(project_condition) => filters.push(Filter::ByProjectCondition(project_condition.clone())),
None => {},
};
match &(*project_city_filter).option {
Some(project_city) => filters.push(Filter::InCity(project_city.clone())),
None => {},
};
match &(*project_district_filter).option {
Some(project_district) => filters.push(Filter::InDistrict(project_district.clone())),
None => {},
};
let search_results_handle = search_results_handle.clone();
let page_counter = page_counter.clone();
wasm_bindgen_futures::spawn_local(async move {
match get_all_projects_with_filters_paged(&(*page_counter), filters).await {
Ok(projects) => {
search_results_handle.set(projects)
},
Err(error) => info!("Error in loading projects: {error}"),
};
info!("done");
});
})};
html!{
<>
<NavigationBar/>
@ -105,12 +198,12 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
{project_type_drop_down}
</div>
<div class={"project-search-filter-container"}>
/*<div class={"project-search-filter-container"}>
<div class={"project-search-filter-label"}>
{"Estatus de Proyecto"}
</div>
{project_state_drop_down}
</div>
</div>*/
<div class={"project-search-filter-container"}>
<div class={"project-search-filter-label"}>
@ -132,15 +225,16 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
</div>
{project_district_drop_down}
</div>
<button class={"project-search-button"} onclick={search_onclick}>
{"Buscar"}
</button>
</div>
<div class={"project-search-divider"}/>
</div>
<div class={"project-search-results-container"}> // Search Results Content
<ProjectCard/>
<ProjectCard/>
<ProjectCard/>
<ProjectCard/>
{(*search_results_handle).clone().into_iter().map(|project| html!{<ProjectCard {project}/>}).collect::<Html>()}
</div>
</div>
</>
@ -149,5 +243,5 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Properties)]
pub struct SearchPageProperties {
pub property_state: ProjectState
}
pub project_state: ProjectState,
}

View File

@ -10,10 +10,10 @@ use crate::{pages::{landing::LandingPage, search::{SearchPage}, details::Details
pub enum Route {
#[at("/")]
LandingPage,
#[at("/search/:property_state")]
Search { property_state: ProjectState },
#[at("/details/:property_id")]
Details { property_id: Uuid },
#[at("/search/:project_state")]
Search { project_state: ProjectState },
#[at("/details/:project_id")]
Details { project_id: Uuid },
#[at("/contact")]
Contact,
@ -25,8 +25,8 @@ pub enum Route {
pub fn switch(routes: Route) -> Html {
match routes {
Route::LandingPage => html! { <LandingPage/> },
Route::Search { property_state } => html! { <SearchPage property_state={property_state}/> },
Route::Details { property_id: _ } => html! { <DetailsPage/> },
Route::Search { project_state } => html! { <SearchPage project_state={project_state}/> },
Route::Details { project_id } => html! { <DetailsPage project_id={project_id}/> },
Route::NotFound => html! { <NotFoundPage/> },
Route::Contact => html! { <ContactPage/> }
}