layout
This commit is contained in:
parent
09d09159f2
commit
b49c6d475b
89
Cargo.lock
generated
89
Cargo.lock
generated
@ -107,6 +107,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"camino",
|
"camino",
|
||||||
"clap",
|
"clap",
|
||||||
|
"confik",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
@ -130,6 +131,7 @@ dependencies = [
|
|||||||
"type-toppings",
|
"type-toppings",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -314,6 +316,9 @@ name = "camino"
|
|||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@ -433,6 +438,33 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "confik"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fcb80c5315aacb4f2a7ef6ddb6b2fe7818191e78098f5ac597372647b9958039"
|
||||||
|
dependencies = [
|
||||||
|
"camino",
|
||||||
|
"cfg-if",
|
||||||
|
"confik-macros",
|
||||||
|
"envious",
|
||||||
|
"serde",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "confik-macros"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "370f641e4f578df8c52e7c37300d5d9d2b12be6fb6f4732b62a794e2b647d1a2"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console_error_panic_hook"
|
name = "console_error_panic_hook"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@ -490,6 +522,41 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
@ -577,6 +644,16 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "envious"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e52788a407588138195a40c991f500621fea2cffa87e7345d86dbab77287dc7"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1215,6 +1292,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@ -3077,6 +3160,12 @@ version = "0.5.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xdg"
|
||||||
|
version = "2.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xxhash-rust"
|
name = "xxhash-rust"
|
||||||
version = "0.8.15"
|
version = "0.8.15"
|
||||||
|
@ -3,4 +3,3 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/ascend.jpg" alt="Logo" width="100%">
|
<img src="docs/ascend.jpg" alt="Logo" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ ron = { version = "0.8" }
|
|||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
web-sys = { version = "0.3.76", features = ["File", "FileList"] }
|
web-sys = { version = "0.3.76", features = ["File", "FileList"] }
|
||||||
smart-default = "0.7.1"
|
smart-default = "0.7.1"
|
||||||
|
confik = { version = "0.12", optional = true, features = ["camino"] }
|
||||||
|
xdg = { version = "2.5", optional = true }
|
||||||
|
|
||||||
[dev-dependencies.serde_json]
|
[dev-dependencies.serde_json]
|
||||||
version = "1"
|
version = "1"
|
||||||
@ -47,6 +49,8 @@ ssr = [
|
|||||||
"dep:tower",
|
"dep:tower",
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
|
"dep:confik",
|
||||||
|
"dep:xdg",
|
||||||
"dep:camino",
|
"dep:camino",
|
||||||
"dep:moonboard-parser",
|
"dep:moonboard-parser",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
|
9
crates/ascend/src/components/button.rs
Normal file
9
crates/ascend/src/components/button.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,10 @@ pub struct HeaderItem {
|
|||||||
/// Header with background color etc.
|
/// Header with background color etc.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
||||||
view! {
|
let fancy = false;
|
||||||
|
|
||||||
|
if fancy {
|
||||||
|
view! {
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
// Left gradient chunk
|
// Left gradient chunk
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
@ -53,6 +56,17 @@ pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
} else {
|
||||||
|
view! {
|
||||||
|
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
|
||||||
|
<div class="container mx-auto" >
|
||||||
|
<Header items />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +98,7 @@ pub fn Header(items: HeaderItems) -> impl IntoView {
|
|||||||
#[component]
|
#[component]
|
||||||
fn Items(items: Vec<HeaderItem>) -> impl IntoView {
|
fn Items(items: Vec<HeaderItem>) -> impl IntoView {
|
||||||
let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view();
|
let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view();
|
||||||
view! {
|
view! { <div class="flex gap-4">{items}</div> }
|
||||||
<div class="flex gap-4">
|
|
||||||
{ items }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -5,6 +5,7 @@ pub mod pages {
|
|||||||
pub mod wall;
|
pub mod wall;
|
||||||
}
|
}
|
||||||
pub mod components {
|
pub mod components {
|
||||||
|
pub mod button;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ use leptos::prelude::*;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Routes() -> impl leptos::IntoView {
|
pub fn Routes() -> impl leptos::IntoView {
|
||||||
@ -44,11 +43,15 @@ pub fn Routes() -> impl leptos::IntoView {
|
|||||||
fn Ready(data: InitialData) -> impl leptos::IntoView {
|
fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||||
tracing::debug!("ready");
|
tracing::debug!("ready");
|
||||||
|
|
||||||
// let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
|
let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
|
||||||
|
|
||||||
|
let onclick = move |_mouse_event| {
|
||||||
|
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard {});
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// <p>"Import problems from"</p>
|
<p>"Import problems from"</p>
|
||||||
// <button on:click=import_from_mini_moonboard>"Mini Moonboard"</button>
|
<button on:click=onclick>"Mini Moonboard"</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,26 +63,24 @@ async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
|||||||
// use crate::server::state::State;
|
// use crate::server::state::State;
|
||||||
// let state = expect_context::<State>();
|
// let state = expect_context::<State>();
|
||||||
|
|
||||||
|
// TODO: provide info on current routes set
|
||||||
|
|
||||||
Ok(RonCodec::new(InitialData {}))
|
Ok(RonCodec::new(InitialData {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(name = ImportFromMiniMoonboard)]
|
#[server(name = ImportFromMiniMoonboard)]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
|
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
|
||||||
|
use crate::server::config::Config;
|
||||||
|
use crate::server::state::State;
|
||||||
|
|
||||||
tracing::info!("Importing mini moonboard problems");
|
tracing::info!("Importing mini moonboard problems");
|
||||||
|
|
||||||
let file_path: PathBuf = todo!();
|
let config = expect_context::<Config>();
|
||||||
|
|
||||||
let problems = crate::server::import_mini_moonboard_problems(&file_path).await?;
|
|
||||||
|
|
||||||
use crate::server::state::State;
|
|
||||||
let state = expect_context::<State>();
|
let state = expect_context::<State>();
|
||||||
state
|
|
||||||
.persistent
|
|
||||||
.update(|s| {
|
|
||||||
s.problems.problems.extend(problems);
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
|
crate::server::operations::import_mini_moonboard_problems(&config, &state).await?;
|
||||||
|
|
||||||
|
// TODO: Return information about what was done
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::codec::ron::RonCodec;
|
use crate::codec::ron::RonCodec;
|
||||||
|
use crate::components::button::Button;
|
||||||
use crate::components::header::HeaderItem;
|
use crate::components::header::HeaderItem;
|
||||||
use crate::components::header::HeaderItems;
|
use crate::components::header::HeaderItems;
|
||||||
use crate::components::header::StyledHeader;
|
use crate::components::header::StyledHeader;
|
||||||
@ -20,7 +21,7 @@ pub fn Wall() -> impl leptos::IntoView {
|
|||||||
let header_items = HeaderItems {
|
let header_items = HeaderItems {
|
||||||
left: vec![],
|
left: vec![],
|
||||||
middle: vec![HeaderItem {
|
middle: vec![HeaderItem {
|
||||||
text: "Ascend".to_string(),
|
text: "ASCEND".to_string(),
|
||||||
link: None,
|
link: None,
|
||||||
}],
|
}],
|
||||||
right: vec![
|
right: vec![
|
||||||
@ -37,9 +38,9 @@ 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="container mx-auto mt-2">
|
<div class="mx-auto m-2">
|
||||||
<Await future=load let:data>
|
<Await future=load let:data>
|
||||||
<Ready data=data.deref().to_owned() />
|
<Ready data=data.deref().to_owned() />
|
||||||
</Await>
|
</Await>
|
||||||
@ -74,8 +75,14 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
|||||||
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", data.wall.rows, data.wall.cols);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class=move || { grid_classes.clone() }>{cells}</div>
|
<div class="grid grid-cols sm:grid-cols-2 gap-8 grid-cols-[auto,1fr]">
|
||||||
<button on:click=move |_| problem_fetcher.mark_dirty()>"Random problem"</button>
|
// Render the wall
|
||||||
|
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" />
|
||||||
|
</ div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,80 +1,24 @@
|
|||||||
//! Server-only features
|
//! Server-only features
|
||||||
|
|
||||||
use crate::models;
|
use cli::Cli;
|
||||||
use models::HoldPosition;
|
use config::Config;
|
||||||
use models::HoldRole;
|
use confik::Configuration;
|
||||||
use models::Problem;
|
use confik::EnvSource;
|
||||||
use persistence::Persistent;
|
use persistence::Persistent;
|
||||||
use state::PersistentState;
|
use state::PersistentState;
|
||||||
use state::State;
|
use state::State;
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use type_toppings::ResultExt;
|
use type_toppings::ResultExt;
|
||||||
|
|
||||||
pub mod cli {
|
mod cli;
|
||||||
//! Server CLI interface
|
pub mod config;
|
||||||
|
mod migrations;
|
||||||
use std::path::PathBuf;
|
pub mod operations;
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(clap::Subcommand, Default)]
|
|
||||||
pub enum Command {
|
|
||||||
#[default]
|
|
||||||
Serve,
|
|
||||||
|
|
||||||
/// Resets state, replacing it with defaults
|
|
||||||
ResetState,
|
|
||||||
|
|
||||||
ImportMiniMoonboardProblems {
|
|
||||||
file_path: PathBuf,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod state {
|
|
||||||
//! Server state
|
|
||||||
|
|
||||||
const STATE_VERSION: u64 = 1;
|
|
||||||
|
|
||||||
use super::persistence::Persistent;
|
|
||||||
use crate::models;
|
|
||||||
use crate::models::Wall;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use smart_default::SmartDefault;
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct State {
|
|
||||||
pub persistent: Persistent<PersistentState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
|
|
||||||
pub struct PersistentState {
|
|
||||||
/// State schema version
|
|
||||||
#[default(STATE_VERSION)]
|
|
||||||
pub version: u64,
|
|
||||||
|
|
||||||
pub wall: Wall,
|
|
||||||
pub problems: Problems,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
|
||||||
pub struct Problems {
|
|
||||||
pub problems: BTreeSet<models::Problem>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
pub const STATE_FILE: &str = "datastore/private/state.ron";
|
pub const STATE_FILE: &str = "datastore/private/state.ron";
|
||||||
|
|
||||||
@ -96,19 +40,9 @@ pub async fn main() {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve => serve().await.unwrap_or_report(),
|
Command::Serve => serve(cli).await.unwrap_or_report(),
|
||||||
Command::ImportMiniMoonboardProblems { file_path } => {
|
|
||||||
let problems = import_mini_moonboard_problems(&file_path).await.unwrap_or_report();
|
|
||||||
let state = load_state().await.unwrap_or_report();
|
|
||||||
state
|
|
||||||
.persistent
|
|
||||||
.update(|s| {
|
|
||||||
s.problems.problems.extend(problems);
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap_or_report();
|
|
||||||
}
|
|
||||||
Command::ResetState => {
|
Command::ResetState => {
|
||||||
let s = PersistentState::default();
|
let s = PersistentState::default();
|
||||||
let p = Path::new(STATE_FILE);
|
let p = Path::new(STATE_FILE);
|
||||||
@ -118,8 +52,8 @@ pub async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(err)]
|
#[tracing::instrument(skip(cli), err)]
|
||||||
async fn serve() -> Result<(), Error> {
|
async fn serve(cli: Cli) -> Result<(), Error> {
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::app::shell;
|
use crate::app::shell;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
@ -127,25 +61,34 @@ async fn serve() -> Result<(), Error> {
|
|||||||
use leptos_axum::LeptosRoutes;
|
use leptos_axum::LeptosRoutes;
|
||||||
use leptos_axum::generate_route_list;
|
use leptos_axum::generate_route_list;
|
||||||
|
|
||||||
run_migrations().await.map_err(self::Error::Migration)?;
|
migrations::run_migrations().await;
|
||||||
|
|
||||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||||
// For deployment these variables are:
|
// For deployment these variables are:
|
||||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||||
// The file would need to be included with the executable when moved to deployment
|
// The file would need to be included with the executable when moved to deployment
|
||||||
let conf = get_configuration(None).unwrap_or_report();
|
let leptos_conf_file = get_configuration(None).unwrap_or_report();
|
||||||
let leptos_options = conf.leptos_options;
|
let leptos_options = leptos_conf_file.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
|
let config = load_config(cli)?;
|
||||||
let server_state = load_state().await?;
|
let server_state = load_state().await?;
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.clone()), {
|
.leptos_routes_with_context(
|
||||||
let leptos_options = leptos_options.clone();
|
&leptos_options,
|
||||||
move || shell(leptos_options.clone())
|
routes,
|
||||||
})
|
move || {
|
||||||
|
provide_context(server_state.clone());
|
||||||
|
provide_context(config.clone())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let leptos_options = leptos_options.clone();
|
||||||
|
move || shell(leptos_options.clone())
|
||||||
|
},
|
||||||
|
)
|
||||||
.nest_service("/files", file_service("datastore/public"))
|
.nest_service("/files", file_service("datastore/public"))
|
||||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||||
.with_state(leptos_options);
|
.with_state(leptos_options);
|
||||||
@ -179,46 +122,17 @@ async fn load_state() -> Result<State, Error> {
|
|||||||
Ok(State { persistent })
|
Ok(State { persistent })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
fn load_config(cli: Cli) -> Result<Config, Error> {
|
||||||
pub(crate) async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> {
|
let mut builder = config::Config::builder();
|
||||||
let mut problems = Vec::new();
|
if cli
|
||||||
|
.config
|
||||||
tracing::info!("Parsing mini moonboard problems from {}", file_path.display());
|
.try_exists()
|
||||||
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path).await?;
|
.expect_or_report_with(|| format!("Failed to look up config file at {}", cli.config))
|
||||||
for problem in mini_moonboard.problems {
|
|
||||||
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
|
||||||
for mv in problem.moves {
|
|
||||||
let row = mv.description.row();
|
|
||||||
let col = mv.description.column();
|
|
||||||
let hold_position = HoldPosition { row, col };
|
|
||||||
|
|
||||||
let role = match (mv.is_start, mv.is_end) {
|
|
||||||
(true, true) => unreachable!(),
|
|
||||||
(true, false) => HoldRole::Start,
|
|
||||||
(false, true) => HoldRole::End,
|
|
||||||
(false, false) => HoldRole::Normal,
|
|
||||||
};
|
|
||||||
holds.insert(hold_position, role);
|
|
||||||
}
|
|
||||||
let route = Problem { holds };
|
|
||||||
problems.push(route);
|
|
||||||
}
|
|
||||||
Ok(problems)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_migrations() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// State file moved to datastore/private
|
|
||||||
{
|
{
|
||||||
let m = PathBuf::from("state.ron");
|
builder.override_with(confik::FileSource::new(cli.config));
|
||||||
if m.try_exists()? {
|
|
||||||
tracing::warn!("MIGRATING STATE FILE");
|
|
||||||
let p = PathBuf::from(STATE_FILE);
|
|
||||||
tokio::fs::create_dir_all(p.parent().unwrap()).await?;
|
|
||||||
tokio::fs::rename(m, &p).await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let config = builder.override_with(EnvSource::new().allow_secrets()).try_build()?;
|
||||||
Ok(())
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
@ -230,4 +144,6 @@ pub enum Error {
|
|||||||
|
|
||||||
#[display("Failed migration")]
|
#[display("Failed migration")]
|
||||||
Migration(Box<dyn std::error::Error>),
|
Migration(Box<dyn std::error::Error>),
|
||||||
|
|
||||||
|
Confik(confik::Error),
|
||||||
}
|
}
|
||||||
|
33
crates/ascend/src/server/cli.rs
Normal file
33
crates/ascend/src/server/cli.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//! Server CLI interface
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Command,
|
||||||
|
|
||||||
|
/// Path to configuration file.
|
||||||
|
#[arg(long, default_value_t = default_config_location())]
|
||||||
|
pub config: camino::Utf8PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand, Default)]
|
||||||
|
pub enum Command {
|
||||||
|
#[default]
|
||||||
|
Serve,
|
||||||
|
|
||||||
|
/// Resets state, replacing it with defaults
|
||||||
|
ResetState,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_config_location() -> camino::Utf8PathBuf {
|
||||||
|
let xdg_dirs = xdg::BaseDirectories::with_prefix("ascend").unwrap();
|
||||||
|
let config_path = xdg_dirs.get_config_file("config.toml");
|
||||||
|
camino::Utf8PathBuf::from_path_buf(config_path).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn verify_cli() {
|
||||||
|
<Cli as clap::CommandFactory>::command().debug_assert()
|
||||||
|
}
|
7
crates/ascend/src/server/config.rs
Normal file
7
crates/ascend/src/server/config.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, confik::Configuration)]
|
||||||
|
pub struct Config {
|
||||||
|
/// The location of the moonboard problems directory.
|
||||||
|
pub moonboard_problems: Utf8PathBuf,
|
||||||
|
}
|
20
crates/ascend/src/server/migrations.rs
Normal file
20
crates/ascend/src/server/migrations.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use crate::server::STATE_FILE;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use type_toppings::ResultExt;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn run_migrations() {
|
||||||
|
migrate_state_file().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State file moved to datastore/private
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn migrate_state_file() {
|
||||||
|
let m = PathBuf::from("state.ron");
|
||||||
|
if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
|
||||||
|
tracing::warn!("MIGRATING STATE FILE");
|
||||||
|
let p = PathBuf::from(STATE_FILE);
|
||||||
|
tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report();
|
||||||
|
tokio::fs::rename(m, &p).await.unwrap_or_report();
|
||||||
|
}
|
||||||
|
}
|
54
crates/ascend/src/server/operations.rs
Normal file
54
crates/ascend/src/server/operations.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//! Server lib module to host re-usable server operations.
|
||||||
|
|
||||||
|
use crate::models::HoldPosition;
|
||||||
|
use crate::models::HoldRole;
|
||||||
|
use crate::models::Problem;
|
||||||
|
use crate::server::config::Config;
|
||||||
|
use crate::server::persistence;
|
||||||
|
use crate::server::state::State;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state))]
|
||||||
|
pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> {
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
|
||||||
|
let file_name = "problems Mini MoonBoard 2020 40.json";
|
||||||
|
let file_path = config.moonboard_problems.join(file_name);
|
||||||
|
|
||||||
|
tracing::info!("Parsing mini moonboard problems from {file_path}");
|
||||||
|
|
||||||
|
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?;
|
||||||
|
for problem in mini_moonboard.problems {
|
||||||
|
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
||||||
|
for mv in problem.moves {
|
||||||
|
let row = mv.description.row();
|
||||||
|
let col = mv.description.column();
|
||||||
|
let hold_position = HoldPosition { row, col };
|
||||||
|
|
||||||
|
let role = match (mv.is_start, mv.is_end) {
|
||||||
|
(true, true) => unreachable!(),
|
||||||
|
(true, false) => HoldRole::Start,
|
||||||
|
(false, true) => HoldRole::End,
|
||||||
|
(false, false) => HoldRole::Normal,
|
||||||
|
};
|
||||||
|
holds.insert(hold_position, role);
|
||||||
|
}
|
||||||
|
let route = Problem { holds };
|
||||||
|
problems.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
.persistent
|
||||||
|
.update(|s| {
|
||||||
|
s.problems.problems.extend(problems);
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
|
pub enum Error {
|
||||||
|
Parser(moonboard_parser::Error),
|
||||||
|
Persistence(persistence::Error),
|
||||||
|
}
|
31
crates/ascend/src/server/state.rs
Normal file
31
crates/ascend/src/server/state.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//! Server state
|
||||||
|
|
||||||
|
const STATE_VERSION: u64 = 1;
|
||||||
|
|
||||||
|
use super::persistence::Persistent;
|
||||||
|
use crate::models;
|
||||||
|
use crate::models::Wall;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use smart_default::SmartDefault;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
pub persistent: Persistent<PersistentState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
|
||||||
|
pub struct PersistentState {
|
||||||
|
/// State schema version
|
||||||
|
#[default(STATE_VERSION)]
|
||||||
|
pub version: u64,
|
||||||
|
|
||||||
|
pub wall: Wall,
|
||||||
|
pub problems: Problems,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
|
pub struct Problems {
|
||||||
|
pub problems: BTreeSet<models::Problem>,
|
||||||
|
}
|
@ -148,6 +148,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
env.RUST_LOG = "info,ascend=trace";
|
env.RUST_LOG = "info,ascend=trace";
|
||||||
|
env.MOONBOARD_PROBLEMS = "moonboard-problems";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
1
justfile
1
justfile
@ -25,7 +25,6 @@ run-release:
|
|||||||
|
|
||||||
reset-state:
|
reset-state:
|
||||||
cargo run --features ssr -- reset-state
|
cargo run --features ssr -- reset-state
|
||||||
cargo run --features ssr -- import-mini-moonboard-problems "moonboard-problems/problems Mini MoonBoard 2020 40.json"
|
|
||||||
|
|
||||||
# Open firewall port for development
|
# Open firewall port for development
|
||||||
open-firewall:
|
open-firewall:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user