Files
ascend/crates/ascend/src/pages/wall.rs

248 lines
10 KiB
Rust

use crate::codec::ron::RonEncoded;
use crate::components::ProblemInfo;
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::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_action = Action::new(move |&(wall_uid, problem_uid): &(models::WallUid, models::ProblemUid)| async move {
tracing::info!("fetching");
crate::server_functions::get_problem(wall_uid, problem_uid)
.await
.map(RonEncoded::into_inner)
});
let problem_signal = Signal::derive(move || {
let v = problem_action.value().read_only().get();
v.and_then(Result::ok)
});
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| {
problem_action.value().write_only().set(None);
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 => {}
}
});
// On change of problem UID, dispatch an action to fetch the problem
Effect::new(move |_prev_value| match problem_uid.get() {
Some(problem_uid) => {
problem_action.dispatch((wall_uid.get(), problem_uid));
}
None => {
problem_action.value().write_only().set(None);
}
});
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())),
},
],
};
let foo = RwSignal::new(false);
let bar = RwSignal::new(false);
leptos::view! {
<div class="min-w-screen min-h-screen bg-neutral-950">
<StyledHeader items=Signal::derive(header_items) />
<div class="m-2">
<Suspense fallback=move || {
view! { <p>"Loading..."</p> }
}>
{move || Suspend::new(async move {
tracing::info!("executing Suspend future");
let wall = wall.await?;
let v = view! {
<div class="grid grid-cols-1 md:grid-cols-[auto,1fr] gap-8">
<div>
<Grid wall=wall.clone() problem=problem_signal />
</div>
<div>
<Button
onclick=move |_| fn_next_problem(&wall)
// TODO: use forward icon
text="➤ Next problem"
/>
<div class="m-4" />
{move || {
problem_signal
.get()
.map(|problem| view! { <ProblemInfo problem /> })
}}
<div class="m-4" />
<div class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium rounded-lg group bg-gradient-to-br from-cyan-500 to-blue-500 text-white hover:brightness-125">
<input
type="checkbox"
id="flash-option"
value=""
class="hidden peer"
required=""
bind:checked=foo
/>
<label for="flash-option" class="cursor-pointer">
<div
class="flex items-center gap-2 relative px-5 py-3.5 transition-all ease-in duration-75 bg-gray-900 rounded-md"
class:bg-transparent=move || foo.get()
>
<div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500 text-white">
<span class=("text-cyan-500", move || foo.get())>
<icons::Check />
</span>
</div>
// <icons::Bolt />
<div class="w-full text-lg font-semibold">Flash!</div>
</div>
</label>
</div>
<div class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium rounded-lg group bg-gradient-to-br from-teal-300 to-lime-300 text-white hover:brightness-125">
<input
type="checkbox"
id="climbed-option"
value=""
class="hidden peer"
required=""
bind:checked=bar
/>
<label for="climbed-option" class="cursor-pointer">
<div
class="flex items-center gap-2 relative px-5 py-3.5 transition-all ease-in duration-75 bg-gray-900 rounded-md"
class:bg-transparent=move || bar.get()
>
<div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500 text-white">
<span class=("text-teal-300", move || bar.get())>
<icons::Check />
</span>
</div>
<div class="w-full text-lg font-semibold">Climbed!</div>
</div>
</label>
</div>
</div>
</div>
};
Ok::<_, ServerFnError>(v)
})}
</Suspense>
</div>
</div>
}
}
#[component]
#[tracing::instrument(skip_all)]
fn Grid(wall: models::Wall, problem: Signal<Option<models::Problem>>) -> impl IntoView {
tracing::debug!("Enter");
let mut cells = vec![];
for (&hold_position, hold) in &wall.holds {
let role = move || problem.get().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<Option<HoldRole>>) -> impl IntoView {
tracing::trace!("Enter");
let class = move || {
let role_classes = match role.get() {
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"),
// None => None,
};
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.map(|img| {
let srcset = img.srcset();
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
});
tracing::trace!("view");
view! { <div class=class>{img}</div> }
}