From 874e44861689bb71c6a5f960aa0af9b5180254c3 Mon Sep 17 00:00:00 2001 From: Sam Tay Date: Thu, 20 Jun 2024 22:29:16 -0400 Subject: [PATCH] Very initial version of navbar --- README.md | 38 ++++++----- src/ui/game/card.rs | 4 +- src/ui/game/mod.rs | 15 ++--- src/ui/mod.rs | 119 ++++++++++++++++++++++------------- src/ui/pages/achievements.rs | 12 ++++ src/ui/pages/birds.rs | 12 ++++ src/ui/pages/listen.rs | 12 ++++ src/ui/pages/mod.rs | 9 +++ src/ui/pages/settings.rs | 12 ++++ 9 files changed, 159 insertions(+), 74 deletions(-) create mode 100644 src/ui/pages/achievements.rs create mode 100644 src/ui/pages/birds.rs create mode 100644 src/ui/pages/listen.rs create mode 100644 src/ui/pages/mod.rs create mode 100644 src/ui/pages/settings.rs diff --git a/README.md b/README.md index 7dd30ab..634b77b 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ See - achievements, streaks, etc. - can I do a "tour" with context state? and once tour is marked as skipped or finished, those elements go away? - #46764e is a great color for text +- for separate server/frontend deployment, see https://discord.com/channels/899851952891002890/1251248438482440302 and e.g. https://github.com/DioxusLabs/dioxus/blob/487570d89751b34bbfd5e9b5ff1e0fd3780bf332/packages/fullstack/examples/axum-desktop/Cargo.toml#L20-L28 #### settings ideas @@ -67,19 +68,21 @@ See - https://dioxuslabs.com/learn/0.5/reference/context#using-shared-state use context for settings -### mvp todo +### todo -MVP should probably just be a game demo using local storage, no user identity, database, etc. - -- [ ] change the flip text (identified / streak is confusing here, it seems like overall streak) - - three dots to fill green would be nice -- [ ] Display level/xp nicely (in header?) -- [ ] Awards? levels? badges? +- [ ] nav bar (with accompanying route) + - Collapse game modes (quiz/learn are the same thing with a setting to differentiate) + - Listen can be a different nav spot (settings for how many times each bird is played default 1, auto skip after \_, etc.) +- [ ] data parsed and hosted -- supabase: data + storage +- [ ] auth via supabase, client side! (try copying t5 logic, make lib) - [ ] Run through a11y tool; looks like at least a bunch of labels are needed - [ ] Simplify all the responsive designs, just assume sm > mobile, md > tablet. - -#### Bonus - +- [ ] making a landing (web-only) www page with links to Login (send to app) + app stores + - this should be an SSG! with any necessary splash screen +- [ ] subscription/payments (stripe - wrapper around pg via supabase) +- [ ] collections / packs +- [ ] Awards? levels? badges? +- [ ] get in touch with Lang Elliot! - [ ] Duolingo also has temporary text "2 in a row!" - [ ] Exiting / navigating away should present the user with a confirm modal: - "Quit and you'll lose your current progress!" @@ -146,18 +149,20 @@ MVP should probably just be a game demo using local storage, no user identity, d - can apply "inert" to the main content to disable focusing within there whenever modal's open - unfortunately there doesn't seem to be a way to escape inert-ness on children - requires opening a modal by storing component/element in a signal of some sort + - also would allow a smoother transition while background blurred, although this could also be done by just putting the static modal backdrop in the root. +- Naming: bird pack? bevy? flock? + # business ### Freemium - Free app with 20 birds, 1 song/call each -- Additional 30-bird packs $5/each for life -- $3/month for access to everything if paying by year -- Note: for life options probably require downloading bird packs rather than streaming, to limit server costs. -- Naming: bird pack? bevy? flock? +- $1/month? +- $10/year? +- $30/life with offline mode ### Cost to run @@ -172,8 +177,9 @@ MVP should probably just be a game demo using local storage, no user identity, d ### Realistically -Just do $3/month for everything. Otherwise you just get demo (maybe you also get -bird pack of the day). +- Just do $3/month for everything. + - Otherwise you just get demo (maybe you also get bird pack of the day). +- Maybe pack of the day increases in difficulty Mon - Fri # template readme diff --git a/src/ui/game/card.rs b/src/ui/game/card.rs index 383dac2..e9496b4 100644 --- a/src/ui/game/card.rs +++ b/src/ui/game/card.rs @@ -113,7 +113,7 @@ fn CardBack(bird: MappedSignal, correct: bool) -> Element { class: "flex flex-col justify-between", div { class: "text-lg font-semibold text-green-800 whitespace-nowrap", - "Nice work!" + "{bird().bird.common_name}" } BirdProgress { bird: bird.clone() } button { @@ -129,7 +129,7 @@ fn CardBack(bird: MappedSignal, correct: bool) -> Element { next_button.set(Some(e.data())); }, disabled: !next_button_enabled(), - "Ok!" + "Continue" } } } diff --git a/src/ui/game/mod.rs b/src/ui/game/mod.rs index bb406f0..e7f82dd 100644 --- a/src/ui/game/mod.rs +++ b/src/ui/game/mod.rs @@ -19,12 +19,12 @@ use card::MultipleChoiceCard; use game_over::GameOverModal; use quiz::{Game, MULTIPLE_CHOICE_SIZE}; +// TODO: this might be unnecessary if Listen is a totally different route #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum GameMode { Listen, + #[default] Learn, - #[default] // TODO change to learn when implemented - Quiz, } impl Display for GameMode { @@ -32,7 +32,6 @@ impl Display for GameMode { let s = match self { GameMode::Listen => "Listen", GameMode::Learn => "Learn", - GameMode::Quiz => "Quiz", }; write!(f, "{}", s) } @@ -45,7 +44,6 @@ impl FromStr for GameMode { match s { "Listen" => Ok(GameMode::Listen), "Learn" => Ok(GameMode::Learn), - "Quiz" => Ok(GameMode::Quiz), s => Err(format!("Invalid game mode: {s}")), } } @@ -55,8 +53,7 @@ impl GameMode { pub fn description(&self) -> &str { match self { GameMode::Listen => "Just listen to birds", - GameMode::Learn => "Listen to birds and then recall them", - GameMode::Quiz => "Think you know these birds? Test yourself", + GameMode::Learn => "Listen to birds and identify them", } } @@ -64,7 +61,6 @@ impl GameMode { match self { GameMode::Listen => "no pressure", GameMode::Learn => "a little pressure", - GameMode::Quiz => "maximum pressure", } } } @@ -181,9 +177,6 @@ impl GameCtx { #[component] pub fn GameView(pack: BirdPack, mode: GameMode) -> Element { - if mode != GameMode::Quiz { - return rsx! { "Not implemented!" }; - } let game_ctx = GameCtx::new(pack); let shuffle = game_ctx.shuffle_memo(); let correct_bird = game_ctx.correct_bird_memo(); @@ -225,7 +218,7 @@ fn ProgressBar() -> Element { tracing::debug!("Progress: {}", progress); rsx! { div { - class: "h-2 w-3/4 m-4 bg-stone-300/75 rounded-full max-w-screen-md", + class: "h-2 w-3/4 m-4 mt-6 bg-stone-300/75 rounded-full max-w-screen-md", div { class: "bg-gradient-to-r from-green-200 to-green-700 min-w-7 h-full rounded-full relative transition-[width,transform]", style: "width: min(calc(100% + 0.5rem), calc({progress}% + 1rem))", // 2 rem == w-8 diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 55fa305..caf4f14 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,12 +1,14 @@ mod components; mod db; mod game; +mod pages; use dioxus::prelude::*; use dioxus_sdk::storage::use_singleton_persistent; use crate::bird::BirdPack; use game::{GameMode, GameView}; +use pages::{Achievements, Birds, Listen, Settings}; const AUDIO_LOOP: bool = true; const AUDIO_AUTOPLAY: bool = true; @@ -48,7 +50,6 @@ impl GameStatus { } pub fn App() -> Element { - // TODO for demo: synced storage containing hashmap of user data (i.e. bird pack learned status) rsx! { Router:: { } @@ -58,35 +59,77 @@ pub fn App() -> Element { #[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[rustfmt::skip] enum Route { - // N.B. landing page will be handled in the future or even on a separate subdomain - // That's where the copyright etc. can live - - // TODO some pages might need this to scroll out of site. - #[layout(HeaderFooter)] + #[layout(Navbar)] #[route("/")] - Index, + Learn {}, - // This could probably just be a popup. - // #[route("/settings")] - // Settings {}, + #[route("/listen")] + Listen {}, - // #[route("/birds")] - // Birds {}, + #[route("/birds")] + Birds {}, // #[route("/packs")] // Packs {}, + + #[route("/achievements")] + Achievements {}, + + #[route("/settings")] + Settings {}, // #[end_layout] +} - // Fuck it this is giving me a headache handling Signal> from upstream - // #[route("/game/:id/:mode")] - // GameView { - // id: String, - // mode: GameMode - // }, +#[component] +fn Navbar() -> Element { + rsx! { + div { + class: "flex sm:flex-row flex-col-reverse h-screen", + div { + class: "grow-0 bg-green-800 text-amber-50 p-2", + img { + class: "w-24 mt-[-1rem] hidden sm:block", + src: "static_logo_transparent.png" + } + nav { + id: "navbar", + class: "flex sm:flex-col gap-2 justify-between sm:justify-start items-center", + Link { + class: "text-amber-50 hover:text-amber-100", + to: Route::Learn {}, "Learn" + } + Link { + class: "text-amber-50 hover:text-amber-100", + to: Route::Listen {}, "Listen" + } + Link { + class: "text-amber-50 hover:text-amber-100", + to: Route::Birds {}, "Birds" + } + // Link { + // class: "text-amber-50 hover:text-amber-100", + // to: Route::Packs {}, "Packs" + // } + Link { + class: "text-amber-50 hover:text-amber-100", + to: Route::Achievements {}, "Achievements" + } + Link { + class: "text-amber-50 hover:text-amber-100", + to: Route::Settings {}, "Settings" + } + } + } + div { + id: "content", + class: "grow no-shrink", + Outlet:: { } + } + } + } } -// TODO: can provide two versions, passing prop to a third internal one for header size? -// TODO: nav in here +// TODO: use this for landing page ( #[layout(HeaderFooter)] ) #[component] fn HeaderFooter() -> Element { rsx! { @@ -105,21 +148,20 @@ fn HeaderFooter() -> Element { Outlet:: { } } - // Use this on landing page - // footer { - // id: "footer", - // class: "shrink sticky top-[100vh] hidden sm:flex justify-items-center justify-center sm:max-lg:landscape:hidden", - // div { - // class: "text-green-800/75", - // "© 2024 birdtalk" - // } - // } + footer { + id: "footer", + class: "shrink sticky top-[100vh] hidden sm:flex justify-items-center justify-center sm:max-lg:landscape:hidden", + div { + class: "text-green-800/75", + "© 2024 birdtalk" + } + } } } } #[component] -fn Index() -> Element { +fn Learn() -> Element { match &*GAME_STATUS.read() { GameStatus::None => { rsx! { @@ -294,7 +336,7 @@ fn ModeSelector(mode: Signal) -> Element { } ul { class: "grid grid-cols-1 sm:grid-cols-3 w-full gap-2 sm:gap-6 items-stretch", - for opt in [GameMode::Listen, GameMode::Learn, GameMode::Quiz] { + for opt in [GameMode::Listen, GameMode::Learn] { li { label { r#for: "{opt}", @@ -306,7 +348,7 @@ fn ModeSelector(mode: Signal) -> Element { value: "{opt}", r#type: "radio", checked: (*mode.read() == opt).then_some(true), - disabled: opt != GameMode::Quiz, + disabled: opt != GameMode::Learn, onchange: move |_| { tracing::debug!("onchange: setting mode to from {:?} to {opt:?}", mode()); *mode.write() = opt; @@ -370,16 +412,3 @@ fn Loading() -> Element { } } } - -#[component] -fn Settings() -> Element { - rsx!( - h1 { - class: "text-2xl text-center", - "Settings" - } - p { - "Settings are consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - } - ) -} diff --git a/src/ui/pages/achievements.rs b/src/ui/pages/achievements.rs new file mode 100644 index 0000000..42a9cd2 --- /dev/null +++ b/src/ui/pages/achievements.rs @@ -0,0 +1,12 @@ +use dioxus::prelude::*; + +#[component] +pub fn Achievements() -> Element { + rsx! { + div { + class: "flex flex-col items-center justify-center h-full w-full", + h1 { "Achievements!" } + p { "Coming soon!" } + } + } +} diff --git a/src/ui/pages/birds.rs b/src/ui/pages/birds.rs new file mode 100644 index 0000000..8b39584 --- /dev/null +++ b/src/ui/pages/birds.rs @@ -0,0 +1,12 @@ +use dioxus::prelude::*; + +#[component] +pub fn Birds() -> Element { + rsx! { + div { + class: "flex flex-col items-center justify-center h-full w-full", + h1 { "Birds; or other types of collections?" } + p { "Coming soon!" } + } + } +} diff --git a/src/ui/pages/listen.rs b/src/ui/pages/listen.rs new file mode 100644 index 0000000..cb90fcb --- /dev/null +++ b/src/ui/pages/listen.rs @@ -0,0 +1,12 @@ +use dioxus::prelude::*; + +#[component] +pub fn Listen() -> Element { + rsx! { + div { + class: "flex flex-col items-center justify-center h-full w-full", + h1 { "Listen mode!" } + p { "Coming soon!" } + } + } +} diff --git a/src/ui/pages/mod.rs b/src/ui/pages/mod.rs new file mode 100644 index 0000000..4c405ce --- /dev/null +++ b/src/ui/pages/mod.rs @@ -0,0 +1,9 @@ +mod achievements; +mod birds; +mod listen; +mod settings; + +pub use achievements::*; +pub use birds::*; +pub use listen::*; +pub use settings::*; diff --git a/src/ui/pages/settings.rs b/src/ui/pages/settings.rs new file mode 100644 index 0000000..89d8a0e --- /dev/null +++ b/src/ui/pages/settings.rs @@ -0,0 +1,12 @@ +use dioxus::prelude::*; + +#[component] +pub fn Settings() -> Element { + rsx! { + div { + class: "flex flex-col items-center justify-center h-full w-full", + h1 { "Settings!" } + p { "Coming soon!" } + } + } +}