diff --git a/crates/ascend/src/components/problem_info.rs b/crates/ascend/src/components/problem_info.rs index b51e240..89ff02c 100644 --- a/crates/ascend/src/components/problem_info.rs +++ b/crates/ascend/src/components/problem_info.rs @@ -3,17 +3,17 @@ use leptos::prelude::*; #[component] #[tracing::instrument(skip_all)] -pub fn ProblemInfo(problem: models::Problem) -> impl IntoView { +pub fn ProblemInfo(#[prop(into)] problem: Signal) -> impl IntoView { tracing::trace!("Enter problem info"); - let name = problem.name; - let set_by = problem.set_by; - let method = problem.method; + let name = Signal::derive(move || problem.read().name.clone()); + let set_by = Signal::derive(move || problem.read().set_by.clone()); + let method = Signal::derive(move || problem.read().method.to_string()); view! {
- +
} @@ -21,9 +21,9 @@ pub fn ProblemInfo(problem: models::Problem) -> impl IntoView { #[component] #[tracing::instrument(skip_all)] -fn NameValue(#[prop(into)] name: String, #[prop(into)] value: String) -> impl IntoView { +fn NameValue(#[prop(into)] name: Signal, #[prop(into)] value: Signal) -> impl IntoView { view! { -

{name}

-

{value}

+

{name.get()}

+

{value.get()}

} } diff --git a/crates/ascend/src/components/show_some.rs b/crates/ascend/src/components/show_some.rs new file mode 100644 index 0000000..f623a57 --- /dev/null +++ b/crates/ascend/src/components/show_some.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; + +#[component] +#[tracing::instrument(skip_all)] +pub fn ShowSome(#[prop(into)] sig: Signal>, foo: C) -> impl IntoView +where + T: Clone + Send + Sync + 'static, + C: Fn(Signal) -> IV + Sync + Send + 'static, + IV: IntoView, +{ + tracing::trace!("Enter"); + + view! { + // + // {move || { + // let new = signal() + + // sig + // .with(|opt| { + // if let Some(inner) = opt.clone() { + // let new_signal = Signal::derive(move || sig.get().unwrap()); + // .into_any() + // } else { + // view! {}.into_any() + // } + // }) + // }} + // + } +} diff --git a/crates/ascend/src/lib.rs b/crates/ascend/src/lib.rs index e0a7d37..0b3abee 100644 --- a/crates/ascend/src/lib.rs +++ b/crates/ascend/src/lib.rs @@ -20,6 +20,7 @@ pub mod components { pub mod outlined_box; pub mod problem; pub mod problem_info; + pub mod show_some; } pub mod gradient; diff --git a/crates/ascend/src/pages/edit_wall.rs b/crates/ascend/src/pages/edit_wall.rs index 800881e..c95e80f 100644 --- a/crates/ascend/src/pages/edit_wall.rs +++ b/crates/ascend/src/pages/edit_wall.rs @@ -105,7 +105,7 @@ fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView { } }; - let upload = Action::from(ServerAction::::new()); + let upload = ServerAction::::new(); let hold = Signal::derive(move || { let refreshed = upload.value().get().map(Result::unwrap); diff --git a/crates/ascend/src/pages/settings.rs b/crates/ascend/src/pages/settings.rs index bc1344c..159979e 100644 --- a/crates/ascend/src/pages/settings.rs +++ b/crates/ascend/src/pages/settings.rs @@ -32,7 +32,7 @@ pub fn Settings() -> impl IntoView { #[component] #[tracing::instrument(skip_all)] fn Import(wall_uid: WallUid) -> impl IntoView { - let import_from_mini_moonboard = Action::from(ServerAction::::new()); + let import_from_mini_moonboard = ServerAction::::new(); let onclick = move |_mouse_event| { import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid }); @@ -44,7 +44,7 @@ fn Import(wall_uid: WallUid) -> impl IntoView { } } -#[server(name = ImportFromMiniMoonboard)] +#[server] #[tracing::instrument] async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> { use crate::server::config::Config; diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 8b94467..6904f9a 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -30,6 +30,7 @@ // | 14 days ago: | // +--------------------------------------+ +use crate::codec::ron::RonEncoded; use crate::components::ProblemInfo; use crate::components::attempt::Attempt; use crate::components::button::Button; @@ -40,6 +41,7 @@ use crate::components::icons::Icon; use crate::gradient::Gradient; use crate::models; use crate::models::HoldRole; +use crate::server_functions; use leptos::Params; use leptos::prelude::*; use leptos_router::params::Params; @@ -57,8 +59,6 @@ pub fn Wall() -> impl IntoView { let route_params = leptos_router::hooks::use_params::(); - let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::("problem"); - let wall_uid = Signal::derive(move || { route_params .get() @@ -68,28 +68,75 @@ pub fn Wall() -> impl IntoView { }); let wall = crate::resources::wall_by_uid(wall_uid); - let problem = crate::resources::problem_by_uid(wall_uid, problem_uid.into()); + + let header_items = move || HeaderItems { + left: vec![], + middle: vec![HeaderItem { + text: "ASCEND".to_string(), + link: None, + }], + right: vec![ + HeaderItem { + text: "Routes".to_string(), + link: Some(format!("/wall/{}/routes", wall_uid.get())), + }, + HeaderItem { + text: "Holds".to_string(), + link: Some(format!("/wall/{}/edit", wall_uid.get())), + }, + ], + }; + + leptos::view! { +
+ + +
+ + {move || Suspend::new(async move { + tracing::info!("executing main suspend"); + let wall = wall.await?; + let v = view! { }; + Ok::<_, ServerFnError>(v) + })} + +
+
+ } +} + +#[component] +#[tracing::instrument(skip_all)] +fn WithProblem(#[prop(into)] problem: Signal) -> impl IntoView { + tracing::trace!("Enter"); + + view! { } +} + +#[component] +#[tracing::instrument(skip_all)] +fn WithWall(#[prop(into)] wall: Signal) -> impl IntoView { + tracing::trace!("Enter"); + + let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::("problem"); + + let wall_uid = Signal::derive(move || wall.read().uid); + + 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()); - // merge outer option (resource hasn't resolved yet) with inner option (there is no problem for the wall) - let problem_sig2 = Signal::derive(move || problem.get().transpose().map(Option::flatten)); + let submit_attempt = ServerAction::>::new(); let fn_next_problem = move |wall: &models::Wall| { set_problem_uid.set(wall.random_problem()); }; // Set a problem when wall is set (loaded) - Effect::new(move |_prev_value| match &*wall.read() { - Some(Ok(wall)) => { - if problem_uid.get().is_none() { - tracing::debug!("Setting next problem"); - fn_next_problem(wall); - } + Effect::new(move |_prev_value| { + if problem_uid.get().is_none() { + tracing::debug!("Setting next problem"); + fn_next_problem(&wall.read()); } - Some(Err(err)) => { - tracing::error!("Error getting wall: {err}"); - } - None => {} }); let (attempt_today, set_attempt_today) = signal(None); @@ -112,24 +159,6 @@ pub fn Wall() -> impl IntoView { } }); - let header_items = move || HeaderItems { - left: vec![], - middle: vec![HeaderItem { - text: "ASCEND".to_string(), - link: None, - }], - right: vec![ - HeaderItem { - text: "Routes".to_string(), - link: Some(format!("/wall/{}/routes", wall_uid.get())), - }, - HeaderItem { - text: "Holds".to_string(), - link: Some(format!("/wall/{}/edit", wall_uid.get())), - }, - ], - }; - // onclick handler helper let set_or_deselect = |s: WriteSignal>, v: models::Attempt| { s.update(|s| match s { @@ -143,6 +172,11 @@ pub fn Wall() -> impl IntoView { }; let onclick_flash = move |_| { set_or_deselect(set_attempt_today, models::Attempt::Flash); + submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt { + wall_uid: wall_uid.get(), + problem_uid: problem.get().uid, + attempt: models::Attempt::Flash, + })); }; let onclick_send = move |_| { set_or_deselect(set_attempt_today, models::Attempt::Send); @@ -196,103 +230,83 @@ pub fn Wall() -> impl IntoView { }) }; - leptos::view! { -
- + // merge outer option (resource hasn't resolved yet) with inner option (there is no problem for the wall) + let problem_signal = Signal::derive(move || problem.get().transpose().map(Option::flatten)); -
- - {move || Suspend::new(async move { - tracing::info!("executing main suspend"); - let wall = wall.await?; - let grid = { + let grid = { + let wall = wall.clone(); + view! { + + { + let wall = wall.clone(); + move || { + let wall = wall.clone(); + Suspend::new(async move { let wall = wall.clone(); - view! { - - { - let wall = wall.clone(); - move || { - let wall = wall.clone(); - Suspend::new(async move { - let wall = wall.clone(); - tracing::info!("executing grid suspend"); - let view = view! { - - }; - Ok::<_, ServerFnError>(view) - }) - } - } - - } - }; - let v = view! { -
-
{grid}
+ tracing::info!("executing grid suspend"); + let view = view! { }; + Ok::<_, ServerFnError>(view) + }) + } + } + + } + }; -
-
{}
+ let v = view! { +
+
{grid}
- +
+
{}
-
-
-
-
+ - +
+
+
+
-
- - {move || Suspend::new(async move { - tracing::info!("executing probleminfo suspend"); - let problem = problem.await?; - let view = problem - .map(|problem| { - view! { } - }); - Ok::<_, ServerFnError>(view) - })} - -
+ - +
+ + {move || Suspend::new(async move { + tracing::info!("executing problem suspend"); + let problem = problem.await?; + let view = problem + .map(|problem| { + view! { } + }); + Ok::<_, ServerFnError>(view) + })} + +
- + - + - {foo()} -
-
- }; - Ok::<_, ServerFnError>(v) - })} - + + + {foo()}
- } + }; } -// TODO: refactor along these lines to limit suspend nesting in a single component? -// #[component] -// #[tracing::instrument(skip_all)] -// fn WithWall(#[prop(into)] wall: Signal) -> impl IntoView { -// tracing::trace!("Enter"); -// } - #[component] #[tracing::instrument(skip_all)] fn AttemptRadio( diff --git a/crates/ascend/src/resources.rs b/crates/ascend/src/resources.rs index af1459f..004a4de 100644 --- a/crates/ascend/src/resources.rs +++ b/crates/ascend/src/resources.rs @@ -16,7 +16,26 @@ pub fn wall_by_uid(wall_uid: Signal) -> RonResource, problem_uid: Signal>) -> RonResource> { +pub fn problem_by_uid(wall_uid: Signal, problem_uid: Signal) -> RonResource { + Resource::new_with_options( + move || (wall_uid.get(), problem_uid.get()), + move |(wall_uid, problem_uid)| async move { + let Some(problem_uid) = problem_uid else { + return Ok(None); + }; + crate::server_functions::get_problem_by_uid(wall_uid, problem_uid) + .await + .map(RonEncoded::into_inner) + }, + false, + ) +} + +/// Version of [problem_by_uid] that short circuits if the input problem_uid signal is None. +pub fn problem_by_uid_optional( + wall_uid: Signal, + problem_uid: Signal>, +) -> RonResource> { Resource::new_with_options( move || (wall_uid.get(), problem_uid.get()), move |(wall_uid, problem_uid)| async move { diff --git a/crates/ascend/src/server_functions.rs b/crates/ascend/src/server_functions.rs index 5449993..418077f 100644 --- a/crates/ascend/src/server_functions.rs +++ b/crates/ascend/src/server_functions.rs @@ -2,6 +2,10 @@ use crate::codec::ron::Ron; use crate::codec::ron::RonEncoded; use crate::models; use crate::models::UserInteraction; +use derive_more::Display; +use derive_more::Error; +use derive_more::From; +use leptos::prelude::*; use leptos::server; use server_fn::ServerFnError; @@ -202,3 +206,74 @@ pub(crate) async fn get_problem_by_uid( Ok(RonEncoded::new(problem)) } + +/// Inserts or updates today's attempt. +#[server( + input = Ron, + output = Ron, + custom = RonEncoded +)] +#[tracing::instrument(err(Debug))] +pub(crate) async fn upsert_todays_attempt( + wall_uid: models::WallUid, + problem_uid: models::ProblemUid, + attempt: models::Attempt, +) -> Result, ServerFnError> { + use crate::server::db::Database; + use crate::server::db::DatabaseOperationError; + use leptos::prelude::expect_context; + + tracing::trace!("Enter"); + + #[derive(Debug, Error, Display, From)] + enum Error { + #[display("Wall not found: {_0:?}")] + WallNotFound(#[error(not(source))] models::WallUid), + + DatabaseOperation(DatabaseOperationError), + } + + async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid, attempt: models::Attempt) -> Result { + let db = expect_context::(); + + let user_interaction = db + .write(|txn| { + let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?; + + let key = (wall_uid, problem_uid); + + // Pop or default + let mut user_interaction = user_table + .remove(key)? + .map(|guard| guard.value()) + .unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem_uid)); + + // If the last entry is from today, remove it + if let Some(entry) = user_interaction.attempted_on.last_entry() { + let today_local_naive = chrono::Local::now().date_naive(); + + let entry_date = entry.key(); + let entry_date_local_naive = entry_date.with_timezone(&chrono::Local).date_naive(); + + if entry_date_local_naive == today_local_naive { + entry.remove(); + } + } + + user_interaction.attempted_on.insert(chrono::Utc::now(), attempt); + + user_table.insert(key, user_interaction.clone())?; + + Ok(user_interaction) + }) + .await?; + + Ok(user_interaction) + } + + inner(wall_uid, problem_uid, attempt) + .await + .map_err(error_reporter::Report::new) + .map_err(ServerFnError::new) + .map(RonEncoded::new) +}