diff --git a/Cargo.toml b/Cargo.toml index 416cea4..ed3d193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ once_cell = "1.18.0" web-sys = "0.3.64" cfg-if = "1.0.0" chrono = { version = "0.4.38", features = ["serde", "wasm-bindgen"] } -axum-login = "0.15.3" password-auth = "1.0.0" lazy_static = "1.5" diff --git a/src/capsules/login_form.rs b/src/capsules/login_form.rs index 7849a39..d669b7d 100644 --- a/src/capsules/login_form.rs +++ b/src/capsules/login_form.rs @@ -2,24 +2,53 @@ use lazy_static::lazy_static; use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; +use web_sys::Event; + +use crate::state_enums::OpenState; lazy_static! { pub static ref LOGIN_FORM: Capsule = get_capsule(); } +#[derive(Serialize, Deserialize, Clone, ReactiveState)] +#[rx(alias = "LoginFormStateRx")] +struct LoginFormState { + username: String, + password: String, +} + +#[derive(Clone)] +pub struct LoginFormProps { + pub open_state: RcSignal, + pub remember_me: bool, + pub endpoint: String, + pub lost_password_url: Option, + pub forgot_password_url: Option, +} + #[auto_scope] fn login_form_capsule( cx: Scope, state: &LoginFormStateRx, props: LoginFormProps, ) -> View { + let close_modal = move |_event: Event| { + #[cfg(client)] + { + let open_state = props.open_state.clone(); + spawn_local_scoped(cx, async move { + open_state.set(OpenState::Closed); + }); + } + }; + view! { cx, div (class="overflow-x-hidden overflow-y-auto fixed h-modal md:h-full top-4 left-0 right-0 md:inset-0 z-50 justify-center items-center"){ div (class="relative w-full max-w-md px-4 h-full md:h-auto") { div (class="bg-white rounded-lg shadow relative dark:bg-gray-700"){ div (class="flex justify-end p-2"){ - button (class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){ + button (on:click = close_modal, class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-800 dark:hover:text-white"){ "Back" } } @@ -55,21 +84,6 @@ fn login_form_capsule( } } -#[derive(Serialize, Deserialize, Clone, ReactiveState)] -#[rx(alias = "LoginFormStateRx")] -struct LoginFormState { - username: String, - password: String, -} - -#[derive(Clone)] -pub struct LoginFormProps { - pub remember_me: bool, - pub endpoint: String, - pub lost_password_url: Option, - pub forgot_password_url: Option, -} - pub fn get_capsule() -> Capsule { Capsule::build(Template::build("login_form").build_state_fn(get_build_state)) .empty_fallback() diff --git a/src/components/header.rs b/src/components/header.rs new file mode 100644 index 0000000..940d689 --- /dev/null +++ b/src/components/header.rs @@ -0,0 +1,103 @@ +use perseus::prelude::*; +use sycamore::prelude::*; +use web_sys::Event; + +use crate::{ + capsules::login_form::{LoginFormProps, LOGIN_FORM}, + state_enums::{GameState, LoginState, OpenState}, + templates::global_state::AppStateRx, +}; + +#[derive(Prop)] +pub struct HeaderProps<'a> { + pub game: GameState, + pub title: &'a str, +} + +#[component] +pub fn Header<'a, G: Html>(cx: Scope<'a>, HeaderProps { game, title }: HeaderProps<'a>) -> View { + // Get global state to get authentication info + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + // Create signal for opening/closing the login modal + let login_modal_state = create_rc_signal(OpenState::Closed); + + let handle_log_in = { + let login_modal_state = login_modal_state.clone(); + move |_event: Event| { + #[cfg(client)] + { + spawn_local_scoped(cx, async move { + login_modal_state.set(OpenState::Open); + }); + } + } + }; + + view! { cx, + header { + div (class = "flex items-center justify-between w-full md:text-center h-20") { + div(class = "flex-1") {} + + // Title + div(class = "text-gray-700 text-2xl font-semibold py-2") { + "Pool Elo - Season 1" + } + + // Login / register or user buttons + div(class = "flex-1 py-2") {( + match *global_state.auth.state.get() { + LoginState::NotAuthenticated => { + view! { cx, + button(class = "text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700") { + "Register" + } + button(on:click = handle_log_in,class = "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800") { + "Login" + } + } + } + LoginState::Authenticated => { + view! { cx, + div { + "Hello {username}!" + } + } + } + // Will only appear for a few seconds + LoginState::Unknown => { + view! { cx, + div (class = "px-5 py-2.5 me-2 mb-2"){ + "Loading..." + } + } + }, + }) + } + } + } + + section { + div(class = "flex-1 py-2") {( + match *login_modal_state.get() { + OpenState::Open => { + let login_modal_state = login_modal_state.clone(); + view! { cx, + (LOGIN_FORM.widget(cx, "", + LoginFormProps{ + open_state: login_modal_state.clone(), + remember_me: true, + endpoint: "".to_string(), + lost_password_url: Some("".to_string()), + forgot_password_url: Some("".to_string()), + } + )) + } + } + OpenState::Closed => { + view!{ cx, } + } + }) + } + } + } +} diff --git a/src/components/layout.rs b/src/components/layout.rs index e7a814e..6c5604e 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -1,6 +1,8 @@ use crate::{ capsules::login_form::{LoginFormProps, LOGIN_FORM}, - templates::global_state::{AppStateRx, LoginState}, + components::header::{Header, HeaderProps}, + state_enums::{GameState, LoginState}, + templates::global_state::AppStateRx, }; use perseus::prelude::*; use sycamore::prelude::*; @@ -8,7 +10,8 @@ use web_sys::Event; #[derive(Prop)] pub struct LayoutProps<'a, G: Html> { - pub _title: &'a str, + pub game: GameState, + pub title: &'a str, pub children: Children<'a, G>, } @@ -18,86 +21,26 @@ pub struct LayoutProps<'a, G: Html> { pub fn Layout<'a, G: Html>( cx: Scope<'a>, LayoutProps { - _title: _, + game, + title, children, }: LayoutProps<'a, G>, ) -> View { let children = children.call(cx); + // Get global state to get authentication info + #[cfg(client)] let global_state = Reactor::::from_cx(cx).get_global_state::(cx); // Check if the client is authenticated or not #[cfg(client)] global_state.auth.detect_state(); - // TODO -> move into function - let handle_log_in = move |_event: Event| { - #[cfg(client)] - { - spawn_local_scoped(cx, async move { - let global_state = Reactor::::from_cx(cx).get_global_state::(cx); - global_state.auth.state.set(LoginState::Authenticated); - }); - } - }; - view! { cx, - // Main page header - header { - div (class = "flex items-center justify-between w-full md:text-center h-20") { - div(class = "flex-1") {} - div(class = "text-gray-700 text-2xl font-semibold py-2") { - "Pool Elo - Season 1" - } - - div(class = "flex-1 py-2") {( - match *global_state.auth.state.get() { - LoginState::NotAuthenticated => { - view! { cx, - button(class = "text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700") { - "Register" - } - button(on:click = handle_log_in,class = "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800") { - "Login" - } - } - } - LoginState::Authenticated => { - view! { cx, - div { - "Hello {username}!" - } - } - } - // Will only appear for a few seconds - LoginState::Unknown => { - view! { cx, - div (class = "px-5 py-2.5 me-2 mb-2"){ - "Loading..." - } - } - }, - }) - } - } - } + // Main page header, including login functionality + Header(game = game, title = title) main(style = "my-8") { - - ( - match *global_state.auth.state.get() { - LoginState::Authenticated => { view! { cx, - (LOGIN_FORM.widget(cx, "", - LoginFormProps{ - remember_me: true, - endpoint: "".to_string(), - lost_password_url: Some("".to_string()), - forgot_password_url: Some("".to_string()) - }) - ) - }}, - _ => { view! { cx, div {} } }}) - // Body header div { div (class = "container mx-auto px-6 py-3") { diff --git a/src/components/mod.rs b/src/components/mod.rs index dd64619..f70771d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1 +1,2 @@ +mod header; pub mod layout; diff --git a/src/main.rs b/src/main.rs index f09b598..e37f41d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod entity; mod error_views; #[cfg(engine)] mod server; +mod state_enums; mod templates; use perseus::prelude::*; diff --git a/src/state_enums.rs b/src/state_enums.rs new file mode 100644 index 0000000..028a43b --- /dev/null +++ b/src/state_enums.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub enum LoginState { + Authenticated, + NotAuthenticated, + Unknown, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum GameState { + None, + Pool, + Pickleball, + TableTennis, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum OpenState { + Open, + Closed, +} diff --git a/src/templates/add_game_form.rs b/src/templates/add_game_form.rs index 673bfed..a02d1a1 100644 --- a/src/templates/add_game_form.rs +++ b/src/templates/add_game_form.rs @@ -1,4 +1,4 @@ -use crate::components::layout::Layout; +use crate::{components::layout::Layout, state_enums::GameState}; use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; @@ -40,7 +40,7 @@ fn add_game_form_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStat }; view! { cx, - Layout(_title = "Add Game Results") { + Layout(title = "Add Game Results", game = GameState::Pool) { div (class = "flex flex-wrap") { select { option (value="red") diff --git a/src/templates/global_state.rs b/src/templates/global_state.rs index 7c71814..bff16af 100644 --- a/src/templates/global_state.rs +++ b/src/templates/global_state.rs @@ -3,6 +3,8 @@ use perseus::{prelude::*, state::GlobalStateCreator}; use serde::{Deserialize, Serialize}; +use crate::state_enums::LoginState; + cfg_if::cfg_if! { if #[cfg(engine)] { @@ -16,13 +18,6 @@ pub struct AppState { pub auth: AuthData, } -#[derive(Serialize, Deserialize, Clone)] -pub enum LoginState { - Authenticated, - NotAuthenticated, - Unknown, -} - #[derive(Serialize, Deserialize, ReactiveState, Clone)] #[rx(alias = "AuthDataRx")] pub struct AuthData { diff --git a/src/templates/index.rs b/src/templates/index.rs index 0228503..dd25099 100644 --- a/src/templates/index.rs +++ b/src/templates/index.rs @@ -1,10 +1,10 @@ -use crate::components::layout::Layout; +use crate::{components::layout::Layout, state_enums::GameState}; use perseus::prelude::*; use sycamore::prelude::*; fn index_page(cx: Scope) -> View { view! { cx, - Layout(_title = "Index") { + Layout(title = "Index", game = GameState::Pool) { // Anything we put in here will be rendered inside the `
` block of the layout p { "Hello World!" } br {} diff --git a/src/templates/one_v_one_board.rs b/src/templates/one_v_one_board.rs index 4a31bec..f4b5954 100644 --- a/src/templates/one_v_one_board.rs +++ b/src/templates/one_v_one_board.rs @@ -1,4 +1,4 @@ -use crate::components::layout::Layout; +use crate::{components::layout::Layout, state_enums::GameState}; use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; @@ -9,7 +9,7 @@ struct PageState {} fn one_v_one_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageStateRx) -> View { view! { cx, - Layout(_title = "1v1 Leaderboard") { + Layout(title = "1v1 Leaderboard", game = GameState::Pool) { p { "leaderboard" } } } diff --git a/src/templates/overall_board.rs b/src/templates/overall_board.rs index 8c4f887..cf60a2b 100644 --- a/src/templates/overall_board.rs +++ b/src/templates/overall_board.rs @@ -1,4 +1,6 @@ -use crate::{components::layout::Layout, templates::global_state::AppStateRx}; +use crate::{ + components::layout::Layout, state_enums::GameState, templates::global_state::AppStateRx, +}; use perseus::prelude::*; use serde::{Deserialize, Serialize}; @@ -12,7 +14,7 @@ fn overall_board_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, _state: &'a PageSta let _global_state = Reactor::::from_cx(cx).get_global_state::(cx); view! { cx, - Layout(_title = "Overall Leaderboard") { + Layout(title = "Overall Leaderboard", game = GameState::Pool) { ul { (View::new_fragment( vec![],