diff --git a/crates/ascend/src/models.rs b/crates/ascend/src/models.rs index bcebb08..01ab594 100644 --- a/crates/ascend/src/models.rs +++ b/crates/ascend/src/models.rs @@ -125,6 +125,12 @@ pub mod v2 { pub fn create() -> Self { Self(uuid::Uuid::new_v4()) } + pub fn min() -> Self { + Self(uuid::Uuid::nil()) + } + pub fn max() -> Self { + Self(uuid::Uuid::max()) + } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display)] diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index c8fefb1..b0218d2 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -46,6 +46,7 @@ use crate::server_functions; use leptos::Params; use leptos::prelude::*; use leptos_router::params::Params; +use std::collections::BTreeMap; use std::collections::BTreeSet; use web_sys::MouseEvent; @@ -71,6 +72,7 @@ pub fn Wall() -> impl IntoView { let wall = crate::resources::wall_by_uid(wall_uid); let problems = crate::resources::problems_for_wall(wall_uid); + let user_interactions = crate::resources::user_interactions(wall_uid); let header_items = move || HeaderItems { left: vec![], @@ -100,7 +102,8 @@ pub fn Wall() -> impl IntoView { tracing::info!("executing main suspend"); let wall = wall.await?; let problems = problems.await?; - let v = view! { }; + let user_interactions = user_interactions.await?; + let v = view! { }; Ok::<_, ServerFnError>(v) })} @@ -119,7 +122,11 @@ fn WithProblem(#[prop(into)] problem: Signal) -> impl IntoView #[component] #[tracing::instrument(skip_all)] -fn WithWall(#[prop(into)] wall: Signal, #[prop(into)] problems: Signal>) -> impl IntoView { +fn WithWall( + #[prop(into)] wall: Signal, + #[prop(into)] problems: Signal>, + #[prop(into)] user_interactions: Signal>, +) -> impl IntoView { tracing::trace!("Enter"); let wall_uid = Signal::derive(move || wall.read().uid); @@ -151,7 +158,13 @@ fn WithWall(#[prop(into)] wall: Signal, #[prop(into)] problems: Si }); let problem = crate::resources::problem_by_uid_optional(wall_uid, problem_uid.into()); - let user_interaction = crate::resources::user_interaction(wall_uid, problem_uid.into()); + let user_interaction = Signal::derive(move || { + let Some(problem_uid) = problem_uid.get() else { + return None; + }; + let user_interactions = user_interactions.read(); + user_interactions.get(&problem_uid).cloned() + }); let fn_next_problem = move || { let problems = filtered_problems.read(); @@ -230,10 +243,69 @@ fn WithWall(#[prop(into)] wall: Signal, #[prop(into)] problems: Si let sep = (!cells.is_empty()).then_some(view! { }); + #[derive(Default)] + struct InteractionCounters { + flash: u64, + send: u64, + attempt: u64, + } + let mut interaction_counters = InteractionCounters::default(); + let interaction_counters_view = { + let user_ints = user_interactions.read(); + for problem in filtered_problems.read().iter() { + if let Some(user_int) = user_ints.get(&problem.uid) { + match user_int.best_attempt() { + Some((_, models::Attempt::Flash)) => interaction_counters.flash += 1, + Some((_, models::Attempt::Send)) => interaction_counters.send += 1, + Some((_, models::Attempt::Attempt)) => interaction_counters.attempt += 1, + None => {} + } + } + } + let flash = (interaction_counters.flash > 0).then(|| { + let class = Gradient::CyanBlue.class_text(); + view! { + + {interaction_counters.flash} + + } + }); + let send = (interaction_counters.send > 0).then(|| { + let class = Gradient::TealLime.class_text(); + view! { + + {interaction_counters.send} + + } + }); + let attempt = (interaction_counters.attempt > 0).then(|| { + let class = Gradient::PinkOrange.class_text(); + view! { + + {interaction_counters.attempt} + + } + }); + + if flash.is_some() || send.is_some() || attempt.is_some() { + view! { + {"("} + {flash} + {send} + {attempt} + {")"} + } + .into_any() + } else { + ().into_any() + } + }; + view! {
{cells}
{sep} {problems_counter} + {interaction_counters_view} } }; @@ -274,23 +346,13 @@ fn WithWall(#[prop(into)] wall: Signal, #[prop(into)] problems: Si - - {move || { - Suspend::new(async move { - tracing::debug!("getting user interaction"); - let user_interaction: Option<_> = user_interaction.await?; - tracing::debug!("got user interaction"); - let Some(problem_uid) = problem_uid.get() else { - return Ok(view! {}.into_any()); - }; - let view = view! { - - } - .into_any(); - Ok::<_, ServerFnError>(view) - }) - }} - + {move || { + let Some(problem_uid) = problem_uid.get() else { + return view! {}.into_any(); + }; + view! { } + .into_any() + }} } @@ -466,7 +528,11 @@ fn Grid( on_click_hold(hold_position); } }; - let cell = view! { }; + let cell = view! { +
+ +
+ }; cells.push(cell); } let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-1", wall.rows, wall.cols,); diff --git a/crates/ascend/src/resources.rs b/crates/ascend/src/resources.rs index ca4b531..3335072 100644 --- a/crates/ascend/src/resources.rs +++ b/crates/ascend/src/resources.rs @@ -5,6 +5,7 @@ use leptos::prelude::Get; use leptos::prelude::Signal; use leptos::server::Resource; use server_fn::ServerFnError; +use std::collections::BTreeMap; type RonResource = Resource, Ron>; @@ -36,6 +37,7 @@ pub fn problem_by_uid_optional( ) } +/// Returns all problems for a wall pub fn problems_for_wall(wall_uid: Signal) -> RonResource> { Resource::new_with_options( move || wall_uid.get(), @@ -44,6 +46,7 @@ pub fn problems_for_wall(wall_uid: Signal) -> RonResource, problem_uid: Signal>, @@ -61,3 +64,12 @@ pub fn user_interaction( false, ) } + +/// Returns all user interactions for a wall +pub fn user_interactions(wall_uid: Signal) -> RonResource> { + Resource::new_with_options( + move || wall_uid.get(), + move |wall_uid| async move { crate::server_functions::get_user_interactions(wall_uid).await.map(RonEncoded::into_inner) }, + false, + ) +} diff --git a/crates/ascend/src/server_functions.rs b/crates/ascend/src/server_functions.rs index 6340dc6..1aa39f3 100644 --- a/crates/ascend/src/server_functions.rs +++ b/crates/ascend/src/server_functions.rs @@ -8,6 +8,7 @@ use derive_more::From; use leptos::prelude::*; use leptos::server; use server_fn::ServerFnError; +use std::collections::BTreeMap; #[server( input = Ron, @@ -126,6 +127,7 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result Result>, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + use leptos::prelude::expect_context; + tracing::trace!("Enter"); + + #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] + enum Error { + DatabaseOperation(DatabaseOperationError), + } + + async fn inner(wall_uid: models::WallUid) -> Result, Error> { + let db = expect_context::(); + + let user_interactions = db + .read(|txn| { + let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?; + let range = user_table.range((wall_uid, models::ProblemUid::min())..=(wall_uid, models::ProblemUid::max()))?; + let user_interactions = range + .map(|guard| { + guard.map(|(_key, val)| { + let val = val.value(); + (val.problem_uid, val) + }) + }) + .collect::>()?; + Ok(user_interactions) + }) + .await?; + + Ok(user_interactions) + } + + let user_interaction = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?; + Ok(RonEncoded::new(user_interaction)) +} + #[server( input = Ron, output = Ron,