From 12095f4141e95d801ce7f66106a2ad2bc3dac5bb Mon Sep 17 00:00:00 2001 From: Franklin Date: Mon, 20 Mar 2023 16:37:16 -0400 Subject: [PATCH] Finished search logic --- Cargo.lock | 10 ++- Cargo.toml | 4 +- css/details.css | 21 ++++-- css/search.css | 12 +++ src/api/backend/mod.rs | 25 ++++++ src/api/base.rs | 63 ++++++++++++++++ src/components/nav_bar.rs | 10 +-- src/components/project_card.rs | 24 ++++-- src/pages/details.rs | 14 +++- src/pages/search.rs | 134 ++++++++++++++++++++++++++++----- src/routes/main_router.rs | 12 +-- 11 files changed, 283 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a31fe3..11933d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index fb5d502..8fc9029 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } \ No newline at end of file +jl-types = { path = "../jl-types", features = ["wasm"] } diff --git a/css/details.css b/css/details.css index ca6a44e..6d08a4f 100644 --- a/css/details.css +++ b/css/details.css @@ -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; } \ No newline at end of file diff --git a/css/search.css b/css/search.css index 57fbe84..cee4d9d 100644 --- a/css/search.css +++ b/css/search.css @@ -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 { diff --git a/src/api/backend/mod.rs b/src/api/backend/mod.rs index e69de29..4b5e12d 100644 --- a/src/api/backend/mod.rs +++ b/src/api/backend/mod.rs @@ -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, err::Error> { + perform_request_without_client::>(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, err::Error> { + perform_request_without_client::>(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) -> Result, err::Error> { + perform_request_without_client::>(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 { + perform_request_without_client::(BASE_URL.into(), Method::GET, format!("read/project/{project_id}"), None, 200, Vec::new(), None).await +} \ No newline at end of file diff --git a/src/api/base.rs b/src/api/base.rs index e69de29..fba6f08 100644 --- a/src/api/base.rs +++ b/src/api/base.rs @@ -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( + base_url: String, + method: reqwest::Method, + path: String, + body: Option, + expected_status_code: u16, + headers: Vec<(String, String)>, + params: Option> +) -> Result { + 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(¶meters) + } + + 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::().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::>().await { + Ok(messages) => messages, + Err(e) => vec![MessageResource::from(e)], + }, + )) + } + } + } + Err(e) => { + // Request couldn't be sent + Err(Error::Network(MessageResource::from(e))) + } + } +} \ No newline at end of file diff --git a/src/components/nav_bar.rs b/src/components/nav_bar.rs index c2c56dc..f65821c 100644 --- a/src/components/nav_bar.rs +++ b/src/components/nav_bar.rs @@ -37,14 +37,14 @@ pub fn navigation_bar() -> Html { {NAVBAR_COL_LANDING} -
- {NAVBAR_COL_PROYECTOS_ACABADOS} -
- -
+
{NAVBAR_COL_PROYECTOS_EN_CONSTRUCCION}
+
+ {NAVBAR_COL_PROYECTOS_ACABADOS} +
+
{NAVBAR_COL_CONTACTO}
diff --git a/src/components/project_card.rs b/src/components/project_card.rs index 07e5966..1c74f60 100644 --- a/src/components/project_card.rs +++ b/src/components/project_card.rs @@ -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!{
- {"project + {"project
{"Suites by refa Piantini"} @@ -41,4 +50,9 @@ pub fn project_card() -> Html {
} +} + +#[derive(Properties, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProjectCardProps { + pub project: Project } \ No newline at end of file diff --git a/src/pages/details.rs b/src/pages/details.rs index e76df9d..780d85c 100644 --- a/src/pages/details.rs +++ b/src/pages/details.rs @@ -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!{ <>
-
+
+
{"Suites by refa piantini"}
+
{"RD$123,130.00"}
+
{"Andres Julio Aybar #39"}
+
{"DescripciĆ³n"}
} +} + +#[derive(Properties, PartialEq)] +pub struct DetailsPageProps { + pub project_id: Uuid } \ No newline at end of file diff --git a/src/pages/search.rs b/src/pages/search.rs index 122e949..0afbdf7 100644 --- a/src/pages/search.rs +++ b/src/pages/search.rs @@ -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> = use_state(|| Vec::new()); + let page_counter: UseStateHandle = 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> = 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> = use_state(|| OptionWrapper::new(None)); // Dropdown let project_condition_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); // Dropdown - let project_state_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); + // let project_state_filter: UseStateHandle> = use_state(|| OptionWrapper::new(Some(props.project_state.clone()))); // Dropdown let project_city_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); // Dropdown let project_district_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); //TODO: Think about price filtering - // TextField + /*// TextField let _project_min_price_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); // TextField - let _project_max_price_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None)); + let _project_max_price_filter: UseStateHandle> = use_state(|| OptionWrapper::new(None));*/ let project_type_drop_down = comp_with::>>(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::>>(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| { @@ -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::>>(DropDownProps { initial: OptionWrapper::new(None), @@ -67,12 +103,25 @@ pub fn search_page(_props: &SearchPageProperties) -> Html { let project_city_drop_down = comp_with::>>(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| { + 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> = 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::>>(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| { @@ -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!{ <> @@ -105,12 +198,12 @@ pub fn search_page(_props: &SearchPageProperties) -> Html { {project_type_drop_down}
-
+ /*
{"Estatus de Proyecto"}
{project_state_drop_down} -
+
*/
@@ -132,15 +225,16 @@ pub fn search_page(_props: &SearchPageProperties) -> Html {
{project_district_drop_down}
+ +
// Search Results Content - - - - + {(*search_results_handle).clone().into_iter().map(|project| html!{}).collect::()}
@@ -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 -} \ No newline at end of file + pub project_state: ProjectState, +} diff --git a/src/routes/main_router.rs b/src/routes/main_router.rs index 556f93a..8c4d032 100644 --- a/src/routes/main_router.rs +++ b/src/routes/main_router.rs @@ -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! { }, - Route::Search { property_state } => html! { }, - Route::Details { property_id: _ } => html! { }, + Route::Search { project_state } => html! { }, + Route::Details { project_id } => html! { }, Route::NotFound => html! { }, Route::Contact => html! { } }