show counter for filter

This commit is contained in:
Asger Juul Brunshøj 2025-03-17 23:14:26 +01:00
parent 83bd8e0e5e
commit f8aa1e29a2
4 changed files with 153 additions and 21 deletions

View File

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

View File

@ -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! { <WithWall wall problems /> };
let user_interactions = user_interactions.await?;
let v = view! { <WithWall wall problems user_interactions /> };
Ok::<_, ServerFnError>(v)
})}
</Transition>
@ -119,7 +122,11 @@ fn WithProblem(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView
#[component]
#[tracing::instrument(skip_all)]
fn WithWall(#[prop(into)] wall: Signal<models::Wall>, #[prop(into)] problems: Signal<Vec<models::Problem>>) -> impl IntoView {
fn WithWall(
#[prop(into)] wall: Signal<models::Wall>,
#[prop(into)] problems: Signal<Vec<models::Problem>>,
#[prop(into)] user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
) -> impl IntoView {
tracing::trace!("Enter");
let wall_uid = Signal::derive(move || wall.read().uid);
@ -151,7 +158,13 @@ fn WithWall(#[prop(into)] wall: Signal<models::Wall>, #[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<models::Wall>, #[prop(into)] problems: Si
let sep = (!cells.is_empty()).then_some(view! { <Separator /> });
#[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! {
<span class="mx-1">
<span class=class>{interaction_counters.flash}</span>
</span>
}
});
let send = (interaction_counters.send > 0).then(|| {
let class = Gradient::TealLime.class_text();
view! {
<span class="mx-1">
<span class=class>{interaction_counters.send}</span>
</span>
}
});
let attempt = (interaction_counters.attempt > 0).then(|| {
let class = Gradient::PinkOrange.class_text();
view! {
<span class="mx-1">
<span class=class>{interaction_counters.attempt}</span>
</span>
}
});
if flash.is_some() || send.is_some() || attempt.is_some() {
view! {
<span>{"("}</span>
{flash}
{send}
{attempt}
<span>{")"}</span>
}
.into_any()
} else {
().into_any()
}
};
view! {
<div class="grid grid-cols-5 gap-1">{cells}</div>
{sep}
{problems_counter}
{interaction_counters_view}
}
};
@ -274,23 +346,13 @@ fn WithWall(#[prop(into)] wall: Signal<models::Wall>, #[prop(into)] problems: Si
<Separator />
<Transition fallback=move || ()>
{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());
return view! {}.into_any();
};
let view = view! {
<WithUserInteraction wall_uid problem_uid user_interaction />
}
.into_any();
Ok::<_, ServerFnError>(view)
})
view! { <WithUserInteraction wall_uid problem_uid user_interaction /> }
.into_any()
}}
</Transition>
</div>
</div>
}
@ -466,7 +528,11 @@ fn Grid(
on_click_hold(hold_position);
}
};
let cell = view! { <Hold on:click=on_click role hold=hold.clone() /> };
let cell = view! {
<div class="cursor-pointer">
<Hold on:click=on_click role hold=hold.clone() />
</div>
};
cells.push(cell);
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-1", wall.rows, wall.cols,);

View File

@ -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<T> = Resource<Result<T, ServerFnError>, 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<models::WallUid>) -> RonResource<Vec<models::Problem>> {
Resource::new_with_options(
move || wall_uid.get(),
@ -44,6 +46,7 @@ pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<m
)
}
/// Returns user interaction for a single problem
pub fn user_interaction(
wall_uid: Signal<models::WallUid>,
problem_uid: Signal<Option<models::ProblemUid>>,
@ -61,3 +64,12 @@ pub fn user_interaction(
false,
)
}
/// Returns all user interactions for a wall
pub fn user_interactions(wall_uid: Signal<models::WallUid>) -> RonResource<BTreeMap<models::ProblemUid, models::UserInteraction>> {
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,
)
}

View File

@ -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<R
Ok(RonEncoded::new(problems))
}
/// Returns user interaction for a single wall problem
#[server(
input = Ron,
output = Ron,
@ -170,6 +172,52 @@ pub(crate) async fn get_user_interaction(
Ok(RonEncoded::new(user_interaction))
}
/// Returns all user interactions for a wall
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interactions(
wall_uid: models::WallUid,
) -> Result<RonEncoded<BTreeMap<models::ProblemUid, models::UserInteraction>>, 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<BTreeMap<models::ProblemUid, models::UserInteraction>, Error> {
let db = expect_context::<Database>();
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::<Result<_, _>>()?;
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,