feat: store attempts
This commit is contained in:
parent
7118b66104
commit
2e83efcf12
@ -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>
|
||||||
|
58
crates/ascend/src/components/attempt.rs
Normal file
58
crates/ascend/src/components/attempt.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use crate::components::icons;
|
||||||
|
use crate::models;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub fn Attempt(#[prop(into)] date: Signal<chrono::DateTime<chrono::Utc>>, #[prop(into)] attempt: Signal<Option<models::Attempt>>) -> impl IntoView {
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
let s = time_ago(date.get());
|
||||||
|
|
||||||
|
let text = move || match attempt.get() {
|
||||||
|
Some(models::Attempt::Flash) => "Flash",
|
||||||
|
Some(models::Attempt::Send) => "Send",
|
||||||
|
Some(models::Attempt::Attempt) => "Learning experience",
|
||||||
|
None => "No attempt",
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_color = match attempt.get() {
|
||||||
|
Some(models::Attempt::Flash) => "text-cyan-500",
|
||||||
|
Some(models::Attempt::Send) => "text-teal-500",
|
||||||
|
Some(models::Attempt::Attempt) => "text-pink-500",
|
||||||
|
None => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = move || match attempt.get() {
|
||||||
|
Some(models::Attempt::Flash) => view! { <icons::BoltSolid /> }.into_any(),
|
||||||
|
Some(models::Attempt::Send) => view! { <icons::Trophy /> }.into_any(),
|
||||||
|
Some(models::Attempt::Attempt) => view! { <icons::ArrowTrendingUp /> }.into_any(),
|
||||||
|
None => view! { <icons::NoSymbol /> }.into_any(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let classes = format!("flex flex-row gap-3 {}", text_color);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-row justify-between my-2">
|
||||||
|
<div>{s}</div>
|
||||||
|
|
||||||
|
<div class=classes>
|
||||||
|
<span>{text}</span>
|
||||||
|
<span>{icon}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn time_ago(dt: chrono::DateTime<chrono::Utc>) -> String {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let duration = now.signed_duration_since(dt);
|
||||||
|
|
||||||
|
if duration.num_days() == 0 {
|
||||||
|
"Today".to_string()
|
||||||
|
} else if duration.num_days() == 1 {
|
||||||
|
"1 day ago".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{} days ago", duration.num_days())
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,79 @@
|
|||||||
|
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 margin = "mx-2 my-1 sm:mx-5 sm:my-2.5";
|
||||||
|
|
||||||
|
let icon_view = icon.get().map(|i| {
|
||||||
|
let icon_view = i.into_view();
|
||||||
|
let mut classes = "self-center".to_string();
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(margin);
|
||||||
|
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 = {
|
||||||
|
let mut classes = "self-center uppercase w-full text-sm sm:text-base md:text-lg font-thin".to_string();
|
||||||
|
classes.push(' ');
|
||||||
|
classes.push_str(margin);
|
||||||
|
|
||||||
|
view! { <div class=classes>{text.get()}</div> }
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button on:click=onclick type="button" class="hover:brightness-125 active:brightness-90">
|
||||||
on:click=onclick
|
<OutlinedBox color highlight>
|
||||||
type="button"
|
<div class="flex items-stretch">{icon_view} {separator} {text_view}</div>
|
||||||
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"
|
</OutlinedBox>
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</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>
|
||||||
|
}
|
||||||
|
}
|
46
crates/ascend/src/components/outlined_box.rs
Normal file
46
crates/ascend/src/components/outlined_box.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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-800",
|
||||||
|
Gradient::TealLime => "bg-teal-700",
|
||||||
|
Gradient::PurplePink => "bg-purple-900",
|
||||||
|
Gradient::PurpleBlue => "bg-purple-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>
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,17 @@ use leptos::prelude::*;
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
|
pub fn ProblemInfo(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
|
||||||
tracing::trace!("Enter problem info");
|
tracing::trace!("Enter problem info");
|
||||||
|
|
||||||
let name = problem.name;
|
let name = Signal::derive(move || problem.read().name.clone());
|
||||||
let set_by = problem.set_by;
|
let set_by = Signal::derive(move || problem.read().set_by.clone());
|
||||||
let method = problem.method;
|
let method = Signal::derive(move || problem.read().method.to_string());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-rows-none grid-cols-[auto,1fr] gap-0.5">
|
<div class="grid grid-rows-none gap-y-1 gap-x-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 />
|
||||||
<NameValue name="Set By:" value=set_by />
|
<NameValue name="Set By:" value=set_by />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -21,9 +21,9 @@ pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[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: Signal<String>, #[prop(into)] value: Signal<String>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<p class="text-orange-300 mr-4 text-right">{name}</p>
|
<p class="text-sm font-light mr-4 text-right text-orange-300">{name.get()}</p>
|
||||||
<p class="text-white font-semibold">{value}</p>
|
<p class="text-white">{value.get()}</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
40
crates/ascend/src/gradient.rs
Normal file
40
crates/ascend/src/gradient.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
|
pub enum Gradient {
|
||||||
|
#[default]
|
||||||
|
PurpleBlue,
|
||||||
|
PinkOrange,
|
||||||
|
CyanBlue,
|
||||||
|
TealLime,
|
||||||
|
PurplePink,
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
Gradient::PurplePink => "from-purple-500",
|
||||||
|
Gradient::PurpleBlue => "from-purple-600",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_to(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Gradient::PinkOrange => "to-orange-400",
|
||||||
|
Gradient::CyanBlue => "to-blue-500",
|
||||||
|
Gradient::TealLime => "to-lime-300",
|
||||||
|
Gradient::PurplePink => "to-pink-500",
|
||||||
|
Gradient::PurpleBlue => "to-blue-500",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn class_text(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Gradient::PinkOrange => "text-pink-500",
|
||||||
|
Gradient::CyanBlue => "text-cyan-500",
|
||||||
|
Gradient::TealLime => "text-teal-300",
|
||||||
|
Gradient::PurplePink => "text-purple-500",
|
||||||
|
Gradient::PurpleBlue => "text-purple-600",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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">
|
||||||
@ -105,7 +105,7 @@ fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let upload = Action::from(ServerAction::<SetImage>::new());
|
let upload = ServerAction::<SetImage>::new();
|
||||||
|
|
||||||
let hold = Signal::derive(move || {
|
let hold = Signal::derive(move || {
|
||||||
let refreshed = upload.value().get().map(Result::unwrap);
|
let refreshed = upload.value().get().map(Result::unwrap);
|
||||||
@ -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>
|
||||||
|
@ -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>
|
<div class="container mx-auto mt-2" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +32,7 @@ pub fn Settings() -> impl IntoView {
|
|||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn Import(wall_uid: WallUid) -> impl IntoView {
|
fn Import(wall_uid: WallUid) -> impl IntoView {
|
||||||
let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
|
let import_from_mini_moonboard = ServerAction::<ImportFromMiniMoonboard>::new();
|
||||||
|
|
||||||
let onclick = move |_mouse_event| {
|
let onclick = move |_mouse_event| {
|
||||||
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
||||||
@ -45,7 +44,7 @@ fn Import(wall_uid: WallUid) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(name = ImportFromMiniMoonboard)]
|
#[server]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> {
|
async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> {
|
||||||
use crate::server::config::Config;
|
use crate::server::config::Config;
|
||||||
|
@ -1,14 +1,51 @@
|
|||||||
|
// +--------------- Filter ----------- ↓ -+
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
|
// +---------------------------+
|
||||||
|
// | Next Problem |
|
||||||
|
// +---------------------------+
|
||||||
|
|
||||||
|
// +--------------- Problem --------------+
|
||||||
|
// | Name: ... |
|
||||||
|
// | Method: ... |
|
||||||
|
// | Set by: ... |
|
||||||
|
// | |
|
||||||
|
// | | Flash | Top | Attempt | |
|
||||||
|
// | |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
|
// +---------+ +---------+ +---------+
|
||||||
|
// | Flash | | Send | | Attempt |
|
||||||
|
// +---------+ +---------+ +---------+
|
||||||
|
|
||||||
|
// +---------- <Latest attempt> ----------+
|
||||||
|
// | Today: <Attempt> |
|
||||||
|
// | 14 days ago: <Attempt> |
|
||||||
|
// +--------------------------------------+
|
||||||
|
|
||||||
use crate::codec::ron::RonEncoded;
|
use crate::codec::ron::RonEncoded;
|
||||||
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::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 crate::server_functions;
|
||||||
use leptos::Params;
|
use leptos::Params;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_router::params::Params;
|
use leptos_router::params::Params;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
#[derive(Params, PartialEq, Clone)]
|
#[derive(Params, PartialEq, Clone)]
|
||||||
struct RouteParams {
|
struct RouteParams {
|
||||||
@ -22,8 +59,6 @@ pub fn Wall() -> impl IntoView {
|
|||||||
|
|
||||||
let route_params = leptos_router::hooks::use_params::<RouteParams>();
|
let route_params = leptos_router::hooks::use_params::<RouteParams>();
|
||||||
|
|
||||||
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
|
|
||||||
|
|
||||||
let wall_uid = Signal::derive(move || {
|
let wall_uid = Signal::derive(move || {
|
||||||
route_params
|
route_params
|
||||||
.get()
|
.get()
|
||||||
@ -34,49 +69,6 @@ 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_action = Action::new(move |&(wall_uid, problem_uid): &(models::WallUid, models::ProblemUid)| async move {
|
|
||||||
tracing::info!("fetching");
|
|
||||||
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| {
|
|
||||||
set_problem_uid.set(wall.random_problem());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set a problem when wall is set (loaded)
|
|
||||||
Effect::new(move |_prev_value| {
|
|
||||||
problem_action.value().write_only().set(None);
|
|
||||||
|
|
||||||
match &*wall.read() {
|
|
||||||
Some(Ok(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 => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// On change of problem UID, dispatch an action to fetch the problem
|
|
||||||
Effect::new(move |_prev_value| match problem_uid.get() {
|
|
||||||
Some(problem_uid) => {
|
|
||||||
problem_action.dispatch((wall_uid.get(), problem_uid));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
problem_action.value().write_only().set(None);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let header_items = move || HeaderItems {
|
let header_items = move || HeaderItems {
|
||||||
left: vec![],
|
left: vec![],
|
||||||
middle: vec![HeaderItem {
|
middle: vec![HeaderItem {
|
||||||
@ -96,37 +88,18 @@ 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 v = view! {
|
let v = view! { <WithWall wall /> };
|
||||||
<div class="grid grid-cols-1 md:grid-cols-[auto,1fr] gap-8">
|
|
||||||
<div>
|
|
||||||
<Grid wall=wall.clone() problem=problem_signal />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
onclick=move |_| fn_next_problem(&wall)
|
|
||||||
text="➤ Next problem"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="m-4"/>
|
|
||||||
|
|
||||||
{move || problem_signal.get().map(|problem| view! { <ProblemInfo problem /> })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
Ok::<_, ServerFnError>(v)
|
Ok::<_, ServerFnError>(v)
|
||||||
})}
|
})}
|
||||||
</Suspense>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -134,12 +107,271 @@ pub fn Wall() -> impl IntoView {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn Grid(wall: models::Wall, problem: Signal<Option<models::Problem>>) -> impl IntoView {
|
fn WithProblem(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
view! { <ProblemInfo problem /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn WithWall(#[prop(into)] wall: Signal<models::Wall>) -> impl IntoView {
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
let wall_uid = Signal::derive(move || wall.read().uid);
|
||||||
|
|
||||||
|
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
|
||||||
|
|
||||||
|
let problem = crate::resources::problem_by_uid_optional(wall_uid, problem_uid.into());
|
||||||
|
let user_interaction = crate::resources::user_interaction(wall_uid, problem_uid.into());
|
||||||
|
|
||||||
|
let fn_next_problem = move |wall: &models::Wall| {
|
||||||
|
set_problem_uid.set(wall.random_problem());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a problem when wall is set (loaded)
|
||||||
|
Effect::new(move |_prev_value| {
|
||||||
|
if problem_uid.get().is_none() {
|
||||||
|
tracing::debug!("Setting next problem");
|
||||||
|
fn_next_problem(&wall.read());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// merge outer option (resource hasn't resolved yet) with inner option (there is no problem for the wall)
|
||||||
|
let problem_signal = Signal::derive(move || problem.get().transpose().map(Option::flatten));
|
||||||
|
|
||||||
|
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.get() problem=problem_signal /> };
|
||||||
|
Ok::<_, ServerFnError>(view)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="inline-grid grid-cols-1 gap-8 md:grid-cols-[auto,1fr]">
|
||||||
|
<div>{grid}</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Section title="Filter">{}</Section>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="self-center">
|
||||||
|
<Button
|
||||||
|
icon=Icon::ArrowPath
|
||||||
|
text="Next problem"
|
||||||
|
onclick=move |_| fn_next_problem(&wall.read())
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Section title="Problem">
|
||||||
|
<Transition fallback=|| ()>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
tracing::info!("executing problem suspend");
|
||||||
|
let problem = problem.await?;
|
||||||
|
let view = problem
|
||||||
|
.map(|problem| {
|
||||||
|
view! { <WithProblem problem /> }
|
||||||
|
});
|
||||||
|
Ok::<_, ServerFnError>(view)
|
||||||
|
})}
|
||||||
|
</Transition>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Transition fallback=move || ()>
|
||||||
|
{move || {
|
||||||
|
Suspend::new(async move {
|
||||||
|
tracing::debug!("getting user interaction");
|
||||||
|
let user_interaction: Option<_> = user_interaction.await?;
|
||||||
|
tracing::debug!("got user interaction");
|
||||||
|
let Some(problem_uid) = problem_uid.get() else {
|
||||||
|
return Ok(view! {}.into_any());
|
||||||
|
};
|
||||||
|
let view = view! {
|
||||||
|
<WithUserInteraction wall_uid problem_uid user_interaction />
|
||||||
|
}
|
||||||
|
.into_any();
|
||||||
|
Ok::<_, ServerFnError>(view)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn WithUserInteraction(
|
||||||
|
#[prop(into)] wall_uid: Signal<models::WallUid>,
|
||||||
|
#[prop(into)] problem_uid: Signal<models::ProblemUid>,
|
||||||
|
#[prop(into)] user_interaction: Signal<Option<models::UserInteraction>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
tracing::debug!("Enter WithUserInteraction");
|
||||||
|
|
||||||
|
let user_interaction_rw = RwSignal::new(None);
|
||||||
|
Effect::new(move || {
|
||||||
|
let i = user_interaction.get();
|
||||||
|
tracing::info!("setting user interaction to parent user interaction value: {i:?}");
|
||||||
|
user_interaction_rw.set(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
let submit_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
|
||||||
|
let submit_attempt_value = submit_attempt.value();
|
||||||
|
Effect::new(move || {
|
||||||
|
tracing::info!("flaf");
|
||||||
|
if let Some(Ok(v)) = submit_attempt_value.get() {
|
||||||
|
tracing::info!("setting user interaction to action return value: {v:?}");
|
||||||
|
user_interaction_rw.set(Some(v.into_inner()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let latest_attempt = move || -> Option<_> {
|
||||||
|
let i = user_interaction_rw.read();
|
||||||
|
let i = (*i).as_ref();
|
||||||
|
let i = i?;
|
||||||
|
i.attempted_on.last_key_value().map(|(date, attempt)| (date.clone(), attempt.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let todays_attempt = move || -> Option<_> {
|
||||||
|
match latest_attempt() {
|
||||||
|
Some((datetime, attempt)) => {
|
||||||
|
let today_local_naive = chrono::Local::now().date_naive();
|
||||||
|
let datetime_local_naive = datetime.with_timezone(&chrono::Local).date_naive();
|
||||||
|
(datetime_local_naive == today_local_naive).then_some(attempt)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ui_is_flash = RwSignal::new(false);
|
||||||
|
let ui_is_send = RwSignal::new(false);
|
||||||
|
let ui_is_attempt = RwSignal::new(false);
|
||||||
|
let ui_is_favorite = RwSignal::new(false);
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
let attempt = todays_attempt();
|
||||||
|
ui_is_flash.set(matches!(attempt, Some(models::Attempt::Flash)));
|
||||||
|
ui_is_send.set(matches!(attempt, Some(models::Attempt::Send)));
|
||||||
|
ui_is_attempt.set(matches!(attempt, Some(models::Attempt::Attempt)));
|
||||||
|
});
|
||||||
|
|
||||||
|
let onclick_flash = move |_| {
|
||||||
|
submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
|
||||||
|
wall_uid: wall_uid.get(),
|
||||||
|
problem_uid: problem_uid.get(),
|
||||||
|
attempt: models::Attempt::Flash,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let onclick_send = move |_| {
|
||||||
|
submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
|
||||||
|
wall_uid: wall_uid.get(),
|
||||||
|
problem_uid: problem_uid.get(),
|
||||||
|
attempt: models::Attempt::Send,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
let onclick_attempt = move |_| {
|
||||||
|
submit_attempt.dispatch(RonEncoded(server_functions::UpsertTodaysAttempt {
|
||||||
|
wall_uid: wall_uid.get(),
|
||||||
|
problem_uid: problem_uid.get(),
|
||||||
|
attempt: models::Attempt::Attempt,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: loop over attempts in user_interaction
|
||||||
|
let v = move || latest_attempt().map(|(date, attempt)| view! { <Attempt date attempt /> });
|
||||||
|
|
||||||
|
let placeholder = move || {
|
||||||
|
latest_attempt().is_none().then(|| {
|
||||||
|
let today = chrono::Utc::now();
|
||||||
|
view! { <Attempt date=today attempt=None /> }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<AttemptRadio
|
||||||
|
flash=ui_is_flash
|
||||||
|
send=ui_is_send
|
||||||
|
attempt=ui_is_attempt
|
||||||
|
onclick_flash
|
||||||
|
onclick_send
|
||||||
|
onclick_attempt
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Section title="History">{placeholder} {v}</Section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn AttemptRadio(
|
||||||
|
#[prop(into)] flash: Signal<bool>,
|
||||||
|
#[prop(into)] send: Signal<bool>,
|
||||||
|
#[prop(into)] attempt: Signal<bool>,
|
||||||
|
onclick_flash: impl FnMut(MouseEvent) + 'static,
|
||||||
|
onclick_send: impl FnMut(MouseEvent) + 'static,
|
||||||
|
onclick_attempt: impl FnMut(MouseEvent) + 'static,
|
||||||
|
) -> impl IntoView {
|
||||||
|
tracing::debug!("Enter");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="gap-2 flex flex-row justify-evenly md:flex-col 2xl:flex-row">
|
||||||
|
<Button
|
||||||
|
onclick=onclick_flash
|
||||||
|
text="Flash"
|
||||||
|
icon=Icon::BoltSolid
|
||||||
|
color=Gradient::CyanBlue
|
||||||
|
highlight=Signal::derive(move || { flash.get() })
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onclick=onclick_send
|
||||||
|
text="Send"
|
||||||
|
icon=Icon::Trophy
|
||||||
|
color=Gradient::TealLime
|
||||||
|
highlight=Signal::derive(move || { send.get() })
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onclick=onclick_attempt
|
||||||
|
text="Attempt"
|
||||||
|
icon=Icon::ArrowTrendingUp
|
||||||
|
color=Gradient::PinkOrange
|
||||||
|
highlight=Signal::derive(move || { attempt.get() })
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
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,16 +390,19 @@ 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 || {
|
||||||
|
let role = role.get()?;
|
||||||
|
|
||||||
|
let class = {
|
||||||
|
let role_classes = match role {
|
||||||
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
|
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
|
||||||
Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"),
|
Some(HoldRole::Normal) => Some("outline outline-offset-2 outline-blue-500"),
|
||||||
Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
|
Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
|
||||||
Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
|
Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
|
||||||
None => Some("brightness-50"),
|
None => Some("brightness-50"),
|
||||||
// None => None,
|
|
||||||
};
|
};
|
||||||
let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
|
let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
|
||||||
if let Some(c) = role_classes {
|
if let Some(c) = role_classes {
|
||||||
@ -177,11 +412,29 @@ fn Hold(hold: models::Hold, role: Signal<Option<HoldRole>>) -> impl IntoView {
|
|||||||
s
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Separator() -> impl IntoView {
|
||||||
|
view! { <div class="m-2 sm:m-3 md:m-4 h-4" /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="bg-neutral-900 px-5 pt-3 pb-8 rounded-lg">
|
||||||
|
<div class="py-3 text-lg text-center text-orange-400 border-t border-orange-400">
|
||||||
|
{move || title.get()}
|
||||||
|
</div>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
63
crates/ascend/src/resources.rs
Normal file
63
crates/ascend/src/resources.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Version of [problem_by_uid] that short circuits if the input problem_uid signal is None.
|
||||||
|
pub fn problem_by_uid_optional(
|
||||||
|
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,11 @@
|
|||||||
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 derive_more::Display;
|
||||||
|
use derive_more::Error;
|
||||||
|
use derive_more::From;
|
||||||
|
use leptos::prelude::*;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
use server_fn::ServerFnError;
|
use server_fn::ServerFnError;
|
||||||
|
|
||||||
@ -14,7 +19,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 +31,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 +40,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 +66,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 +79,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 +123,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 +131,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 {
|
||||||
@ -160,3 +206,74 @@ pub(crate) async fn get_problem(wall_uid: models::WallUid, problem_uid: models::
|
|||||||
|
|
||||||
Ok(RonEncoded::new(problem))
|
Ok(RonEncoded::new(problem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts or updates today's attempt.
|
||||||
|
#[server(
|
||||||
|
input = Ron,
|
||||||
|
output = Ron,
|
||||||
|
custom = RonEncoded
|
||||||
|
)]
|
||||||
|
#[tracing::instrument(err(Debug))]
|
||||||
|
pub(crate) async fn upsert_todays_attempt(
|
||||||
|
wall_uid: models::WallUid,
|
||||||
|
problem_uid: models::ProblemUid,
|
||||||
|
attempt: models::Attempt,
|
||||||
|
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
|
||||||
|
use crate::server::db::Database;
|
||||||
|
use crate::server::db::DatabaseOperationError;
|
||||||
|
use leptos::prelude::expect_context;
|
||||||
|
|
||||||
|
tracing::trace!("Enter");
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Display, 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, attempt: models::Attempt) -> Result<UserInteraction, Error> {
|
||||||
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
|
let user_interaction = db
|
||||||
|
.write(|txn| {
|
||||||
|
let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||||
|
|
||||||
|
let key = (wall_uid, problem_uid);
|
||||||
|
|
||||||
|
// Pop or default
|
||||||
|
let mut user_interaction = user_table
|
||||||
|
.remove(key)?
|
||||||
|
.map(|guard| guard.value())
|
||||||
|
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem_uid));
|
||||||
|
|
||||||
|
// If the last entry is from today, remove it
|
||||||
|
if let Some(entry) = user_interaction.attempted_on.last_entry() {
|
||||||
|
let today_local_naive = chrono::Local::now().date_naive();
|
||||||
|
|
||||||
|
let entry_date = entry.key();
|
||||||
|
let entry_date_local_naive = entry_date.with_timezone(&chrono::Local).date_naive();
|
||||||
|
|
||||||
|
if entry_date_local_naive == today_local_naive {
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user_interaction.attempted_on.insert(chrono::Utc::now(), attempt);
|
||||||
|
|
||||||
|
user_table.insert(key, user_interaction.clone())?;
|
||||||
|
|
||||||
|
Ok(user_interaction)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user_interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner(wall_uid, problem_uid, attempt)
|
||||||
|
.await
|
||||||
|
.map_err(error_reporter::Report::new)
|
||||||
|
.map_err(ServerFnError::new)
|
||||||
|
.map(RonEncoded::new)
|
||||||
|
}
|
||||||
|
@ -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": {
|
||||||
|
@ -130,8 +130,10 @@
|
|||||||
basecamp.mkShell pkgs {
|
basecamp.mkShell pkgs {
|
||||||
rust.enable = true;
|
rust.enable = true;
|
||||||
rust.toolchain.targets = [ "wasm32-unknown-unknown" ];
|
rust.toolchain.targets = [ "wasm32-unknown-unknown" ];
|
||||||
|
rust.toolchain.components.rust-analyzer.nightly = true;
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
|
pkgs.bacon
|
||||||
pkgs.cargo-leptos
|
pkgs.cargo-leptos
|
||||||
pkgs.leptosfmt
|
pkgs.leptosfmt
|
||||||
pkgs.dart-sass
|
pkgs.dart-sass
|
||||||
@ -142,7 +144,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?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user