248 lines
10 KiB
Rust
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> }
|
|
}
|