diff --git a/src/components/header.rs b/src/components/header.rs index c81d623..9b05869 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -3,8 +3,9 @@ use sycamore::prelude::*; use web_sys::Event; use crate::{ + components::static_components::menu_button::MenuButtonSvg, global_state::AppStateRx, - state_enums::{GameState, LoginState}, + state_enums::{ContentState, LoginState}, }; cfg_if::cfg_if! { @@ -17,12 +18,81 @@ cfg_if::cfg_if! { #[derive(Prop)] pub struct HeaderProps { - pub game: GameState, + pub content_state: ContentState, +} + +// TODO update to have user preferences +#[component] +fn LinkList(cx: Scope) -> View { + // Get global state to get style info + let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + + view! { cx, + li { + a (href = "pool") { + label (class = "swap") { + input ( + type="radio", + name = "theme-dropdown", + class = "theme-controller", + value = (*global_state.style.theme.pool.get()), + ) {} + p (class = ( + if *global_state.style.theme.current.get() == *global_state.style.theme.pool.get(){ + "font-bold" + } + else { + "" + } + )){ "Pool" } + } + } + } + li { + a (href = "table_tennis") { + label (class = "swap") { + input ( + type="radio", + name = "theme-dropdown", + class = "theme-controller", + value = (*global_state.style.theme.table_tennis.get()), + ) {} + p (class = ( + if *global_state.style.theme.current.get() == *global_state.style.theme.table_tennis.get(){ + "font-bold" + } + else { + "" + } + )){ "Table Tennis" } + } + } + } + li { + a (href = "pickleball") { + label (class = "swap") { + input ( + type="radio", + name = "theme-dropdown", + class = "theme-controller", + value = (*global_state.style.theme.pickleball.get()), + ) {} + p (class = ( + if *global_state.style.theme.current.get() == *global_state.style.theme.pickleball.get(){ + "font-bold" + } + else { + "" + } + )){ "Pickleball" } + } + } + } + } } #[component] pub fn Header(cx: Scope, props: HeaderProps) -> View { - // Get global state to get authentication info let global_state = Reactor::::from_cx(cx).get_global_state::(cx); let handle_log_in = move |_event: Event| { @@ -56,50 +126,46 @@ pub fn Header(cx: Scope, props: HeaderProps) -> View { }; 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") { - (props.game.to_string()) " - Season 1" + header (class="navbar bg-base-100") { + // Navigation + div (class="navbar-start") { + div (class="dropdown") { + div (tabindex="0", role="button", class="btn btn-ghost lg:hidden") { MenuButtonSvg {} } + ul (tabindex = "0", class = "menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow" ) { + LinkList {} + } } - - // Login / register or user buttons - div(class = "flex-1 py-2") {( - match *global_state.auth.state.get() { - LoginState::NotAuthenticated => { - view! { cx, - button(on:click = handle_register, 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") { - "Log in" - } - } - } - LoginState::Authenticated => { - view! { cx, - div { - "Hello " - (global_state.auth.username.get().as_ref().clone().unwrap_or("".to_owned())) - } - button(on:click = handle_log_out, 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") { - "Log out" - } - } - } - // Will only appear for a few seconds - LoginState::Unknown => { - view! { cx, - div (class = "px-5 py-2.5 me-2 mb-2"){ - "Loading..." - } - } - }, - }) + ul (class="menu menu-horizontal px-1 hidden lg:flex") { + LinkList {} } } + // Title + div (class="navbar-center lg:flex") { + (props.content_state.to_string()) + } + // User buttons + div (class="navbar-end") { + (match *global_state.auth.state.get() { + LoginState::Authenticated => { view! { cx, + button(on:click = handle_log_out, class = "btn btn-primary mr-2") { + "Log out" + } + } }, + LoginState::NotAuthenticated => { view! { cx, + button(on:click = handle_register, class = "btn btn-primary mr-2") { + "Register" + } + button(on:click = handle_log_in, class = "btn btn-secondary mr-2") { + "Log in" + } + } }, + LoginState::Unknown => { view! { cx, + div (class = "px-5 py-2.5 me-2 mb-2") { + "Loading..." + } + } }, + }) + } } } } diff --git a/src/components/layout.rs b/src/components/layout.rs index 6eddfa5..c5aa8df 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -6,14 +6,14 @@ use crate::{ }, components::header::Header, global_state::AppStateRx, - state_enums::{GameState, OpenState}, + state_enums::{ContentState, OpenState}, }; use perseus::prelude::*; use sycamore::prelude::*; #[derive(Prop)] pub struct LayoutProps<'a, G: Html> { - pub game: GameState, + pub content_state: ContentState, pub children: Children<'a, G>, } @@ -22,19 +22,39 @@ pub struct LayoutProps<'a, G: Html> { #[component] pub fn Layout<'a, G: Html>( cx: Scope<'a>, - LayoutProps { game, children }: LayoutProps<'a, G>, + LayoutProps { + content_state, + children, + }: LayoutProps<'a, G>, ) -> View { - let children = children.call(cx); - let global_state = Reactor::::from_cx(cx).get_global_state::(cx); + // Set the theme + global_state.style.theme.current.set(match content_state { + ContentState::None => (*global_state.style.theme.default.get()).clone(), + ContentState::Pool => (*global_state.style.theme.pool.get()).clone(), + ContentState::Pickleball => (*global_state.style.theme.pickleball.get()).clone(), + ContentState::TableTennis => (*global_state.style.theme.table_tennis.get()).clone(), + }); + #[cfg(client)] + let _ = web_sys::window() + .unwrap() + .document() + .unwrap() + .document_element() + .unwrap() + .set_attribute("data-theme", &global_state.style.theme.current.get()); + + let children = children.call(cx); // Check if the client is authenticated or not #[cfg(client)] global_state.auth.detect_state(); + let content_state_header = content_state.clone(); + view! { cx, // Main page header, including login functionality - Header(game = game) + Header(content_state = content_state_header) // Modals section(class = "flex-2") { @@ -83,32 +103,37 @@ pub fn Layout<'a, G: Html>( } main(style = "my-8") { - // Body header - div { - div (class = "container mx-auto px-6 py-3") { - nav (class = "sm:flex sm:justify-center sm:items-center mt-4 hidden") { - div (class = "flex flex-col sm:flex-row"){ - a(href = "add-game-form", - class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" - ) { "Add game result" } - a(href = "one-v-one-board", - class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" - ) { "1v1 Leaderboard" } - a(href = "overall-board", - class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" - ) { "Overall Leaderboard" } + (match content_state { + ContentState::None => view!{ cx, }, + ContentState::Pool => view!{ cx, + // Body header + div (class = "container mx-auto px-6 py-3") { + nav (class = "sm:flex sm:justify-center sm:items-center mt-4 hidden") { + div (class = "flex flex-col sm:flex-row"){ + a(href = "pool/add-game-form", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "Add game result" } + a(href = "pool/one-v-one-board", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "1v1 Leaderboard" } + a(href = "pool/overall-board", + class = "mt-3 text-gray-600 hover:underline sm:mx-3 sm:mt-0" + ) { "Overall Leaderboard" } + } } } - } - } - // Content body - div(class = "container mx-auto px-6") { - div(class = "md:flex mt-8 md:-mx-4") { - div(class = "rounded-md overflow-hidden bg-cover bg-center") { - (children) + // Content body + div(class = "container mx-auto px-6") { + div(class = "md:flex mt-8 md:-mx-4") { + div(class = "rounded-md overflow-hidden bg-cover bg-center") { + (children) + } + } } - } - } + }, + ContentState::Pickleball => view!{ cx, }, + ContentState::TableTennis => view!{ cx, }, + }) } } } diff --git a/src/components/static_components/menu_button.rs b/src/components/static_components/menu_button.rs new file mode 100644 index 0000000..3798289 --- /dev/null +++ b/src/components/static_components/menu_button.rs @@ -0,0 +1,20 @@ +use perseus::prelude::*; +use sycamore::prelude::*; + +#[component] +pub fn MenuButtonSvg(cx: Scope) -> View { + view! { cx, + svg ( + xmlns="http://www.w3.org/2000/svg", + class="h-5 w-5", + fill="none", + viewBox="0 0 24 24", + stroke="currentColor"){ + path ( + stroke-linecap="round", + stroke-linejoin="round", + stroke-width="2", + d="M4 6h16M4 12h8m-8 6h16") {} + } + } +} diff --git a/src/components/static_components/mod.rs b/src/components/static_components/mod.rs index 39eaca8..d52e3d0 100644 --- a/src/components/static_components/mod.rs +++ b/src/components/static_components/mod.rs @@ -1,2 +1,3 @@ pub mod close_button; pub mod indicator; +pub mod menu_button; diff --git a/src/global_state.rs b/src/global_state.rs index 1f7d3e0..18533fb 100644 --- a/src/global_state.rs +++ b/src/global_state.rs @@ -15,6 +15,8 @@ pub struct AppState { pub auth: AuthData, #[rx(nested)] pub modals_open: ModalOpenData, + #[rx(nested)] + pub style: StyleData, } #[derive(Serialize, Deserialize, ReactiveState, Clone)] @@ -27,6 +29,62 @@ pub struct AuthData { pub auth_info: Option, } +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "ModalOpenDataRx")] +pub struct ModalOpenData { + pub login: OpenState, + pub register: OpenState, + pub forgot_password: OpenState, +} + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "StyleDataRx")] +pub struct StyleData { + #[rx(nested)] + pub theme: ThemeData, +} + +#[derive(Serialize, Deserialize, ReactiveState, Clone)] +#[rx(alias = "ThemeDataRx")] +pub struct ThemeData { + pub current: String, + pub pool: String, + pub pickleball: String, + pub table_tennis: String, + pub default: String, +} + +pub fn get_global_state_creator() -> GlobalStateCreator { + GlobalStateCreator::new().build_state_fn(get_build_state) +} + +#[engine_only_fn] +pub async fn get_build_state() -> AppState { + AppState { + auth: AuthData { + state: LoginState::Unknown, + pending_username: String::new(), + username: None, + remember_me: None, + auth_info: None, + }, + modals_open: ModalOpenData { + login: OpenState::Closed, + register: OpenState::Closed, + forgot_password: OpenState::Closed, + }, + style: StyleData { + theme: ThemeData { + current: "light".to_owned(), + pool: "autumn".to_owned(), + pickleball: "lemonade".to_owned(), + table_tennis: "nord".to_owned(), + default: "light".to_owned(), + }, + }, + } +} + impl AuthDataRx { #[cfg(client)] pub fn handle_log_in(&self, auth_info: WebAuthInfo) { @@ -73,36 +131,6 @@ impl AuthDataRx { } } -#[derive(Serialize, Deserialize, ReactiveState, Clone)] -#[rx(alias = "ModalOpenDataRx")] -pub struct ModalOpenData { - pub login: OpenState, - pub register: OpenState, - pub forgot_password: OpenState, -} - -pub fn get_global_state_creator() -> GlobalStateCreator { - GlobalStateCreator::new().build_state_fn(get_build_state) -} - -#[engine_only_fn] -pub async fn get_build_state() -> AppState { - AppState { - auth: AuthData { - state: LoginState::Unknown, - pending_username: String::new(), - username: None, - remember_me: None, - auth_info: None, - }, - modals_open: ModalOpenData { - login: OpenState::Closed, - register: OpenState::Closed, - forgot_password: OpenState::Closed, - }, - } -} - // Client only code to check if they're authenticated #[cfg(client)] impl AuthDataRx { diff --git a/src/main.rs b/src/main.rs index caa410a..b2a7d6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,17 +70,20 @@ pub fn main() -> PerseusApp { PerseusApp::new() .global_state_creator(crate::global_state::get_global_state_creator()) + .template(crate::templates::index::get_template()) + .template(crate::templates::pickleball::index::get_template()) .template(crate::templates::pool::index::get_template()) .template(crate::templates::pool::add_game_form::get_template()) .template(crate::templates::pool::one_v_one_board::get_template()) .template(crate::templates::pool::overall_board::get_template()) + .template(crate::templates::table_tennis::index::get_template()) .capsule_ref(&*crate::capsules::login_form::LOGIN_FORM) .capsule_ref(&*crate::capsules::forgot_password_form::FORGOT_PASSWORD_FORM) .capsule_ref(&*crate::capsules::register_form::REGISTER_FORM) .error_views(crate::error_views::get_error_views()) .index_view(|cx| { view! { cx, - html (class = "flex w-full h-full"){ + html (class = "flex w-full h-full", data-theme = "light"){ head { meta(charset = "UTF-8") meta(name = "viewport", content = "width=device-width, initial-scale=1.0") diff --git a/src/state_enums.rs b/src/state_enums.rs index 15d226a..a7a1a39 100644 --- a/src/state_enums.rs +++ b/src/state_enums.rs @@ -10,23 +10,23 @@ pub enum LoginState { } #[derive(Serialize, Deserialize, Clone)] -pub enum GameState { +pub enum ContentState { None, Pool, Pickleball, TableTennis, } -impl Display for GameState { +impl Display for ContentState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { - GameState::None => "", - GameState::Pool => "Pool", - GameState::Pickleball => "Pickle Ball", - GameState::TableTennis => "Table Tennis", + ContentState::None => "", + ContentState::Pool => "Pool", + ContentState::Pickleball => "Pickle Ball", + ContentState::TableTennis => "Table Tennis", } ) } diff --git a/src/templates/index.rs b/src/templates/index.rs new file mode 100644 index 0000000..bca5971 --- /dev/null +++ b/src/templates/index.rs @@ -0,0 +1,24 @@ +use crate::{components::layout::Layout, state_enums::ContentState}; +use perseus::prelude::*; +use sycamore::prelude::*; + +fn index_page(cx: Scope) -> View { + view! { cx, + Layout(content_state = ContentState::None) { + // Anything we put in here will be rendered inside the `
` block of the layout + p { "Hello World!" } + br {} + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Index Page" } + } +} + +pub fn get_template() -> Template { + Template::build("").view(index_page).head(head).build() +} diff --git a/src/templates/mod.rs b/src/templates/mod.rs index 951ffb5..7664311 100644 --- a/src/templates/mod.rs +++ b/src/templates/mod.rs @@ -1,3 +1,4 @@ +pub mod index; pub mod pickleball; pub mod pool; pub mod table_tennis; diff --git a/src/templates/pickleball/index.rs b/src/templates/pickleball/index.rs new file mode 100644 index 0000000..8859892 --- /dev/null +++ b/src/templates/pickleball/index.rs @@ -0,0 +1,27 @@ +use crate::{components::layout::Layout, state_enums::ContentState}; +use perseus::prelude::*; +use sycamore::prelude::*; + +fn index_page(cx: Scope) -> View { + view! { cx, + Layout(content_state = ContentState::Pickleball) { + // Anything we put in here will be rendered inside the `
` block of the layout + p { "Hello World!" } + br {} + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Index Page" } + } +} + +pub fn get_template() -> Template { + Template::build("pickleball") + .view(index_page) + .head(head) + .build() +} diff --git a/src/templates/pickleball/mod.rs b/src/templates/pickleball/mod.rs index e69de29..33edc95 100644 --- a/src/templates/pickleball/mod.rs +++ b/src/templates/pickleball/mod.rs @@ -0,0 +1 @@ +pub mod index; diff --git a/src/templates/pool/add_game_form.rs b/src/templates/pool/add_game_form.rs index 7c39863..0b74c2c 100644 --- a/src/templates/pool/add_game_form.rs +++ b/src/templates/pool/add_game_form.rs @@ -1,4 +1,4 @@ -use crate::{components::layout::Layout, state_enums::GameState}; +use crate::{components::layout::Layout, state_enums::ContentState}; use perseus::prelude::*; use serde::{Deserialize, Serialize}; use sycamore::prelude::*; @@ -31,7 +31,7 @@ fn add_game_form_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStat }; view! { cx, - Layout(game = GameState::Pool) { + Layout(content_state = ContentState::Pool) { div (class = "flex flex-wrap") { select { option (value="red") @@ -91,7 +91,7 @@ fn head(cx: Scope) -> View { // Template pub fn get_template() -> Template { - Template::build("add-game-form") + Template::build("pool/add-game-form") .request_state_fn(get_request_state) .view_with_state(add_game_form_page) .head(head) diff --git a/src/templates/pool/index.rs b/src/templates/pool/index.rs index b43b89f..c7b7c94 100644 --- a/src/templates/pool/index.rs +++ b/src/templates/pool/index.rs @@ -1,10 +1,10 @@ -use crate::{components::layout::Layout, state_enums::GameState}; +use crate::{components::layout::Layout, state_enums::ContentState}; use perseus::prelude::*; use sycamore::prelude::*; fn index_page(cx: Scope) -> View { view! { cx, - Layout(game = GameState::Pool) { + Layout(content_state = ContentState::Pool) { // Anything we put in here will be rendered inside the `
` block of the layout p { "Hello World!" } br {} @@ -20,5 +20,5 @@ fn head(cx: Scope) -> View { } pub fn get_template() -> Template { - Template::build("").view(index_page).head(head).build() + Template::build("pool").view(index_page).head(head).build() } diff --git a/src/templates/pool/one_v_one_board.rs b/src/templates/pool/one_v_one_board.rs index 8a0e3f9..b5ee66a 100644 --- a/src/templates/pool/one_v_one_board.rs +++ b/src/templates/pool/one_v_one_board.rs @@ -1,4 +1,4 @@ -use crate::{components::layout::Layout, state_enums::GameState}; +use crate::{components::layout::Layout, state_enums::ContentState}; 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(game = GameState::Pool) { + Layout(content_state = ContentState::Pool) { p { "leaderboard" } } } @@ -31,7 +31,7 @@ fn head(cx: Scope) -> View { } pub fn get_template() -> Template { - Template::build("one-v-one-board") + Template::build("pool/one-v-one-board") .request_state_fn(get_request_state) .view_with_state(one_v_one_board_page) .head(head) diff --git a/src/templates/pool/overall_board.rs b/src/templates/pool/overall_board.rs index 79465fe..4b0a3e4 100644 --- a/src/templates/pool/overall_board.rs +++ b/src/templates/pool/overall_board.rs @@ -1,4 +1,4 @@ -use crate::{components::layout::Layout, global_state::AppStateRx, state_enums::GameState}; +use crate::{components::layout::Layout, global_state::AppStateRx, state_enums::ContentState}; use perseus::prelude::*; use serde::{Deserialize, Serialize}; @@ -12,7 +12,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(game = GameState::Pool) { + Layout(content_state = ContentState::Pool) { ul { (View::new_fragment( vec![], @@ -38,7 +38,7 @@ fn head(cx: Scope) -> View { } pub fn get_template() -> Template { - Template::build("overall-board") + Template::build("pool/overall-board") .request_state_fn(get_request_state) .view_with_state(overall_board_page) .head(head) diff --git a/src/templates/table_tennis/index.rs b/src/templates/table_tennis/index.rs new file mode 100644 index 0000000..b8fe66c --- /dev/null +++ b/src/templates/table_tennis/index.rs @@ -0,0 +1,27 @@ +use crate::{components::layout::Layout, state_enums::ContentState}; +use perseus::prelude::*; +use sycamore::prelude::*; + +fn index_page(cx: Scope) -> View { + view! { cx, + Layout(content_state = ContentState::TableTennis) { + // Anything we put in here will be rendered inside the `
` block of the layout + p { "Hello World!" } + br {} + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Index Page" } + } +} + +pub fn get_template() -> Template { + Template::build("table_tennis") + .view(index_page) + .head(head) + .build() +} diff --git a/src/templates/table_tennis/mod.rs b/src/templates/table_tennis/mod.rs index e69de29..33edc95 100644 --- a/src/templates/table_tennis/mod.rs +++ b/src/templates/table_tennis/mod.rs @@ -0,0 +1 @@ +pub mod index; diff --git a/tailwind.config.js b/tailwind.config.js index 5d57aca..77173f3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,6 +14,6 @@ module.exports = { variants: {}, plugins: [require("daisyui")], daisyui: { - themes: ["light"], + themes: ["light", "dark", "lemonade", "autumn", "nord"], }, };