Compare commits
2 Commits
c2ecce0324
...
99b4d9af1a
| Author | SHA1 | Date | |
|---|---|---|---|
| 99b4d9af1a | |||
| 462ca81a15 |
@@ -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"
|
||||
|
||||
|
||||
@@ -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
97
src/components/header.rs
Normal 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, }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
mod header;
|
||||
pub mod layout;
|
||||
|
||||
@@ -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
22
src/state_enums.rs
Normal 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,
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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![],
|
||||
|
||||
Reference in New Issue
Block a user