feat: refactor to controller component and redesign
This commit is contained in:
parent
58698a1087
commit
e403be8090
@ -42,7 +42,7 @@ pub fn App() -> impl IntoView {
|
||||
<Router>
|
||||
<Routes fallback=|| "Not found">
|
||||
<Route path=path!("/") view=Home />
|
||||
<Route path=path!("/wall/:wall_uid") view=pages::wall::Wall />
|
||||
<Route path=path!("/wall/:wall_uid") view=pages::wall::Page />
|
||||
<Route path=path!("/wall/:wall_uid/edit") view=pages::edit_wall::EditWall />
|
||||
<Route path=path!("/wall/:wall_uid/routes") view=pages::routes::Routes />
|
||||
</Routes>
|
||||
|
27
crates/ascend/src/components.rs
Normal file
27
crates/ascend/src/components.rs
Normal file
@ -0,0 +1,27 @@
|
||||
pub use attempt::Attempt;
|
||||
pub use button::Button;
|
||||
pub use header::StyledHeader;
|
||||
pub use problem::Problem;
|
||||
pub use problem_info::ProblemInfo;
|
||||
|
||||
pub mod attempt;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod header;
|
||||
pub mod header_v2;
|
||||
pub mod icons;
|
||||
pub mod outlined_box;
|
||||
pub mod problem;
|
||||
pub mod problem_info;
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn OnHoverRed(children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div class="group relative">
|
||||
<div>{children()}</div>
|
||||
<div class="absolute inset-0 bg-red-500 opacity-0 group-hover:opacity-50"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
13
crates/ascend/src/components/header_v2.rs
Normal file
13
crates/ascend/src/components/header_v2.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn Header(children: Children) -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
view! {
|
||||
<div class="w-full text-black bg-orange-300 border-b-2 border-b-orange-400">
|
||||
{children()}
|
||||
</div>
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highli
|
||||
Gradient::TealLime => "bg-teal-700",
|
||||
Gradient::PurplePink => "bg-purple-900",
|
||||
Gradient::PurpleBlue => "bg-purple-900",
|
||||
Gradient::Orange => "bg-orange-900",
|
||||
};
|
||||
|
||||
c.push(' ');
|
||||
|
@ -1,11 +1,12 @@
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub enum Gradient {
|
||||
#[default]
|
||||
PurpleBlue,
|
||||
PinkOrange,
|
||||
CyanBlue,
|
||||
TealLime,
|
||||
PurplePink,
|
||||
#[default]
|
||||
Orange,
|
||||
}
|
||||
impl Gradient {
|
||||
pub fn class_from(&self) -> &str {
|
||||
@ -15,6 +16,7 @@ impl Gradient {
|
||||
Gradient::TealLime => "from-teal-300",
|
||||
Gradient::PurplePink => "from-purple-500",
|
||||
Gradient::PurpleBlue => "from-purple-600",
|
||||
Gradient::Orange => "from-orange-400",
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +27,7 @@ impl Gradient {
|
||||
Gradient::TealLime => "to-lime-300",
|
||||
Gradient::PurplePink => "to-pink-500",
|
||||
Gradient::PurpleBlue => "to-blue-500",
|
||||
Gradient::Orange => "to-orange-500",
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +38,7 @@ impl Gradient {
|
||||
Gradient::TealLime => "text-teal-300",
|
||||
Gradient::PurplePink => "text-purple-500",
|
||||
Gradient::PurpleBlue => "text-purple-600",
|
||||
Gradient::Orange => "text-orange-400",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,13 @@
|
||||
pub mod app;
|
||||
pub mod pages {
|
||||
pub mod edit_wall;
|
||||
pub mod routes;
|
||||
pub mod settings;
|
||||
pub mod wall;
|
||||
}
|
||||
pub mod components {
|
||||
pub use attempt::Attempt;
|
||||
pub use button::Button;
|
||||
pub use header::StyledHeader;
|
||||
pub use problem::Problem;
|
||||
pub use problem_info::ProblemInfo;
|
||||
|
||||
pub mod attempt;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod header;
|
||||
pub mod icons;
|
||||
pub mod outlined_box;
|
||||
pub mod problem;
|
||||
pub mod problem_info;
|
||||
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn OnHoverRed(children: Children) -> impl IntoView {
|
||||
view! {
|
||||
<div class="group relative">
|
||||
<div>{children()}</div>
|
||||
<div class="absolute inset-0 bg-red-500 opacity-0 group-hover:opacity-50"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod gradient;
|
||||
|
||||
pub mod resources;
|
||||
|
||||
pub mod css;
|
||||
|
||||
pub mod codec;
|
||||
pub mod components;
|
||||
pub mod css;
|
||||
pub mod gradient;
|
||||
pub mod models;
|
||||
pub mod pages;
|
||||
pub mod resources;
|
||||
pub mod server_functions;
|
||||
pub mod tracing;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod server;
|
||||
@ -57,12 +22,12 @@ pub fn hydrate() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::level_filters::LevelFilter::DEBUG.into())
|
||||
.with_default_directive(::tracing::level_filters::LevelFilter::DEBUG.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their JS backtrace
|
||||
tracing_subscriber_wasm::MakeConsoleWriter::default().map_trace_level_to(tracing::Level::DEBUG),
|
||||
tracing_subscriber_wasm::MakeConsoleWriter::default().map_trace_level_to(::tracing::Level::DEBUG),
|
||||
)
|
||||
// For some reason, if we don't do this in the browser, we get a runtime error.
|
||||
.without_time()
|
||||
|
@ -59,6 +59,7 @@ impl From<(&DateTime<Utc>, &Attempt)> for DatedAttempt {
|
||||
}
|
||||
|
||||
impl WallUid {
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn create() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
|
4
crates/ascend/src/pages.rs
Normal file
4
crates/ascend/src/pages.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod edit_wall;
|
||||
pub mod routes;
|
||||
pub mod settings;
|
||||
pub mod wall;
|
@ -3,9 +3,6 @@ use crate::components::OnHoverRed;
|
||||
use crate::components::ProblemInfo;
|
||||
use crate::components::attempt::Attempt;
|
||||
use crate::components::button::Button;
|
||||
use crate::components::header::HeaderItem;
|
||||
use crate::components::header::HeaderItems;
|
||||
use crate::components::header::StyledHeader;
|
||||
use crate::components::icons::Icon;
|
||||
use crate::gradient::Gradient;
|
||||
use crate::models;
|
||||
@ -24,8 +21,8 @@ struct RouteParams {
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn Wall() -> impl IntoView {
|
||||
tracing::debug!("Enter");
|
||||
pub fn Page() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let route_params = leptos_router::hooks::use_params::<RouteParams>();
|
||||
|
||||
@ -41,40 +38,171 @@ pub fn Wall() -> impl IntoView {
|
||||
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![],
|
||||
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! {
|
||||
<div class="min-h-screen min-w-screen bg-neutral-950">
|
||||
<StyledHeader items=Signal::derive(header_items) />
|
||||
<Suspense fallback=|| {
|
||||
"loading"
|
||||
}>
|
||||
{move || Suspend::new(async move {
|
||||
tracing::debug!("executing main suspend");
|
||||
let wall = wall.await?;
|
||||
let problems = problems.await?;
|
||||
let user_interactions = user_interactions.await?;
|
||||
let user_interactions = RwSignal::new(user_interactions);
|
||||
Ok::<_, ServerFnError>(view! { <Controller wall problems user_interactions /> })
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="m-2">
|
||||
<Transition fallback=|| ()>
|
||||
{move || Suspend::new(async move {
|
||||
tracing::info!("executing main suspend");
|
||||
let wall = wall.await?;
|
||||
let problems = problems.await?;
|
||||
let user_interactions = user_interactions.await?;
|
||||
let user_interactions = RwSignal::new(user_interactions);
|
||||
let v = view! { <WithWall wall problems user_interactions /> };
|
||||
Ok::<_, ServerFnError>(v)
|
||||
})}
|
||||
</Transition>
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Context {
|
||||
wall: Signal<models::Wall>,
|
||||
user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
||||
problem: Signal<Option<models::Problem>>,
|
||||
filtered_problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
||||
user_interaction: Signal<Option<models::UserInteraction>>,
|
||||
todays_attempt: Signal<Option<models::Attempt>>,
|
||||
latest_attempt: Signal<Option<models::DatedAttempt>>,
|
||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||
cb_click_hold: Callback<models::HoldPosition>,
|
||||
cb_remove_hold_from_filter: Callback<models::HoldPosition>,
|
||||
cb_next_problem: Callback<()>,
|
||||
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn Controller(
|
||||
#[prop(into)] wall: Signal<models::Wall>,
|
||||
#[prop(into)] problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
||||
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
||||
) -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
// Extract data from URL
|
||||
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
|
||||
|
||||
// Filter
|
||||
let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
|
||||
let cb_remove_hold_from_filter: Callback<models::HoldPosition> = Callback::new(move |hold_pos: models::HoldPosition| {
|
||||
set_filter_holds.update(move |set| {
|
||||
set.remove(&hold_pos);
|
||||
});
|
||||
});
|
||||
|
||||
// Derive signals
|
||||
let wall_uid = signals::wall_uid(wall);
|
||||
let problem = signals::problem(problems, problem_uid.into());
|
||||
let user_interaction = signals::user_interaction(user_interactions.into(), problem_uid.into());
|
||||
let filtered_problems = signals::filtered_problems(problems, filter_holds.into());
|
||||
let todays_attempt = signals::todays_attempt(user_interaction);
|
||||
let latest_attempt = signals::latest_attempt(user_interaction);
|
||||
|
||||
// Submit attempt action
|
||||
let upsert_todays_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
|
||||
let cb_upsert_todays_attempt = Callback::new(move |attempt| {
|
||||
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
||||
});
|
||||
|
||||
// Callback: Set next problem to a random problem
|
||||
let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
|
||||
// TODO: remove current problem from population
|
||||
let population = filtered_problems.read();
|
||||
let population = population.keys().copied();
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
let mut rng = rand::rng();
|
||||
let problem_uid = population.choose(&mut rng);
|
||||
|
||||
set_problem_uid.set(problem_uid);
|
||||
});
|
||||
|
||||
// Callback: On click hold, Add/Remove hold position to problem filter
|
||||
let cb_click_hold: Callback<models::HoldPosition> = Callback::new(move |hold_position| {
|
||||
set_filter_holds.update(|set| {
|
||||
if !set.remove(&hold_position) {
|
||||
set.insert(hold_position);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Set a problem when wall is set (loaded)
|
||||
Effect::new(move |_prev_value| {
|
||||
if problem_uid.get().is_none() {
|
||||
tracing::debug!("Setting initial problem");
|
||||
cb_set_random_problem.run(());
|
||||
}
|
||||
});
|
||||
|
||||
// Update user interactions after submitting an attempt
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(v)) = upsert_todays_attempt.value().get() {
|
||||
let v = v.into_inner();
|
||||
user_interactions.update(|map| {
|
||||
map.insert(v.problem_uid, v);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
provide_context(Context {
|
||||
wall,
|
||||
problem,
|
||||
cb_click_hold,
|
||||
user_interaction,
|
||||
latest_attempt,
|
||||
cb_upsert_todays_attempt,
|
||||
cb_remove_hold_from_filter,
|
||||
cb_next_problem: cb_set_random_problem,
|
||||
todays_attempt,
|
||||
filter_holds: filter_holds.into(),
|
||||
filtered_problems: filtered_problems.into(),
|
||||
user_interactions: user_interactions.into(),
|
||||
});
|
||||
|
||||
view! { <View /> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn View() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
view! {
|
||||
<div class="flex">
|
||||
<div class="flex-initial">
|
||||
<Wall />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col" style="width:38rem">
|
||||
<Section title="Filter">
|
||||
<Filter />
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<NextProblemButton />
|
||||
|
||||
<Separator />
|
||||
|
||||
<Section title="Problem">
|
||||
{move || ctx.problem.get().map(|problem| view! { <ProblemInfo problem /> })}
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<AttemptRadioGroup />
|
||||
|
||||
<Separator />
|
||||
|
||||
<History />
|
||||
</div>
|
||||
|
||||
<div class="flex-auto flex flex-row justify-end items-start px-5 pt-3">
|
||||
<HoldsButton />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -82,104 +210,56 @@ pub fn Wall() -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn WithProblem(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
|
||||
tracing::trace!("Enter");
|
||||
fn HoldsButton() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
view! { <ProblemInfo problem /> }
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let link = move || format!("/wall/{}/edit", ctx.wall.read().uid);
|
||||
|
||||
view! {
|
||||
<a href=link>
|
||||
<Button text="Holds" icon=Icon::WrenchSolid />
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn WithWall(
|
||||
#[prop(into)] wall: Signal<models::Wall>,
|
||||
#[prop(into)] problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
||||
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
||||
) -> impl IntoView {
|
||||
tracing::trace!("Enter");
|
||||
fn NextProblemButton() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let wall_uid = Signal::derive(move || wall.read().uid);
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
|
||||
let on_click = move |_| ctx.cb_next_problem.run(());
|
||||
view! {
|
||||
<div class="flex flex-col">
|
||||
<div class="self-center">
|
||||
<Button
|
||||
icon=Icon::ArrowPath
|
||||
text="Next problem"
|
||||
on:click=on_click
|
||||
color=Gradient::PurpleBlue
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let submit_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(v)) = submit_attempt.value().get() {
|
||||
let v = v.into_inner();
|
||||
user_interactions.update(|map| {
|
||||
map.insert(v.problem_uid, v);
|
||||
});
|
||||
}
|
||||
});
|
||||
let submit_attempt_cb = StoredValue::new(move |attempt: server_functions::UpsertTodaysAttempt| {
|
||||
submit_attempt.dispatch(RonEncoded(attempt));
|
||||
});
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn Filter() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let problem = signals::problem(problems, problem_uid.into());
|
||||
let user_interaction = signals::user_interaction(user_interactions.into(), problem_uid.into());
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
// Filter
|
||||
let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
|
||||
let _filter_add_hold = move |hold_pos: models::HoldPosition| {
|
||||
set_filter_holds.update(move |set| {
|
||||
set.insert(hold_pos);
|
||||
});
|
||||
};
|
||||
let filter_remove_hold = move |hold_pos: models::HoldPosition| {
|
||||
set_filter_holds.update(move |set| {
|
||||
set.remove(&hold_pos);
|
||||
});
|
||||
};
|
||||
|
||||
let filtered_problems = Memo::new(move |_prev_val| {
|
||||
let filter_holds = filter_holds.get();
|
||||
problems.with(|problems| {
|
||||
problems
|
||||
.iter()
|
||||
.filter(|(_, problem)| filter_holds.iter().all(|hold_pos| problem.holds.contains_key(hold_pos)))
|
||||
.map(|(problem_uid, problem)| (*problem_uid, problem.clone()))
|
||||
.collect::<BTreeMap<models::ProblemUid, models::Problem>>()
|
||||
})
|
||||
});
|
||||
|
||||
let fn_next_problem = move || {
|
||||
let problems = filtered_problems.read();
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
let mut rng = rand::rng();
|
||||
let problem_uid = problems.keys().copied().choose(&mut rng);
|
||||
|
||||
set_problem_uid.set(problem_uid);
|
||||
};
|
||||
|
||||
// Set a problem when wall is set (loaded)
|
||||
Effect::new(move |_prev_value| {
|
||||
if problem_uid.get().is_none() {
|
||||
tracing::debug!("Setting next problem");
|
||||
fn_next_problem();
|
||||
}
|
||||
});
|
||||
|
||||
let on_click_hold = move |hold_position: models::HoldPosition| {
|
||||
// Add/Remove hold position to problem filter
|
||||
set_filter_holds.update(|set| {
|
||||
if !set.remove(&hold_position) {
|
||||
set.insert(hold_position);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let grid = move || {
|
||||
let wall = wall.get();
|
||||
view! { <Grid wall problem on_click_hold /> }
|
||||
};
|
||||
|
||||
let filter = move || {
|
||||
move || {
|
||||
let mut cells = vec![];
|
||||
for hold_pos in filter_holds.get() {
|
||||
let w = &*wall.read();
|
||||
for hold_pos in ctx.filter_holds.get() {
|
||||
let w = &*ctx.wall.read();
|
||||
if let Some(hold) = w.holds.get(&hold_pos).cloned() {
|
||||
let onclick = move |_| {
|
||||
filter_remove_hold(hold_pos);
|
||||
ctx.cb_remove_hold_from_filter.run(hold_pos);
|
||||
};
|
||||
|
||||
let v = view! {
|
||||
@ -195,7 +275,7 @@ fn WithWall(
|
||||
|
||||
let problems_counter = {
|
||||
let name = view! { <p class="font-light mr-4 text-right text-orange-300">{"Problems:"}</p> };
|
||||
let value = view! { <p class="text-white">{filtered_problems.read().len()}</p> };
|
||||
let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> };
|
||||
view! {
|
||||
<div class="grid grid-rows-none gap-y-1 gap-x-0.5 grid-cols-[auto_1fr]">
|
||||
{name} {value}
|
||||
@ -213,8 +293,8 @@ fn WithWall(
|
||||
}
|
||||
let mut interaction_counters = InteractionCounters::default();
|
||||
let interaction_counters_view = {
|
||||
let user_ints = user_interactions.read();
|
||||
for problem_uid in filtered_problems.read().keys() {
|
||||
let user_ints = ctx.user_interactions.read();
|
||||
for problem_uid in ctx.filtered_problems.read().keys() {
|
||||
if let Some(user_int) = user_ints.get(problem_uid) {
|
||||
match user_int.best_attempt().map(|da| da.attempt) {
|
||||
Some(models::Attempt::Flash) => interaction_counters.flash += 1,
|
||||
@ -269,101 +349,43 @@ fn WithWall(
|
||||
{problems_counter}
|
||||
{interaction_counters_view}
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="inline-grid grid-cols-1 gap-8 md:grid-cols-[auto_1fr]">
|
||||
<div>{grid}</div>
|
||||
|
||||
<div class="flex flex-col" style="width:38rem">
|
||||
<Section title="Filter">{filter}</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="self-center">
|
||||
<Button
|
||||
icon=Icon::ArrowPath
|
||||
text="Next problem"
|
||||
on:click=move |_| fn_next_problem()
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Section title="Problem">
|
||||
{move || problem.get().map(|p| view! { <WithProblem problem=p /> })}
|
||||
</Section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{move || {
|
||||
let Some(problem_uid) = problem_uid.get() else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
view! {
|
||||
<WithUserInteraction
|
||||
wall_uid
|
||||
problem_uid
|
||||
user_interaction
|
||||
submit_attempt=submit_attempt_cb
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn WithUserInteraction(
|
||||
#[prop(into)] wall_uid: Signal<models::WallUid>,
|
||||
#[prop(into)] problem_uid: Signal<models::ProblemUid>,
|
||||
#[prop(into)] user_interaction: Signal<Option<models::UserInteraction>>,
|
||||
submit_attempt: StoredValue<impl Fn(server_functions::UpsertTodaysAttempt) + Sync + Send + 'static>,
|
||||
) -> impl IntoView {
|
||||
tracing::debug!("Enter WithUserInteraction");
|
||||
fn AttemptRadioGroup() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let todays_attempt = signals::todays_attempt(user_interaction);
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let problem_uid = Signal::derive(move || ctx.problem.read().as_ref().map(|p| p.uid));
|
||||
|
||||
let mut attempt_radio_buttons = vec![];
|
||||
for variant in [models::Attempt::Flash, models::Attempt::Send, models::Attempt::Attempt] {
|
||||
let ui_toggle = Signal::derive(move || todays_attempt.get() == Some(variant));
|
||||
let ui_toggle = Signal::derive(move || ctx.todays_attempt.get() == Some(variant));
|
||||
|
||||
let onclick = move |_| {
|
||||
let attempt = if ui_toggle.get() { None } else { Some(variant) };
|
||||
submit_attempt.read_value()(server_functions::UpsertTodaysAttempt {
|
||||
wall_uid: wall_uid.get(),
|
||||
problem_uid: problem_uid.get(),
|
||||
attempt,
|
||||
});
|
||||
|
||||
if let Some(problem_uid) = problem_uid.get() {
|
||||
ctx.cb_upsert_todays_attempt.run(server_functions::UpsertTodaysAttempt {
|
||||
wall_uid: ctx.wall.read().uid,
|
||||
problem_uid,
|
||||
attempt,
|
||||
});
|
||||
}
|
||||
};
|
||||
attempt_radio_buttons.push(view! { <AttemptRadioButton on:click=onclick variant selected=ui_toggle /> });
|
||||
}
|
||||
|
||||
view! {
|
||||
<AttemptRadio>{attempt_radio_buttons}</AttemptRadio>
|
||||
<Separator />
|
||||
<History user_interaction />
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn AttemptRadio(children: Children) -> impl IntoView {
|
||||
tracing::debug!("Enter");
|
||||
|
||||
view! {
|
||||
<div class="gap-2 flex flex-row justify-evenly md:flex-col 2xl:flex-row">{children()}</div>
|
||||
}
|
||||
view! { <div class="gap-2 flex flex-col justify-evenly 2xl:flex-row">{attempt_radio_buttons}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn AttemptRadioButton(variant: models::Attempt, #[prop(into)] selected: Signal<bool>) -> impl IntoView {
|
||||
tracing::debug!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let text = variant.to_string();
|
||||
let icon = variant.icon();
|
||||
@ -377,13 +399,13 @@ fn AttemptRadioButton(variant: models::Attempt, #[prop(into)] selected: Signal<b
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn History(#[prop(into)] user_interaction: Signal<Option<models::UserInteraction>>) -> impl IntoView {
|
||||
tracing::debug!("Enter");
|
||||
fn History() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let latest_attempt = signals::latest_attempt(user_interaction);
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let attempts = move || {
|
||||
user_interaction
|
||||
ctx.user_interaction
|
||||
.read()
|
||||
.as_ref()
|
||||
.iter()
|
||||
@ -397,7 +419,7 @@ fn History(#[prop(into)] user_interaction: Signal<Option<models::UserInteraction
|
||||
};
|
||||
|
||||
let placeholder = move || {
|
||||
latest_attempt.read().is_none().then(|| {
|
||||
ctx.latest_attempt.read().is_none().then(|| {
|
||||
let today = chrono::Utc::now();
|
||||
view! { <Attempt date=today attempt=None /> }
|
||||
})
|
||||
@ -408,44 +430,42 @@ fn History(#[prop(into)] user_interaction: Signal<Option<models::UserInteraction
|
||||
|
||||
#[component]
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn Grid(
|
||||
wall: models::Wall,
|
||||
#[prop(into)] problem: Signal<Option<models::Problem>>,
|
||||
on_click_hold: impl Fn(models::HoldPosition) + 'static,
|
||||
) -> impl IntoView {
|
||||
tracing::debug!("Enter");
|
||||
fn Wall() -> impl IntoView {
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
let on_click_hold = std::rc::Rc::new(on_click_hold);
|
||||
let ctx = use_context::<Context>().unwrap();
|
||||
|
||||
let mut cells = vec![];
|
||||
for (&hold_position, hold) in &wall.holds {
|
||||
let role = Signal::derive(move || problem.get().and_then(|p| p.holds.get(&hold_position).copied()));
|
||||
move || {
|
||||
let wall = ctx.wall.read();
|
||||
|
||||
let on_click = {
|
||||
let on_click_hold = std::rc::Rc::clone(&on_click_hold);
|
||||
move |_| {
|
||||
on_click_hold(hold_position);
|
||||
}
|
||||
let mut cells = vec![];
|
||||
for (&hold_position, hold) in &wall.holds {
|
||||
let hold_role = signals::hold_role(ctx.problem, hold_position);
|
||||
|
||||
let on_click = move |_| {
|
||||
ctx.cb_click_hold.run(hold_position);
|
||||
};
|
||||
|
||||
let cell = view! {
|
||||
<div class="cursor-pointer">
|
||||
<Hold on:click=on_click role=hold_role hold=hold.clone() />
|
||||
</div>
|
||||
};
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
let style = {
|
||||
let grid_rows = crate::css::grid_rows_n(wall.rows);
|
||||
let grid_cols = crate::css::grid_cols_n(wall.cols);
|
||||
let max_width = format!("{}vh", wall.cols as f64 / wall.rows as f64 * 100.);
|
||||
format!("max-height: 100vh; max-width: {max_width}; {}", [grid_rows, grid_cols].join(" "))
|
||||
};
|
||||
let cell = view! {
|
||||
<div class="cursor-pointer">
|
||||
<Hold on:click=on_click role hold=hold.clone() />
|
||||
</div>
|
||||
};
|
||||
cells.push(cell);
|
||||
}
|
||||
let style = {
|
||||
let grid_rows = crate::css::grid_rows_n(wall.rows);
|
||||
let grid_cols = crate::css::grid_cols_n(wall.cols);
|
||||
format!("max-height: 90vh; max-width: 90vh; {}", [grid_rows, grid_cols].join(" "))
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="grid grid-cols-[auto_1fr]">
|
||||
<div style=style class="grid gap-1">
|
||||
view! {
|
||||
<div style=style class="p-1 grid gap-1">
|
||||
{cells}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -459,10 +479,10 @@ fn Hold(
|
||||
#[prop(into)]
|
||||
role: Option<Signal<Option<HoldRole>>>,
|
||||
) -> impl IntoView {
|
||||
tracing::trace!("Enter");
|
||||
crate::tracing::on_enter!();
|
||||
|
||||
move || {
|
||||
let mut class = "bg-sky-100 aspect-square rounded-sm hover:brightness-125".to_string();
|
||||
let mut class = "bg-sky-100 max-w-full max-h-full aspect-square rounded-sm hover:brightness-125".to_string();
|
||||
if let Some(role) = role {
|
||||
let role = role.get();
|
||||
|
||||
@ -509,6 +529,7 @@ mod signals {
|
||||
use crate::models;
|
||||
use leptos::prelude::*;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> {
|
||||
Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt))
|
||||
@ -518,6 +539,10 @@ mod signals {
|
||||
Signal::derive(move || latest_attempt.read().as_ref().and_then(models::UserInteraction::todays_attempt))
|
||||
}
|
||||
|
||||
pub fn wall_uid(wall: Signal<models::Wall>) -> Signal<models::WallUid> {
|
||||
Signal::derive(move || wall.read().uid)
|
||||
}
|
||||
|
||||
pub fn user_interaction(
|
||||
user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
||||
problem_uid: Signal<Option<models::ProblemUid>>,
|
||||
@ -539,4 +564,24 @@ mod signals {
|
||||
problems.get(&problem_uid).cloned()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn filtered_problems(
|
||||
problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||
) -> Memo<BTreeMap<models::ProblemUid, models::Problem>> {
|
||||
Memo::new(move |_prev_val| {
|
||||
let filter_holds = filter_holds.read();
|
||||
problems.with(|problems| {
|
||||
problems
|
||||
.iter()
|
||||
.filter(|(_, problem)| filter_holds.iter().all(|hold_pos| problem.holds.contains_key(hold_pos)))
|
||||
.map(|(problem_uid, problem)| (*problem_uid, problem.clone()))
|
||||
.collect::<BTreeMap<models::ProblemUid, models::Problem>>()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
|
||||
Signal::derive(move || problem.get().and_then(|p| p.holds.get(&hold_position).copied()))
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,38 @@
|
||||
use super::db::Database;
|
||||
use super::db::DatabaseOperationError;
|
||||
use super::db::{self};
|
||||
use crate::models;
|
||||
use redb::ReadableTable;
|
||||
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if is_at_version(db, 2).await? {
|
||||
migrate_to_v3(db).await?;
|
||||
}
|
||||
|
||||
migrate_wall(db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub async fn migrate_wall(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::warn!("MIGRATING WALL");
|
||||
|
||||
db.write(|txn| {
|
||||
let mut table = txn.open_table(db::current::TABLE_WALLS)?;
|
||||
let wall = table
|
||||
.get(models::WallUid(uuid::Uuid::parse_str("8a00ab39-89f5-4fc5-b9c6-f86b4c040f68").unwrap()))?
|
||||
.map(|x| x.value());
|
||||
if let Some(mut wall) = wall {
|
||||
wall.rows = 11;
|
||||
wall.holds.retain(|k, _| k.row < 11);
|
||||
table.insert(wall.uid, wall)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
22
crates/ascend/src/tracing.rs
Normal file
22
crates/ascend/src/tracing.rs
Normal file
@ -0,0 +1,22 @@
|
||||
macro_rules! where_am_i {
|
||||
() => {{
|
||||
fn f() {}
|
||||
|
||||
fn type_name_of<T>(_: T) -> &'static str {
|
||||
std::any::type_name::<T>()
|
||||
}
|
||||
|
||||
let name = type_name_of(f);
|
||||
|
||||
// `3` is the length of the `::f`.
|
||||
&name[..name.len() - 3]
|
||||
}};
|
||||
}
|
||||
pub(crate) use where_am_i;
|
||||
|
||||
macro_rules! on_enter {
|
||||
() => {
|
||||
tracing::trace!("Entering {}", crate::tracing::where_am_i!());
|
||||
};
|
||||
}
|
||||
pub(crate) use on_enter;
|
Loading…
x
Reference in New Issue
Block a user