Compare commits
11 Commits
master
...
976a416081
| Author | SHA1 | Date | |
|---|---|---|---|
| 976a416081 | |||
| 0da5cc573e | |||
| 3c4952ce50 | |||
| df90835aac | |||
| 0ca9d6b9c6 | |||
| 12f78d5acc | |||
| 1280e1fc20 | |||
| abe52b2b66 | |||
| 3afd9cd8b2 | |||
| 4ebd49a31e | |||
| 54bf4ddaec |
@@ -5,9 +5,15 @@ language-servers = ["rust-analyzer", "tailwindcss-ls"]
|
|||||||
[language-server.rust-analyzer.config]
|
[language-server.rust-analyzer.config]
|
||||||
# procMacro = { ignored = { leptos_macro = ["server"] } }
|
# procMacro = { ignored = { leptos_macro = ["server"] } }
|
||||||
cargo = { features = ["ssr", "hydrate"] }
|
cargo = { features = ["ssr", "hydrate"] }
|
||||||
|
check = { command = "check" }
|
||||||
|
|
||||||
[language-server.rust-analyzer.config.check]
|
rustfmt = { overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"] }
|
||||||
command = "clippy"
|
|
||||||
|
# rustfmt = { overrideCommand = [
|
||||||
|
# "sh",
|
||||||
|
# "-c",
|
||||||
|
# "set -euo pipefail; rustfmt --emit stdout --edition 2024 | leptosfmt --stdin",
|
||||||
|
# ] }
|
||||||
|
|
||||||
[language-server.tailwindcss-ls]
|
[language-server.tailwindcss-ls]
|
||||||
config = { userLanguages = { rust = "html", "*.rs" = "html" } }
|
config = { userLanguages = { rust = "html", "*.rs" = "html" } }
|
||||||
|
|||||||
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -1760,9 +1760,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos"
|
name = "leptos"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78329c12843d64766d8f00216aae665416d804327302ce8e0ab83884dfa91887"
|
checksum = "88613d81f70f4e267473b2ee107e1ee70cf765a3c3dfee945929c8e9c520b957"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -1823,9 +1823,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_config"
|
name = "leptos_config"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "132a18e8ffc4fbe2d624f3743d88a1b4989bff2d5e12be2b0d2749201d9dfb52"
|
checksum = "4172cfee12576224775ccfbb9d3e76625017a8b4207c4641a2f9b96a70e6d524"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"config",
|
"config",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -1836,9 +1836,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_dom"
|
name = "leptos_dom"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d468f638f2f13d70d99d9952be98d671a75366034472f3828e586ba62d770049"
|
checksum = "a41f6dc3ddaa09d876d7015f08f4f3905787da4ea5460cef130c365419483a89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"or_poisoned",
|
"or_poisoned",
|
||||||
@@ -1852,9 +1852,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_hot_reload"
|
name = "leptos_hot_reload"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ba37d76693fc6228554e0bb06a9aa41c59e2b5180caf423c7913557b81d01dd"
|
checksum = "31f5c961e5d9b2aa6deab39d5d842272e8b1b165744b5caf674770d5cf0daa04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"camino",
|
"camino",
|
||||||
@@ -1885,9 +1885,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_macro"
|
name = "leptos_macro"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "064d0c8b144b93f8d7e84b30c16d1da0e64a63c7e91b9a872f7be63601c5868b"
|
checksum = "2b9165909eabb02188a4b33b0ab6acff408bdf440018bf65b30bba0d38d61b19"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"attribute-derive",
|
"attribute-derive",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -1961,9 +1961,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_server"
|
name = "leptos_server"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fb1779f1f0570915066c132fb11f999add8b13d02ca5221735193eb02b3fa69a"
|
checksum = "4fee9ed4526484b17561bc8ce1532c613e37be2c01788fed3d1c4104db674dd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -2663,9 +2663,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reactive_graph"
|
name = "reactive_graph"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "059aede5acae8f5c25b1d34b6df34700006418b3c493db3698b7ebcd4a8a6287"
|
checksum = "9996b4c0f501d64a755ff3dfbe9276e9f834d105d7d45059ad4bd6d2a56477d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"async-lock",
|
"async-lock",
|
||||||
@@ -2685,9 +2685,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reactive_stores"
|
name = "reactive_stores"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c7edacf4298579a5772285b8e2dc0b9953c8fbaa9c3f56c3dd69d56e5af7a48"
|
checksum = "74c3d2a20d8edd8ac6628718209f743da86349d7f10a4458304666c2ddfc082e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"guardian",
|
"guardian",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
@@ -2700,9 +2700,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reactive_stores_macro"
|
name = "reactive_stores_macro"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "178b1cd8b2871a45bfc8e13ff8076049b6e9a5132e72414e5cab3894c4a6adb3"
|
checksum = "6d4d8e40112b8ee1424e5ec636fcbc9764c1a099e81f8fa818f6762b43cc10cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"proc-macro-error2",
|
"proc-macro-error2",
|
||||||
@@ -2941,9 +2941,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server_fn"
|
name = "server_fn"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c183c31152fd00e994a3ea0ca43e6017056ccf7812160b0ae008acc3de8241c"
|
checksum = "055476c2a42c9a98a69e3f0ce29b86aa3acbdef19a84e0523330f095097defcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2978,9 +2978,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server_fn_macro"
|
name = "server_fn_macro"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c43b2266308c118be1a1cc60602f8efb07a64e72deed8d317704d5cfda092ca1"
|
checksum = "e65737414a9583ce3b43dddd4e5dfb33fe385a6933ed79a9b539b8eb0767cd07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const_format",
|
"const_format",
|
||||||
"convert_case",
|
"convert_case",
|
||||||
@@ -2992,9 +2992,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server_fn_macro_default"
|
name = "server_fn_macro_default"
|
||||||
version = "0.7.5"
|
version = "0.7.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "087eca61bc8f93d868b8c10ca058da358fd7aaeb7bc8415b572f9f3f27ce0b93"
|
checksum = "563909a43390341403ab76fbc33fde306712613da02244e692eabeae8ffde949"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"server_fn_macro",
|
"server_fn_macro",
|
||||||
"syn",
|
"syn",
|
||||||
@@ -3148,9 +3148,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tachys"
|
name = "tachys"
|
||||||
version = "0.1.5"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59a3bbcf8e3b52cad5f0aa860837d4d1796c7c4873b083c9520a1bbba4747973"
|
checksum = "4c05fed41ed4e334257090500510df21bb1611680c0cfd3be14acec7ffdf3d95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ derive_more = { version = "2", features = [
|
|||||||
] }
|
] }
|
||||||
http = "1"
|
http = "1"
|
||||||
image = { version = "0.25", optional = true }
|
image = { version = "0.25", optional = true }
|
||||||
leptos = { version = "0.7.4", features = ["tracing"] }
|
leptos = { version = "0.7.7", features = ["tracing"] }
|
||||||
leptos_axum = { version = "0.7", optional = true }
|
leptos_axum = { version = "0.7", optional = true }
|
||||||
leptos_meta = { version = "0.7" }
|
leptos_meta = { version = "0.7" }
|
||||||
leptos_router = { version = "0.7.0" }
|
leptos_router = { version = "0.7.0" }
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
|||||||
<HydrationScripts options />
|
<HydrationScripts options />
|
||||||
<MetaTags />
|
<MetaTags />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-950 text-white">
|
<body class="text-white bg-slate-950">
|
||||||
<App />
|
<App />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
30
crates/ascend/src/components/attempt.rs
Normal file
30
crates/ascend/src/components/attempt.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::components::icons;
|
||||||
|
use crate::models;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub fn Attempt(#[prop(into)] attempt: Signal<Option<models::Attempt>>) -> impl IntoView {
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
let text = move || match attempt.get() {
|
||||||
|
Some(models::Attempt::Attempt) => "Learning experience",
|
||||||
|
Some(models::Attempt::Send) => "Send",
|
||||||
|
Some(models::Attempt::Flash) => "Flash",
|
||||||
|
None => "No attempt",
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = move || match attempt.get() {
|
||||||
|
Some(models::Attempt::Attempt) => view! { <icons::BoltSlashSolid /> }.into_any(),
|
||||||
|
Some(models::Attempt::Send) => view! { <icons::PaperAirplaneSolid /> }.into_any(),
|
||||||
|
Some(models::Attempt::Flash) => view! { <icons::BoltSolid /> }.into_any(),
|
||||||
|
None => view! { <icons::NoSymbol /> }.into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex gap-2 justify-center items-center">
|
||||||
|
<span>{icon}</span>
|
||||||
|
<span>{text}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,73 @@
|
|||||||
|
use super::icons::Icon;
|
||||||
|
use crate::components::outlined_box::OutlinedBox;
|
||||||
|
use crate::gradient::Gradient;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Button(#[prop(into)] text: String, onclick: impl Fn(MouseEvent) + 'static) -> impl IntoView {
|
pub fn Button(
|
||||||
|
#[prop(into, optional)] icon: MaybeProp<Icon>,
|
||||||
|
|
||||||
|
#[prop(into)] text: Signal<String>,
|
||||||
|
|
||||||
|
#[prop(optional)] color: Gradient,
|
||||||
|
|
||||||
|
#[prop(into, optional)] highlight: MaybeProp<bool>,
|
||||||
|
|
||||||
|
onclick: impl FnMut(MouseEvent) + 'static,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let icon_view = icon.get().map(|i| {
|
||||||
|
let icon_view = i.into_view();
|
||||||
|
let mut classes = "self-center mx-5 my-2.5 rounded-sm".to_string();
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_text());
|
||||||
|
|
||||||
|
view! { <div class=classes>{icon_view}</div> }
|
||||||
|
});
|
||||||
|
|
||||||
|
let separator = icon.get().is_some().then(|| {
|
||||||
|
let mut classes = "w-0.5 bg-gradient-to-br min-w-0.5".to_string();
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_from());
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_to());
|
||||||
|
|
||||||
|
view! { <div class=classes /> }
|
||||||
|
});
|
||||||
|
|
||||||
|
let text_view = view! { <div class="self-center mx-5 my-2.5 uppercase w-full text-lg font-thin">{text.get()}</div> };
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button
|
||||||
on:click=onclick
|
on:click=onclick
|
||||||
type="button"
|
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"
|
class="mb-2 me-2 hover:brightness-125 active:brightness-90"
|
||||||
>
|
>
|
||||||
{text}
|
<OutlinedBox color highlight>
|
||||||
|
<div class="flex items-stretch">{icon_view} {separator} {text_view}</div>
|
||||||
|
</OutlinedBox>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn baseline() {
|
||||||
|
let text = "foo";
|
||||||
|
let onclick = |_| {};
|
||||||
|
|
||||||
|
view! { <Button text onclick /> };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
let icon = Icon::ForwardSolid;
|
||||||
|
let text = "foo";
|
||||||
|
let onclick = |_| {};
|
||||||
|
|
||||||
|
view! { <Button icon text onclick /> };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
50
crates/ascend/src/components/checkbox.rs
Normal file
50
crates/ascend/src/components/checkbox.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::components::icons;
|
||||||
|
use crate::components::outlined_box::OutlinedBox;
|
||||||
|
use crate::gradient::Gradient;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Checkbox(checked: RwSignal<bool>, #[prop(into)] text: Signal<String>, #[prop(optional)] color: Gradient) -> impl IntoView {
|
||||||
|
let unique_id = Oco::from(format!("checkbox-{}", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
|
let checkbox_view = view! {
|
||||||
|
<div class="self-center text-white bg-white rounded-sm aspect-square mx-5 my-2.5">
|
||||||
|
<span class=("text-gray-950", move || checked.get())>
|
||||||
|
<icons::Check />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
let separator = {
|
||||||
|
let mut classes = "w-0.5 bg-gradient-to-br min-w-0.5".to_string();
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_from());
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_to());
|
||||||
|
view! { <div class=classes /> }
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_view = view! {
|
||||||
|
<div class="self-center mx-5 my-2.5 uppercase w-full text-lg font-thin">
|
||||||
|
{move || text.get()}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="inline-block mb-2 me-2 hover:brightness-125 active:brightness-90">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id=unique_id.clone()
|
||||||
|
value=""
|
||||||
|
class="hidden peer"
|
||||||
|
required=""
|
||||||
|
bind:checked=checked
|
||||||
|
/>
|
||||||
|
<label for=unique_id class="cursor-pointer">
|
||||||
|
<OutlinedBox color>
|
||||||
|
<div class="flex">{checkbox_view} {separator} {text_view}</div>
|
||||||
|
</OutlinedBox>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ pub struct HeaderItem {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn StyledHeader(#[prop(into)] items: Signal<HeaderItems>) -> impl IntoView {
|
pub fn StyledHeader(#[prop(into)] items: Signal<HeaderItems>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
|
<div class="text-black bg-orange-300 border-b-2 border-b-orange-400">
|
||||||
// <div class="container mx-auto" >
|
// <div class="container mx-auto" >
|
||||||
<Header items />
|
<Header items />
|
||||||
// </div>
|
// </div>
|
||||||
@@ -33,7 +33,7 @@ pub fn Header(#[prop(into)] items: Signal<HeaderItems>) -> impl IntoView {
|
|||||||
let right = move || items.read().right.clone();
|
let right = move || items.read().right.clone();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-cols-[1fr_3fr_1fr] text-xl font-semibold p-4">
|
<div class="grid p-4 text-xl font-semibold grid-cols-[1fr_3fr_1fr]">
|
||||||
// Left side of header
|
// Left side of header
|
||||||
<div class="justify-self-start">
|
<div class="justify-self-start">
|
||||||
<Items items=Signal::derive(left) />
|
<Items items=Signal::derive(left) />
|
||||||
|
|||||||
228
crates/ascend/src/components/icons.rs
Normal file
228
crates/ascend/src/components/icons.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Copy, Debug, Clone)]
|
||||||
|
pub enum Icon {
|
||||||
|
BoltSolid,
|
||||||
|
BoltSlashSolid,
|
||||||
|
WrenchSolid,
|
||||||
|
ForwardSolid,
|
||||||
|
Check,
|
||||||
|
HeartOutline,
|
||||||
|
ArrowPath,
|
||||||
|
PaperAirplaneSolid,
|
||||||
|
NoSymbol,
|
||||||
|
Trophy,
|
||||||
|
ArrowTrendingUp,
|
||||||
|
}
|
||||||
|
impl Icon {
|
||||||
|
// TODO: Actually impl IntoView for Icon instead
|
||||||
|
pub fn into_view(self) -> impl IntoView {
|
||||||
|
match self {
|
||||||
|
Icon::BoltSolid => view! { <BoltSolid /> }.into_any(),
|
||||||
|
Icon::BoltSlashSolid => view! { <BoltSlashSolid /> }.into_any(),
|
||||||
|
Icon::WrenchSolid => view! { <WrenchSolid /> }.into_any(),
|
||||||
|
Icon::ForwardSolid => view! { <ForwardSolid /> }.into_any(),
|
||||||
|
Icon::Check => view! { <Check /> }.into_any(),
|
||||||
|
Icon::HeartOutline => view! { <HeartOutline /> }.into_any(),
|
||||||
|
Icon::ArrowPath => view! { <ArrowPath /> }.into_any(),
|
||||||
|
Icon::PaperAirplaneSolid => view! { <PaperAirplaneSolid /> }.into_any(),
|
||||||
|
Icon::NoSymbol => view! { <NoSymbol /> }.into_any(),
|
||||||
|
Icon::Trophy => view! { <Trophy /> }.into_any(),
|
||||||
|
Icon::ArrowTrendingUp => view! { <ArrowTrendingUp /> }.into_any(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BoltSolid() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn BoltSlashSolid() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path d="m20.798 11.012-3.188 3.416L9.462 6.28l4.24-4.542a.75.75 0 0 1 1.272.71L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262ZM3.202 12.988 6.39 9.572l8.148 8.148-4.24 4.542a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262ZM3.53 2.47a.75.75 0 0 0-1.06 1.06l18 18a.75.75 0 1 0 1.06-1.06l-18-18Z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn WrenchSolid() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ForwardSolid() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path d="M5.055 7.06C3.805 6.347 2.25 7.25 2.25 8.69v8.122c0 1.44 1.555 2.343 2.805 1.628L12 14.471v2.34c0 1.44 1.555 2.343 2.805 1.628l7.108-4.061c1.26-.72 1.26-2.536 0-3.256l-7.108-4.061C13.555 6.346 12 7.249 12 8.689v2.34L5.055 7.061Z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Check() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn HeartOutline() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ArrowPath() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903h-3.183a.75.75 0 1 0 0 1.5h4.992a.75.75 0 0 0 .75-.75V4.356a.75.75 0 0 0-1.5 0v3.18l-1.9-1.9A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm15.408 3.352a.75.75 0 0 0-.919.53 7.5 7.5 0 0 1-12.548 3.364l-1.902-1.903h3.183a.75.75 0 0 0 0-1.5H2.984a.75.75 0 0 0-.75.75v4.992a.75.75 0 0 0 1.5 0v-3.18l1.9 1.9a9 9 0 0 0 15.059-4.035.75.75 0 0 0-.53-.918Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn PaperAirplaneSolid() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn NoSymbol() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="m6.72 5.66 11.62 11.62A8.25 8.25 0 0 0 6.72 5.66Zm10.56 12.68L5.66 6.72a8.25 8.25 0 0 0 11.62 11.62ZM5.105 5.106c3.807-3.808 9.98-3.808 13.788 0 3.808 3.807 3.808 9.98 0 13.788-3.807 3.808-9.98 3.808-13.788 0-3.808-3.807-3.808-9.98 0-13.788Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Trophy() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ArrowTrendingUp() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
44
crates/ascend/src/components/outlined_box.rs
Normal file
44
crates/ascend/src/components/outlined_box.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use crate::gradient::Gradient;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highlight: MaybeProp<bool>) -> impl IntoView {
|
||||||
|
let highlight = move || highlight.get().unwrap_or(false);
|
||||||
|
|
||||||
|
let outer_classes = move || {
|
||||||
|
let mut c = "p-0.5 bg-gradient-to-br rounded-lg".to_string();
|
||||||
|
c.push(' ');
|
||||||
|
c.push_str(color.class_from());
|
||||||
|
c.push(' ');
|
||||||
|
c.push_str(color.class_to());
|
||||||
|
if highlight() {
|
||||||
|
c.push(' ');
|
||||||
|
c.push_str("brightness-110");
|
||||||
|
}
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
let inner_classes = move || {
|
||||||
|
let mut c = "py-1.5 rounded-md".to_string();
|
||||||
|
if highlight() {
|
||||||
|
let bg = match color {
|
||||||
|
Gradient::PinkOrange => "bg-pink-900",
|
||||||
|
Gradient::CyanBlue => "bg-cyan-900",
|
||||||
|
Gradient::TealLime => "bg-teal-900",
|
||||||
|
};
|
||||||
|
|
||||||
|
c.push(' ');
|
||||||
|
c.push_str(bg);
|
||||||
|
} else {
|
||||||
|
c.push(' ');
|
||||||
|
c.push_str("bg-gray-900");
|
||||||
|
}
|
||||||
|
c
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class=outer_classes>
|
||||||
|
<div class=inner_classes>{children()}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,7 @@ pub fn Problem(
|
|||||||
let grid_classes = move || format!("grid grid-rows-{} grid-cols-{} gap-3", dim.get().rows, dim.get().cols);
|
let grid_classes = move || format!("grid grid-rows-{} grid-cols-{} gap-3", dim.get().rows, dim.get().cols);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-cols-[auto,1fr] gap-8">
|
<div class="grid gap-8 grid-cols-[auto,1fr]">
|
||||||
<div class=move || { grid_classes }>{holds}</div>
|
<div class=move || { grid_classes }>{holds}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
|
|||||||
let method = problem.method;
|
let method = problem.method;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-rows-none grid-cols-[auto,1fr] gap-0.5">
|
<div class="grid grid-rows-none gap-0.5 grid-cols-[auto,1fr]">
|
||||||
<NameValue name="Name:" value=name />
|
<NameValue name="Name:" value=name />
|
||||||
<NameValue name="Method:" value=method.to_string() />
|
<NameValue name="Method:" value=method.to_string() />
|
||||||
<NameValue name="Set By:" value=set_by />
|
<NameValue name="Set By:" value=set_by />
|
||||||
@@ -23,7 +23,7 @@ pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
|
|||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn NameValue(#[prop(into)] name: String, #[prop(into)] value: String) -> impl IntoView {
|
fn NameValue(#[prop(into)] name: String, #[prop(into)] value: String) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<p class="text-orange-300 mr-4 text-right">{name}</p>
|
<p class="mr-4 text-right text-orange-300">{name}</p>
|
||||||
<p class="text-white font-semibold">{value}</p>
|
<p class="font-semibold text-white">{value}</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
crates/ascend/src/gradient.rs
Normal file
32
crates/ascend/src/gradient.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
|
pub enum Gradient {
|
||||||
|
#[default]
|
||||||
|
PinkOrange,
|
||||||
|
CyanBlue,
|
||||||
|
TealLime,
|
||||||
|
}
|
||||||
|
impl Gradient {
|
||||||
|
pub fn class_from(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Gradient::PinkOrange => "from-pink-500",
|
||||||
|
Gradient::CyanBlue => "from-cyan-500",
|
||||||
|
Gradient::TealLime => "from-teal-300",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_to(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Gradient::PinkOrange => "to-orange-400",
|
||||||
|
Gradient::CyanBlue => "to-blue-500",
|
||||||
|
Gradient::TealLime => "to-lime-300",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_text(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Gradient::PinkOrange => "text-pink-500",
|
||||||
|
Gradient::CyanBlue => "text-cyan-500",
|
||||||
|
Gradient::TealLime => "text-teal-300",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,55 +6,26 @@ pub mod pages {
|
|||||||
pub mod wall;
|
pub mod wall;
|
||||||
}
|
}
|
||||||
pub mod components {
|
pub mod components {
|
||||||
|
pub use attempt::Attempt;
|
||||||
pub use button::Button;
|
pub use button::Button;
|
||||||
pub use header::StyledHeader;
|
pub use header::StyledHeader;
|
||||||
pub use problem::Problem;
|
pub use problem::Problem;
|
||||||
pub use problem_info::ProblemInfo;
|
pub use problem_info::ProblemInfo;
|
||||||
|
|
||||||
|
pub mod attempt;
|
||||||
pub mod button;
|
pub mod button;
|
||||||
|
pub mod checkbox;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
pub mod icons;
|
||||||
|
pub mod outlined_box;
|
||||||
pub mod problem;
|
pub mod problem;
|
||||||
pub mod problem_info;
|
pub mod problem_info;
|
||||||
}
|
}
|
||||||
pub mod resources {
|
|
||||||
use crate::codec::ron::Ron;
|
|
||||||
use crate::codec::ron::RonEncoded;
|
|
||||||
use crate::models::{self};
|
|
||||||
use leptos::prelude::Get;
|
|
||||||
use leptos::prelude::Signal;
|
|
||||||
use leptos::server::Resource;
|
|
||||||
use server_fn::ServerFnError;
|
|
||||||
|
|
||||||
type RonResource<T> = Resource<Result<T, ServerFnError>, Ron>;
|
pub mod gradient;
|
||||||
|
|
||||||
pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wall> {
|
pub mod resources;
|
||||||
Resource::new_with_options(
|
|
||||||
move || wall_uid.get(),
|
|
||||||
move |wall_uid| async move { crate::server_functions::get_wall(wall_uid).await.map(RonEncoded::into_inner) },
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn problem_by_uid(wall_uid: Signal<models::WallUid>, problem_uid: Signal<models::ProblemUid>) -> RonResource<models::Problem> {
|
|
||||||
Resource::new_with_options(
|
|
||||||
move || (wall_uid.get(), problem_uid.get()),
|
|
||||||
move |(wall_uid, problem_uid)| async move {
|
|
||||||
crate::server_functions::get_problem(wall_uid, problem_uid)
|
|
||||||
.await
|
|
||||||
.map(RonEncoded::into_inner)
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<models::Problem>> {
|
|
||||||
Resource::new_with_options(
|
|
||||||
move || wall_uid.get(),
|
|
||||||
move |wall_uid| async move { crate::server_functions::get_problems_for_wall(wall_uid).await.map(RonEncoded::into_inner) },
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod server_functions;
|
pub mod server_functions;
|
||||||
|
|||||||
@@ -14,6 +14,61 @@ pub use v2::Root;
|
|||||||
pub use v2::Wall;
|
pub use v2::Wall;
|
||||||
pub use v2::WallDimensions;
|
pub use v2::WallDimensions;
|
||||||
pub use v2::WallUid;
|
pub use v2::WallUid;
|
||||||
|
pub use v3::Attempt;
|
||||||
|
pub use v3::UserInteraction;
|
||||||
|
|
||||||
|
pub mod v3 {
|
||||||
|
use super::v2;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// Registers user interaction with a problem
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct UserInteraction {
|
||||||
|
pub wall_uid: v2::WallUid,
|
||||||
|
pub problem_uid: v2::ProblemUid,
|
||||||
|
|
||||||
|
/// Dates on which this problem was attempted, and how it went
|
||||||
|
pub attempted_on: BTreeMap<chrono::DateTime<chrono::Utc>, Attempt>,
|
||||||
|
|
||||||
|
/// Is among favorite problems
|
||||||
|
pub is_favorite: bool,
|
||||||
|
|
||||||
|
/// Added to personal challenges
|
||||||
|
pub is_saved: bool,
|
||||||
|
}
|
||||||
|
impl UserInteraction {
|
||||||
|
pub fn new(wall_uid: v2::WallUid, problem_uid: v2::ProblemUid) -> Self {
|
||||||
|
Self {
|
||||||
|
wall_uid,
|
||||||
|
problem_uid,
|
||||||
|
is_favorite: false,
|
||||||
|
attempted_on: BTreeMap::new(),
|
||||||
|
is_saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn best_attempt(&self) -> Option<(chrono::DateTime<chrono::Utc>, Attempt)> {
|
||||||
|
self.attempted_on
|
||||||
|
.iter()
|
||||||
|
.max_by_key(|(_date, attempt)| *attempt)
|
||||||
|
.map(|(date, attempt)| (*date, *attempt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
|
||||||
|
pub enum Attempt {
|
||||||
|
/// Tried to climb problem, but was not able to.
|
||||||
|
Attempt,
|
||||||
|
|
||||||
|
/// Climbed problem, but not flashed.
|
||||||
|
Send,
|
||||||
|
|
||||||
|
/// Flashed problem.
|
||||||
|
Flash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod v2 {
|
pub mod v2 {
|
||||||
use super::v1;
|
use super::v1;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub fn EditWall() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
leptos::view! {
|
leptos::view! {
|
||||||
<div class="min-w-screen min-h-screen bg-slate-900">
|
<div class="min-h-screen min-w-screen bg-slate-900">
|
||||||
<StyledHeader items=Signal::derive(header_items) />
|
<StyledHeader items=Signal::derive(header_items) />
|
||||||
|
|
||||||
<div class="container mx-auto mt-2">
|
<div class="container mx-auto mt-2">
|
||||||
@@ -155,7 +155,7 @@ fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<button on:click=open_camera>
|
<button on:click=open_camera>
|
||||||
<div class="bg-indigo-100 aspect-square rounded">{img}</div>
|
<div class="bg-indigo-100 rounded aspect-square">{img}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ pub fn Routes() -> impl IntoView {
|
|||||||
let wall = crate::resources::wall_by_uid(wall_uid);
|
let wall = crate::resources::wall_by_uid(wall_uid);
|
||||||
let problems = crate::resources::problems_for_wall(wall_uid);
|
let problems = crate::resources::problems_for_wall(wall_uid);
|
||||||
|
|
||||||
let header_items = HeaderItems {
|
let header_items = move || HeaderItems {
|
||||||
left: vec![HeaderItem {
|
left: vec![HeaderItem {
|
||||||
text: "← Ascend".to_string(),
|
text: "← Ascend".to_string(),
|
||||||
link: Some("/".to_string()),
|
link: Some(format!("/wall/{}", wall_uid.get())),
|
||||||
}],
|
}],
|
||||||
middle: vec![HeaderItem {
|
middle: vec![HeaderItem {
|
||||||
text: "Routes".to_string(),
|
text: "Routes".to_string(),
|
||||||
@@ -63,7 +63,10 @@ pub fn Routes() -> impl IntoView {
|
|||||||
each=problems_sample
|
each=problems_sample
|
||||||
key=|problem| problem.uid
|
key=|problem| problem.uid
|
||||||
children=move |problem: models::Problem| {
|
children=move |problem: models::Problem| {
|
||||||
view! { <Problem dim=wall_dimensions problem /> <hr class="h-px my-8 border-0 bg-gray-700" />}
|
view! {
|
||||||
|
<Problem dim=wall_dimensions problem />
|
||||||
|
<hr class="my-8 h-px bg-gray-700 border-0" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,8 +78,8 @@ pub fn Routes() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="min-w-screen min-h-screen bg-neutral-950">
|
<div class="min-h-screen min-w-screen bg-neutral-950">
|
||||||
<StyledHeader items=header_items />
|
<StyledHeader items=Signal::derive(header_items) />
|
||||||
|
|
||||||
<div class="container mx-auto mt-6">
|
<div class="container mx-auto mt-6">
|
||||||
<Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense>
|
<Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense>
|
||||||
@@ -91,7 +94,7 @@ fn Problem(#[prop(into)] dim: Signal<models::WallDimensions>, #[prop(into)] prob
|
|||||||
view! {
|
view! {
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<components::Problem dim problem />
|
<components::Problem dim problem />
|
||||||
</div>
|
</div>
|
||||||
<components::ProblemInfo problem=problem.get() />
|
<components::ProblemInfo problem=problem.get() />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ pub fn Settings() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="min-w-screen min-h-screen bg-neutral-950">
|
<div class="min-h-screen min-w-screen bg-neutral-950">
|
||||||
<StyledHeader items=header_items />
|
<StyledHeader items=header_items />
|
||||||
|
|
||||||
<div class="container mx-auto mt-2">
|
// {move || view! { <Import wall_uid=wall_uid.get() /> }}
|
||||||
// {move || view! { <Import wall_uid=wall_uid.get() /> }}
|
<div class="container mx-auto mt-2" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,44 @@
|
|||||||
use crate::codec::ron::RonEncoded;
|
// +--------------- Filter ----------- ↓ -+
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
|
// +---------------------------+
|
||||||
|
// | Next Problem |
|
||||||
|
// +---------------------------+
|
||||||
|
|
||||||
|
// +--------------- Problem --------------+
|
||||||
|
// | Name: ... |
|
||||||
|
// | Method: ... |
|
||||||
|
// | Set by: ... |
|
||||||
|
// | |
|
||||||
|
// | | Flash | Top | Attempt | |
|
||||||
|
// | |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
|
// +---------+ +---------+ +---------+
|
||||||
|
// | Flash | | Send | | Attempt |
|
||||||
|
// +---------+ +---------+ +---------+
|
||||||
|
|
||||||
|
// +---------- <Latest attempt> ----------+
|
||||||
|
// | Today: <Attempt> |
|
||||||
|
// | 14 days ago: <Attempt> |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
use crate::components::ProblemInfo;
|
use crate::components::ProblemInfo;
|
||||||
|
use crate::components::attempt::Attempt;
|
||||||
use crate::components::button::Button;
|
use crate::components::button::Button;
|
||||||
|
use crate::components::checkbox::Checkbox;
|
||||||
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;
|
||||||
|
use crate::components::icons::Icon;
|
||||||
|
use crate::gradient::Gradient;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::HoldRole;
|
use crate::models::HoldRole;
|
||||||
use leptos::Params;
|
use leptos::Params;
|
||||||
@@ -33,47 +68,45 @@ pub fn Wall() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let wall = crate::resources::wall_by_uid(wall_uid);
|
let wall = crate::resources::wall_by_uid(wall_uid);
|
||||||
|
let problem = crate::resources::problem_by_uid(wall_uid, problem_uid.into());
|
||||||
|
let user_interaction = crate::resources::user_interaction(wall_uid, problem_uid.into());
|
||||||
|
|
||||||
let problem_action = Action::new(move |&(wall_uid, problem_uid): &(models::WallUid, models::ProblemUid)| async move {
|
// merge outer option (resource hasn't resolved yet) with inner option (there is no problem for the wall)
|
||||||
tracing::info!("fetching");
|
let problem_sig2 = Signal::derive(move || problem.get().transpose().map(Option::flatten));
|
||||||
crate::server_functions::get_problem(wall_uid, problem_uid)
|
|
||||||
.await
|
|
||||||
.map(RonEncoded::into_inner)
|
|
||||||
});
|
|
||||||
let problem_signal = Signal::derive(move || {
|
|
||||||
let v = problem_action.value().read_only().get();
|
|
||||||
v.and_then(Result::ok)
|
|
||||||
});
|
|
||||||
|
|
||||||
let fn_next_problem = move |wall: &models::Wall| {
|
let fn_next_problem = move |wall: &models::Wall| {
|
||||||
set_problem_uid.set(wall.random_problem());
|
set_problem_uid.set(wall.random_problem());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set a problem when wall is set (loaded)
|
// Set a problem when wall is set (loaded)
|
||||||
Effect::new(move |_prev_value| {
|
Effect::new(move |_prev_value| match &*wall.read() {
|
||||||
problem_action.value().write_only().set(None);
|
Some(Ok(wall)) => {
|
||||||
|
if problem_uid.get().is_none() {
|
||||||
match &*wall.read() {
|
tracing::debug!("Setting next problem");
|
||||||
Some(Ok(wall)) => {
|
fn_next_problem(wall);
|
||||||
if problem_uid.get().is_none() {
|
|
||||||
tracing::debug!("Setting next problem");
|
|
||||||
fn_next_problem(wall);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
|
||||||
tracing::error!("Error getting wall: {err}");
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
|
Some(Err(err)) => {
|
||||||
|
tracing::error!("Error getting wall: {err}");
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// On change of problem UID, dispatch an action to fetch the problem
|
let ui_is_flash = RwSignal::new(false);
|
||||||
Effect::new(move |_prev_value| match problem_uid.get() {
|
let ui_is_climbed = RwSignal::new(false);
|
||||||
Some(problem_uid) => {
|
let ui_is_attempt = RwSignal::new(false);
|
||||||
problem_action.dispatch((wall_uid.get(), problem_uid));
|
let ui_is_favorite = RwSignal::new(false);
|
||||||
}
|
|
||||||
None => {
|
// On reception of user interaction state, set UI signals
|
||||||
problem_action.value().write_only().set(None);
|
Effect::new(move |_prev_value| {
|
||||||
|
if let Some(user_interaction) = user_interaction.get() {
|
||||||
|
let user_interaction = user_interaction.ok().flatten();
|
||||||
|
|
||||||
|
if let Some(user_interaction) = user_interaction {
|
||||||
|
ui_is_favorite.set(user_interaction.is_favorite);
|
||||||
|
} else {
|
||||||
|
ui_is_favorite.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,50 +129,150 @@ pub fn Wall() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
leptos::view! {
|
leptos::view! {
|
||||||
<div class="min-w-screen min-h-screen bg-neutral-950">
|
<div class="min-h-screen min-w-screen bg-neutral-950">
|
||||||
<StyledHeader items=Signal::derive(header_items) />
|
<StyledHeader items=Signal::derive(header_items) />
|
||||||
|
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<Suspense fallback=move || {
|
<Transition fallback=|| ()>
|
||||||
view! { <p>"Loading..."</p> }
|
|
||||||
}>
|
|
||||||
{move || Suspend::new(async move {
|
{move || Suspend::new(async move {
|
||||||
tracing::info!("executing Suspend future");
|
tracing::info!("executing main suspend");
|
||||||
let wall = wall.await?;
|
let wall = wall.await?;
|
||||||
|
let grid = {
|
||||||
|
let wall = wall.clone();
|
||||||
|
view! {
|
||||||
|
<Transition fallback=|| ()>
|
||||||
|
{
|
||||||
|
let wall = wall.clone();
|
||||||
|
move || {
|
||||||
|
let wall = wall.clone();
|
||||||
|
Suspend::new(async move {
|
||||||
|
let wall = wall.clone();
|
||||||
|
tracing::info!("executing grid suspend");
|
||||||
|
let view = view! {
|
||||||
|
<Grid wall=wall.clone() problem=problem_sig2 />
|
||||||
|
};
|
||||||
|
Ok::<_, ServerFnError>(view)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
}
|
||||||
|
};
|
||||||
let v = view! {
|
let v = view! {
|
||||||
<div class="grid grid-cols-1 md:grid-cols-[auto,1fr] gap-8">
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-[auto,1fr]">
|
||||||
<div>
|
<div>{grid}</div>
|
||||||
<Grid wall=wall.clone() problem=problem_signal />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<div class="flex">
|
||||||
|
<Button
|
||||||
|
onclick=move |_| {
|
||||||
|
ui_is_flash
|
||||||
|
.update(|x| {
|
||||||
|
*x = !*x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
text="Flash"
|
||||||
|
icon=Icon::BoltSolid
|
||||||
|
color=Gradient::CyanBlue
|
||||||
|
highlight=Signal::derive(move || { ui_is_flash.get() })
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onclick=move |_| {
|
||||||
|
ui_is_climbed
|
||||||
|
.update(|x| {
|
||||||
|
*x = !*x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
text="Send"
|
||||||
|
icon=Icon::Trophy
|
||||||
|
color=Gradient::TealLime
|
||||||
|
highlight=Signal::derive(move || { ui_is_climbed.get() })
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onclick=move |_| {
|
||||||
|
ui_is_attempt
|
||||||
|
.update(|x| {
|
||||||
|
*x = !*x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
text="Attempt"
|
||||||
|
icon=Icon::ArrowTrendingUp
|
||||||
|
color=Gradient::PinkOrange
|
||||||
|
highlight=Signal::derive(move || { ui_is_attempt.get() })
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
icon=Icon::ArrowPath
|
||||||
|
text="Next problem"
|
||||||
onclick=move |_| fn_next_problem(&wall)
|
onclick=move |_| fn_next_problem(&wall)
|
||||||
text="➤ Next problem"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="m-4"/>
|
<div class="m-4" />
|
||||||
|
|
||||||
{move || problem_signal.get().map(|problem| view! { <ProblemInfo problem /> })}
|
<Transition fallback=|| ()>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
tracing::info!("executing probleminfo suspend");
|
||||||
|
let problem = problem.await?;
|
||||||
|
let problem_view = problem
|
||||||
|
.map(|problem| view! { <ProblemInfo problem /> });
|
||||||
|
let view = view! { {problem_view} };
|
||||||
|
Ok::<_, ServerFnError>(view)
|
||||||
|
})}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<div class="m-4" />
|
||||||
|
|
||||||
|
<Suspense fallback=move || {
|
||||||
|
view! {}
|
||||||
|
}>
|
||||||
|
{move || {
|
||||||
|
let x = 10;
|
||||||
|
let attempt_suspend = Suspend::new(async move {
|
||||||
|
let user_interaction = user_interaction.await;
|
||||||
|
let user_interaction = user_interaction.ok().flatten();
|
||||||
|
let best_attempt = user_interaction
|
||||||
|
.and_then(|x| x.best_attempt());
|
||||||
|
let best_attempt_date = move || {
|
||||||
|
best_attempt.map(|pair| pair.0)
|
||||||
|
};
|
||||||
|
let best_attempt_attempt = move || {
|
||||||
|
best_attempt.map(|pair| pair.1)
|
||||||
|
};
|
||||||
|
view! {
|
||||||
|
<Attempt attempt=Signal::derive(best_attempt_attempt) />
|
||||||
|
}
|
||||||
|
});
|
||||||
|
attempt_suspend
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
Ok::<_, ServerFnError>(v)
|
Ok::<_, ServerFnError>(v)
|
||||||
})}
|
})}
|
||||||
</Suspense>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[component]
|
||||||
|
// #[tracing::instrument(skip_all)]
|
||||||
|
// fn WithWall(#[prop(into)] wall: Signal<models::Wall>) -> impl IntoView {
|
||||||
|
// tracing::trace!("Enter");
|
||||||
|
// }
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn Grid(wall: models::Wall, problem: Signal<Option<models::Problem>>) -> impl IntoView {
|
fn Grid(wall: models::Wall, #[prop(into)] problem: Signal<Result<Option<models::Problem>, ServerFnError>>) -> impl IntoView {
|
||||||
tracing::debug!("Enter");
|
tracing::debug!("Enter");
|
||||||
|
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
for (&hold_position, hold) in &wall.holds {
|
for (&hold_position, hold) in &wall.holds {
|
||||||
let role = move || problem.get().and_then(|p| p.holds.get(&hold_position).copied());
|
let role = move || problem.get().map(|o| o.and_then(|p| p.holds.get(&hold_position).copied()));
|
||||||
let role = Signal::derive(role);
|
let role = Signal::derive(role);
|
||||||
let cell = view! { <Hold role hold=hold.clone() /> };
|
let cell = view! { <Hold role hold=hold.clone() /> };
|
||||||
cells.push(cell);
|
cells.push(cell);
|
||||||
@@ -158,30 +291,34 @@ fn Grid(wall: models::Wall, problem: Signal<Option<models::Problem>>) -> impl In
|
|||||||
// TODO: refactor this to use the Problem component
|
// TODO: refactor this to use the Problem component
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn Hold(hold: models::Hold, role: Signal<Option<HoldRole>>) -> impl IntoView {
|
fn Hold(hold: models::Hold, role: Signal<Result<Option<HoldRole>, ServerFnError>>) -> impl IntoView {
|
||||||
tracing::trace!("Enter");
|
tracing::trace!("Enter");
|
||||||
let class = move || {
|
|
||||||
let role_classes = match role.get() {
|
move || {
|
||||||
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
|
let role = role.get()?;
|
||||||
Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"),
|
|
||||||
Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
|
let class = {
|
||||||
Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
|
let role_classes = match role {
|
||||||
None => Some("brightness-50"),
|
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
|
||||||
// None => None,
|
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-sky-100 aspect-square rounded hover:brightness-125".to_string();
|
||||||
|
if let Some(c) = role_classes {
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(c);
|
||||||
|
}
|
||||||
|
s
|
||||||
};
|
};
|
||||||
let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
|
|
||||||
if let Some(c) = role_classes {
|
|
||||||
s.push(' ');
|
|
||||||
s.push_str(c);
|
|
||||||
}
|
|
||||||
s
|
|
||||||
};
|
|
||||||
|
|
||||||
let img = hold.image.map(|img| {
|
let img = hold.image.as_ref().map(|img| {
|
||||||
let srcset = img.srcset();
|
let srcset = img.srcset();
|
||||||
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
|
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
|
||||||
});
|
});
|
||||||
|
|
||||||
tracing::trace!("view");
|
let view = view! { <div class=class>{img}</div> };
|
||||||
view! { <div class=class>{img}</div> }
|
Ok::<_, ServerFnError>(view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
crates/ascend/src/resources.rs
Normal file
59
crates/ascend/src/resources.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::codec::ron::Ron;
|
||||||
|
use crate::codec::ron::RonEncoded;
|
||||||
|
use crate::models::{self};
|
||||||
|
use leptos::prelude::Get;
|
||||||
|
use leptos::prelude::Signal;
|
||||||
|
use leptos::server::Resource;
|
||||||
|
use server_fn::ServerFnError;
|
||||||
|
|
||||||
|
type RonResource<T> = Resource<Result<T, ServerFnError>, Ron>;
|
||||||
|
|
||||||
|
pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wall> {
|
||||||
|
Resource::new_with_options(
|
||||||
|
move || wall_uid.get(),
|
||||||
|
move |wall_uid| async move { crate::server_functions::get_wall_by_uid(wall_uid).await.map(RonEncoded::into_inner) },
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn problem_by_uid(wall_uid: Signal<models::WallUid>, problem_uid: Signal<Option<models::ProblemUid>>) -> RonResource<Option<models::Problem>> {
|
||||||
|
Resource::new_with_options(
|
||||||
|
move || (wall_uid.get(), problem_uid.get()),
|
||||||
|
move |(wall_uid, problem_uid)| async move {
|
||||||
|
let Some(problem_uid) = problem_uid else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
crate::server_functions::get_problem_by_uid(wall_uid, problem_uid)
|
||||||
|
.await
|
||||||
|
.map(RonEncoded::into_inner)
|
||||||
|
.map(Some)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<models::Problem>> {
|
||||||
|
Resource::new_with_options(
|
||||||
|
move || wall_uid.get(),
|
||||||
|
move |wall_uid| async move { crate::server_functions::get_problems_for_wall(wall_uid).await.map(RonEncoded::into_inner) },
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_interaction(
|
||||||
|
wall_uid: Signal<models::WallUid>,
|
||||||
|
problem_uid: Signal<Option<models::ProblemUid>>,
|
||||||
|
) -> RonResource<Option<models::UserInteraction>> {
|
||||||
|
Resource::new_with_options(
|
||||||
|
move || (wall_uid.get(), problem_uid.get()),
|
||||||
|
move |(wall_uid, problem_uid)| async move {
|
||||||
|
let Some(problem_uid) = problem_uid else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
crate::server_functions::get_user_interaction(wall_uid, problem_uid)
|
||||||
|
.await
|
||||||
|
.map(RonEncoded::into_inner)
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -56,6 +56,16 @@ impl Database {
|
|||||||
self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into))
|
self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn set_version(&self, version: Version) -> Result<(), DatabaseOperationError> {
|
||||||
|
self.write(|txn| {
|
||||||
|
let mut table = txn.open_table(TABLE_VERSION)?;
|
||||||
|
table.insert((), version)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
@@ -75,7 +85,7 @@ impl DatabaseOperationError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
|
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
|
||||||
#[derive(Serialize, Deserialize, Debug, derive_more::Display)]
|
#[derive(Serialize, Deserialize, Debug, derive_more::Display, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[display("{version}")]
|
#[display("{version}")]
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
pub version: u64,
|
pub version: u64,
|
||||||
@@ -86,6 +96,7 @@ impl Version {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: implement test
|
||||||
#[tracing::instrument(skip_all, err)]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> {
|
pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> {
|
||||||
db.write(|txn| {
|
db.write(|txn| {
|
||||||
@@ -116,6 +127,13 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
|
|||||||
let table = txn.open_table(current::TABLE_PROBLEMS)?;
|
let table = txn.open_table(current::TABLE_PROBLEMS)?;
|
||||||
assert!(table.is_empty()?);
|
assert!(table.is_empty()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User table
|
||||||
|
{
|
||||||
|
// Opening the table creates the table
|
||||||
|
let table = txn.open_table(current::TABLE_USER)?;
|
||||||
|
assert!(table.is_empty()?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -126,7 +144,26 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
|
|||||||
}
|
}
|
||||||
|
|
||||||
use crate::models;
|
use crate::models;
|
||||||
pub use v2 as current;
|
pub mod current {
|
||||||
|
use super::v2;
|
||||||
|
use super::v3;
|
||||||
|
pub use v2::TABLE_PROBLEMS;
|
||||||
|
pub use v2::TABLE_ROOT;
|
||||||
|
pub use v2::TABLE_WALLS;
|
||||||
|
pub use v3::TABLE_USER;
|
||||||
|
pub use v3::VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod v3 {
|
||||||
|
use crate::models;
|
||||||
|
use crate::server::db::bincode::Bincode;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
|
||||||
|
pub const VERSION: u64 = 3;
|
||||||
|
|
||||||
|
pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v3::UserInteraction>> =
|
||||||
|
TableDefinition::new("user");
|
||||||
|
}
|
||||||
|
|
||||||
pub mod v2 {
|
pub mod v2 {
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
use super::db::Database;
|
use super::db::Database;
|
||||||
|
use super::db::DatabaseOperationError;
|
||||||
|
use super::db::{self};
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub async fn run_migrations(_db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if is_at_version(db, 2).await? {
|
||||||
|
migrate_to_v3(db).await?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
pub async fn migrate_to_v3(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use redb::ReadableTableMetadata;
|
||||||
|
tracing::warn!("MIGRATING TO VERSION 3");
|
||||||
|
|
||||||
|
db.write(|txn| {
|
||||||
|
// Opening the table creates the table
|
||||||
|
let table = txn.open_table(db::current::TABLE_USER)?;
|
||||||
|
assert!(table.is_empty()?);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
db.set_version(db::Version { version: db::v3::VERSION }).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_at_version(db: &Database, version: u64) -> Result<bool, DatabaseOperationError> {
|
||||||
|
let v = db.get_version().await?;
|
||||||
|
Ok(v == Some(db::Version { version }))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::codec::ron::Ron;
|
use crate::codec::ron::Ron;
|
||||||
use crate::codec::ron::RonEncoded;
|
use crate::codec::ron::RonEncoded;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
use crate::models::UserInteraction;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
use server_fn::ServerFnError;
|
use server_fn::ServerFnError;
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
|
|||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
use redb::ReadableTable;
|
use redb::ReadableTable;
|
||||||
tracing::debug!("Enter");
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
let db = expect_context::<Database>();
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
@@ -26,8 +27,6 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::debug!("Exit");
|
|
||||||
|
|
||||||
Ok(RonEncoded::new(walls))
|
Ok(RonEncoded::new(walls))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,11 +36,11 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
|
|||||||
custom = RonEncoded
|
custom = RonEncoded
|
||||||
)]
|
)]
|
||||||
#[tracing::instrument(skip_all, err(Debug))]
|
#[tracing::instrument(skip_all, err(Debug))]
|
||||||
pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
|
pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
|
||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
tracing::debug!("Enter");
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -63,8 +62,6 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<mod
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::debug!("ok");
|
|
||||||
|
|
||||||
Ok(RonEncoded::new(wall))
|
Ok(RonEncoded::new(wall))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +75,7 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<R
|
|||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
tracing::debug!("Enter");
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -122,8 +119,6 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<R
|
|||||||
|
|
||||||
let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
|
let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
|
||||||
|
|
||||||
tracing::debug!("ok");
|
|
||||||
|
|
||||||
Ok(RonEncoded::new(problems))
|
Ok(RonEncoded::new(problems))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +127,59 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<R
|
|||||||
output = Ron,
|
output = Ron,
|
||||||
custom = RonEncoded
|
custom = RonEncoded
|
||||||
)]
|
)]
|
||||||
#[tracing::instrument(skip_all, err(Debug))]
|
#[tracing::instrument(err(Debug))]
|
||||||
pub(crate) async fn get_problem(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<RonEncoded<models::Problem>, ServerFnError> {
|
pub(crate) async fn get_user_interaction(
|
||||||
|
wall_uid: models::WallUid,
|
||||||
|
problem_uid: models::ProblemUid,
|
||||||
|
) -> Result<RonEncoded<Option<models::UserInteraction>>, ServerFnError> {
|
||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
tracing::debug!("Enter");
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
|
enum Error {
|
||||||
|
#[display("Wall not found: {_0:?}")]
|
||||||
|
WallNotFound(#[error(not(source))] models::WallUid),
|
||||||
|
|
||||||
|
DatabaseOperation(DatabaseOperationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<Option<UserInteraction>, Error> {
|
||||||
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
|
let user_interaction = db
|
||||||
|
.read(|txn| {
|
||||||
|
let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||||
|
let user_interaction = user_table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
|
||||||
|
Ok(user_interaction)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user_interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_interaction = inner(wall_uid, problem_uid)
|
||||||
|
.await
|
||||||
|
.map_err(error_reporter::Report::new)
|
||||||
|
.map_err(ServerFnError::new)?;
|
||||||
|
Ok(RonEncoded::new(user_interaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(
|
||||||
|
input = Ron,
|
||||||
|
output = Ron,
|
||||||
|
custom = RonEncoded
|
||||||
|
)]
|
||||||
|
#[tracing::instrument(skip_all, err(Debug))]
|
||||||
|
pub(crate) async fn get_problem_by_uid(
|
||||||
|
wall_uid: models::WallUid,
|
||||||
|
problem_uid: models::ProblemUid,
|
||||||
|
) -> Result<RonEncoded<models::Problem>, ServerFnError> {
|
||||||
|
use crate::server::db::Database;
|
||||||
|
use crate::server::db::DatabaseOperationError;
|
||||||
|
use leptos::prelude::expect_context;
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||||
enum Error {
|
enum Error {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
},
|
},
|
||||||
// https://tailwindcss.com/docs/content-configuration#using-regular-expressions
|
// https://tailwindcss.com/docs/content-configuration#using-regular-expressions
|
||||||
safelist: [
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern: /bg-transparent/,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pattern: /grid-cols-.+/,
|
pattern: /grid-cols-.+/,
|
||||||
},
|
},
|
||||||
|
|||||||
18
flake.lock
generated
18
flake.lock
generated
@@ -10,11 +10,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1740139153,
|
"lastModified": 1740523129,
|
||||||
"narHash": "sha256-Xa1wCQBbsFHCaXgVBjtraZcWywuXBN+YhdqGle4nLVc=",
|
"narHash": "sha256-q/k/T9Hf+aCo8/xQnqyw+E7dYx8Nq1u7KQ2ylORcP+M=",
|
||||||
"owner": "plul",
|
"owner": "plul",
|
||||||
"repo": "basecamp",
|
"repo": "basecamp",
|
||||||
"rev": "0a29da733dc2f7b386dd3667b63a51c55238fbfd",
|
"rev": "0882906c106ab0bf193b3417c845c5accbec2419",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1740396192,
|
"lastModified": 1740547748,
|
||||||
"narHash": "sha256-ATMHHrg3sG1KgpQA5x8I+zcYpp5Sf17FaFj/fN+8OoQ=",
|
"narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d9b69c3ec2a2e2e971c534065bdd53374bd68b97",
|
"rev": "3a05eebede89661660945da1f151959900903b6a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -53,11 +53,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1740450604,
|
"lastModified": 1740623427,
|
||||||
"narHash": "sha256-T/lqASXzCzp5lJISCUw+qwfRmImVUnhKgAhn8ymRClI=",
|
"narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "5961ca311c85c31fc5f51925b4356899eed36221",
|
"rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
rust.toolchain.targets = [ "wasm32-unknown-unknown" ];
|
rust.toolchain.targets = [ "wasm32-unknown-unknown" ];
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
|
pkgs.bacon
|
||||||
pkgs.cargo-leptos
|
pkgs.cargo-leptos
|
||||||
pkgs.leptosfmt
|
pkgs.leptosfmt
|
||||||
pkgs.dart-sass
|
pkgs.dart-sass
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
pkgs.binaryen
|
pkgs.binaryen
|
||||||
];
|
];
|
||||||
|
|
||||||
env.RUST_LOG = "info,ascend=trace";
|
env.RUST_LOG = "info,ascend=debug";
|
||||||
env.MOONBOARD_PROBLEMS = "moonboard-problems";
|
env.MOONBOARD_PROBLEMS = "moonboard-problems";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
3
justfile
3
justfile
@@ -53,3 +53,6 @@ leptos-discord:
|
|||||||
|
|
||||||
leptos-issues:
|
leptos-issues:
|
||||||
xdg-open "https://github.com/leptos-rs/leptos/issues"
|
xdg-open "https://github.com/leptos-rs/leptos/issues"
|
||||||
|
|
||||||
|
icons:
|
||||||
|
xdg-open "https://heroicons.com/"
|
||||||
|
|||||||
4
leptosfmt.toml
Normal file
4
leptosfmt.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
closing_tag_style = "SelfClosing"
|
||||||
|
|
||||||
|
[attr_values]
|
||||||
|
class = "Tailwind"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
style_edition = "2024"
|
edition = "2024"
|
||||||
unstable_features = true
|
unstable_features = true
|
||||||
imports_granularity = "Item"
|
imports_granularity = "Item"
|
||||||
group_imports = "One"
|
group_imports = "One"
|
||||||
|
|||||||
3
todo.md
3
todo.md
@@ -9,5 +9,6 @@
|
|||||||
- decide on holds vs wall-edit terminology
|
- decide on holds vs wall-edit terminology
|
||||||
- clock
|
- clock
|
||||||
- hotkeys (enter =next problem, arrow = shift left/right up/down)
|
- hotkeys (enter =next problem, arrow = shift left/right up/down)
|
||||||
- remove brightness reduction when mousing over holds
|
|
||||||
- impl `sizes` hint next to `srcset`
|
- impl `sizes` hint next to `srcset`
|
||||||
|
- add refresh wall button for when a hold is changed
|
||||||
|
- fix a font, or why does font-thin not do anything?
|
||||||
|
|||||||
Reference in New Issue
Block a user