feat/Search by company

pull/3/head
Miroito 3 years ago
parent b8a4ce5f04
commit f34a2e1fb5

@ -1,8 +1,16 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::components::base_async_select::IntoAsyncSelectListItem;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Company { pub struct Company {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub slug: String, pub slug: String,
} }
impl IntoAsyncSelectListItem for Company {
fn to_select_list_item(&self) -> String {
format!("{}", self.name)
}
}

@ -0,0 +1,108 @@
use serde::Deserialize;
use sycamore::prelude::*;
pub trait IntoAsyncSelectListItem {
fn to_select_list_item(&self) -> String;
}
#[derive(Clone)]
pub struct AsyncSelectRx<T>
where
T: 'static + PartialEq + Clone + IntoAsyncSelectListItem,
{
pub selected_item: Signal<Option<T>>,
}
#[component(BaseAsyncSelect<G>)]
pub fn create_component<T>(AsyncSelectRx { selected_item }: AsyncSelectRx<T>) -> View<G>
where
T: 'static + PartialEq + Clone + IntoAsyncSelectListItem,
for<'de> T: Deserialize<'de>,
{
let input = Signal::new("".to_string());
let visible = Signal::new(false);
let hide_dropdown = cloned!((visible, selected_item, input) => move |_| {
visible.set(false);
if selected_item.get().is_none() {
input.set("".to_string());
}
});
let item_list: Signal<Vec<T>> = Signal::new(vec![]);
let selected = Signal::new(false);
create_effect(
cloned!((input, visible, item_list, selected, selected_item) => move || {
// Early return if:
// - The input is empty, there is nothing to search for nor to show
// - We just selected an item
if input.get().is_empty() || *selected.get_untracked() {
selected.set(false);
visible.set(false);
return;
}
selected_item.set(None);
let url = "http://localhost:8000/v1/company/";
if G::IS_BROWSER {
perseus::spawn_local(
cloned!((input, visible, item_list) => async move {
let res = reqwasm::http::Request::get(
&format!( "{}{}?limit={}", url, input.get(), 5)
)
.send()
.await
.unwrap()
.json::<Vec<T>>()
.await
.unwrap();
visible.set(!res.is_empty());
item_list.set(res);
}),
);
}
}),
);
let input2 = input.clone();
view! {
input (bind:value=input, class="p-2 w-full rounded-md bg-slate-200 dark:bg-slate-700", on:blur=hide_dropdown) {}
div (class="relative") {
(
cloned!((visible, item_list, input2, selected_item, selected) => {
if *visible.get() {
view! {
div (class="absolute -top-1 w-80 rounded-b-md dark:bg-slate-700 bg-slate-200") {
ul {
Indexed(IndexedProps {
iterable: item_list.handle(),
template: move |x| {
view! {
li (
class="w-full p-2 cursor-pointer dark:hover:bg-slate-800 hover:bg-slate-300",
on:mousedown=cloned!(
(x, input2, selected_item, visible, selected) => move |_| {
selected.set(true);
selected_item.set(Some(x.clone()));
input2.set(x.clone().to_select_list_item());
visible.set(false);
}
),
)
{
(x.to_select_list_item())
}
}
},
})
}
}
}
} else {
view!{ p (class="hidden") { "Placeholder" } }
}
})
)
}
}
}

@ -0,0 +1,29 @@
use sycamore::prelude::*;
#[derive(Clone)]
pub struct BaseButtonStateRx {
pub label: ReadSignal<String>,
pub disabled: ReadSignal<bool>,
pub clicked: Signal<bool>,
}
#[component(BaseButton<G>)]
pub fn create_component(
BaseButtonStateRx {
label,
disabled,
clicked,
}: BaseButtonStateRx,
) -> View<G> {
let click_event = cloned!((clicked) => move |_| { clicked.set(true) });
view! {
button (
class="my-2 z-0 p-2 bg-slate-300 dark:bg-slate-700 hover:bg-slate-400 dark:hover:bg-slate-800 disabled:cursor-not-allowed hover:cursor-pointer rounded-md ",
disabled=*disabled.get(),
on:click=click_event,
) {
(label.get())
}
}
}

@ -1,3 +1,5 @@
pub mod base_async_select;
pub mod base_button;
pub mod base_table; pub mod base_table;
pub mod loading; pub mod loading;
pub mod paginated_data_table; pub mod paginated_data_table;

@ -1,14 +1,15 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use perseus::{Html, RenderFnResult, RenderFnResultWithCause, SsrNode, Template}; use perseus::{navigate, Html, RenderFnResult, RenderFnResultWithCause, SsrNode, Template};
use sycamore::{ use sycamore::prelude::*;
prelude::{view, View},
reactive::{cloned, Signal},
};
use crate::{ use crate::{
api::types::transaction::TransactionCompany, api::types::{company::Company, transaction::TransactionCompany},
components::paginated_data_table::{PaginatedTable, PaginatedTableStateRx}, components::{
base_async_select::{AsyncSelectRx, BaseAsyncSelect},
base_button::{BaseButton, BaseButtonStateRx},
paginated_data_table::{PaginatedTable, PaginatedTableStateRx},
},
global_state::AppStateRx, global_state::AppStateRx,
}; };
@ -30,6 +31,25 @@ pub fn index_page(IndexPageStateRx { req }: IndexPageStateRx, global_state: AppS
ph_data: Signal::new(PhantomData), ph_data: Signal::new(PhantomData),
}; };
let async_select_prop: AsyncSelectRx<Company> = AsyncSelectRx {
selected_item: Signal::new(None),
};
let async_select_prop2 = async_select_prop.clone();
let search_button = BaseButtonStateRx {
label: Signal::new("Search".to_string()).handle(),
disabled: create_memo(cloned!((async_select_prop) => move || {
async_select_prop.selected_item.get().is_none()
})),
clicked: Signal::new(false),
};
create_effect(cloned!((search_button) => move || {
if *search_button.clicked.get() {
navigate(&format!("/index/{}", (*async_select_prop2.selected_item.get()).clone().map_or("".to_string(), |c| c.slug)));
}
}));
view! { view! {
main (class=if *dark_mode_3.get() { "dark" } else { "" }) { main (class=if *dark_mode_3.get() { "dark" } else { "" }) {
link (rel="stylesheet", href = "/.perseus/static/tailwind.css") {} link (rel="stylesheet", href = "/.perseus/static/tailwind.css") {}
@ -52,10 +72,15 @@ pub fn index_page(IndexPageStateRx { req }: IndexPageStateRx, global_state: AppS
a (class="hover:underline", href="/") { a (class="hover:underline", href="/") {
h1 ( h1 (
class="text-center text-lg" class="text-center text-lg"
) { ) {
"Insider Transactions published by the AMF" "Insider Transactions published by the AMF"
} }
} }
div (class="w-80") {
p () {"Search for a company:"}
BaseAsyncSelect(async_select_prop)
BaseButton(search_button)
}
PaginatedTable(paginated_table_state) PaginatedTable(paginated_table_state)
} }
} }

@ -513,6 +513,14 @@ video {
--tw-backdrop-sepia: ; --tw-backdrop-sepia: ;
} }
.visible {
visibility: visible;
}
.\!visible {
visibility: visible !important;
}
.static { .static {
position: static; position: static;
} }
@ -521,6 +529,22 @@ video {
position: fixed; position: fixed;
} }
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.-top-1 {
top: -0.25rem;
}
.z-0 {
z-index: 0;
}
.m-2 { .m-2 {
margin: 0.5rem; margin: 0.5rem;
} }
@ -548,6 +572,11 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.my-10 {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
}
.mb-20 { .mb-20 {
margin-bottom: 5rem; margin-bottom: 5rem;
} }
@ -556,6 +585,10 @@ video {
margin-right: 2.5rem; margin-right: 2.5rem;
} }
.inline {
display: inline;
}
.flex { .flex {
display: flex; display: flex;
} }
@ -564,6 +597,10 @@ video {
display: table; display: table;
} }
.hidden {
display: none;
}
.h-10 { .h-10 {
height: 2.5rem; height: 2.5rem;
} }
@ -572,6 +609,10 @@ video {
height: 3rem; height: 3rem;
} }
.h-2 {
height: 0.5rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -580,6 +621,14 @@ video {
width: 80%; width: 80%;
} }
.w-10 {
width: 2.5rem;
}
.w-80 {
width: 20rem;
}
.table-auto { .table-auto {
table-layout: auto; table-layout: auto;
} }
@ -588,6 +637,10 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
.cursor-pointer {
cursor: pointer;
}
.flex-row { .flex-row {
flex-direction: row; flex-direction: row;
} }
@ -624,6 +677,11 @@ video {
border-radius: 9999px; border-radius: 9999px;
} }
.rounded-b-md {
border-bottom-right-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@ -666,6 +724,11 @@ video {
background-color: rgb(251 207 232 / var(--tw-bg-opacity)); background-color: rgb(251 207 232 / var(--tw-bg-opacity));
} }
.bg-slate-300 {
--tw-bg-opacity: 1;
background-color: rgb(203 213 225 / var(--tw-bg-opacity));
}
.p-2 { .p-2 {
padding: 0.5rem; padding: 0.5rem;
} }
@ -739,6 +802,20 @@ video {
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
} }
.hover\:cursor-pointer:hover {
cursor: pointer;
}
.hover\:bg-slate-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(203 213 225 / var(--tw-bg-opacity));
}
.hover\:bg-slate-400:hover {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
}
.hover\:font-bold:hover { .hover\:font-bold:hover {
font-weight: 700; font-weight: 700;
} }
@ -752,6 +829,24 @@ video {
text-decoration-line: underline; text-decoration-line: underline;
} }
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
.disabled\:bg-slate-200:disabled {
--tw-bg-opacity: 1;
background-color: rgb(226 232 240 / var(--tw-bg-opacity));
}
.disabled\:text-red-500:disabled {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.disabled\:opacity-50:disabled {
opacity: 0.5;
}
.dark .dark\:bg-slate-800 { .dark .dark\:bg-slate-800 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity)); background-color: rgb(30 41 59 / var(--tw-bg-opacity));
@ -786,11 +881,26 @@ video {
color: rgb(165 180 252 / var(--tw-text-opacity)); color: rgb(165 180 252 / var(--tw-text-opacity));
} }
.dark .dark\:hover\:bg-slate-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity));
}
.dark .dark\:hover\:bg-slate-300:hover {
--tw-bg-opacity: 1;
background-color: rgb(203 213 225 / var(--tw-bg-opacity));
}
.dark .dark\:hover\:text-indigo-600:hover { .dark .dark\:hover\:text-indigo-600:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity)); color: rgb(79 70 229 / var(--tw-text-opacity));
} }
.dark .dark\:disabled\:bg-slate-700:disabled {
--tw-bg-opacity: 1;
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:p-2 { .sm\:p-2 {
padding: 0.5rem; padding: 0.5rem;

@ -15,3 +15,7 @@ tailwind:
serve: serve:
cd client && \ cd client && \
perseus serve -w perseus serve -w
client-deploy:
cd client && \
perseus deploy

@ -1,5 +1,5 @@
use rocket::{http::Status, response::status::Custom}; use rocket::{http::Status, response::status::Custom};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
use sea_orm_rocket::rocket::serde::json::Json; use sea_orm_rocket::rocket::serde::json::Json;
use sea_orm_rocket::Connection; use sea_orm_rocket::Connection;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -27,14 +27,16 @@ pub async fn get_all(
Ok(Json(res)) Ok(Json(res))
} }
#[get("/company/<name>")] #[get("/company/<name>?<limit>")]
pub async fn get_by_isin( pub async fn get_by_isin(
conn: Connection<'_, Db>, conn: Connection<'_, Db>,
name: String, name: String,
limit: Option<u64>,
) -> Result<Json<Vec<company::Model>>, Custom<String>> { ) -> Result<Json<Vec<company::Model>>, Custom<String>> {
let db = conn.into_inner(); let db = conn.into_inner();
let res = company::Entity::find() let res = company::Entity::find()
.filter(company::Column::Name.contains(&name)) .filter(company::Column::Name.contains(&name))
.limit(limit.unwrap_or(10))
.all(db) .all(db)
.await .await
.map_err(|e| { .map_err(|e| {

Loading…
Cancel
Save