diff --git a/crates/ascend/src/components/icons.rs b/crates/ascend/src/components/icons.rs
new file mode 100644
index 0000000..68b5b9f
--- /dev/null
+++ b/crates/ascend/src/components/icons.rs
@@ -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! {
}.into_any(),
+ Icon::BoltSlashSolid => view! {
}.into_any(),
+ Icon::WrenchSolid => view! {
}.into_any(),
+ Icon::ForwardSolid => view! {
}.into_any(),
+ Icon::Check => view! {
}.into_any(),
+ Icon::HeartOutline => view! {
}.into_any(),
+ Icon::ArrowPath => view! {
}.into_any(),
+ Icon::PaperAirplaneSolid => view! {
}.into_any(),
+ Icon::NoSymbol => view! {
}.into_any(),
+ Icon::Trophy => view! {
}.into_any(),
+ Icon::ArrowTrendingUp => view! {
}.into_any(),
+ }
+ }
+}
+
+#[component]
+pub fn BoltSolid() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn BoltSlashSolid() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn WrenchSolid() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn ForwardSolid() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn Check() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn HeartOutline() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn ArrowPath() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn PaperAirplaneSolid() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn NoSymbol() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn Trophy() -> impl IntoView {
+ view! {
+
+ }
+}
+
+#[component]
+pub fn ArrowTrendingUp() -> impl IntoView {
+ view! {
+
+ }
+}
diff --git a/crates/ascend/src/components/outlined_box.rs b/crates/ascend/src/components/outlined_box.rs
new file mode 100644
index 0000000..4822bb8
--- /dev/null
+++ b/crates/ascend/src/components/outlined_box.rs
@@ -0,0 +1,46 @@
+use crate::gradient::Gradient;
+use leptos::prelude::*;
+
+#[component]
+pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highlight: MaybeProp
) -> 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! {
+
+ }
+}
diff --git a/crates/ascend/src/components/problem.rs b/crates/ascend/src/components/problem.rs
index d4236b6..a05cf36 100644
--- a/crates/ascend/src/components/problem.rs
+++ b/crates/ascend/src/components/problem.rs
@@ -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);
view! {
-
+
}
diff --git a/crates/ascend/src/components/problem_info.rs b/crates/ascend/src/components/problem_info.rs
index e5062d5..89ff02c 100644
--- a/crates/ascend/src/components/problem_info.rs
+++ b/crates/ascend/src/components/problem_info.rs
@@ -3,17 +3,17 @@ use leptos::prelude::*;
#[component]
#[tracing::instrument(skip_all)]
-pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
+pub fn ProblemInfo(#[prop(into)] problem: Signal
) -> impl IntoView {
tracing::trace!("Enter problem info");
- let name = problem.name;
- let set_by = problem.set_by;
- let method = problem.method;
+ let name = Signal::derive(move || problem.read().name.clone());
+ let set_by = Signal::derive(move || problem.read().set_by.clone());
+ let method = Signal::derive(move || problem.read().method.to_string());
view! {
-
+
-
+
}
@@ -21,9 +21,9 @@ pub fn ProblemInfo(problem: models::Problem) -> impl IntoView {
#[component]
#[tracing::instrument(skip_all)]
-fn NameValue(#[prop(into)] name: String, #[prop(into)] value: String) -> impl IntoView {
+fn NameValue(#[prop(into)] name: Signal
, #[prop(into)] value: Signal) -> impl IntoView {
view! {
- {name}
- {value}
+ {name.get()}
+ {value.get()}
}
}
diff --git a/crates/ascend/src/gradient.rs b/crates/ascend/src/gradient.rs
new file mode 100644
index 0000000..55095a3
--- /dev/null
+++ b/crates/ascend/src/gradient.rs
@@ -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",
+ }
+ }
+}
diff --git a/crates/ascend/src/lib.rs b/crates/ascend/src/lib.rs
index c3763a9..e0a7d37 100644
--- a/crates/ascend/src/lib.rs
+++ b/crates/ascend/src/lib.rs
@@ -6,55 +6,26 @@ pub mod pages {
pub mod wall;
}
pub mod components {
+ pub use attempt::Attempt;
pub use button::Button;
pub use header::StyledHeader;
pub use problem::Problem;
pub use problem_info::ProblemInfo;
+ pub mod attempt;
pub mod button;
+ pub mod checkbox;
pub mod header;
+ pub mod icons;
+ pub mod outlined_box;
pub mod problem;
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 = Resource, Ron>;
+pub mod gradient;
- pub fn wall_by_uid(wall_uid: Signal) -> RonResource {
- 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 mod resources;
- pub fn problem_by_uid(wall_uid: Signal, problem_uid: Signal) -> RonResource {
- 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) -> RonResource> {
- 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 models;
pub mod server_functions;
diff --git a/crates/ascend/src/models.rs b/crates/ascend/src/models.rs
index 68146bd..9973c4e 100644
--- a/crates/ascend/src/models.rs
+++ b/crates/ascend/src/models.rs
@@ -14,6 +14,61 @@ pub use v2::Root;
pub use v2::Wall;
pub use v2::WallDimensions;
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, 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, 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 {
use super::v1;
diff --git a/crates/ascend/src/pages/edit_wall.rs b/crates/ascend/src/pages/edit_wall.rs
index dd859a2..c95e80f 100644
--- a/crates/ascend/src/pages/edit_wall.rs
+++ b/crates/ascend/src/pages/edit_wall.rs
@@ -49,7 +49,7 @@ pub fn EditWall() -> impl IntoView {
};
leptos::view! {
-
+
@@ -105,7 +105,7 @@ fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
}
};
- let upload = Action::from(ServerAction::
::new());
+ let upload = ServerAction::::new();
let hold = Signal::derive(move || {
let refreshed = upload.value().get().map(Result::unwrap);
@@ -155,7 +155,7 @@ fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
view! {
impl IntoView {
let wall = crate::resources::wall_by_uid(wall_uid);
let problems = crate::resources::problems_for_wall(wall_uid);
- let header_items = HeaderItems {
+ let header_items = move || HeaderItems {
left: vec![HeaderItem {
text: "← Ascend".to_string(),
- link: Some("/".to_string()),
+ link: Some(format!("/wall/{}", wall_uid.get())),
}],
middle: vec![HeaderItem {
text: "Routes".to_string(),
@@ -63,7 +63,10 @@ pub fn Routes() -> impl IntoView {
each=problems_sample
key=|problem| problem.uid
children=move |problem: models::Problem| {
- view! {
}
+ view! {
+
+
+ }
}
/>
@@ -75,8 +78,8 @@ pub fn Routes() -> impl IntoView {
};
view! {
-
-
+
+
"loading" }>{suspend}
@@ -91,7 +94,7 @@ fn Problem(#[prop(into)] dim: Signal
, #[prop(into)] prob
view! {
diff --git a/crates/ascend/src/pages/settings.rs b/crates/ascend/src/pages/settings.rs
index f12bd3a..159979e 100644
--- a/crates/ascend/src/pages/settings.rs
+++ b/crates/ascend/src/pages/settings.rs
@@ -20,12 +20,11 @@ pub fn Settings() -> impl IntoView {
};
view! {
-
+
-
- // {move || view! { }}
-
+ // {move || view! {
}}
+
}
}
@@ -33,7 +32,7 @@ pub fn Settings() -> impl IntoView {
#[component]
#[tracing::instrument(skip_all)]
fn Import(wall_uid: WallUid) -> impl IntoView {
- let import_from_mini_moonboard = Action::from(ServerAction::
::new());
+ let import_from_mini_moonboard = ServerAction::::new();
let onclick = move |_mouse_event| {
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]
async fn import_from_mini_moonboard(wall_uid: WallUid) -> Result<(), ServerFnError> {
use crate::server::config::Config;
diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs
index 5cc0fa8..627dd21 100644
--- a/crates/ascend/src/pages/wall.rs
+++ b/crates/ascend/src/pages/wall.rs
@@ -1,14 +1,51 @@
+// +--------------- Filter ----------- ↓ -+
+// | |
+// | |
+// | |
+// | |
+// | |
+// | |
+// | |
+// +--------------------------------------+
+
+// +---------------------------+
+// | Next Problem |
+// +---------------------------+
+
+// +--------------- Problem --------------+
+// | Name: ... |
+// | Method: ... |
+// | Set by: ... |
+// | |
+// | | Flash | Top | Attempt | |
+// | |
+// +--------------------------------------+
+
+// +---------+ +---------+ +---------+
+// | Flash | | Send | | Attempt |
+// +---------+ +---------+ +---------+
+
+// +---------- ----------+
+// | Today: |
+// | 14 days ago: |
+// +--------------------------------------+
+
use crate::codec::ron::RonEncoded;
use crate::components::ProblemInfo;
+use crate::components::attempt::Attempt;
use crate::components::button::Button;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
+use crate::components::icons::Icon;
+use crate::gradient::Gradient;
use crate::models;
use crate::models::HoldRole;
+use crate::server_functions;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
+use web_sys::MouseEvent;
#[derive(Params, PartialEq, Clone)]
struct RouteParams {
@@ -22,8 +59,6 @@ pub fn Wall() -> impl IntoView {
let route_params = leptos_router::hooks::use_params::();
- let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::("problem");
-
let wall_uid = Signal::derive(move || {
route_params
.get()
@@ -34,49 +69,6 @@ pub fn Wall() -> impl IntoView {
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 {
left: vec![],
middle: vec![HeaderItem {
@@ -96,37 +88,18 @@ pub fn Wall() -> impl IntoView {
};
leptos::view! {
-
+
-
"Loading..." }
- }>
+
{move || Suspend::new(async move {
- tracing::info!("executing Suspend future");
+ tracing::info!("executing main suspend");
let wall = wall.await?;
- let v = view! {
-
-
-
-
-
-
-
-
-
-
- {move || problem_signal.get().map(|problem| view! {
})}
-
-
- };
+ let v = view! { };
Ok::<_, ServerFnError>(v)
})}
-
+
}
@@ -134,12 +107,271 @@ pub fn Wall() -> impl IntoView {
#[component]
#[tracing::instrument(skip_all)]
-fn Grid(wall: models::Wall, problem: Signal