This commit is contained in:
Asger Juul Brunshøj 2025-03-20 16:44:30 +01:00
parent f8aa1e29a2
commit 7d95e48941
4 changed files with 145 additions and 73 deletions

View File

@ -1,10 +1,12 @@
use crate::components::icons; use crate::components::icons;
use crate::models; use crate::models;
use chrono::DateTime;
use chrono::Utc;
use leptos::prelude::*; use leptos::prelude::*;
#[component] #[component]
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn Attempt(#[prop(into)] date: Signal<chrono::DateTime<chrono::Utc>>, #[prop(into)] attempt: Signal<Option<models::Attempt>>) -> impl IntoView { pub fn Attempt(#[prop(into)] date: Signal<DateTime<Utc>>, #[prop(into)] attempt: Signal<Option<models::Attempt>>) -> impl IntoView {
tracing::trace!("Enter"); tracing::trace!("Enter");
let s = time_ago(date.get()); let s = time_ago(date.get());

View File

@ -16,6 +16,21 @@ pub use v2::WallDimensions;
pub use v2::WallUid; pub use v2::WallUid;
pub use v3::Attempt; pub use v3::Attempt;
pub use v3::UserInteraction; pub use v3::UserInteraction;
pub use v4::DatedAttempt;
mod semantics;
pub mod v4 {
use super::v3;
use chrono::DateTime;
use chrono::Utc;
#[derive(Debug, Clone, Copy)]
pub struct DatedAttempt {
pub date_time: DateTime<Utc>,
pub attempt: v3::Attempt,
}
}
pub mod v3 { pub mod v3 {
use super::v2; use super::v2;
@ -38,24 +53,6 @@ pub mod v3 {
/// Added to personal challenges /// Added to personal challenges
pub is_saved: bool, 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub enum Attempt { pub enum Attempt {
@ -103,11 +100,6 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
pub struct WallUid(pub uuid::Uuid); pub struct WallUid(pub uuid::Uuid);
impl WallUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Problem { pub struct Problem {
@ -121,17 +113,6 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
pub struct ProblemUid(pub uuid::Uuid); pub struct ProblemUid(pub uuid::Uuid);
impl ProblemUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
pub fn min() -> Self {
Self(uuid::Uuid::nil())
}
pub fn max() -> Self {
Self(uuid::Uuid::max())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display)]
pub enum Method { pub enum Method {
@ -156,15 +137,6 @@ pub mod v2 {
pub uid: ImageUid, pub uid: ImageUid,
pub resolutions: BTreeMap<ImageResolution, ImageFilename>, pub resolutions: BTreeMap<ImageResolution, ImageFilename>,
} }
impl Image {
pub(crate) fn srcset(&self) -> String {
self.resolutions
.iter()
.map(|(res, filename)| format!("/files/holds/{} {}w", filename.filename, res.width))
.collect::<Vec<String>>()
.join(", ")
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageResolution { pub struct ImageResolution {
@ -179,11 +151,6 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
pub struct ImageUid(pub uuid::Uuid); pub struct ImageUid(pub uuid::Uuid);
impl ImageUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}
} }
pub mod v1 { pub mod v1 {

View File

@ -0,0 +1,93 @@
use super::*;
use chrono::DateTime;
use chrono::Utc;
use std::collections::BTreeMap;
impl UserInteraction {
pub fn new(wall_uid: WallUid, problem_uid: ProblemUid) -> Self {
Self {
wall_uid,
problem_uid,
is_favorite: false,
attempted_on: BTreeMap::new(),
is_saved: false,
}
}
pub fn latest_attempt(&self) -> Option<DatedAttempt> {
self.attempted_on.last_key_value().map(Into::into)
}
pub fn todays_attempt(&self) -> Option<Attempt> {
self.latest_attempt()
.filter(|latest_attempt| {
let today_local_naive = chrono::Local::now().date_naive();
let datetime_local_naive = latest_attempt.date_time.with_timezone(&chrono::Local).date_naive();
datetime_local_naive == today_local_naive
})
.map(|dated_attempt| dated_attempt.attempt)
}
pub fn best_attempt(&self) -> Option<DatedAttempt> {
self.attempted_on.iter().max_by_key(|(_date_time, attempt)| *attempt).map(Into::into)
}
pub fn attempted_on(&self) -> impl IntoIterator<Item = DatedAttempt> {
self.attempted_on.iter().map(Into::into)
}
}
impl From<(DateTime<Utc>, Attempt)> for DatedAttempt {
fn from(value: (DateTime<Utc>, Attempt)) -> Self {
let (date_time, attempt) = value;
DatedAttempt { date_time, attempt }
}
}
impl From<&(DateTime<Utc>, Attempt)> for DatedAttempt {
fn from(value: &(DateTime<Utc>, Attempt)) -> Self {
let &(date_time, attempt) = value;
DatedAttempt { date_time, attempt }
}
}
impl From<(&DateTime<Utc>, &Attempt)> for DatedAttempt {
fn from(value: (&DateTime<Utc>, &Attempt)) -> Self {
let (&date_time, &attempt) = value;
DatedAttempt { date_time, attempt }
}
}
impl WallUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}
impl ProblemUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
pub fn min() -> Self {
Self(uuid::Uuid::nil())
}
pub fn max() -> Self {
Self(uuid::Uuid::max())
}
}
impl Image {
pub(crate) fn srcset(&self) -> String {
self.resolutions
.iter()
.map(|(res, filename)| format!("/files/holds/{} {}w", filename.filename, res.width))
.collect::<Vec<String>>()
.join(", ")
}
}
impl ImageUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}

View File

@ -254,10 +254,10 @@ fn WithWall(
let user_ints = user_interactions.read(); let user_ints = user_interactions.read();
for problem in filtered_problems.read().iter() { for problem in filtered_problems.read().iter() {
if let Some(user_int) = user_ints.get(&problem.uid) { if let Some(user_int) = user_ints.get(&problem.uid) {
match user_int.best_attempt() { match user_int.best_attempt().map(|da| da.attempt) {
Some((_, models::Attempt::Flash)) => interaction_counters.flash += 1, Some(models::Attempt::Flash) => interaction_counters.flash += 1,
Some((_, models::Attempt::Send)) => interaction_counters.send += 1, Some(models::Attempt::Send) => interaction_counters.send += 1,
Some((_, models::Attempt::Attempt)) => interaction_counters.attempt += 1, Some(models::Attempt::Attempt) => interaction_counters.attempt += 1,
None => {} None => {}
} }
} }
@ -384,23 +384,8 @@ fn WithUserInteraction(
} }
}); });
let latest_attempt = move || -> Option<_> { let latest_attempt = signals::latest_attempt(user_interaction_rw.into());
let i = user_interaction_rw.read(); let todays_attempt = signals::todays_attempt(user_interaction_rw.into());
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_flash = RwSignal::new(false);
let ui_is_send = RwSignal::new(false); let ui_is_send = RwSignal::new(false);
@ -408,7 +393,7 @@ fn WithUserInteraction(
let ui_is_favorite = RwSignal::new(false); let ui_is_favorite = RwSignal::new(false);
Effect::new(move || { Effect::new(move || {
let attempt = todays_attempt(); let attempt = todays_attempt.get();
ui_is_flash.set(matches!(attempt, Some(models::Attempt::Flash))); ui_is_flash.set(matches!(attempt, Some(models::Attempt::Flash)));
ui_is_send.set(matches!(attempt, Some(models::Attempt::Send))); ui_is_send.set(matches!(attempt, Some(models::Attempt::Send)));
ui_is_attempt.set(matches!(attempt, Some(models::Attempt::Attempt))); ui_is_attempt.set(matches!(attempt, Some(models::Attempt::Attempt)));
@ -440,10 +425,22 @@ fn WithUserInteraction(
}; };
// TODO: loop over attempts in user_interaction // TODO: loop over attempts in user_interaction
let v = move || latest_attempt().map(|(date, attempt)| view! { <Attempt date attempt /> }); let v = move || {
user_interaction_rw
.read()
.as_ref()
.iter()
.flat_map(|x| x.attempted_on())
.map(|dated_attempt| {
let date = dated_attempt.date_time;
let attempt = dated_attempt.attempt;
view! { <Attempt date attempt /> }
})
.collect_view()
};
let placeholder = move || { let placeholder = move || {
latest_attempt().is_none().then(|| { latest_attempt.read().is_none().then(|| {
let today = chrono::Utc::now(); let today = chrono::Utc::now();
view! { <Attempt date=today attempt=None /> } view! { <Attempt date=today attempt=None /> }
}) })
@ -602,3 +599,16 @@ fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl I
</div> </div>
} }
} }
mod signals {
use crate::models;
use leptos::prelude::*;
pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> {
Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt))
}
pub fn todays_attempt(latest_attempt: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::Attempt>> {
Signal::derive(move || latest_attempt.read().as_ref().and_then(models::UserInteraction::todays_attempt))
}
}