feat: refactor to controller component and redesign

This commit is contained in:
Asger Juul Brunshøj 2025-03-26 16:26:14 +01:00
parent 58698a1087
commit e403be8090
11 changed files with 390 additions and 282 deletions

View File

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

View 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>
}
}

View 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>
}
}

View File

@ -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(' ');

View File

@ -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",
}
}
}

View File

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

View File

@ -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())
}

View File

@ -0,0 +1,4 @@
pub mod edit_wall;
pub mod routes;
pub mod settings;
pub mod wall;

View File

@ -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,145 +38,228 @@ 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) />
<div class="m-2">
<Transition fallback=|| ()>
<Suspense fallback=|| {
"loading"
}>
{move || Suspend::new(async move {
tracing::info!("executing main suspend");
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);
let v = view! { <WithWall wall problems user_interactions /> };
Ok::<_, ServerFnError>(v)
Ok::<_, ServerFnError>(view! { <Controller wall problems user_interactions /> })
})}
</Transition>
</div>
</Suspense>
</div>
}
}
#[component]
#[tracing::instrument(skip_all)]
fn WithProblem(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
tracing::trace!("Enter");
view! { <ProblemInfo problem /> }
#[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 WithWall(
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 {
tracing::trace!("Enter");
let wall_uid = Signal::derive(move || wall.read().uid);
crate::tracing::on_enter!();
// Extract data from URL
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
let submit_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
// 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)) = submit_attempt.value().get() {
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);
});
}
});
let submit_attempt_cb = StoredValue::new(move |attempt: server_functions::UpsertTodaysAttempt| {
submit_attempt.dispatch(RonEncoded(attempt));
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(),
});
let problem = signals::problem(problems, problem_uid.into());
let user_interaction = signals::user_interaction(user_interactions.into(), problem_uid.into());
view! { <View /> }
}
// 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);
});
};
#[component]
#[tracing::instrument(skip_all)]
fn View() -> impl IntoView {
crate::tracing::on_enter!();
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 ctx = use_context::<Context>().unwrap();
let fn_next_problem = move || {
let problems = filtered_problems.read();
view! {
<div class="flex">
<div class="flex-initial">
<Wall />
</div>
use rand::seq::IteratorRandom;
let mut rng = rand::rng();
let problem_uid = problems.keys().copied().choose(&mut rng);
<div class="flex flex-col" style="width:38rem">
<Section title="Filter">
<Filter />
</Section>
set_problem_uid.set(problem_uid);
};
<Separator />
// 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();
<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>
}
});
}
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);
#[component]
#[tracing::instrument(skip_all)]
fn HoldsButton() -> impl IntoView {
crate::tracing::on_enter!();
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>
}
});
};
}
let grid = move || {
let wall = wall.get();
view! { <Grid wall problem on_click_hold /> }
};
#[component]
#[tracing::instrument(skip_all)]
fn NextProblemButton() -> impl IntoView {
crate::tracing::on_enter!();
let filter = move || {
let ctx = use_context::<Context>().unwrap();
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>
}
}
#[component]
#[tracing::instrument(skip_all)]
fn Filter() -> impl IntoView {
crate::tracing::on_enter!();
let ctx = use_context::<Context>().unwrap();
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(),
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();
move || {
let wall = ctx.wall.read();
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()));
let hold_role = signals::hold_role(ctx.problem, hold_position);
let on_click = {
let on_click_hold = std::rc::Rc::clone(&on_click_hold);
move |_| {
on_click_hold(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=hold.clone() />
<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);
format!("max-height: 90vh; max-width: 90vh; {}", [grid_rows, grid_cols].join(" "))
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(" "))
};
view! {
<div class="grid grid-cols-[auto_1fr]">
<div style=style class="grid gap-1">
<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()))
}
}

View File

@ -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(())
}

View 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;