Files
ascend/crates/ascend/src/pages/wall.rs
2025-03-04 17:36:13 +01:00

360 lines
17 KiB
Rust

// +--------------- Filter ----------- ↓ -+
// | |
// | |
// | |
// | |
// | |
// | |
// | |
// +--------------------------------------+
//
// +---------------------------+
// | Next Problem |
// +---------------------------+
//
// +--------------- Problem --------------+
// | Name: ... |
// | Method: ... |
// | Set by: ... |
// | |
// | | Flash | Top | Attempt | |
// | |
// +--------------------------------------+
// + ------- + + ------ -+ +---------+
// | Flash | | Top | | Attempt |
// + ------- + + ------ -+ +---------+
//
// +---------- <Latest attempt> ----------+
// | Today: <Attempt> |
// | 14 days ago: <Attempt> |
// +--------------------------------------+
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;
use crate::components::icons::Icon;
use crate::models;
use crate::models::HoldRole;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
#[derive(Params, PartialEq, Clone)]
struct RouteParams {
wall_uid: Option<models::WallUid>,
}
#[component]
#[tracing::instrument(skip_all)]
pub fn Wall() -> impl IntoView {
tracing::debug!("Enter");
let route_params = leptos_router::hooks::use_params::<RouteParams>();
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
let wall_uid = Signal::derive(move || {
route_params
.get()
.expect("gets wall_uid from URL")
.wall_uid
.expect("wall_uid param is never None")
});
let wall = crate::resources::wall_by_uid(wall_uid);
let problem = crate::resources::problem_by_uid(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 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);
}
}
Some(Err(err)) => {
tracing::error!("Error getting wall: {err}");
}
None => {}
});
let ui_is_flash = RwSignal::new(false);
let ui_is_climbed = RwSignal::new(false);
let ui_is_favorite = RwSignal::new(false);
// On reception of user interaction state, set UI signals
Effect::new(move |_prev_value| {
if let Some(user_interaction) = user_interaction.get() {
let user_interaction = user_interaction.ok().flatten();
if let Some(user_interaction) = user_interaction {
ui_is_favorite.set(user_interaction.is_favorite);
} else {
ui_is_favorite.set(false);
}
}
});
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=|| ()>
{move || Suspend::new(async move {
tracing::info!("executing main suspend");
let wall = wall.await?;
let grid = {
let wall = wall.clone();
view! {
<Transition fallback=|| ()>
{
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! {
<Grid wall=wall.clone() problem=problem_sig2 />
};
Ok::<_, ServerFnError>(view)
})
}
}
</Transition>
}
};
let v = view! {
<div class="grid grid-cols-1 gap-8 md:grid-cols-[auto,1fr]">
<div>{grid}</div>
<div>
<Button
icon=Icon::ArrowPath
text="Next problem"
onclick=move |_| fn_next_problem(&wall)
/>
<div class="m-4" />
<Transition fallback=|| ()>
{move || Suspend::new(async move {
tracing::info!("executing probleminfo suspend");
let problem = problem.await?;
let problem_view = problem
.map(|problem| view! { <ProblemInfo problem /> });
let view = view! { {problem_view} };
Ok::<_, ServerFnError>(view)
})}
</Transition>
<div class="m-4" />
<div class="inline-flex overflow-hidden relative justify-center items-center p-0.5 mb-2 text-sm font-medium text-white bg-gradient-to-br from-cyan-500 to-blue-500 rounded-lg me-2 group hover:brightness-125">
<input
type="checkbox"
id="flash-option"
value=""
class="hidden peer"
required=""
bind:checked=ui_is_flash
/>
<label for="flash-option" class="cursor-pointer">
<div
class="flex relative gap-2 items-center py-3.5 px-5 bg-gray-900 rounded-md transition-all duration-75 ease-in"
class:bg-transparent=move || ui_is_flash.get()
>
<div class="text-white bg-white rounded-sm border-gray-500 ring-offset-gray-700 aspect-square">
<span class=("text-cyan-500", move || ui_is_flash.get())>
<icons::Check />
</span>
</div>
// <icons::Bolt />
<div class="w-full text-lg font-semibold">Flash</div>
</div>
</label>
</div>
<div class="inline-flex overflow-hidden relative justify-center items-center p-0.5 mb-2 text-sm font-medium text-white bg-gradient-to-br from-teal-300 to-lime-300 rounded-lg me-2 group hover:brightness-125">
<input
type="checkbox"
id="climbed-option"
value=""
class="hidden peer"
required=""
bind:checked=ui_is_climbed
/>
<label for="climbed-option" class="cursor-pointer">
<div
class="flex relative gap-2 items-center py-3.5 px-5 text-white bg-gray-900 rounded-md transition-all duration-75 ease-in"
class:bg-transparent=move || ui_is_climbed.get()
>
<div class="bg-white rounded-sm border-gray-500 ring-offset-gray-700 aspect-square">
<span class=("text-black", move || ui_is_climbed.get())>
<icons::Check />
</span>
</div>
<div
class="w-full text-lg font-semibold"
class=("text-black", move || ui_is_climbed.get())
>
Climbed
</div>
</div>
</label>
</div>
<div class="inline-flex overflow-hidden relative justify-center items-center p-0.5 mb-2 text-sm font-medium text-white bg-gradient-to-br from-pink-500 to-orange-400 rounded-lg me-2 group hover:brightness-125">
<input
type="checkbox"
id="favorite-option"
value=""
class="hidden peer"
required=""
bind:checked=ui_is_favorite
/>
<label for="favorite-option" class="cursor-pointer">
<div
class="flex relative gap-2 items-center py-3.5 px-5 bg-gray-900 rounded-md transition-all duration-75 ease-in"
class:bg-transparent=move || ui_is_favorite.get()
>
<div class="text-pink-500 rounded-sm border-gray-500 ring-offset-gray-700 aspect-square">
<span class=("text-white", move || ui_is_favorite.get())>
<icons::HeartOutline />
</span>
</div>
<div class="w-full text-lg font-semibold">Favorite</div>
</div>
</label>
</div>
<Suspense fallback=move || {
view! {}
}>
{move || {
let x = 10;
let attempt_suspend = Suspend::new(async move {
let user_interaction = user_interaction.await;
let user_interaction = user_interaction.ok().flatten();
let best_attempt = user_interaction
.and_then(|x| x.best_attempt());
let best_attempt_date = move || {
best_attempt.map(|pair| pair.0)
};
let best_attempt_attempt = move || {
best_attempt.map(|pair| pair.1)
};
view! {
<Attempt attempt=Signal::derive(best_attempt_attempt) />
}
});
attempt_suspend
}}
</Suspense>
</div>
</div>
};
Ok::<_, ServerFnError>(v)
})}
</Transition>
</div>
</div>
}
}
// #[component]
// #[tracing::instrument(skip_all)]
// fn WithWall(#[prop(into)] wall: Signal<models::Wall>) -> impl IntoView {
// tracing::trace!("Enter");
// }
#[component]
#[tracing::instrument(skip_all)]
fn Grid(wall: models::Wall, #[prop(into)] problem: Signal<Result<Option<models::Problem>, ServerFnError>>) -> impl IntoView {
tracing::debug!("Enter");
let mut cells = vec![];
for (&hold_position, hold) in &wall.holds {
let role = move || problem.get().map(|o| o.and_then(|p| p.holds.get(&hold_position).copied()));
let role = Signal::derive(role);
let cell = view! { <Hold role hold=hold.clone() /> };
cells.push(cell);
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-1", wall.rows, wall.cols,);
view! {
<div class="grid grid-cols-[auto,1fr]">
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>
{cells}
</div>
</div>
}
}
// TODO: refactor this to use the Problem component
#[component]
#[tracing::instrument(skip_all)]
fn Hold(hold: models::Hold, role: Signal<Result<Option<HoldRole>, ServerFnError>>) -> impl IntoView {
tracing::trace!("Enter");
move || {
let role = role.get()?;
let class = {
let role_classes = match role {
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"),
Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
None => Some("brightness-50"),
};
let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
if let Some(c) = role_classes {
s.push(' ');
s.push_str(c);
}
s
};
let img = hold.image.as_ref().map(|img| {
let srcset = img.srcset();
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
});
let view = view! { <div class=class>{img}</div> };
Ok::<_, ServerFnError>(view)
}
}