feat: prepare to edit wall

This commit is contained in:
Asger Juul Brunshøj 2025-01-15 22:54:20 +01:00
parent 5f9ffcde27
commit 4daec21e2d
8 changed files with 148 additions and 68 deletions

1
Cargo.lock generated
View File

@ -108,6 +108,7 @@ dependencies = [
"leptos_meta",
"leptos_router",
"moonboard-parser",
"rand",
"ron",
"serde",
"serde_json",

View File

@ -32,6 +32,7 @@ tracing-subscriber = { version = "0.3.18", features = [
"env-filter",
], optional = true }
ron = { version = "0.8" }
rand = { version = "0.8", optional = true }
[dev-dependencies.serde_json]
version = "1"
@ -41,6 +42,7 @@ hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:rand",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",

View File

@ -1,4 +1,8 @@
use crate::pages::edit_wall::EditWall;
use crate::pages::wall::Wall;
use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;
pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags;
@ -13,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
<HydrationScripts options />
<MetaTags />
</head>
<body>
<body class="bg-slate-950">
<App />
</body>
</html>
@ -35,18 +39,20 @@ pub fn App() -> impl leptos::IntoView {
leptos::view! {
<Stylesheet id="leptos" href="/pkg/ascend.css" />
// sets the document title
<Title text="Ascend" />
<main>
<Ascend />
</main>
<Router>
// <nav class="shadow-md mb-2 bg-white border-gray-200 px-4 lg:px-6 py-2.5">
// <div class="flex flex-wrap justify-start items-center gap-4 max-w-screen-xl">
// <HeaderItem text="Home" link="/"/>
// </div>
// </nav>
<main>
<Routes fallback=|| "Not found">
<Route path=path!("/wall") view=Wall />
<Route path=path!("/wall/edit") view=EditWall/>
</Routes>
</main>
</Router>
}
}
#[component]
fn Ascend() -> impl leptos::IntoView {
use crate::pages::wall::Wall;
leptos::view! { <Wall /> }
}

View File

@ -0,0 +1,6 @@
use leptos::prelude::*;
#[component]
pub fn Header() -> impl leptos::IntoView {
leptos::view! { <div>"header"</div> }
}

View File

@ -1,8 +1,11 @@
pub mod app;
pub mod pages {
pub mod edit_wall;
pub mod wall;
}
pub mod components {}
pub mod components {
pub mod header;
}
pub mod codec;

View File

@ -0,0 +1,69 @@
use crate::components::header::Header;
use crate::models::HoldPosition;
use crate::models::HoldRole;
use crate::models::Wall;
use leptos::prelude::*;
use serde::Deserialize;
use serde::Serialize;
#[component]
pub fn EditWall() -> impl leptos::IntoView {
let load = async move {
// TODO: What to do about this unwrap?
load_initial_data().await.unwrap()
};
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<div class="container mx-auto">
<Header />
<Await future=load let:data>
<Ready data=data.to_owned() />
</Await>
</div>
</div>
}
}
#[component]
fn Ready(data: InitialData) -> impl leptos::IntoView {
leptos::logging::log!("ready");
let mut hold_positions = vec![];
for row in 0..(data.wall.rows) {
for col in 0..(data.wall.cols) {
hold_positions.push(HoldPosition { row, col });
}
}
let mut cells = vec![];
for &_hold_position in &hold_positions {
let cell = view! { <Hold /> };
cells.push(cell);
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-4", data.wall.rows, data.wall.cols);
view! {
<div class=move || { grid_classes.clone() }>{cells}</div>
}
}
#[component]
fn Hold() -> impl leptos::IntoView {
view! { <div class="bg-indigo-100 aspect-square rounded"></div> }
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InitialData {
wall: Wall,
}
#[server]
async fn load_initial_data() -> Result<InitialData, ServerFnError> {
use crate::server::state::State;
let state = expect_context::<State>();
let wall = state.persistent.with(|s| s.wall.clone()).await;
Ok(InitialData { wall })
}

View File

@ -1,8 +1,10 @@
use crate::codec::ron::RonCodec;
use crate::components::header::Header;
use crate::models;
use crate::models::HoldPosition;
use crate::models::HoldRole;
use leptos::prelude::*;
use leptos::reactive::graph::ReactiveNode;
use serde::Deserialize;
use serde::Serialize;
@ -14,14 +16,19 @@ pub fn Wall() -> impl leptos::IntoView {
};
leptos::view! {
<Await future=load let:data>
<WallReady data=data.to_owned() />
</Await>
<div class="min-w-screen min-h-screen bg-slate-900">
<div class="container mx-auto">
<Header />
<Await future=load let:data>
<Ready data=data.to_owned() />
</Await>
</div>
</div>
}
}
#[component]
fn WallReady(data: InitialData) -> impl leptos::IntoView {
fn Ready(data: InitialData) -> impl leptos::IntoView {
let mut hold_positions = vec![];
for row in 0..(data.wall.rows) {
for col in 0..(data.wall.cols) {
@ -30,7 +37,7 @@ fn WallReady(data: InitialData) -> impl leptos::IntoView {
}
let (current_problem, current_problem_writer) = signal(None::<models::Problem>);
LocalResource::new(move || async move {
let problem_fetcher = LocalResource::new(move || async move {
leptos::logging::log!("Loading random problem");
let problem = get_random_problem().await.expect("cannot get random problem");
current_problem_writer.set(Some(problem.into_inner()));
@ -38,40 +45,32 @@ fn WallReady(data: InitialData) -> impl leptos::IntoView {
let mut cells = vec![];
for &hold_position in &hold_positions {
let role = move || {
let x = current_problem
.get()
.map(|problem| {
let role = problem.holds.get(&hold_position)?;
// leptos::logging::log!("{hold_position:?}: {role:?}");
Some(*role)
})
.flatten();
x
};
let role = move || current_problem.get().map(|problem| problem.holds.get(&hold_position).copied()).flatten();
let role = Signal::derive(role);
let cell = view! { <Cell role/> };
let cell = view! { <Hold role /> };
cells.push(cell);
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-4", data.wall.rows, data.wall.cols);
view! {
<div class="container mx-auto border">
<div class="grid grid-rows-4 grid-cols-12 gap-4">{cells}</div>
</div>
<div class=move || { grid_classes.clone() }>{cells}</div>
<button on:click=move |_| problem_fetcher.mark_dirty()>"Random problem"</button>
}
}
#[component]
fn Cell(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
let classes = move || {
let role_classes = role.get().map(|role| match role {
HoldRole::Start => "outline outline-offset-2 outline-green-500",
HoldRole::Normal => "outline outline-offset-2 outline-blue-500",
HoldRole::Zone => "outline outline-offset-2 outline-amber-500",
HoldRole::End => "outline outline-offset-2 outline-red-500",
});
let mut s = "aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100".to_string();
fn Hold(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
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"),
};
let mut s = "bg-indigo-100 aspect-square rounded".to_string();
if let Some(c) = role_classes {
s.push_str(" ");
s.push_str(c);
@ -79,29 +78,7 @@ fn Cell(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
s
};
// let is_start = move || matches!(role.get(), Some(HoldRole::Start));
// let is_normal = move || matches!(role.get(), Some(HoldRole::Normal));
// let is_end = move || matches!(role.get(), Some(HoldRole::End));
// let classes_start = "outline outline-offset-2 outline-green-500";
view! {
<div
class=move || classes()
// class="aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100"
// class=move || (is_start().then_some("outline outline-offset-2 outline-green-500"))
// class=(["outline", "outline-offset-2", "outline-green-500"], is_start)
// class=(["outline", "outline-offset-2", "outline-blue-500"], is_normal)
// class=(["outline", "outline-offset-2", "outline-red-500"], is_end)
>
"o"
</div>
}
// view! {
// {format!("{:?}", role)}
// }
view! { <div class=class></div> }
}
#[derive(Serialize, Deserialize, Clone)]
@ -122,11 +99,18 @@ async fn load_initial_data() -> Result<InitialData, ServerFnError> {
#[server]
async fn get_random_problem() -> Result<RonCodec<models::Problem>, ServerFnError> {
use crate::server::state::State;
use rand::seq::IteratorRandom;
let state = expect_context::<State>();
// TODO: Actually randomize
let problem = state.persistent.with(|s| s.problems.problems.iter().next().unwrap().clone()).await;
let problem = state
.persistent
.with(|s| {
let problems = &s.problems.problems;
let rng = &mut rand::thread_rng();
problems.iter().choose(rng).unwrap().clone()
})
.await;
tracing::debug!("Returning randomized problem: {problem:?}");

View File

@ -4,9 +4,18 @@
relative: true,
files: ["*.html", "./src/**/*.rs"],
},
// https://tailwindcss.com/docs/content-configuration#using-regular-expressions
safelist: [
{
pattern: /grid-cols-.+/,
},
{
pattern: /grid-rows-.+/,
},
],
theme: {
extend: {},
},
plugins: [],
}