2 Commits

Author SHA1 Message Date
99b4d9af1a Moved modal state into global state
All checks were successful
Build Crate / build (push) Successful in 1m39s
unfortunate but easiest way
2024-08-25 16:36:13 -04:00
462ca81a15 notwork - login form capsule with rcsignal 2024-08-25 14:07:27 -04:00
12 changed files with 186 additions and 103 deletions

View File

@@ -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"

View File

@@ -2,24 +2,51 @@ use lazy_static::lazy_static;
use perseus::prelude::*;
use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
use web_sys::Event;
use crate::{state_enums::OpenState, templates::global_state::AppStateRx};
lazy_static! {
pub static ref LOGIN_FORM: Capsule<PerseusNodeType, LoginFormProps> = get_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<String>,
pub forgot_password_url: Option<String>,
}
#[auto_scope]
fn login_form_capsule<G: Html>(
cx: Scope,
state: &LoginFormStateRx,
props: LoginFormProps,
) -> View<G> {
view! {
cx,
let close_modal = move |_event: Event| {
#[cfg(client)]
{
spawn_local_scoped(cx, async move {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
global_state.modals_open.login.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="relative md:mx-auto w-full md:w-1/2 lg:w-1/3 z-0 my-10") {
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 +82,6 @@ fn login_form_capsule<G: Html>(
}
}
#[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<String>,
pub forgot_password_url: Option<String>,
}
pub fn get_capsule<G: Html>() -> Capsule<G, LoginFormProps> {
Capsule::build(Template::build("login_form").build_state_fn(get_build_state))
.empty_fallback()

97
src/components/header.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::sync::Arc;
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<G> {
// Get global state to get authentication info
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
let handle_log_in = move |_event: Event| {
#[cfg(client)]
{
spawn_local_scoped(cx, async move {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
global_state.modals_open.login.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(class = "flex-2") {
(match *global_state.modals_open.login.get() {
OpenState::Open => {
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()),
}
))
}
}
OpenState::Closed => {
view!{ cx, }
}
})
}
}
}

View File

@@ -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<G> {
let children = children.call(cx);
// Get global state to get authentication info
#[cfg(client)]
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(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::<G>::from_cx(cx).get_global_state::<AppStateRx>(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") {

View File

@@ -1 +1,2 @@
mod header;
pub mod layout;

View File

@@ -6,6 +6,7 @@ mod entity;
mod error_views;
#[cfg(engine)]
mod server;
mod state_enums;
mod templates;
use perseus::prelude::*;

22
src/state_enums.rs Normal file
View File

@@ -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,
}

View File

@@ -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")

View File

@@ -3,6 +3,8 @@
use perseus::{prelude::*, state::GlobalStateCreator};
use serde::{Deserialize, Serialize};
use crate::state_enums::{LoginState, OpenState};
cfg_if::cfg_if! {
if #[cfg(engine)] {
@@ -14,13 +16,8 @@ cfg_if::cfg_if! {
pub struct AppState {
#[rx(nested)]
pub auth: AuthData,
}
#[derive(Serialize, Deserialize, Clone)]
pub enum LoginState {
Authenticated,
NotAuthenticated,
Unknown,
#[rx(nested)]
pub modals_open: ModalOpenData,
}
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
@@ -31,6 +28,12 @@ pub struct AuthData {
pub claims: Claims,
}
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
#[rx(alias = "ModalOpenDataRx")]
pub struct ModalOpenData {
pub login: OpenState,
}
#[derive(Serialize, Deserialize, ReactiveState, Clone)]
#[rx(alias = "ClaimsRx")]
pub struct Claims {}
@@ -47,6 +50,9 @@ pub async fn get_build_state() -> AppState {
username: None,
claims: Claims {},
},
modals_open: ModalOpenData {
login: OpenState::Closed,
},
}
}

View File

@@ -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<G: Html>(cx: Scope) -> View<G> {
view! { cx,
Layout(_title = "Index") {
Layout(title = "Index", game = GameState::Pool) {
// Anything we put in here will be rendered inside the `<main>` block of the layout
p { "Hello World!" }
br {}

View File

@@ -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<G> {
view! { cx,
Layout(_title = "1v1 Leaderboard") {
Layout(title = "1v1 Leaderboard", game = GameState::Pool) {
p { "leaderboard" }
}
}

View File

@@ -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::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
view! { cx,
Layout(_title = "Overall Leaderboard") {
Layout(title = "Overall Leaderboard", game = GameState::Pool) {
ul {
(View::new_fragment(
vec![],