This commit is contained in:
2025-02-04 23:32:53 +01:00
parent 73abd96f10
commit 503eeef20e
11 changed files with 250 additions and 233 deletions

View File

@@ -27,7 +27,7 @@ leptos_axum = { version = "0.7", optional = true }
leptos_meta = { version = "0.7" }
leptos_router = { version = "0.7.0" }
moonboard-parser = { workspace = true, optional = true }
rand = { version = "0.8", optional = true }
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
ron = { version = "0.8" }
serde = { version = "1", features = ["derive"] }
server_fn = { version = "0.7.4", features = ["cbor"] }
@@ -58,7 +58,6 @@ ssr = [
"dep:redb",
"dep:bincode",
"dep:tokio",
"dep:rand",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",

View File

@@ -1,3 +1,4 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models;
use crate::pages;
@@ -5,7 +6,6 @@ use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;
use std::sync::Arc;
use type_toppings::ResultExt;
pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags;
@@ -45,9 +45,9 @@ pub fn App() -> impl leptos::IntoView {
<Router>
<Routes fallback=|| "Not found">
<Route path=path!("/") view=Home />
<Route path=path!("/wall/:id") view=pages::wall::Wall />
<Route path=path!("/wall/:id/edit") view=pages::edit_wall::EditWall />
<Route path=path!("/wall/:id/routes") view=pages::routes::Routes />
<Route path=path!("/wall/:wall_uid") view=pages::wall::Wall />
<Route path=path!("/wall/:wall_uid/edit") view=pages::edit_wall::EditWall />
<Route path=path!("/wall/:wall_uid/routes") view=pages::routes::Routes />
</Routes>
</Router>
}
@@ -86,7 +86,11 @@ pub fn Home() -> impl leptos::IntoView {
leptos::view! {}
}
#[server]
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
use redb::ReadableTable;
@@ -105,8 +109,6 @@ async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
tracing::debug!("opened table");
let walls: Vec<models::Wall> = walls_table.iter()?.map(|r| r.map(|(_, v)| v.value())).collect::<Result<_, _>>()?;
tracing::debug!("got walls {walls:?}");
Ok(walls)
})
.await??;

View File

@@ -69,35 +69,6 @@ pub mod ron {
}
}
// impl<T> serde::Serialize for RonEncoded<T>
// where
// T: serde::Serialize,
// {
// #[tracing::instrument(skip_all, err)]
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: serde::Serializer,
// {
// let serialized = ron::to_string(&self.0).map_err(serde::ser::Error::custom)?;
// serializer.serialize_str(&serialized)
// }
// }
// impl<'de, T> serde::Deserialize<'de> for RonEncoded<T>
// where
// T: serde::de::DeserializeOwned + 'static,
// {
// #[tracing::instrument(skip_all, err)]
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// where
// D: serde::Deserializer<'de>,
// {
// let s = String::deserialize(deserializer)?;
// let t: T = ron::from_str(&s).map_err(serde::de::Error::custom)?;
// Ok(Self(t))
// }
// }
// IntoReq
impl<T, Request, Err> IntoReq<Ron, Request, Err> for RonEncoded<T>
where
@@ -151,7 +122,6 @@ pub mod ron {
#[cfg(test)]
mod tests {
use super::Ron;
use super::RonEncoded;
use codee::Decoder;
use codee::Encoder;
use serde::Deserialize;
@@ -163,27 +133,6 @@ pub mod ron {
value: i32,
}
// #[test]
// fn test_ron_wrapper() {
// let original = TestStruct {
// name: "Test".to_string(),
// value: 42,
// };
// // Wrap in RonCodec
// let wrapped = RonEncoded::new(original.clone());
// // Serialize
// let serialized = serde_json::to_string(&wrapped).expect("Serialization failed");
// println!("Serialized: {}", serialized);
// // Deserialize
// let deserialized: RonEncoded<TestStruct> = serde_json::from_str(&serialized).expect("Deserialization failed");
// // Compare
// assert_eq!(deserialized.into_inner(), original);
// }
#[test]
fn test_ron_codec() {
let original = TestStruct {

View File

@@ -4,6 +4,12 @@ use web_sys::MouseEvent;
#[component]
pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) -> () + 'static) -> impl IntoView {
view! {
<button on:click=onclick type="button" class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none">{ text }</button>
<button
on:click=onclick
type="button"
class="text-black bg-orange-300 hover:bg-orange-400 focus:ring-4 focus:ring-orange-500 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
>
{text}
</button>
}
}

View File

@@ -17,8 +17,8 @@ pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
view! {
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
// <div class="container mx-auto" >
<Header items />
// </div>
<Header items />
// </div>
</div>
}
}

View File

@@ -11,6 +11,8 @@ pub mod components {
pub mod codec;
pub mod server_functions;
#[cfg(feature = "ssr")]
pub mod server;

View File

@@ -1,3 +1,4 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
@@ -35,17 +36,17 @@ pub fn EditWall() -> impl leptos::IntoView {
right: vec![],
};
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items />
// leptos::view! {
// <div class="min-w-screen min-h-screen bg-slate-900">
// <StyledHeader items=header_items />
<div class="container mx-auto mt-2">
<Await future=load let:data>
<Ready data=data.deref().to_owned() />
</Await>
</div>
</div>
}
// <div class="container mx-auto mt-2">
// <Await future=load let:data>
// <Ready data=data.deref().to_owned() />
// </Await>
// </div>
// </div>
// }
}
#[component]
@@ -149,7 +150,12 @@ pub struct Image {
file_contents: Vec<u8>,
}
#[server]
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn load_initial_data() -> Result<RonEncoded<InitialData>, ServerFnError> {
todo!()
// let wall = state.persistent.with(|s| s.wall.clone()).await;
@@ -157,7 +163,7 @@ async fn load_initial_data() -> Result<RonEncoded<InitialData>, ServerFnError> {
}
#[server(name = SetImage, input = Cbor)]
#[tracing::instrument(skip(image))]
#[tracing::instrument(skip(image), err)]
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());

View File

@@ -1,20 +1,42 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::models;
use crate::models::WallUid;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;
use std::ops::Deref;
#[derive(Params, PartialEq, Clone)]
struct RouteParams {
wall_uid: Option<models::WallUid>,
}
#[component]
#[tracing::instrument(skip_all)]
pub fn Routes() -> impl leptos::IntoView {
let load = async move {
// TODO: What to do about this unwrap?
load_initial_data().await.unwrap()
};
tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<RouteParams>();
let problems = Resource::<Option<models::Wall>, Ron>::new_with_options(
move || params.get().map(|p| p.wall_uid),
move |wall_uid: Result<Option<WallUid>, _>| async move {
if let Ok(Some(wall_uid)) = wall_uid {
let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner();
Some(wall)
} else {
None
}
},
false,
);
let header_items = HeaderItems {
left: vec![HeaderItem {
@@ -57,27 +79,6 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InitialData {
problems: BTreeSet<models::Problem>,
}
#[server]
async fn load_initial_data() -> Result<RonEncoded<InitialData>, ServerFnError> {
todo!()
// let state = expect_context::<State>();
// let problems = state
// .persistent
// .with(|s| {
// let problems = &s.problems.problems;
// problems.clone()
// })
// .await;
// Ok(RonCodec::new(InitialData { problems }))
}
#[server(name = ImportFromMiniMoonboard)]
#[tracing::instrument]
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {

View File

@@ -1,5 +1,4 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::components::button::Button;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
@@ -11,45 +10,33 @@ use leptos::Params;
use leptos::prelude::*;
use leptos::reactive::graph::ReactiveNode;
use leptos_router::params::Params;
use std::sync::Arc;
use rand::SeedableRng;
#[derive(Params, PartialEq, Clone)]
struct WallParams {
id: Option<models::WallUid>,
struct RouteParams {
wall_uid: Option<models::WallUid>,
}
#[component]
#[tracing::instrument(skip_all)]
pub fn Wall() -> impl leptos::IntoView {
tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<WallParams>();
// TODO
let wall = Resource::<models::Wall, Ron>::new_with_options(
move || params.get().unwrap().id,
{
move |wall_uid: Option<WallUid>| async move {
let wall_uid = wall_uid.unwrap();
let wall = get_wall(wall_uid).await.unwrap().into_inner();
wall
let params = leptos_router::hooks::use_params::<RouteParams>();
let wall = Resource::<Option<models::Wall>, Ron>::new_with_options(
move || params.get().map(|p| p.wall_uid),
move |wall_uid: Result<Option<WallUid>, _>| async move {
if let Ok(Some(wall_uid)) = wall_uid {
let wall = crate::server_functions::get_wall(wall_uid).await.unwrap().into_inner();
Some(wall)
} else {
None
}
},
false,
);
// // TODO
// let wall = Resource::new(
// move || params.get().unwrap().id,
// move |wall_id| async move {
// if let Some(wall_id) = wall_id {
// let wall = get_wall(wall_id).await.unwrap();
// Some(wall)
// } else {
// None
// }
// },
// );
let header_items = HeaderItems {
left: vec![],
middle: vec![HeaderItem {
@@ -69,23 +56,23 @@ pub fn Wall() -> impl leptos::IntoView {
};
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items />
<div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items />
<div class="m-2">
<Suspense fallback=move || view! {<p>"Loading..."</p>}>
{move || Suspend::new(async move{
let wall: Option<Option<RonEncoded<models::Wall>>> = wall.get();
wall.map(|wall|{let wall = wall.unwrap();
view! {
<Ready wall=wall.into_inner() />
}
})
})}
</Suspense>
</div>
<div class="m-2">
<Suspense fallback=move || {
view! { <p>"Loading..."</p> }
}>
{move || Suspend::new(async move {
let wall: Option<models::Wall> = wall.get().flatten();
wall.map(|wall| {
view! { <Ready wall/> }
})
})}
</Suspense>
</div>
}
</div>
}
}
#[component]
@@ -93,14 +80,33 @@ fn Ready(wall: models::Wall) -> impl leptos::IntoView {
tracing::debug!("ready");
let (current_problem, current_problem_writer) = signal(None::<models::Problem>);
let problem_fetcher = LocalResource::new(move || async move {
tracing::info!("Loading random problem");
let problem = get_random_problem().await.expect("cannot get random problem");
if problem.is_none() {
tracing::info!("No problem returned by server in response to request for random problem");
}
current_problem_writer.set(problem.into_inner());
});
let problem_fetcher = {
LocalResource::new(move || {
let wall_uid = wall.uid;
let problems = wall.problems.clone();
async move {
tracing::info!("Loading random problem");
// TODO: seed properly
use rand::seq::IteratorRandom;
let rng = &mut rand::rngs::StdRng::seed_from_u64(0);
let random_problem = problems.iter().choose(rng);
let problem = if let Some(random_problem) = random_problem {
crate::server_functions::get_problem(wall_uid, *random_problem)
.await
.expect("cannot get random problem")
.into_inner()
} else {
tracing::info!("Wall has no problems");
None
};
current_problem_writer.set(problem);
}
})
};
let mut cells = vec![];
for (&hold_position, hold) in &wall.holds {
@@ -117,17 +123,18 @@ fn Ready(wall: models::Wall) -> impl leptos::IntoView {
view! {
<div class="grid grid-cols-[auto,1fr] gap-8">
// Render the wall
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>
{cells}
</div>
<div>
<div>
// TODO:
// <p>{current_problem.read().as_ref().map(|p| p.name.clone())}</p>
// <p>{current_problem.read().as_ref().map(|p| p.set_by.clone())}</p>
<div>// TODO:
// <p>{current_problem.read().as_ref().map(|p| p.name.clone())}</p>
// <p>{current_problem.read().as_ref().map(|p| p.set_by.clone())}</p>
</div>
<Button onclick=move |_| problem_fetcher.mark_dirty() text="➤ Next problem" />
</ div>
</div>
</div>
}
}
@@ -161,74 +168,3 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp
tracing::trace!("view");
view! { <div class=class>{img}</div> }
}
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
tracing::debug!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error {
#[display("Wall not found: {_0:?}")]
NotFound(#[error(not(source))] models::WallUid),
}
let db = expect_context::<Arc<redb::Database>>();
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
let read_txn = db.begin_read()?;
let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?;
let wall = walls_table.get(wall_uid)?.ok_or(Error::NotFound(wall_uid))?.value();
Ok(wall)
})
.await??;
tracing::debug!("ok");
Ok(RonEncoded::new(wall))
}
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_problem(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<RonEncoded<Option<models::Problem>>, ServerFnError> {
tracing::debug!("Enter");
let db = expect_context::<Arc<redb::Database>>();
let problem = tokio::task::spawn_blocking(move || -> Result<Option<models::Problem>, ServerFnError> {
let read_txn = db.begin_read()?;
let table = read_txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
Ok(problem)
})
.await??;
// use rand::seq::IteratorRandom;
// let state = expect_context::<State>();
// let problem = state
// .persistent
// .with(|s| {
// let problems = &s.problems.problems;
// let rng = &mut rand::thread_rng();
// problems.iter().choose(rng).cloned()
// })
// .await;
// tracing::debug!("Returning randomized problem: {problem:?}");
Ok(RonEncoded::new(problem))
}

View File

@@ -0,0 +1,66 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models;
use leptos::prelude::expect_context;
use leptos::server;
use server_fn::ServerFnError;
use std::sync::Arc;
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
tracing::debug!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error {
#[display("Wall not found: {_0:?}")]
NotFound(#[error(not(source))] models::WallUid),
}
let db = expect_context::<Arc<redb::Database>>();
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
let read_txn = db.begin_read()?;
let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?;
let wall = walls_table.get(wall_uid)?.ok_or(Error::NotFound(wall_uid))?.value();
Ok(wall)
})
.await??;
tracing::debug!("ok");
Ok(RonEncoded::new(wall))
}
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
pub(crate) async fn get_problem(
wall_uid: models::WallUid,
problem_uid: models::ProblemUid,
) -> Result<RonEncoded<Option<models::Problem>>, ServerFnError> {
tracing::debug!("Enter");
let db = expect_context::<Arc<redb::Database>>();
let problem = tokio::task::spawn_blocking(move || -> Result<Option<models::Problem>, ServerFnError> {
let read_txn = db.begin_read()?;
let table = read_txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
Ok(problem)
})
.await??;
Ok(RonEncoded::new(problem))
}