From 22367f45f2a9d82b3e2239b5d332ab3ef8bb5d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Juul=20Brunsh=C3=B8j?= Date: Sun, 6 Apr 2025 22:44:47 +0200 Subject: [PATCH] feat: like button --- crates/ascend/src/components/attempt.rs | 4 +- crates/ascend/src/components/button.rs | 52 +++++++++-------- crates/ascend/src/components/icons.rs | 16 ++++++ crates/ascend/src/components/outlined_box.rs | 8 ++- crates/ascend/src/gradient.rs | 20 +++++-- crates/ascend/src/models/semantics.rs | 9 +++ crates/ascend/src/pages/wall.rs | 59 +++++++++++++++++--- crates/ascend/src/server_functions.rs | 59 ++++++++++++++++++++ 8 files changed, 183 insertions(+), 44 deletions(-) diff --git a/crates/ascend/src/components/attempt.rs b/crates/ascend/src/components/attempt.rs index 24f947f..eb21cb5 100644 --- a/crates/ascend/src/components/attempt.rs +++ b/crates/ascend/src/components/attempt.rs @@ -19,9 +19,7 @@ pub fn Attempt(#[prop(into)] date: Signal>, #[prop(into)] 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", + Some(attempt) => attempt.gradient().class_text(), None => "", }; diff --git a/crates/ascend/src/components/button.rs b/crates/ascend/src/components/button.rs index 845c75f..115ad24 100644 --- a/crates/ascend/src/components/button.rs +++ b/crates/ascend/src/components/button.rs @@ -19,34 +19,40 @@ pub fn Button( ) -> 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()); + let icon_view = move || { + 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! {
{icon_view}
} - }); + view! {
{icon_view}
} + }) + }; - let separator = (icon.read().is_some() && text.read().is_some()).then(|| { - let mut classes = "w-0.5 bg-linear-to-br min-w-0.5".to_string(); - classes.push(' '); - classes.push_str(color.class_from()); - classes.push(' '); - classes.push_str(color.class_to()); + let separator = move || { + (icon.read().is_some() && text.read().is_some()).then(|| { + let mut classes = "w-0.5 bg-linear-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! {
} - }); + view! {
} + }) + }; - let text_view = text.get().map(|text| { - 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); + let text_view = move || { + text.get().map(|text| { + 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! {
{text}
} - }); + view! {
{text}
} + }) + }; let class = move || { let mut classes = vec![]; diff --git a/crates/ascend/src/components/icons.rs b/crates/ascend/src/components/icons.rs index 33e41a6..c028e2d 100644 --- a/crates/ascend/src/components/icons.rs +++ b/crates/ascend/src/components/icons.rs @@ -7,6 +7,7 @@ pub enum Icon { WrenchSolid, ForwardSolid, Check, + Heart, HeartOutline, ArrowPath, PaperAirplaneSolid, @@ -26,6 +27,7 @@ impl Icon { Icon::WrenchSolid => view! { }.into_any(), Icon::ForwardSolid => view! { }.into_any(), Icon::Check => view! { }.into_any(), + Icon::Heart => view! { }.into_any(), Icon::HeartOutline => view! { }.into_any(), Icon::ArrowPath => view! { }.into_any(), Icon::PaperAirplaneSolid => view! { }.into_any(), @@ -123,6 +125,20 @@ pub fn Check() -> impl IntoView { } } +#[component] +pub fn Heart() -> impl IntoView { + view! { + + + + } +} + #[component] pub fn HeartOutline() -> impl IntoView { view! { diff --git a/crates/ascend/src/components/outlined_box.rs b/crates/ascend/src/components/outlined_box.rs index 6b94c42..9c01db9 100644 --- a/crates/ascend/src/components/outlined_box.rs +++ b/crates/ascend/src/components/outlined_box.rs @@ -22,12 +22,14 @@ pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highli let mut c = "py-1.5 rounded-md".to_string(); if highlight() { let bg = match color { - Gradient::PinkOrange => "bg-pink-900", + Gradient::PinkOrange => "bg-rose-900", Gradient::CyanBlue => "bg-cyan-800", - Gradient::TealLime => "bg-teal-700", - Gradient::PurplePink => "bg-purple-900", + Gradient::TealLime => "bg-emerald-700", + Gradient::PurplePink => "bg-fuchsia-950", Gradient::PurpleBlue => "bg-purple-900", Gradient::Orange => "bg-orange-900", + Gradient::Pink => "bg-pink-900", + Gradient::PinkRed => "bg-red-900", }; c.push(' '); diff --git a/crates/ascend/src/gradient.rs b/crates/ascend/src/gradient.rs index 50a3078..2729380 100644 --- a/crates/ascend/src/gradient.rs +++ b/crates/ascend/src/gradient.rs @@ -7,9 +7,11 @@ pub enum Gradient { PurplePink, #[default] Orange, + Pink, + PinkRed, } impl Gradient { - pub fn class_from(&self) -> &str { + pub fn class_from(&self) -> &'static str { match self { Gradient::PinkOrange => "from-pink-500", Gradient::CyanBlue => "from-cyan-500", @@ -17,10 +19,12 @@ impl Gradient { Gradient::PurplePink => "from-purple-500", Gradient::PurpleBlue => "from-purple-600", Gradient::Orange => "from-orange-400", + Gradient::Pink => "from-pink-400", + Gradient::PinkRed => "from-pink-400", } } - pub fn class_to(&self) -> &str { + pub fn class_to(&self) -> &'static str { match self { Gradient::PinkOrange => "to-orange-400", Gradient::CyanBlue => "to-blue-500", @@ -28,17 +32,21 @@ impl Gradient { Gradient::PurplePink => "to-pink-500", Gradient::PurpleBlue => "to-blue-500", Gradient::Orange => "to-orange-500", + Gradient::Pink => "to-pink-500", + Gradient::PinkRed => "to-red-500", } } - pub fn class_text(&self) -> &str { + pub fn class_text(&self) -> &'static str { match self { - Gradient::PinkOrange => "text-pink-500", + Gradient::PinkOrange => "text-rose-400", Gradient::CyanBlue => "text-cyan-500", - Gradient::TealLime => "text-teal-300", - Gradient::PurplePink => "text-purple-500", + Gradient::TealLime => "text-emerald-300", + Gradient::PurplePink => "text-fuchsia-500", Gradient::PurpleBlue => "text-purple-600", Gradient::Orange => "text-orange-400", + Gradient::Pink => "text-pink-400", + Gradient::PinkRed => "text-pink-400", } } } diff --git a/crates/ascend/src/models/semantics.rs b/crates/ascend/src/models/semantics.rs index 0c7b040..c45c942 100644 --- a/crates/ascend/src/models/semantics.rs +++ b/crates/ascend/src/models/semantics.rs @@ -1,4 +1,5 @@ use super::*; +use crate::gradient::Gradient; use chrono::DateTime; use chrono::Utc; use std::collections::BTreeMap; @@ -217,6 +218,14 @@ impl Attempt { Attempt::Flash => Icon::BoltSolid, } } + + pub(crate) fn gradient(&self) -> Gradient { + match self { + Attempt::Attempt => Gradient::PinkOrange, + Attempt::Send => Gradient::TealLime, + Attempt::Flash => Gradient::CyanBlue, + } + } } impl std::str::FromStr for Problem { diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 144b5c3..bf09e86 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -8,6 +8,7 @@ use crate::gradient::Gradient; use crate::models; use crate::models::HoldRole; use crate::server_functions; +use crate::server_functions::SetIsFavorite; use leptos::Params; use leptos::prelude::*; use leptos_router::params::Params; @@ -71,9 +72,7 @@ struct Context { cb_next_problem: Callback<()>, cb_set_problem: Callback, cb_upsert_todays_attempt: Callback, - - #[expect(dead_code)] - filtered_problems: Signal>, + cb_set_is_favorite: Callback, } #[component] @@ -99,9 +98,6 @@ fn Controller( let user_interaction = signals::user_interaction(user_interactions.into(), problem.into()); let problem_transformations = signals::problem_transformations(wall); - // TODO: still used? - let filtered_problems = signals::filtered_problems(wall, filter_holds.into()); - let filtered_problem_transformations = signals::filtered_problem_transformations(problem_transformations.into(), filter_holds.into()); let todays_attempt = signals::todays_attempt(user_interaction); let latest_attempt = signals::latest_attempt(user_interaction); @@ -112,6 +108,20 @@ fn Controller( upsert_todays_attempt.dispatch(RonEncoded(attempt)); }); + // Set favorite + let set_is_favorite = ServerAction::>::new(); + let cb_set_is_favorite = Callback::new(move |is_favorite| { + let wall_uid = wall.read().uid; + let Some(problem) = problem.get() else { + return; + }; + set_is_favorite.dispatch(RonEncoded(SetIsFavorite { + wall_uid, + problem, + is_favorite, + })); + }); + // Callback: Set specific problem let cb_set_problem: Callback = Callback::new(move |problem| { set_problem.set(Some(problem)); @@ -166,6 +176,16 @@ fn Controller( } }); + // Update user interactions after setting favorite + Effect::new(move || { + if let Some(Ok(v)) = set_is_favorite.value().get() { + let v = v.into_inner(); + user_interactions.update(|map| { + map.insert(v.problem.clone(), v); + }); + } + }); + provide_context(Context { wall, problem: problem.into(), @@ -176,9 +196,9 @@ fn Controller( cb_remove_hold_from_filter, cb_next_problem: cb_set_random_problem, cb_set_problem, + cb_set_is_favorite, todays_attempt, filter_holds: filter_holds.into(), - filtered_problems: filtered_problems.into(), filtered_problem_transformations: filtered_problem_transformations.into(), user_interactions: user_interactions.into(), }); @@ -214,8 +234,10 @@ fn View() -> impl IntoView {
{move || ctx.problem.get().map(|problem| view! { })} - - +
+ + +
@@ -296,6 +318,24 @@ fn HoldsButton() -> impl IntoView { } } +#[component] +#[tracing::instrument(skip_all)] +fn FavoriteButton() -> impl IntoView { + crate::tracing::on_enter!(); + + let ctx = use_context::().unwrap(); + + let ui_toggle = Signal::derive(move || { + let guard = ctx.user_interaction.read(); + guard.as_ref().map(|user_interaction| user_interaction.is_favorite).unwrap_or(false) + }); + let on_click = Callback::new(move |_| { + ctx.cb_set_is_favorite.run(!ui_toggle.get()); + }); + let icon = Signal::derive(move || if ui_toggle.get() { Icon::Heart } else { Icon::HeartOutline }); + view! {