This commit is contained in:
2025-02-04 01:17:27 +01:00
parent 83ad4ca784
commit 51ada6c9bd
8 changed files with 134 additions and 53 deletions

View File

@@ -15,7 +15,12 @@ chrono = { version = "0.4.39", features = ["now", "serde"] }
clap = { version = "4.5.7", features = ["derive"] } clap = { version = "4.5.7", features = ["derive"] }
confik = { version = "0.12", optional = true, features = ["camino"] } confik = { version = "0.12", optional = true, features = ["camino"] }
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
derive_more = { version = "1", features = ["display", "error", "from"] } derive_more = { version = "1", features = [
"display",
"error",
"from",
"from_str",
] }
http = "1" http = "1"
leptos = { version = "0.7.4", features = ["tracing"] } leptos = { version = "0.7.4", features = ["tracing"] }
leptos_axum = { version = "0.7", optional = true } leptos_axum = { version = "0.7", optional = true }

View File

@@ -1,7 +1,10 @@
use crate::codec::ron::RonCodec;
use crate::models;
use crate::pages; use crate::pages;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_router::components::*; use leptos_router::components::*;
use leptos_router::path; use leptos_router::path;
use std::sync::Arc;
pub fn shell(options: LeptosOptions) -> impl IntoView { pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags; use leptos_meta::MetaTags;
@@ -39,13 +42,67 @@ pub fn App() -> impl leptos::IntoView {
<Title text="Ascend" /> <Title text="Ascend" />
<Router> <Router>
<main> <Routes fallback=|| "Not found">
<Routes fallback=|| "Not found"> <Route path=path!("/") view=Home />
<Route path=path!("/") view=pages::wall::Wall /> <Route path=path!("/wall/:id") view=pages::wall::Wall />
<Route path=path!("/wall/edit") view=pages::edit_wall::EditWall /> <Route path=path!("/wall/:id/edit") view=pages::edit_wall::EditWall />
<Route path=path!("/wall/routes") view=pages::routes::Routes /> <Route path=path!("/wall/:id/routes") view=pages::routes::Routes />
</Routes> </Routes>
</main>
</Router> </Router>
} }
} }
#[component]
pub fn Home() -> impl leptos::IntoView {
// TODO: show cards with walls, and a "new wall" button
tracing::debug!("Rendering home component");
let action = Action::new(|()| async move {
tracing::debug!("running action");
let walls = get_walls().await.unwrap().into_inner();
let wall = walls.first();
if let Some(wall) = wall {
let navigate = leptos_router::hooks::use_navigate();
let url = format!("/wall/{}", wall.uid);
navigate(&url, Default::default());
}
});
tracing::debug!("dispatching action...");
action.dispatch(());
tracing::debug!("dispatched action");
leptos::view! {}
}
#[server]
#[tracing::instrument(skip_all, err)]
async fn get_walls() -> Result<RonCodec<Vec<models::Wall>>, ServerFnError> {
use redb::ReadableTable;
tracing::debug!("get walls");
let db = expect_context::<Arc<redb::Database>>();
tracing::debug!("got db from context");
let walls = tokio::task::spawn_blocking(move || -> Result<Vec<models::Wall>, ServerFnError> {
tracing::debug!("beginning read transaction");
let read_txn = db.begin_read()?;
tracing::debug!("opening table");
let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?;
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??;
Ok(RonCodec::new(walls))
}

View File

@@ -6,10 +6,10 @@ pub use v1::HoldRole;
pub use v1::Image; pub use v1::Image;
pub use v2::Method; pub use v2::Method;
pub use v2::Problem; pub use v2::Problem;
pub use v2::ProblemId; pub use v2::ProblemUid;
pub use v2::Root; pub use v2::Root;
pub use v2::Wall; pub use v2::Wall;
pub use v2::WallId; pub use v2::WallUid;
pub mod v2 { pub mod v2 {
use super::v1; use super::v1;
@@ -20,21 +20,21 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Root { pub struct Root {
pub walls: BTreeSet<WallId>, pub walls: BTreeSet<WallUid>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Wall { pub struct Wall {
pub uid: WallId, pub uid: WallUid,
pub rows: u64, pub rows: u64,
pub cols: u64, pub cols: u64,
pub holds: BTreeMap<v1::HoldPosition, v1::Hold>, pub holds: BTreeMap<v1::HoldPosition, v1::Hold>,
pub problems: BTreeSet<ProblemId>, pub problems: BTreeSet<ProblemUid>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, derive_more::FromStr, derive_more::Display)]
pub struct WallId(pub uuid::Uuid); pub struct WallUid(pub uuid::Uuid);
impl WallId { impl WallUid {
pub fn new() -> Self { pub fn new() -> Self {
Self(uuid::Uuid::new_v4()) Self(uuid::Uuid::new_v4())
} }
@@ -42,7 +42,7 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Problem { pub struct Problem {
pub uid: ProblemId, pub uid: ProblemUid,
pub name: String, pub name: String,
pub set_by: String, pub set_by: String,
pub holds: BTreeMap<v1::HoldPosition, v1::HoldRole>, pub holds: BTreeMap<v1::HoldPosition, v1::HoldRole>,
@@ -51,8 +51,8 @@ pub mod v2 {
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct ProblemId(pub uuid::Uuid); pub struct ProblemUid(pub uuid::Uuid);
impl ProblemId { impl ProblemUid {
pub fn new() -> Self { pub fn new() -> Self {
Self(uuid::Uuid::new_v4()) Self(uuid::Uuid::new_v4())
} }

View File

@@ -5,19 +5,32 @@ use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader; use crate::components::header::StyledHeader;
use crate::models; use crate::models;
use crate::models::HoldRole; use crate::models::HoldRole;
use leptos::Params;
use leptos::prelude::*; use leptos::prelude::*;
use leptos::reactive::graph::ReactiveNode; use leptos::reactive::graph::ReactiveNode;
use serde::Deserialize; use leptos_router::params::Params;
use serde::Serialize;
use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
#[derive(Params, PartialEq, Clone)]
struct WallParams {
id: Option<models::WallUid>,
}
#[component] #[component]
pub fn Wall() -> impl leptos::IntoView { pub fn Wall() -> impl leptos::IntoView {
let load = async move { let params = leptos_router::hooks::use_params::<WallParams>();
// TODO: What to do about this unwrap?
load_initial_data().await.unwrap() 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().into_inner();
Some(wall)
} else {
None
}
},
);
let header_items = HeaderItems { let header_items = HeaderItems {
left: vec![], left: vec![],
@@ -38,20 +51,27 @@ pub fn Wall() -> impl leptos::IntoView {
}; };
leptos::view! { leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900"> <div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items /> <StyledHeader items=header_items />
<div class="m-2"> <div class="m-2">
<Await future=load let:data> <Suspense fallback=move || view! {<p>"Loading..."</p>}>
<Ready data=data.deref().to_owned() /> {move || Suspend::new(async move{
</Await> let wall: Option<Option<models::Wall>> = wall.get();
wall.map(|wall|{let wall = wall.unwrap();
view! {
<Ready wall />
}
})
})}
</Suspense>
</div>
</div> </div>
</div> }
}
} }
#[component] #[component]
fn Ready(data: InitialData) -> impl leptos::IntoView { fn Ready(wall: models::Wall) -> impl leptos::IntoView {
tracing::debug!("ready"); tracing::debug!("ready");
let (current_problem, current_problem_writer) = signal(None::<models::Problem>); let (current_problem, current_problem_writer) = signal(None::<models::Problem>);
@@ -65,7 +85,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
}); });
let mut cells = vec![]; let mut cells = vec![];
for (&hold_position, hold) in &data.wall.holds { for (&hold_position, hold) in &wall.holds {
let role = move || current_problem.get().and_then(|problem| problem.holds.get(&hold_position).copied()); let role = move || current_problem.get().and_then(|problem| problem.holds.get(&hold_position).copied());
let role = Signal::derive(role); let role = Signal::derive(role);
@@ -73,7 +93,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
cells.push(cell); cells.push(cell);
} }
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols); let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols);
view! { view! {
<div class="grid grid-cols-[auto,1fr] gap-8"> <div class="grid grid-cols-[auto,1fr] gap-8">
@@ -120,20 +140,15 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp
view! { <div class=class>{img}</div> } view! { <div class=class>{img}</div> }
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct InitialData {
wall: models::Wall,
}
#[server] #[server]
#[tracing::instrument(skip_all, err)] #[tracing::instrument(skip_all, err)]
async fn load_initial_data(wall_id: models::WallId) -> Result<RonCodec<InitialData>, ServerFnError> { async fn get_wall(wall_id: models::WallUid) -> Result<RonCodec<models::Wall>, ServerFnError> {
let db = expect_context::<Arc<redb::Database>>(); let db = expect_context::<Arc<redb::Database>>();
#[derive(Debug, derive_more::Error, derive_more::Display)] #[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error { enum Error {
#[display("Wall not found: {_0:?}")] #[display("Wall not found: {_0:?}")]
NotFound(#[error(not(source))] models::WallId), NotFound(#[error(not(source))] models::WallUid),
} }
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> { let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
@@ -146,13 +161,14 @@ async fn load_initial_data(wall_id: models::WallId) -> Result<RonCodec<InitialDa
}) })
.await??; .await??;
Ok(RonCodec::new(InitialData { wall })) Ok(RonCodec::new(wall))
} }
#[server] #[server]
#[tracing::instrument(skip_all, err)] #[tracing::instrument(skip_all, err)]
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> { async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
todo!() Ok(RonCodec::new(None))
// use rand::seq::IteratorRandom; // use rand::seq::IteratorRandom;
// let state = expect_context::<State>(); // let state = expect_context::<State>();

View File

@@ -73,8 +73,8 @@ async fn serve(cli: Cli) -> Result<(), Error> {
&leptos_options, &leptos_options,
routes, routes,
move || { move || {
leptos::prelude::provide_context(Arc::clone(&db)); leptos::prelude::provide_context::<Arc<redb::Database>>(Arc::clone(&db));
leptos::prelude::provide_context(config.clone()) leptos::prelude::provide_context::<Config>(config.clone())
}, },
{ {
let leptos_options = leptos_options.clone(); let leptos_options = leptos_options.clone();

View File

@@ -51,8 +51,8 @@ pub mod v2 {
pub const VERSION: u64 = 2; pub const VERSION: u64 = 2;
pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root"); pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root");
pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallId>, Bincode<models::v2::Wall>> = TableDefinition::new("walls"); pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v2::Wall>> = TableDefinition::new("walls");
pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallId, models::v2::ProblemId)>, Bincode<models::v2::Problem>> = pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v2::Problem>> =
TableDefinition::new("problems"); TableDefinition::new("problems");
} }

View File

@@ -110,6 +110,7 @@ async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>>
let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?; let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?;
let root_v1 = root_table_v1.get(())?.unwrap().value(); let root_v1 = root_table_v1.get(())?.unwrap().value();
drop(root_table_v1);
txn.delete_table(db::v1::TABLE_ROOT)?; txn.delete_table(db::v1::TABLE_ROOT)?;
let models::v1::PersistentState { version: _, wall, problems } = root_v1; let models::v1::PersistentState { version: _, wall, problems } = root_v1;
@@ -118,7 +119,7 @@ async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>>
drop(problems); drop(problems);
let mut walls = BTreeSet::new(); let mut walls = BTreeSet::new();
let wall_uid = models::v2::WallId(uuid::Uuid::new_v4()); let wall_uid = models::v2::WallUid(uuid::Uuid::new_v4());
let holds = wall let holds = wall
.holds .holds
.into_iter() .into_iter()
@@ -152,9 +153,11 @@ async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>>
let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?; let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?;
root_table_v2.insert((), root_v2)?; root_table_v2.insert((), root_v2)?;
drop(root_table_v2);
let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?; let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?;
walls_table.insert(wall_v2.uid, wall_v2)?; walls_table.insert(wall_v2.uid, wall_v2)?;
drop(walls_table);
} }
} }
txn.commit()?; txn.commit()?;

View File

@@ -11,7 +11,7 @@ use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Database>, wall_id: models::WallId) -> Result<(), Error> { pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Database>, wall_id: models::WallUid) -> Result<(), Error> {
use moonboard_parser::mini_moonboard; use moonboard_parser::mini_moonboard;
let mut problems = Vec::new(); let mut problems = Vec::new();
@@ -48,7 +48,7 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Data
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard, mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
}; };
let problem_id = models::ProblemId::new(); let problem_id = models::ProblemUid::new();
let problem = models::Problem { let problem = models::Problem {
uid: problem_id, uid: problem_id,