Merge pull request 'New Index page with more interesting recent insights' (#26) from daily-transactions into master
Reviewed-on: #26pull/30/head
commit
213b5ffb76
@ -0,0 +1,21 @@
|
||||
use sycamore::prelude::*;
|
||||
|
||||
#[derive(Prop)]
|
||||
pub struct MainProps<'a, G: Html> {
|
||||
children: Children<'a, G>,
|
||||
useless_prop: u8, // Without another prop, the view doesn't render with children
|
||||
}
|
||||
|
||||
/// This component is used to contain the main contents of pages
|
||||
#[component]
|
||||
pub fn MainContentContainer<'a, G: Html>(cx: Scope<'a>, props: MainProps<'a, G>) -> View<G> {
|
||||
let children = props.children.call(cx);
|
||||
|
||||
view! {cx,
|
||||
div (id="main", class="flex flex-col items-center justify-center ") {
|
||||
div (class="w-4/5 m-10 p-3 bg-slate-100 dark:bg-slate-600 rounded-lg items-center justify-center") {
|
||||
(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,48 @@
|
||||
use perseus::prelude::*;
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::global_state::AppStateRx;
|
||||
|
||||
#[component]
|
||||
pub fn the_header<G: Html>(cx: Scope) -> View<G> {
|
||||
pub fn TheHeader<'a, G: Html>(cx: Scope<'a>) -> View<G> {
|
||||
// This is ugly and is only caused by the get_global_state function panicking when running on the server at build time
|
||||
let global_state_sig: &Signal<Option<&AppStateRx>> = create_signal(cx, None);
|
||||
|
||||
#[cfg(client)]
|
||||
global_state_sig.set(Some(
|
||||
Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx),
|
||||
));
|
||||
|
||||
let dark_mode = create_signal(cx, true);
|
||||
create_effect(cx, move || {
|
||||
if let Some(gstate) = (*global_state_sig.get()).clone() {
|
||||
dark_mode.set(*gstate.dark_mode.get());
|
||||
}
|
||||
});
|
||||
let toggle_dark_mode = move |_| {
|
||||
if let Some(gstate) = *global_state_sig.get() {
|
||||
gstate.dark_mode.set(!*dark_mode.get());
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
"Don't use until global state is recheable from a component"
|
||||
// header (class="shadow-md h-10 sm:p-2 w-full mb-20 bg-gray-100 dark:bg-slate-500/30 backdrop-blur-lg") {
|
||||
// nav () {
|
||||
// div (class="fixed flex justify-between") {
|
||||
// div (class="font-mono mr-10") { "Fast Insiders" }
|
||||
// div (class="flex justify-end") {
|
||||
// button (on:click=toggle_dark_mode, class="mx-1 p-1 bg-pink-200 dark:bg-pink-600 rounded-full") { "toggle dark mode" }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
header (class="shadow-md h-11 p-2 align-middle w-full bg-gray-100 dark:bg-slate-500/30 backdrop-blur-lg") {
|
||||
div (class="flex") {
|
||||
div (class="flex-none mr-12") {
|
||||
a (href="/", class="hover:underline") {
|
||||
"Fast Insiders"
|
||||
}
|
||||
}
|
||||
div (class="grow text-left") {
|
||||
a (id="header-all-transactions", href="/transactions", class="hover:underline") {
|
||||
"All transactions"
|
||||
}
|
||||
}
|
||||
div (class="flex-none") {
|
||||
button (on:click=toggle_dark_mode, class="mx-1 py-1 px-2 bg-slate-200 dark:bg-slate-800 rounded-full")
|
||||
{ "Toggle dark mode" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
pub mod index;
|
||||
pub mod transactions;
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
use perseus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sycamore::prelude::*;
|
||||
|
||||
use crate::{
|
||||
api::routes::transaction::get_transactions,
|
||||
api::types::{company::Company, transaction::TransactionCompany},
|
||||
components::{
|
||||
base_async_select::{AsyncSelectRx, BaseAsyncSelect},
|
||||
base_button::{BaseButton, BaseButtonStateRx},
|
||||
main_content_container::MainContentContainer,
|
||||
paginated_data_table::{PaginatedTable, PaginatedTableStateRx},
|
||||
the_header::TheHeader,
|
||||
},
|
||||
global_state::AppStateRx,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, ReactiveState)]
|
||||
#[rx(alias = "TransactionsPageStateRx")]
|
||||
pub struct TransactionsPageState {
|
||||
pub company_slug: Option<String>,
|
||||
}
|
||||
|
||||
#[auto_scope]
|
||||
fn transactions_page<G: Html>(cx: Scope, state: &TransactionsPageStateRx) -> View<G> {
|
||||
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
|
||||
let dark_mode = &global_state.dark_mode;
|
||||
|
||||
let expand = create_signal(cx, false);
|
||||
let filter_expand = BaseButtonStateRx {
|
||||
label: create_signal(cx, "Filters".to_string()),
|
||||
disabled: create_signal(cx, false),
|
||||
clicked: create_signal(cx, false),
|
||||
};
|
||||
|
||||
create_effect(cx, move || {
|
||||
if *filter_expand.clicked.get() {
|
||||
filter_expand.clicked.set(false);
|
||||
expand.set(!*expand.get());
|
||||
}
|
||||
});
|
||||
|
||||
let paginated_table_state: PaginatedTableStateRx<TransactionCompany, _, _> =
|
||||
PaginatedTableStateRx {
|
||||
record_label: "transactions".to_owned(),
|
||||
route: get_transactions,
|
||||
filter: (*state.company_slug.get()).clone(),
|
||||
table_class: create_ref(cx, "".to_string()),
|
||||
};
|
||||
|
||||
let async_select_prop: AsyncSelectRx<Company> = AsyncSelectRx {
|
||||
remote_list: create_signal(cx, format!("{}company/", "http://localhost:8000/v1/")),
|
||||
selected_item: create_signal(cx, None),
|
||||
};
|
||||
|
||||
let search_button = BaseButtonStateRx {
|
||||
label: create_signal(cx, "Search".to_string()),
|
||||
disabled: create_memo(cx, move || async_select_prop.selected_item.get().is_none()),
|
||||
clicked: create_signal(cx, false),
|
||||
};
|
||||
|
||||
create_effect(cx, || {
|
||||
if *search_button.clicked.get() {
|
||||
search_button.clicked.set(false);
|
||||
navigate(&format!(
|
||||
"/transactions/{}",
|
||||
(*async_select_prop.selected_item.get())
|
||||
.clone()
|
||||
.map_or("".to_string(), |c| c.slug)
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
view! {cx,
|
||||
main (class=if *dark_mode.get() { "dark" } else { "" }) {
|
||||
div (class="bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-100 font-sans") {
|
||||
TheHeader()
|
||||
MainContentContainer(useless_prop=1) {
|
||||
a (class="hover:underline", href="/transactions") {
|
||||
h1 (
|
||||
class="text-center text-lg"
|
||||
) {
|
||||
"Insider Transactions published by the AMF"
|
||||
}
|
||||
}
|
||||
BaseButton(filter_expand)
|
||||
div (id="filters",
|
||||
class=format!("p-2 border rounded-lg border-slate-200 dark:border-slate-800 bg-slate-200 dark:bg-slate-700 transition-all ease-in {}",
|
||||
if *expand.get() { "h-40 visible" } else { "h-0 collapse" },
|
||||
)
|
||||
)
|
||||
{
|
||||
div (class="w-80") {
|
||||
p () {"Search for a company:"}
|
||||
BaseAsyncSelect(async_select_prop)
|
||||
BaseButton(search_button)
|
||||
}
|
||||
}
|
||||
PaginatedTable(paginated_table_state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_template<G: Html>() -> Template<G> {
|
||||
Template::build("transactions")
|
||||
.head(head)
|
||||
.build_state_fn(get_build_state)
|
||||
.build_paths_fn(get_build_paths)
|
||||
.incremental_generation()
|
||||
.view_with_state(transactions_page)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[engine_only_fn]
|
||||
fn head(cx: Scope) -> View<SsrNode> {
|
||||
view! {cx,
|
||||
title { "Fast Insiders" }
|
||||
}
|
||||
}
|
||||
|
||||
#[engine_only_fn]
|
||||
async fn get_build_state(
|
||||
StateGeneratorInfo { path, .. }: StateGeneratorInfo<()>,
|
||||
) -> Result<TransactionsPageState, BlamedError<std::io::Error>> {
|
||||
let company_slug = if path.is_empty() { None } else { Some(path) };
|
||||
Ok(TransactionsPageState { company_slug })
|
||||
}
|
||||
|
||||
#[engine_only_fn]
|
||||
async fn get_build_paths() -> BuildPaths {
|
||||
BuildPaths {
|
||||
paths: vec!["".to_string()],
|
||||
extra: ().into(),
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Transaction::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Transaction::CreatedAtUtc)
|
||||
.date_time()
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = Query::update()
|
||||
.table(Transaction::Table)
|
||||
.value(
|
||||
Transaction::CreatedAtUtc,
|
||||
Expr::col(Transaction::DatePublished),
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(query).await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Transaction::Table)
|
||||
.drop_column(Transaction::CreatedAtUtc)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum Transaction {
|
||||
Table,
|
||||
DatePublished,
|
||||
CreatedAtUtc,
|
||||
}
|
||||
Loading…
Reference in new issue