wip: parse moonboard
This commit is contained in:
111
crates/ascend/Cargo.toml
Normal file
111
crates/ascend/Cargo.toml
Normal file
@@ -0,0 +1,111 @@
|
||||
[package]
|
||||
name = "ascend"
|
||||
version = "0.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { version = "0.7.3" }
|
||||
leptos_axum = { version = "0.7", optional = true }
|
||||
leptos_meta = { version = "0.7" }
|
||||
leptos_router = { version = "0.7.0" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
wasm-bindgen = "=0.2.99"
|
||||
http = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
derive_more = { version = "1", features = ["display", "error", "from"] }
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
camino = { version = "1.1", optional = true }
|
||||
type-toppings = { version = "0.2.1", features = ["result"] }
|
||||
|
||||
# Tracing
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tracing-subscriber = { version = "0.3.18", features = [
|
||||
"env-filter",
|
||||
], optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tokio",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:leptos_axum",
|
||||
"dep:camino",
|
||||
"dep:serde_json",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
|
||||
"tracing",
|
||||
]
|
||||
tracing = ["leptos/tracing", "dep:tracing", "dep:tracing-subscriber"]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "ascend"
|
||||
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
|
||||
tailwind-input-file = "style/tailwind.css"
|
||||
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "style/main.scss"
|
||||
# Assets source dir. All files found here will be copied and synchronized to site-root.
|
||||
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
|
||||
#
|
||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||
assets-dir = "public"
|
||||
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
# The profile to use for the lib target when compiling for release
|
||||
#
|
||||
# Optional. Defaults to "release".
|
||||
lib-profile-release = "wasm-release"
|
||||
BIN
crates/ascend/public/favicon.ico
Normal file
BIN
crates/ascend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
49
crates/ascend/src/app.rs
Normal file
49
crates/ascend/src/app.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
use leptos_meta::MetaTags;
|
||||
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options />
|
||||
<MetaTags />
|
||||
</head>
|
||||
<body>
|
||||
<App />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[leptos::component]
|
||||
pub fn App() -> impl leptos::IntoView {
|
||||
use leptos_meta::Stylesheet;
|
||||
use leptos_meta::Title;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
tracing::debug!("Rendering root component");
|
||||
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
leptos_meta::provide_meta_context();
|
||||
|
||||
leptos::view! {
|
||||
<Stylesheet id="leptos" href="/pkg/ascend.css" />
|
||||
|
||||
// sets the document title
|
||||
<Title text="Ascend" />
|
||||
|
||||
<main>
|
||||
<Ascend />
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[leptos::component]
|
||||
fn Ascend() -> impl leptos::IntoView {
|
||||
leptos::view! { <crate::pages::wall::Wall /> }
|
||||
}
|
||||
16
crates/ascend/src/lib.rs
Normal file
16
crates/ascend/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
pub mod app;
|
||||
pub mod pages {
|
||||
pub mod wall;
|
||||
}
|
||||
pub mod components {}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod server;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::*;
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
15
crates/ascend/src/main.rs
Normal file
15
crates/ascend/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
/// Server-side main function
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
ascend::server::main().await
|
||||
}
|
||||
|
||||
/// Client-side main function
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for a purely client-side app
|
||||
// see lib.rs for hydration function instead
|
||||
eprintln!("Main function is empty. Run with the \"ssr\" feature.");
|
||||
}
|
||||
21
crates/ascend/src/pages/wall.rs
Normal file
21
crates/ascend/src/pages/wall.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[leptos::component]
|
||||
pub fn Wall() -> impl leptos::IntoView {
|
||||
let cells = (1..=(12 * 12))
|
||||
.map(|i| {
|
||||
let i = i.to_string();
|
||||
view! {
|
||||
<div class="aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100">
|
||||
{i}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
leptos::view! {
|
||||
<div class="container mx-auto border">
|
||||
<div class="grid grid-rows-4 grid-cols-12 gap-4">{cells}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
178
crates/ascend/src/server.rs
Normal file
178
crates/ascend/src/server.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
//! Server-only features
|
||||
|
||||
use persistence::Persistent;
|
||||
use state::PersistentState;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use type_toppings::ResultExt;
|
||||
|
||||
pub mod cli {
|
||||
//! Server CLI interface
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod state {
|
||||
//! Server state
|
||||
|
||||
use super::persistence::Persistent;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct State {
|
||||
pub persistent: Persistent<PersistentState>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct PersistentState {
|
||||
pub wall: Wall,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct Wall {
|
||||
pub rows: u32,
|
||||
pub cols: u32,
|
||||
pub holds: BTreeMap<HoldPosition, Hold>,
|
||||
pub routes: BTreeSet<route::Route>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct HoldPosition {
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct Hold {
|
||||
pub position: HoldPosition,
|
||||
pub image: Option<Image>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct Image {}
|
||||
|
||||
mod route {
|
||||
use super::HoldPosition;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Route {
|
||||
pub holds: BTreeMap<HoldPosition, HoldRole>,
|
||||
}
|
||||
|
||||
/// The role of a hold on a route
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum HoldRole {
|
||||
/// Start hold
|
||||
Start,
|
||||
|
||||
/// Any hold on the route without a specific role
|
||||
Normal,
|
||||
|
||||
/// Zone hold
|
||||
Zone,
|
||||
|
||||
/// End hold
|
||||
End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod persistence;
|
||||
|
||||
pub const STATE_FILE: &str = "state.json";
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn main() {
|
||||
use crate::server::cli::Cli;
|
||||
use crate::server::cli::Command;
|
||||
use clap::Parser as _;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.without_time()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_target(false)
|
||||
.with_ansi(true)
|
||||
.with_env_filter(EnvFilter::builder().with_default_directive(LevelFilter::DEBUG.into()).from_env_lossy())
|
||||
.pretty()
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Serve => serve().await.unwrap_or_report(),
|
||||
Command::ResetState => {
|
||||
let s = PersistentState::default();
|
||||
let p = camino::Utf8Path::new(STATE_FILE);
|
||||
tracing::info!("Resetting state to default: {p}");
|
||||
Persistent::persist(p, &s).await.unwrap_or_report();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(err)]
|
||||
async fn serve() -> Result<(), Error> {
|
||||
use crate::app::App;
|
||||
use crate::app::shell;
|
||||
use crate::server::state::State;
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::LeptosRoutes;
|
||||
use leptos_axum::generate_route_list;
|
||||
|
||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||
// For deployment these variables are:
|
||||
// <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")
|
||||
// The file would need to be included with the executable when moved to deployment
|
||||
let conf = get_configuration(None).unwrap_or_report();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
tracing::info!("Loading state");
|
||||
let server_state = State {
|
||||
persistent: Persistent::<PersistentState>::load(STATE_FILE.into()).await?,
|
||||
};
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.leptos_routes_with_context(&leptos_options, routes, move || provide_context(server_state.clone()), {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
tracing::info!("Listening on http://{addr}");
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||
#[display("Server crash")]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
Persistence(persistence::Error),
|
||||
}
|
||||
122
crates/ascend/src/server/persistence.rs
Normal file
122
crates/ascend/src/server/persistence.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use camino::Utf8Path;
|
||||
use camino::Utf8PathBuf;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Persistent<T> {
|
||||
state: Arc<Mutex<T>>,
|
||||
file_path: Utf8PathBuf,
|
||||
}
|
||||
|
||||
impl<T> Persistent<T> {
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub fn new(state: T, file_path: Utf8PathBuf) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(state)),
|
||||
file_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiates state from file system
|
||||
#[tracing::instrument]
|
||||
pub async fn load(file_path: Utf8PathBuf) -> Result<Self, Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read {
|
||||
file_path: file_path.to_owned(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let t = serde_json::from_str(&content).map_err(|source| Error::Deserialize {
|
||||
file_path: file_path.to_owned(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let persistent = Self {
|
||||
state: Arc::new(Mutex::new(t)),
|
||||
file_path,
|
||||
};
|
||||
Ok(persistent)
|
||||
}
|
||||
|
||||
/// Returns state
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
let state = self.state.lock().await;
|
||||
state.clone()
|
||||
}
|
||||
|
||||
/// Returns state
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn with<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&T) -> R,
|
||||
{
|
||||
let state = self.state.lock().await;
|
||||
f(&state)
|
||||
}
|
||||
|
||||
/// Updates and persists state
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn update<F>(&self, f: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnOnce(&mut T),
|
||||
T: Serialize,
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
f(&mut state);
|
||||
Self::persist(&self.file_path, &state).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets and persists state
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn set(&self, new_state: T) -> Result<(), Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
self.update(move |state| {
|
||||
*state = new_state;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Persist.
|
||||
///
|
||||
/// Implicitly called by `set` and `update`.
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub async fn persist(file_path: &Utf8Path, state: &T) -> Result<(), Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let serialized = serde_json::to_string(state).map_err(|source| Error::Serialize { source })?;
|
||||
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
|
||||
file_path: file_path.to_owned(),
|
||||
source,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||
#[display("Persistent state error: {_variant}")]
|
||||
pub enum Error {
|
||||
#[display("Failed to read file: {file_path}")]
|
||||
Read { file_path: Utf8PathBuf, source: std::io::Error },
|
||||
|
||||
#[display("Failed to deserialize state from file: {file_path}")]
|
||||
Deserialize { file_path: Utf8PathBuf, source: serde_json::Error },
|
||||
|
||||
#[display("Failed to serialize state")]
|
||||
Serialize { source: serde_json::Error },
|
||||
|
||||
#[display("Failed to write file: {file_path}")]
|
||||
Write { file_path: Utf8PathBuf, source: std::io::Error },
|
||||
}
|
||||
1
crates/ascend/style/main.scss
Normal file
1
crates/ascend/style/main.scss
Normal file
@@ -0,0 +1 @@
|
||||
body { }
|
||||
3
crates/ascend/style/tailwind.css
Normal file
3
crates/ascend/style/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
12
crates/ascend/tailwind.config.js
Normal file
12
crates/ascend/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
relative: true,
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user