Compare commits
6 Commits
221e15d7ac
...
bd8b0fecf1
| Author | SHA1 | Date | |
|---|---|---|---|
| bd8b0fecf1 | |||
| c15db2847d | |||
| 0a95aca872 | |||
| 91bea767d0 | |||
| ed6aa4b9c9 | |||
| d11f8510b4 |
@@ -51,12 +51,12 @@ codee = { version = "0.3" }
|
|||||||
error_reporter = { version = "1" }
|
error_reporter = { version = "1" }
|
||||||
getrandom = { version = "0.3.1" }
|
getrandom = { version = "0.3.1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
test-try = "0.1"
|
||||||
|
|
||||||
[dev-dependencies.serde_json]
|
[dev-dependencies.serde_json]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|
||||||
[dev-dependencies.test-try]
|
|
||||||
version = "0.1"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
|
hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
|
||||||
ssr = [
|
ssr = [
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ pub fn App() -> impl IntoView {
|
|||||||
<Routes fallback=|| "Not found">
|
<Routes fallback=|| "Not found">
|
||||||
<Route path=path!("/") view=Home />
|
<Route path=path!("/") view=Home />
|
||||||
<Route path=path!("/wall/:wall_uid") view=pages::wall::Page />
|
<Route path=path!("/wall/:wall_uid") view=pages::wall::Page />
|
||||||
<Route path=path!("/wall/:wall_uid/edit") view=pages::edit_wall::EditWall />
|
<Route path=path!("/wall/:wall_uid/holds") view=pages::holds::Page />
|
||||||
<Route path=path!("/wall/:wall_uid/routes") view=pages::routes::Routes />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ use leptos::prelude::*;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn OnHoverRed(children: Children) -> impl IntoView {
|
pub fn OnHoverRed(children: Children) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="group relative">
|
<div class="relative group">
|
||||||
<div>{children()}</div>
|
<div>{children()}</div>
|
||||||
<div class="absolute inset-0 bg-red-500 opacity-0 group-hover:opacity-50"></div>
|
<div class="absolute inset-0 bg-red-500 opacity-0 group-hover:opacity-50" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub fn Checkbox(checked: RwSignal<bool>, #[prop(into)] text: Signal<String>, #[p
|
|||||||
let unique_id = Oco::from(format!("checkbox-{}", uuid::Uuid::new_v4()));
|
let unique_id = Oco::from(format!("checkbox-{}", uuid::Uuid::new_v4()));
|
||||||
|
|
||||||
let checkbox_view = view! {
|
let checkbox_view = view! {
|
||||||
<div class="self-center text-white bg-white rounded-xs aspect-square mx-5 my-2.5">
|
<div class="self-center my-2.5 mx-5 text-white bg-white rounded-xs aspect-square">
|
||||||
<span class=("text-gray-950", move || checked.get())>
|
<span class=("text-gray-950", move || checked.get())>
|
||||||
<icons::Check />
|
<icons::Check />
|
||||||
</span>
|
</span>
|
||||||
@@ -25,7 +25,7 @@ pub fn Checkbox(checked: RwSignal<bool>, #[prop(into)] text: Signal<String>, #[p
|
|||||||
};
|
};
|
||||||
|
|
||||||
let text_view = view! {
|
let text_view = view! {
|
||||||
<div class="self-center mx-5 my-2.5 uppercase w-full text-lg font-thin">
|
<div class="self-center my-2.5 mx-5 w-full text-lg font-thin uppercase">
|
||||||
{move || text.get()}
|
{move || text.get()}
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub fn Problem(
|
|||||||
for row in 0..dim.get().rows {
|
for row in 0..dim.get().rows {
|
||||||
for col in 0..dim.get().cols {
|
for col in 0..dim.get().cols {
|
||||||
let hold_position = models::HoldPosition { row, col };
|
let hold_position = models::HoldPosition { row, col };
|
||||||
let role = move || problem.get().holds.get(&hold_position).copied();
|
let role = move || problem.read().pattern.pattern.get(&hold_position).copied();
|
||||||
let role = Signal::derive(role);
|
let role = Signal::derive(role);
|
||||||
let hold = view! { <Hold role /> };
|
let hold = view! { <Hold role /> };
|
||||||
holds.push(hold);
|
holds.push(hold);
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ use leptos::prelude::*;
|
|||||||
pub fn ProblemInfo(#[prop(into)] problem: Signal<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 = 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());
|
let method = Signal::derive(move || problem.read().method.to_string());
|
||||||
|
// let name = Signal::derive(move || problem.read().name.clone());
|
||||||
|
// let set_by = Signal::derive(move || problem.read().set_by.clone());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-rows-none gap-y-1 gap-x-0.5 grid-cols-[auto_1fr]">
|
<div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]">
|
||||||
<NameValue name="Name:" value=name />
|
|
||||||
<NameValue name="Method:" value=method />
|
<NameValue name="Method:" value=method />
|
||||||
<NameValue name="Set By:" value=set_by />
|
// <NameValue name="Name:" value=name />
|
||||||
|
// <NameValue name="Set By:" value=set_by />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ pub fn ProblemInfo(#[prop(into)] problem: Signal<models::Problem>) -> impl IntoV
|
|||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn NameValue(#[prop(into)] name: Signal<String>, #[prop(into)] value: Signal<String>) -> impl IntoView {
|
fn NameValue(#[prop(into)] name: Signal<String>, #[prop(into)] value: Signal<String>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<p class="font-light mr-4 text-right text-orange-300">{name.get()}</p>
|
<p class="mr-4 font-light text-right text-orange-300">{name.get()}</p>
|
||||||
<p class="text-white">{value.get()}</p>
|
<p class="text-white">{value.get()}</p>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,22 +8,68 @@ pub use v2::ImageFilename;
|
|||||||
pub use v2::ImageResolution;
|
pub use v2::ImageResolution;
|
||||||
pub use v2::ImageUid;
|
pub use v2::ImageUid;
|
||||||
pub use v2::Method;
|
pub use v2::Method;
|
||||||
pub use v2::Problem;
|
|
||||||
pub use v2::ProblemUid;
|
|
||||||
pub use v2::Root;
|
pub use v2::Root;
|
||||||
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::Attempt;
|
||||||
pub use v3::UserInteraction;
|
|
||||||
pub use v4::DatedAttempt;
|
pub use v4::DatedAttempt;
|
||||||
|
pub use v4::Pattern;
|
||||||
|
pub use v4::Problem;
|
||||||
|
pub use v4::Transformation;
|
||||||
|
pub use v4::UserInteraction;
|
||||||
|
pub use v4::Wall;
|
||||||
|
|
||||||
mod semantics;
|
mod semantics;
|
||||||
|
|
||||||
pub mod v4 {
|
pub mod v4 {
|
||||||
|
use super::v1;
|
||||||
|
use super::v2;
|
||||||
use super::v3;
|
use super::v3;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Wall {
|
||||||
|
pub uid: v2::WallUid,
|
||||||
|
pub wall_dimensions: v2::WallDimensions,
|
||||||
|
pub holds: BTreeMap<v1::HoldPosition, v2::Hold>,
|
||||||
|
|
||||||
|
/// Canonicalized.
|
||||||
|
pub problems: BTreeSet<Problem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Problem {
|
||||||
|
pub pattern: Pattern,
|
||||||
|
pub method: v2::Method,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Pattern {
|
||||||
|
pub pattern: BTreeMap<v1::HoldPosition, v1::HoldRole>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default)]
|
||||||
|
pub struct Transformation {
|
||||||
|
pub shift_right: u64,
|
||||||
|
pub mirror: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct UserInteraction {
|
||||||
|
pub wall_uid: v2::WallUid,
|
||||||
|
pub problem: Problem,
|
||||||
|
|
||||||
|
/// Dates on which this problem was attempted, and how it went
|
||||||
|
pub attempted_on: BTreeMap<chrono::DateTime<chrono::Utc>, v3::Attempt>,
|
||||||
|
|
||||||
|
/// Is among favorite problems
|
||||||
|
pub is_favorite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct DatedAttempt {
|
pub struct DatedAttempt {
|
||||||
@@ -85,7 +131,6 @@ pub mod v2 {
|
|||||||
pub struct Wall {
|
pub struct Wall {
|
||||||
pub uid: WallUid,
|
pub uid: WallUid,
|
||||||
|
|
||||||
// TODO: Replace by walldimensions
|
|
||||||
pub rows: u64,
|
pub rows: u64,
|
||||||
pub cols: u64,
|
pub cols: u64,
|
||||||
|
|
||||||
@@ -93,7 +138,7 @@ pub mod v2 {
|
|||||||
pub problems: BTreeSet<ProblemUid>,
|
pub problems: BTreeSet<ProblemUid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct WallDimensions {
|
pub struct WallDimensions {
|
||||||
pub rows: u64,
|
pub rows: u64,
|
||||||
pub cols: u64,
|
pub cols: u64,
|
||||||
@@ -115,16 +160,16 @@ 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);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display, Copy)]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
#[display("Feet follow hands")]
|
#[display("Feet follow hands")]
|
||||||
FeetFollowHands,
|
FeetFollowHands,
|
||||||
|
|
||||||
#[display("Footless")]
|
|
||||||
Footless,
|
|
||||||
|
|
||||||
#[display("Footless plus kickboard")]
|
#[display("Footless plus kickboard")]
|
||||||
FootlessPlusKickboard,
|
FootlessPlusKickboard,
|
||||||
|
|
||||||
|
#[display("Footless")]
|
||||||
|
Footless,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
|||||||
@@ -3,14 +3,100 @@ use chrono::DateTime;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
#[must_use]
|
||||||
|
pub fn canonicalize(&self) -> Self {
|
||||||
|
let mut pattern = self.clone();
|
||||||
|
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
||||||
|
pattern.pattern = pattern
|
||||||
|
.pattern
|
||||||
|
.iter()
|
||||||
|
.map(|(hold_position, hold_role)| {
|
||||||
|
let hold_position = HoldPosition {
|
||||||
|
row: hold_position.row,
|
||||||
|
col: hold_position.col - min_col,
|
||||||
|
};
|
||||||
|
(hold_position, *hold_role)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
std::cmp::min(pattern.mirror(), pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn shift_left(&self, shift: u64) -> Option<Self> {
|
||||||
|
// Out of bounds check
|
||||||
|
if let Some(min_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).min() {
|
||||||
|
if shift > min_col {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern: BTreeMap<HoldPosition, HoldRole> = self
|
||||||
|
.pattern
|
||||||
|
.iter()
|
||||||
|
.map(|(hold_position, hold_role)| {
|
||||||
|
let mut hold_position = hold_position.clone();
|
||||||
|
hold_position.col -= shift;
|
||||||
|
(hold_position, *hold_role)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(Self { pattern })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn shift_right(&self, wall_dimensions: WallDimensions, shift: u64) -> Option<Self> {
|
||||||
|
// Out of bounds check
|
||||||
|
if let Some(max_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).max() {
|
||||||
|
if max_col + shift >= wall_dimensions.cols {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern: BTreeMap<HoldPosition, HoldRole> = self
|
||||||
|
.pattern
|
||||||
|
.iter()
|
||||||
|
.map(|(hold_position, hold_role)| {
|
||||||
|
let mut hold_position = hold_position.clone();
|
||||||
|
hold_position.col += shift;
|
||||||
|
(hold_position, *hold_role)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(Self { pattern })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn mirror(&self) -> Self {
|
||||||
|
let mut pattern = self.clone();
|
||||||
|
|
||||||
|
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
||||||
|
let max_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).max().unwrap_or(0);
|
||||||
|
|
||||||
|
pattern.pattern = pattern
|
||||||
|
.pattern
|
||||||
|
.iter()
|
||||||
|
.map(|(hold_position, hold_role)| {
|
||||||
|
let HoldPosition { row, col } = *hold_position;
|
||||||
|
let mut mirrored_col = col;
|
||||||
|
mirrored_col += 2 * (max_col - col);
|
||||||
|
mirrored_col -= max_col - min_col;
|
||||||
|
let hold_position = HoldPosition { row, col: mirrored_col };
|
||||||
|
(hold_position, *hold_role)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UserInteraction {
|
impl UserInteraction {
|
||||||
pub(crate) fn new(wall_uid: WallUid, problem_uid: ProblemUid) -> Self {
|
pub(crate) fn new(wall_uid: WallUid, problem: Problem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
wall_uid,
|
wall_uid,
|
||||||
problem_uid,
|
problem,
|
||||||
is_favorite: false,
|
is_favorite: false,
|
||||||
attempted_on: BTreeMap::new(),
|
attempted_on: BTreeMap::new(),
|
||||||
is_saved: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +149,11 @@ impl WallUid {
|
|||||||
pub(crate) fn create() -> Self {
|
pub(crate) fn create() -> Self {
|
||||||
Self(uuid::Uuid::new_v4())
|
Self(uuid::Uuid::new_v4())
|
||||||
}
|
}
|
||||||
}
|
#[expect(dead_code)]
|
||||||
|
|
||||||
impl ProblemUid {
|
|
||||||
pub(crate) fn create() -> Self {
|
|
||||||
Self(uuid::Uuid::new_v4())
|
|
||||||
}
|
|
||||||
pub(crate) fn min() -> Self {
|
pub(crate) fn min() -> Self {
|
||||||
Self(uuid::Uuid::nil())
|
Self(uuid::Uuid::nil())
|
||||||
}
|
}
|
||||||
|
#[expect(dead_code)]
|
||||||
pub(crate) fn max() -> Self {
|
pub(crate) fn max() -> Self {
|
||||||
Self(uuid::Uuid::max())
|
Self(uuid::Uuid::max())
|
||||||
}
|
}
|
||||||
@@ -103,3 +185,69 @@ impl Attempt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Problem {
|
||||||
|
type Err = ron::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let problem = ron::from_str(s)?;
|
||||||
|
Ok(problem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Problem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = ron::to_string(self).unwrap();
|
||||||
|
write!(f, "{s}")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test_try::test_try]
|
||||||
|
fn canonicalize_empty_pattern() {
|
||||||
|
let pattern = Pattern {
|
||||||
|
pattern: [].into_iter().collect(),
|
||||||
|
};
|
||||||
|
let canonicalized = pattern.canonicalize();
|
||||||
|
assert_eq!(pattern, canonicalized);
|
||||||
|
let mirrored = pattern.mirror();
|
||||||
|
assert_eq!(pattern, mirrored);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_try::test_try]
|
||||||
|
fn canonicalize_pattern() {
|
||||||
|
let pattern = Pattern {
|
||||||
|
pattern: [
|
||||||
|
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
|
||||||
|
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let canonicalized = pattern.canonicalize();
|
||||||
|
|
||||||
|
assert_eq!(canonicalized.pattern[&HoldPosition { row: 0, col: 0 }], HoldRole::End);
|
||||||
|
assert_eq!(canonicalized.pattern[&HoldPosition { row: 7, col: 5 }], HoldRole::Start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_try::test_try]
|
||||||
|
fn mirror_pattern() {
|
||||||
|
let pattern = Pattern {
|
||||||
|
pattern: [
|
||||||
|
(HoldPosition { row: 0, col: 1 }, HoldRole::End),
|
||||||
|
(HoldPosition { row: 7, col: 6 }, HoldRole::Start),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mirrored = pattern.mirror();
|
||||||
|
|
||||||
|
assert_eq!(mirrored.pattern[&HoldPosition { row: 0, col: 6 }], HoldRole::End);
|
||||||
|
assert_eq!(mirrored.pattern[&HoldPosition { row: 7, col: 1 }], HoldRole::Start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod edit_wall;
|
pub mod holds;
|
||||||
pub mod routes;
|
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod wall;
|
pub mod wall;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct RouteParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EditWall() -> impl IntoView {
|
pub fn Page() -> impl IntoView {
|
||||||
let params = leptos_router::hooks::use_params::<RouteParams>();
|
let params = leptos_router::hooks::use_params::<RouteParams>();
|
||||||
let wall_uid = Signal::derive(move || {
|
let wall_uid = Signal::derive(move || {
|
||||||
params
|
params
|
||||||
@@ -85,8 +85,8 @@ fn Ready(wall: models::Wall) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let style = {
|
let style = {
|
||||||
let grid_rows = crate::css::grid_rows_n(wall.rows);
|
let grid_rows = crate::css::grid_rows_n(wall.wall_dimensions.rows);
|
||||||
let grid_cols = crate::css::grid_cols_n(wall.cols);
|
let grid_cols = crate::css::grid_cols_n(wall.wall_dimensions.cols);
|
||||||
[grid_rows, grid_cols].join(" ")
|
[grid_rows, grid_cols].join(" ")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
use crate::components;
|
|
||||||
use crate::components::header::HeaderItem;
|
|
||||||
use crate::components::header::HeaderItems;
|
|
||||||
use crate::components::header::StyledHeader;
|
|
||||||
use crate::models;
|
|
||||||
use leptos::Params;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use leptos_router::params::Params;
|
|
||||||
|
|
||||||
#[derive(Params, PartialEq, Clone)]
|
|
||||||
struct RouteParams {
|
|
||||||
// Is never None
|
|
||||||
wall_uid: Option<models::WallUid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub fn Routes() -> impl IntoView {
|
|
||||||
tracing::debug!("Enter");
|
|
||||||
|
|
||||||
let params = leptos_router::hooks::use_params::<RouteParams>();
|
|
||||||
let wall_uid = Signal::derive(move || {
|
|
||||||
params
|
|
||||||
.get()
|
|
||||||
.expect("gets wall_uid from URL")
|
|
||||||
.wall_uid
|
|
||||||
.expect("wall_uid param is never None")
|
|
||||||
});
|
|
||||||
|
|
||||||
let wall = crate::resources::wall_by_uid(wall_uid);
|
|
||||||
let problems = crate::resources::problems_for_wall(wall_uid);
|
|
||||||
|
|
||||||
let header_items = move || HeaderItems {
|
|
||||||
left: vec![HeaderItem {
|
|
||||||
text: "← Ascend".to_string(),
|
|
||||||
link: Some(format!("/wall/{}", wall_uid.get())),
|
|
||||||
}],
|
|
||||||
middle: vec![HeaderItem {
|
|
||||||
text: "Routes".to_string(),
|
|
||||||
link: None,
|
|
||||||
}],
|
|
||||||
right: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let suspend = move || {
|
|
||||||
Suspend::new(async move {
|
|
||||||
let wall = wall.await;
|
|
||||||
let problems = problems.await;
|
|
||||||
|
|
||||||
let v = move || -> Result<_, ServerFnError> {
|
|
||||||
let wall = wall.clone()?;
|
|
||||||
let problems = problems.clone()?;
|
|
||||||
|
|
||||||
let wall_dimensions = models::WallDimensions {
|
|
||||||
rows: wall.rows,
|
|
||||||
cols: wall.cols,
|
|
||||||
};
|
|
||||||
let problems_sample = move || problems.values().take(10).cloned().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
Ok(view! {
|
|
||||||
<div>
|
|
||||||
<For
|
|
||||||
each=problems_sample
|
|
||||||
key=|problem| problem.uid
|
|
||||||
children=move |problem: models::Problem| {
|
|
||||||
view! {
|
|
||||||
<Problem dim=wall_dimensions problem />
|
|
||||||
<hr class="my-8 h-px bg-gray-700 border-0" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
view! { <ErrorBoundary fallback=|_errors| "error">{v}</ErrorBoundary> }
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class="min-h-screen min-w-screen bg-neutral-950">
|
|
||||||
<StyledHeader items=Signal::derive(header_items) />
|
|
||||||
|
|
||||||
<div class="container mx-auto mt-6">
|
|
||||||
<Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
fn Problem(#[prop(into)] dim: Signal<models::WallDimensions>, #[prop(into)] problem: Signal<models::Problem>) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-none">
|
|
||||||
<components::Problem dim problem />
|
|
||||||
</div>
|
|
||||||
<components::ProblemInfo problem=problem.get() />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ use leptos::prelude::*;
|
|||||||
use leptos_router::params::Params;
|
use leptos_router::params::Params;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[derive(Params, PartialEq, Clone)]
|
#[derive(Params, PartialEq, Clone)]
|
||||||
struct RouteParams {
|
struct RouteParams {
|
||||||
@@ -35,8 +36,7 @@ pub fn Page() -> 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 user_interactions = crate::resources::user_interactions_for_wall(wall_uid);
|
||||||
let user_interactions = crate::resources::user_interactions(wall_uid);
|
|
||||||
|
|
||||||
leptos::view! {
|
leptos::view! {
|
||||||
<div class="min-h-screen min-w-screen bg-neutral-950">
|
<div class="min-h-screen min-w-screen bg-neutral-950">
|
||||||
@@ -46,10 +46,9 @@ pub fn Page() -> impl IntoView {
|
|||||||
{move || Suspend::new(async move {
|
{move || Suspend::new(async move {
|
||||||
tracing::debug!("executing main suspend");
|
tracing::debug!("executing main suspend");
|
||||||
let wall = wall.await?;
|
let wall = wall.await?;
|
||||||
let problems = problems.await?;
|
|
||||||
let user_interactions = user_interactions.await?;
|
let user_interactions = user_interactions.await?;
|
||||||
let user_interactions = RwSignal::new(user_interactions);
|
let user_interactions = RwSignal::new(user_interactions);
|
||||||
Ok::<_, ServerFnError>(view! { <Controller wall problems user_interactions /> })
|
Ok::<_, ServerFnError>(view! { <Controller wall user_interactions /> })
|
||||||
})}
|
})}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,9 +58,9 @@ pub fn Page() -> impl IntoView {
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct Context {
|
struct Context {
|
||||||
wall: Signal<models::Wall>,
|
wall: Signal<models::Wall>,
|
||||||
user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
|
||||||
problem: Signal<Option<models::Problem>>,
|
problem: Signal<Option<models::Problem>>,
|
||||||
filtered_problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
filtered_problems: Signal<BTreeSet<models::Problem>>,
|
||||||
user_interaction: Signal<Option<models::UserInteraction>>,
|
user_interaction: Signal<Option<models::UserInteraction>>,
|
||||||
todays_attempt: Signal<Option<models::Attempt>>,
|
todays_attempt: Signal<Option<models::Attempt>>,
|
||||||
latest_attempt: Signal<Option<models::DatedAttempt>>,
|
latest_attempt: Signal<Option<models::DatedAttempt>>,
|
||||||
@@ -69,6 +68,7 @@ struct Context {
|
|||||||
cb_click_hold: Callback<models::HoldPosition>,
|
cb_click_hold: Callback<models::HoldPosition>,
|
||||||
cb_remove_hold_from_filter: Callback<models::HoldPosition>,
|
cb_remove_hold_from_filter: Callback<models::HoldPosition>,
|
||||||
cb_next_problem: Callback<()>,
|
cb_next_problem: Callback<()>,
|
||||||
|
cb_set_problem: Callback<models::Problem>,
|
||||||
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +76,12 @@ struct Context {
|
|||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn Controller(
|
fn Controller(
|
||||||
#[prop(into)] wall: Signal<models::Wall>,
|
#[prop(into)] wall: Signal<models::Wall>,
|
||||||
#[prop(into)] problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::Problem, models::UserInteraction>>,
|
||||||
#[prop(into)] user_interactions: RwSignal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
crate::tracing::on_enter!();
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
// Extract data from URL
|
// Extract data from URL
|
||||||
let (problem_uid, set_problem_uid) = leptos_router::hooks::query_signal::<models::ProblemUid>("problem");
|
let (problem, set_problem) = leptos_router::hooks::query_signal::<models::Problem>("problem");
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
|
let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
|
||||||
@@ -93,10 +92,8 @@ fn Controller(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Derive signals
|
// Derive signals
|
||||||
let wall_uid = signals::wall_uid(wall);
|
let user_interaction = signals::user_interaction(user_interactions.into(), problem.into());
|
||||||
let problem = signals::problem(problems, problem_uid.into());
|
let filtered_problems = signals::filtered_problems(wall, filter_holds.into());
|
||||||
let user_interaction = signals::user_interaction(user_interactions.into(), problem_uid.into());
|
|
||||||
let filtered_problems = signals::filtered_problems(problems, filter_holds.into());
|
|
||||||
let todays_attempt = signals::todays_attempt(user_interaction);
|
let todays_attempt = signals::todays_attempt(user_interaction);
|
||||||
let latest_attempt = signals::latest_attempt(user_interaction);
|
let latest_attempt = signals::latest_attempt(user_interaction);
|
||||||
|
|
||||||
@@ -106,17 +103,22 @@ fn Controller(
|
|||||||
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback: Set specific problem
|
||||||
|
let cb_set_problem: Callback<models::Problem> = Callback::new(move |problem| {
|
||||||
|
set_problem.set(Some(problem));
|
||||||
|
});
|
||||||
|
|
||||||
// Callback: Set next problem to a random problem
|
// Callback: Set next problem to a random problem
|
||||||
let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
|
let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
|
||||||
// TODO: remove current problem from population
|
// TODO: remove current problem from population
|
||||||
let population = filtered_problems.read();
|
let population = filtered_problems.read();
|
||||||
let population = population.keys().copied();
|
let population = population.deref();
|
||||||
|
|
||||||
use rand::seq::IteratorRandom;
|
use rand::seq::IteratorRandom;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let problem_uid = population.choose(&mut rng);
|
let problem = population.iter().choose(&mut rng);
|
||||||
|
|
||||||
set_problem_uid.set(problem_uid);
|
set_problem.set(problem.cloned());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback: On click hold, Add/Remove hold position to problem filter
|
// Callback: On click hold, Add/Remove hold position to problem filter
|
||||||
@@ -130,7 +132,7 @@ fn Controller(
|
|||||||
|
|
||||||
// Set a problem when wall is set (loaded)
|
// Set a problem when wall is set (loaded)
|
||||||
Effect::new(move |_prev_value| {
|
Effect::new(move |_prev_value| {
|
||||||
if problem_uid.get().is_none() {
|
if problem.read().is_none() {
|
||||||
tracing::debug!("Setting initial problem");
|
tracing::debug!("Setting initial problem");
|
||||||
cb_set_random_problem.run(());
|
cb_set_random_problem.run(());
|
||||||
}
|
}
|
||||||
@@ -141,20 +143,21 @@ fn Controller(
|
|||||||
if let Some(Ok(v)) = upsert_todays_attempt.value().get() {
|
if let Some(Ok(v)) = upsert_todays_attempt.value().get() {
|
||||||
let v = v.into_inner();
|
let v = v.into_inner();
|
||||||
user_interactions.update(|map| {
|
user_interactions.update(|map| {
|
||||||
map.insert(v.problem_uid, v);
|
map.insert(v.problem.clone(), v);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
provide_context(Context {
|
provide_context(Context {
|
||||||
wall,
|
wall,
|
||||||
problem,
|
problem: problem.into(),
|
||||||
cb_click_hold,
|
cb_click_hold,
|
||||||
user_interaction,
|
user_interaction,
|
||||||
latest_attempt,
|
latest_attempt,
|
||||||
cb_upsert_todays_attempt,
|
cb_upsert_todays_attempt,
|
||||||
cb_remove_hold_from_filter,
|
cb_remove_hold_from_filter,
|
||||||
cb_next_problem: cb_set_random_problem,
|
cb_next_problem: cb_set_random_problem,
|
||||||
|
cb_set_problem,
|
||||||
todays_attempt,
|
todays_attempt,
|
||||||
filter_holds: filter_holds.into(),
|
filter_holds: filter_holds.into(),
|
||||||
filtered_problems: filtered_problems.into(),
|
filtered_problems: filtered_problems.into(),
|
||||||
@@ -184,7 +187,6 @@ fn View() -> impl IntoView {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div class="flex flex-row justify-around">
|
<div class="flex flex-row justify-around">
|
||||||
<Transformations />
|
|
||||||
<NextProblemButton />
|
<NextProblemButton />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -193,11 +195,12 @@ fn View() -> impl IntoView {
|
|||||||
|
|
||||||
<Section title="Current problem">
|
<Section title="Current problem">
|
||||||
{move || ctx.problem.get().map(|problem| view! { <ProblemInfo problem /> })}
|
{move || ctx.problem.get().map(|problem| view! { <ProblemInfo problem /> })}
|
||||||
<Separator /> <AttemptRadioGroup /> <Separator /> <History />
|
<Separator /> <Transformations /> <Separator /><AttemptRadioGroup />
|
||||||
|
<Separator /> <History />
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-auto flex flex-row justify-end items-start px-2 pt-3">
|
<div class="flex flex-row flex-auto justify-end items-start px-2 pt-3">
|
||||||
<HoldsButton />
|
<HoldsButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,21 +214,49 @@ fn Transformations() -> impl IntoView {
|
|||||||
|
|
||||||
let ctx = use_context::<Context>().unwrap();
|
let ctx = use_context::<Context>().unwrap();
|
||||||
|
|
||||||
let on_left = Callback::new(move |()| {
|
let left = Signal::derive(move || {
|
||||||
tracing::info!("left");
|
let mut problem = ctx.problem.get()?;
|
||||||
});
|
let new_pattern = problem.pattern.shift_left(1)?;
|
||||||
let on_mirror = Callback::new(move |()| {
|
problem.pattern = new_pattern;
|
||||||
tracing::info!("mirror");
|
Some(problem)
|
||||||
});
|
|
||||||
let on_right = Callback::new(move |()| {
|
|
||||||
tracing::info!("right");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let right = Signal::derive(move || {
|
||||||
|
let mut problem = ctx.problem.get()?;
|
||||||
|
let wall_dimensions = ctx.wall.read().wall_dimensions;
|
||||||
|
let new_pattern = problem.pattern.shift_right(wall_dimensions, 1)?;
|
||||||
|
problem.pattern = new_pattern;
|
||||||
|
Some(problem)
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_click_left = Callback::new(move |()| {
|
||||||
|
tracing::debug!("left");
|
||||||
|
if let Some(problem) = left.get() {
|
||||||
|
ctx.cb_set_problem.run(problem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let on_click_mirror = Callback::new(move |()| {
|
||||||
|
tracing::debug!("mirror");
|
||||||
|
if let Some(mut problem) = ctx.problem.get() {
|
||||||
|
problem.pattern = problem.pattern.mirror();
|
||||||
|
ctx.cb_set_problem.run(problem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let on_click_right = Callback::new(move |()| {
|
||||||
|
tracing::debug!("right");
|
||||||
|
if let Some(problem) = right.get() {
|
||||||
|
ctx.cb_set_problem.run(problem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let left_disabled = Signal::derive(move || left.read().is_none());
|
||||||
|
let right_disabled = Signal::derive(move || right.read().is_none());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-row justify-center gap-2">
|
<div class="flex flex-row gap-2 justify-center">
|
||||||
<Button icon=Icon::ChevronLeft disabled=true on_click=on_left />
|
<Button icon=Icon::ChevronLeft disabled=left_disabled on_click=on_click_left />
|
||||||
<Button icon=Icon::CodeBracketSquare on_click=on_mirror />
|
<Button icon=Icon::CodeBracketSquare on_click=on_click_mirror />
|
||||||
<Button icon=Icon::ChevronRight on_click=on_right />
|
<Button icon=Icon::ChevronRight disabled=right_disabled on_click=on_click_right />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +285,7 @@ fn NextProblemButton() -> impl IntoView {
|
|||||||
let ctx = use_context::<Context>().unwrap();
|
let ctx = use_context::<Context>().unwrap();
|
||||||
|
|
||||||
let on_click = Callback::new(move |_| ctx.cb_next_problem.run(()));
|
let on_click = Callback::new(move |_| ctx.cb_next_problem.run(()));
|
||||||
view! { <Button icon=Icon::ArrowPath text="Next problem" on_click color=Gradient::PurpleBlue /> }
|
view! { <Button icon=Icon::ArrowPath text="Randomize" on_click color=Gradient::PurpleBlue /> }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -285,10 +316,10 @@ fn Filter() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let problems_counter = {
|
let problems_counter = {
|
||||||
let name = view! { <p class="font-light mr-4 text-right text-orange-300">{"Problems:"}</p> };
|
let name = view! { <p class="mr-4 font-light text-right text-orange-300">{"Problems:"}</p> };
|
||||||
let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> };
|
let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> };
|
||||||
view! {
|
view! {
|
||||||
<div class="grid grid-rows-none gap-y-1 gap-x-0.5 grid-cols-[auto_1fr]">
|
<div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]">
|
||||||
{name} {value}
|
{name} {value}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -305,8 +336,8 @@ fn Filter() -> impl IntoView {
|
|||||||
let mut interaction_counters = InteractionCounters::default();
|
let mut interaction_counters = InteractionCounters::default();
|
||||||
let interaction_counters_view = {
|
let interaction_counters_view = {
|
||||||
let user_ints = ctx.user_interactions.read();
|
let user_ints = ctx.user_interactions.read();
|
||||||
for problem_uid in ctx.filtered_problems.read().keys() {
|
for problem in ctx.filtered_problems.read().iter() {
|
||||||
if let Some(user_int) = user_ints.get(problem_uid) {
|
if let Some(user_int) = user_ints.get(problem) {
|
||||||
match user_int.best_attempt().map(|da| da.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,
|
||||||
@@ -370,7 +401,7 @@ fn AttemptRadioGroup() -> impl IntoView {
|
|||||||
|
|
||||||
let ctx = use_context::<Context>().unwrap();
|
let ctx = use_context::<Context>().unwrap();
|
||||||
|
|
||||||
let problem_uid = Signal::derive(move || ctx.problem.read().as_ref().map(|p| p.uid));
|
let problem = ctx.problem;
|
||||||
|
|
||||||
let mut attempt_radio_buttons = vec![];
|
let mut attempt_radio_buttons = vec![];
|
||||||
for variant in [models::Attempt::Flash, models::Attempt::Send, models::Attempt::Attempt] {
|
for variant in [models::Attempt::Flash, models::Attempt::Send, models::Attempt::Attempt] {
|
||||||
@@ -379,10 +410,10 @@ fn AttemptRadioGroup() -> impl IntoView {
|
|||||||
let onclick = move |_| {
|
let onclick = move |_| {
|
||||||
let attempt = if ui_toggle.get() { None } else { Some(variant) };
|
let attempt = if ui_toggle.get() { None } else { Some(variant) };
|
||||||
|
|
||||||
if let Some(problem_uid) = problem_uid.get() {
|
if let Some(problem) = problem.get() {
|
||||||
ctx.cb_upsert_todays_attempt.run(server_functions::UpsertTodaysAttempt {
|
ctx.cb_upsert_todays_attempt.run(server_functions::UpsertTodaysAttempt {
|
||||||
wall_uid: ctx.wall.read().uid,
|
wall_uid: ctx.wall.read().uid,
|
||||||
problem_uid,
|
problem,
|
||||||
attempt,
|
attempt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -390,7 +421,7 @@ fn AttemptRadioGroup() -> impl IntoView {
|
|||||||
attempt_radio_buttons.push(view! { <AttemptRadioButton on:click=onclick variant selected=ui_toggle /> });
|
attempt_radio_buttons.push(view! { <AttemptRadioButton on:click=onclick variant selected=ui_toggle /> });
|
||||||
}
|
}
|
||||||
|
|
||||||
view! { <div class="gap-2 flex flex-col justify-evenly 2xl:flex-row">{attempt_radio_buttons}</div> }
|
view! { <div class="flex flex-col gap-2 justify-evenly 2xl:flex-row">{attempt_radio_buttons}</div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -469,14 +500,14 @@ fn Wall() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let style = {
|
let style = {
|
||||||
let grid_rows = crate::css::grid_rows_n(wall.rows);
|
let grid_rows = crate::css::grid_rows_n(wall.wall_dimensions.rows);
|
||||||
let grid_cols = crate::css::grid_cols_n(wall.cols);
|
let grid_cols = crate::css::grid_cols_n(wall.wall_dimensions.cols);
|
||||||
let max_width = format!("{}vh", wall.cols as f64 / wall.rows as f64 * 100.);
|
let max_width = format!("{}vh", wall.wall_dimensions.cols as f64 / wall.wall_dimensions.rows as f64 * 100.);
|
||||||
format!("max-height: 100vh; max-width: {max_width}; {}", [grid_rows, grid_cols].join(" "))
|
format!("max-height: 100vh; max-width: {max_width}; {}", [grid_rows, grid_cols].join(" "))
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div style=style class="p-1 grid gap-1">
|
<div style=style class="grid gap-1 p-1">
|
||||||
{cells}
|
{cells}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -524,13 +555,13 @@ fn Hold(
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn Separator() -> impl IntoView {
|
fn Separator() -> impl IntoView {
|
||||||
view! { <div class="m-2 sm:m-3 md:m-4 h-4" /> }
|
view! { <div class="m-2 h-4 sm:m-3 md:m-4" /> }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl IntoView {
|
fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="bg-neutral-900 px-5 pt-3 pb-8 rounded-lg">
|
<div class="px-5 pt-3 pb-8 rounded-lg bg-neutral-900">
|
||||||
<div class="py-3 text-lg text-center text-orange-400 border-t border-orange-400">
|
<div class="py-3 text-lg text-center text-orange-400 border-t border-orange-400">
|
||||||
{move || title.get()}
|
{move || title.get()}
|
||||||
</div>
|
</div>
|
||||||
@@ -553,49 +584,39 @@ mod signals {
|
|||||||
Signal::derive(move || latest_attempt.read().as_ref().and_then(models::UserInteraction::todays_attempt))
|
Signal::derive(move || latest_attempt.read().as_ref().and_then(models::UserInteraction::todays_attempt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code)]
|
||||||
pub fn wall_uid(wall: Signal<models::Wall>) -> Signal<models::WallUid> {
|
pub fn wall_uid(wall: Signal<models::Wall>) -> Signal<models::WallUid> {
|
||||||
Signal::derive(move || wall.read().uid)
|
Signal::derive(move || wall.read().uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_interaction(
|
pub fn user_interaction(
|
||||||
user_interactions: Signal<BTreeMap<models::ProblemUid, models::UserInteraction>>,
|
user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
|
||||||
problem_uid: Signal<Option<models::ProblemUid>>,
|
problem: Signal<Option<models::Problem>>,
|
||||||
) -> Signal<Option<models::UserInteraction>> {
|
) -> Signal<Option<models::UserInteraction>> {
|
||||||
Signal::derive(move || {
|
Signal::derive(move || {
|
||||||
let problem_uid = problem_uid.get()?;
|
let problem = problem.get()?;
|
||||||
let user_interactions = user_interactions.read();
|
let user_interactions = user_interactions.read();
|
||||||
user_interactions.get(&problem_uid).cloned()
|
user_interactions.get(&problem).cloned()
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn problem(
|
|
||||||
problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
|
||||||
problem_uid: Signal<Option<models::ProblemUid>>,
|
|
||||||
) -> Signal<Option<models::Problem>> {
|
|
||||||
Signal::derive(move || {
|
|
||||||
let problem_uid = problem_uid.get()?;
|
|
||||||
let problems = problems.read();
|
|
||||||
problems.get(&problem_uid).cloned()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn filtered_problems(
|
pub(crate) fn filtered_problems(
|
||||||
problems: Signal<BTreeMap<models::ProblemUid, models::Problem>>,
|
wall: Signal<models::Wall>,
|
||||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||||
) -> Memo<BTreeMap<models::ProblemUid, models::Problem>> {
|
) -> Memo<BTreeSet<models::Problem>> {
|
||||||
Memo::new(move |_prev_val| {
|
Memo::new(move |_prev_val| {
|
||||||
let filter_holds = filter_holds.read();
|
let filter_holds = filter_holds.read();
|
||||||
problems.with(|problems| {
|
wall.with(|wall| {
|
||||||
problems
|
wall.problems
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, problem)| filter_holds.iter().all(|hold_pos| problem.holds.contains_key(hold_pos)))
|
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
||||||
.map(|(problem_uid, problem)| (*problem_uid, problem.clone()))
|
.map(|problem| problem.clone())
|
||||||
.collect::<BTreeMap<models::ProblemUid, models::Problem>>()
|
.collect::<BTreeSet<models::Problem>>()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
|
pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
|
||||||
Signal::derive(move || problem.get().and_then(|p| p.holds.get(&hold_position).copied()))
|
Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,47 +17,15 @@ pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wal
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns all problems for a wall
|
|
||||||
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<BTreeMap<models::ProblemUid, 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns user interaction for a single problem
|
/// Returns user interaction for a single problem
|
||||||
pub fn user_interaction(
|
pub fn user_interaction(wall_uid: Signal<models::WallUid>, problem: Signal<Option<models::Problem>>) -> RonResource<Option<models::UserInteraction>> {
|
||||||
wall_uid: Signal<models::WallUid>,
|
|
||||||
problem_uid: Signal<Option<models::ProblemUid>>,
|
|
||||||
) -> RonResource<Option<models::UserInteraction>> {
|
|
||||||
Resource::new_with_options(
|
Resource::new_with_options(
|
||||||
move || (wall_uid.get(), problem_uid.get()),
|
move || (wall_uid.get(), problem.get()),
|
||||||
move |(wall_uid, problem_uid)| async move {
|
move |(wall_uid, problem)| async move {
|
||||||
let Some(problem_uid) = problem_uid else {
|
let Some(problem) = problem else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
crate::server_functions::get_user_interaction(wall_uid, problem_uid)
|
crate::server_functions::get_user_interaction(wall_uid, problem)
|
||||||
.await
|
.await
|
||||||
.map(RonEncoded::into_inner)
|
.map(RonEncoded::into_inner)
|
||||||
},
|
},
|
||||||
@@ -66,10 +34,14 @@ pub fn user_interaction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all user interactions for a wall
|
/// Returns all user interactions for a wall
|
||||||
pub fn user_interactions(wall_uid: Signal<models::WallUid>) -> RonResource<BTreeMap<models::ProblemUid, models::UserInteraction>> {
|
pub fn user_interactions_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<BTreeMap<models::Problem, models::UserInteraction>> {
|
||||||
Resource::new_with_options(
|
Resource::new_with_options(
|
||||||
move || wall_uid.get(),
|
move || wall_uid.get(),
|
||||||
move |wall_uid| async move { crate::server_functions::get_user_interactions(wall_uid).await.map(RonEncoded::into_inner) },
|
move |wall_uid| async move {
|
||||||
|
crate::server_functions::get_user_interactions_for_wall(wall_uid)
|
||||||
|
.await
|
||||||
|
.map(RonEncoded::into_inner)
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,13 +121,6 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
|
|||||||
assert!(table.is_empty()?);
|
assert!(table.is_empty()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Problems table
|
|
||||||
{
|
|
||||||
// Opening the table creates the table
|
|
||||||
let table = txn.open_table(current::TABLE_PROBLEMS)?;
|
|
||||||
assert!(table.is_empty()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// User table
|
// User table
|
||||||
{
|
{
|
||||||
// Opening the table creates the table
|
// Opening the table creates the table
|
||||||
@@ -147,11 +140,23 @@ use crate::models;
|
|||||||
pub mod current {
|
pub mod current {
|
||||||
use super::v2;
|
use super::v2;
|
||||||
use super::v3;
|
use super::v3;
|
||||||
pub use v2::TABLE_PROBLEMS;
|
use super::v4;
|
||||||
pub use v2::TABLE_ROOT;
|
pub use v2::TABLE_ROOT;
|
||||||
pub use v2::TABLE_WALLS;
|
|
||||||
pub use v3::TABLE_USER;
|
|
||||||
pub use v3::VERSION;
|
pub use v3::VERSION;
|
||||||
|
pub use v4::TABLE_USER;
|
||||||
|
pub use v4::TABLE_WALLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod v4 {
|
||||||
|
use crate::models;
|
||||||
|
use crate::server::db::bincode::Bincode;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
|
||||||
|
pub const VERSION: u64 = 4;
|
||||||
|
|
||||||
|
pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallUid>, Bincode<models::v4::Wall>> = TableDefinition::new("walls");
|
||||||
|
pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v4::Problem)>, Bincode<models::v4::UserInteraction>> =
|
||||||
|
TableDefinition::new("user");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod v3 {
|
pub mod v3 {
|
||||||
|
|||||||
@@ -3,12 +3,116 @@ use super::db::DatabaseOperationError;
|
|||||||
use super::db::{self};
|
use super::db::{self};
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use redb::ReadableTable;
|
use redb::ReadableTable;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[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? {
|
if is_at_version(db, 2).await? {
|
||||||
migrate_to_v3(db).await?;
|
migrate_to_v3(db).await?;
|
||||||
}
|
}
|
||||||
|
if is_at_version(db, 3).await? {
|
||||||
|
migrate_to_v4(db).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// migrate: walls table
|
||||||
|
/// migrate: user table
|
||||||
|
/// remove: problems table
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
pub async fn migrate_to_v4(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing::warn!("MIGRATING TO VERSION 4");
|
||||||
|
|
||||||
|
db.write(|txn| {
|
||||||
|
let walls_dump = txn
|
||||||
|
.open_table(db::v2::TABLE_WALLS)?
|
||||||
|
.iter()?
|
||||||
|
.map(|el| {
|
||||||
|
let (k, v) = el.unwrap();
|
||||||
|
(k.value(), v.value())
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
let problems_dump = txn
|
||||||
|
.open_table(db::v2::TABLE_PROBLEMS)?
|
||||||
|
.iter()?
|
||||||
|
.map(|el| {
|
||||||
|
let (k, v) = el.unwrap();
|
||||||
|
(k.value(), v.value())
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
let user_dump = txn
|
||||||
|
.open_table(db::v3::TABLE_USER)?
|
||||||
|
.iter()?
|
||||||
|
.map(|el| {
|
||||||
|
let (k, v) = el.unwrap();
|
||||||
|
(k.value(), v.value())
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
txn.delete_table(db::v2::TABLE_WALLS)?;
|
||||||
|
txn.delete_table(db::v2::TABLE_PROBLEMS)?;
|
||||||
|
txn.delete_table(db::v3::TABLE_USER)?;
|
||||||
|
|
||||||
|
let mut new_walls_table = txn.open_table(db::current::TABLE_WALLS)?;
|
||||||
|
let mut new_user_table = txn.open_table(db::current::TABLE_USER)?;
|
||||||
|
|
||||||
|
for (wall_uid, wall) in walls_dump.into_iter() {
|
||||||
|
let models::v2::Wall {
|
||||||
|
uid: _,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
holds,
|
||||||
|
problems,
|
||||||
|
} = wall;
|
||||||
|
|
||||||
|
let problems = problems
|
||||||
|
.into_iter()
|
||||||
|
.map(|problem_uid| {
|
||||||
|
let old_prob = &problems_dump[&(wall_uid, problem_uid)];
|
||||||
|
let method = old_prob.method;
|
||||||
|
let problem = models::Problem {
|
||||||
|
pattern: models::Pattern {
|
||||||
|
pattern: old_prob.holds.clone(),
|
||||||
|
},
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
problem
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let wall = models::Wall {
|
||||||
|
uid: wall_uid,
|
||||||
|
wall_dimensions: models::WallDimensions { rows, cols },
|
||||||
|
holds,
|
||||||
|
problems,
|
||||||
|
};
|
||||||
|
|
||||||
|
new_walls_table.insert(wall_uid, wall)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((wall_uid, problem_uid), user_interaction) in user_dump.into_iter() {
|
||||||
|
let old_prob = &problems_dump[&(wall_uid, problem_uid)];
|
||||||
|
let problem = models::Problem {
|
||||||
|
pattern: models::Pattern {
|
||||||
|
pattern: old_prob.holds.clone(),
|
||||||
|
},
|
||||||
|
method: old_prob.method,
|
||||||
|
};
|
||||||
|
let key = (wall_uid, problem.clone());
|
||||||
|
let value = models::UserInteraction {
|
||||||
|
wall_uid,
|
||||||
|
problem,
|
||||||
|
attempted_on: user_interaction.attempted_on,
|
||||||
|
is_favorite: user_interaction.is_favorite,
|
||||||
|
};
|
||||||
|
new_user_table.insert(key, value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
db.set_version(db::Version { version: db::v4::VERSION }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
|
|||||||
|
|
||||||
tracing::info!("Parsing mini moonboard problems from {file_path}");
|
tracing::info!("Parsing mini moonboard problems from {file_path}");
|
||||||
|
|
||||||
let set_by = "mini-mb-2020-parser";
|
|
||||||
|
|
||||||
let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?;
|
let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?;
|
||||||
for mini_mb_problem in mini_moonboard.problems {
|
for mini_mb_problem in mini_moonboard.problems {
|
||||||
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
let mut pattern = BTreeMap::<HoldPosition, HoldRole>::new();
|
||||||
for mv in mini_mb_problem.moves {
|
for mv in mini_mb_problem.moves {
|
||||||
let row = mv.description.row();
|
let row = mv.description.row();
|
||||||
let col = mv.description.column();
|
let col = mv.description.column();
|
||||||
@@ -36,43 +34,29 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
|
|||||||
(false, true) => HoldRole::End,
|
(false, true) => HoldRole::End,
|
||||||
(false, false) => HoldRole::Normal,
|
(false, false) => HoldRole::Normal,
|
||||||
};
|
};
|
||||||
holds.insert(hold_position, role);
|
pattern.insert(hold_position, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = mini_mb_problem.name;
|
|
||||||
|
|
||||||
let method = match mini_mb_problem.method {
|
let method = match mini_mb_problem.method {
|
||||||
mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
|
mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
|
||||||
mini_moonboard::Method::Footless => models::Method::Footless,
|
mini_moonboard::Method::Footless => models::Method::Footless,
|
||||||
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
let problem_id = models::ProblemUid::create();
|
|
||||||
|
|
||||||
let problem = models::Problem {
|
let problem = models::Problem {
|
||||||
uid: problem_id,
|
pattern: models::Pattern { pattern },
|
||||||
name,
|
|
||||||
set_by: set_by.to_owned(),
|
|
||||||
holds,
|
|
||||||
method,
|
method,
|
||||||
date_added: chrono::Utc::now(),
|
|
||||||
};
|
};
|
||||||
problems.push(problem);
|
problems.push(problem);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.write(|txn| {
|
db.write(|txn| {
|
||||||
let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?;
|
let mut walls_table = txn.open_table(db::current::TABLE_WALLS)?;
|
||||||
let mut problems_table = txn.open_table(db::current::TABLE_PROBLEMS)?;
|
|
||||||
|
|
||||||
let mut wall = walls_table.get(wall_uid)?.unwrap().value();
|
let mut wall = walls_table.get(wall_uid)?.unwrap().value();
|
||||||
wall.problems.extend(problems.iter().map(|p| p.uid));
|
wall.problems.extend(problems);
|
||||||
walls_table.insert(wall_uid, wall)?;
|
walls_table.insert(wall_uid, wall)?;
|
||||||
|
|
||||||
for problem in problems {
|
|
||||||
let key = (wall_uid, problem.uid);
|
|
||||||
problems_table.insert(key, problem)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use leptos::prelude::*;
|
|||||||
use leptos::server;
|
use leptos::server;
|
||||||
use server_fn::ServerFnError;
|
use server_fn::ServerFnError;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use type_toppings::IteratorExt;
|
|
||||||
|
|
||||||
#[server(
|
#[server(
|
||||||
input = Ron,
|
input = Ron,
|
||||||
@@ -71,68 +70,6 @@ pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEnco
|
|||||||
Ok(RonEncoded::new(wall))
|
Ok(RonEncoded::new(wall))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(
|
|
||||||
input = Ron,
|
|
||||||
output = Ron,
|
|
||||||
custom = RonEncoded
|
|
||||||
)]
|
|
||||||
#[tracing::instrument(err(Debug))]
|
|
||||||
pub(crate) async fn get_problems_for_wall(
|
|
||||||
wall_uid: models::WallUid,
|
|
||||||
) -> Result<RonEncoded<BTreeMap<models::ProblemUid, 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_more::From)]
|
|
||||||
enum Error {
|
|
||||||
#[display("Wall not found: {_0:?}")]
|
|
||||||
WallNotFound(#[error(not(source))] models::WallUid),
|
|
||||||
|
|
||||||
DatabaseOperation(DatabaseOperationError),
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::ProblemUid, models::Problem>, Error> {
|
|
||||||
let db = expect_context::<Database>();
|
|
||||||
|
|
||||||
let problems = db
|
|
||||||
.read(|txn| {
|
|
||||||
let walls_table = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
|
|
||||||
tracing::debug!("getting wall");
|
|
||||||
let wall = walls_table
|
|
||||||
.get(wall_uid)?
|
|
||||||
.ok_or(Error::WallNotFound(wall_uid))
|
|
||||||
.map_err(DatabaseOperationError::custom)?
|
|
||||||
.value();
|
|
||||||
tracing::debug!("got wall");
|
|
||||||
drop(walls_table);
|
|
||||||
|
|
||||||
tracing::debug!("open problems table");
|
|
||||||
let problems_table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
|
|
||||||
tracing::debug!("opened problems table");
|
|
||||||
|
|
||||||
let problems = wall
|
|
||||||
.problems
|
|
||||||
.iter()
|
|
||||||
.map(|problem_uid| problems_table.get(&(wall_uid, *problem_uid)))
|
|
||||||
.filter_map(|res| res.transpose())
|
|
||||||
.map_res(|guard| guard.value())
|
|
||||||
.map_res(|problem| (problem.uid, problem))
|
|
||||||
.collect::<Result<BTreeMap<models::ProblemUid, models::Problem>, _>>()?;
|
|
||||||
|
|
||||||
Ok(problems)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(problems)
|
|
||||||
}
|
|
||||||
|
|
||||||
let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
|
|
||||||
|
|
||||||
Ok(RonEncoded::new(problems))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns user interaction for a single wall problem
|
/// Returns user interaction for a single wall problem
|
||||||
#[server(
|
#[server(
|
||||||
input = Ron,
|
input = Ron,
|
||||||
@@ -142,7 +79,7 @@ pub(crate) async fn get_problems_for_wall(
|
|||||||
#[tracing::instrument(err(Debug))]
|
#[tracing::instrument(err(Debug))]
|
||||||
pub(crate) async fn get_user_interaction(
|
pub(crate) async fn get_user_interaction(
|
||||||
wall_uid: models::WallUid,
|
wall_uid: models::WallUid,
|
||||||
problem_uid: models::ProblemUid,
|
problem: models::Problem,
|
||||||
) -> Result<RonEncoded<Option<models::UserInteraction>>, ServerFnError> {
|
) -> 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;
|
||||||
@@ -157,13 +94,13 @@ pub(crate) async fn get_user_interaction(
|
|||||||
DatabaseOperation(DatabaseOperationError),
|
DatabaseOperation(DatabaseOperationError),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<Option<UserInteraction>, Error> {
|
async fn inner(wall_uid: models::WallUid, problem: models::Problem) -> Result<Option<UserInteraction>, Error> {
|
||||||
let db = expect_context::<Database>();
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
let user_interaction = db
|
let user_interaction = db
|
||||||
.read(|txn| {
|
.read(|txn| {
|
||||||
let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
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());
|
let user_interaction = user_table.get(&(wall_uid, problem))?.map(|guard| guard.value());
|
||||||
Ok(user_interaction)
|
Ok(user_interaction)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -171,7 +108,7 @@ pub(crate) async fn get_user_interaction(
|
|||||||
Ok(user_interaction)
|
Ok(user_interaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_interaction = inner(wall_uid, problem_uid)
|
let user_interaction = inner(wall_uid, problem)
|
||||||
.await
|
.await
|
||||||
.map_err(error_reporter::Report::new)
|
.map_err(error_reporter::Report::new)
|
||||||
.map_err(ServerFnError::new)?;
|
.map_err(ServerFnError::new)?;
|
||||||
@@ -185,12 +122,13 @@ pub(crate) async fn get_user_interaction(
|
|||||||
custom = RonEncoded
|
custom = RonEncoded
|
||||||
)]
|
)]
|
||||||
#[tracing::instrument(err(Debug))]
|
#[tracing::instrument(err(Debug))]
|
||||||
pub(crate) async fn get_user_interactions(
|
pub(crate) async fn get_user_interactions_for_wall(
|
||||||
wall_uid: models::WallUid,
|
wall_uid: models::WallUid,
|
||||||
) -> Result<RonEncoded<BTreeMap<models::ProblemUid, models::UserInteraction>>, ServerFnError> {
|
) -> Result<RonEncoded<BTreeMap<models::Problem, 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;
|
||||||
|
use redb::ReadableTable;
|
||||||
tracing::trace!("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)]
|
||||||
@@ -198,18 +136,28 @@ pub(crate) async fn get_user_interactions(
|
|||||||
DatabaseOperation(DatabaseOperationError),
|
DatabaseOperation(DatabaseOperationError),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::ProblemUid, models::UserInteraction>, Error> {
|
async fn inner(wall_uid: models::WallUid) -> Result<BTreeMap<models::Problem, models::UserInteraction>, Error> {
|
||||||
let db = expect_context::<Database>();
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
let user_interactions = db
|
let user_interactions = db
|
||||||
.read(|txn| {
|
.read(|txn| {
|
||||||
let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
let user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||||
let range = user_table.range((wall_uid, models::ProblemUid::min())..=(wall_uid, models::ProblemUid::max()))?;
|
let user_interactions = user_table
|
||||||
let user_interactions = range
|
.iter()?
|
||||||
|
.filter(|guard| {
|
||||||
|
guard
|
||||||
|
.as_ref()
|
||||||
|
.map(|(key, _val)| {
|
||||||
|
let (wall_uid, _problem) = key.value();
|
||||||
|
wall_uid
|
||||||
|
})
|
||||||
|
.map(|wall_uid_| wall_uid_ == wall_uid)
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
.map(|guard| {
|
.map(|guard| {
|
||||||
guard.map(|(_key, val)| {
|
guard.map(|(_key, val)| {
|
||||||
let val = val.value();
|
let val = val.value();
|
||||||
(val.problem_uid, val)
|
(val.problem.clone(), val)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
@@ -224,43 +172,6 @@ pub(crate) async fn get_user_interactions(
|
|||||||
Ok(RonEncoded::new(user_interaction))
|
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)]
|
|
||||||
enum Error {
|
|
||||||
#[display("Problem not found: {_0:?}")]
|
|
||||||
NotFound(#[error(not(source))] models::ProblemUid),
|
|
||||||
}
|
|
||||||
|
|
||||||
let db = expect_context::<Database>();
|
|
||||||
let problem = db
|
|
||||||
.read(|txn| {
|
|
||||||
let table = txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
|
|
||||||
let problem = table
|
|
||||||
.get((wall_uid, problem_uid))?
|
|
||||||
.ok_or(Error::NotFound(problem_uid))
|
|
||||||
.map_err(DatabaseOperationError::custom)?
|
|
||||||
.value();
|
|
||||||
Ok(problem)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(RonEncoded::new(problem))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inserts or updates today's attempt.
|
/// Inserts or updates today's attempt.
|
||||||
#[server(
|
#[server(
|
||||||
input = Ron,
|
input = Ron,
|
||||||
@@ -270,7 +181,7 @@ pub(crate) async fn get_problem_by_uid(
|
|||||||
#[tracing::instrument(err(Debug))]
|
#[tracing::instrument(err(Debug))]
|
||||||
pub(crate) async fn upsert_todays_attempt(
|
pub(crate) async fn upsert_todays_attempt(
|
||||||
wall_uid: models::WallUid,
|
wall_uid: models::WallUid,
|
||||||
problem_uid: models::ProblemUid,
|
problem: models::Problem,
|
||||||
attempt: Option<models::Attempt>,
|
attempt: Option<models::Attempt>,
|
||||||
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
|
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
|
||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
@@ -287,20 +198,20 @@ pub(crate) async fn upsert_todays_attempt(
|
|||||||
DatabaseOperation(DatabaseOperationError),
|
DatabaseOperation(DatabaseOperationError),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn inner(wall_uid: models::WallUid, problem_uid: models::ProblemUid, attempt: Option<models::Attempt>) -> Result<UserInteraction, Error> {
|
async fn inner(wall_uid: models::WallUid, problem: models::Problem, attempt: Option<models::Attempt>) -> Result<UserInteraction, Error> {
|
||||||
let db = expect_context::<Database>();
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
let user_interaction = db
|
let user_interaction = db
|
||||||
.write(|txn| {
|
.write(|txn| {
|
||||||
let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||||
|
|
||||||
let key = (wall_uid, problem_uid);
|
let key = (wall_uid, problem.clone());
|
||||||
|
|
||||||
// Pop or default
|
// Pop or default
|
||||||
let mut user_interaction = user_table
|
let mut user_interaction = user_table
|
||||||
.remove(key)?
|
.remove(&key)?
|
||||||
.map(|guard| guard.value())
|
.map(|guard| guard.value())
|
||||||
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem_uid));
|
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));
|
||||||
|
|
||||||
// If the last entry is from today, remove it
|
// If the last entry is from today, remove it
|
||||||
if let Some(entry) = user_interaction.attempted_on.last_entry() {
|
if let Some(entry) = user_interaction.attempted_on.last_entry() {
|
||||||
@@ -327,7 +238,7 @@ pub(crate) async fn upsert_todays_attempt(
|
|||||||
Ok(user_interaction)
|
Ok(user_interaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
inner(wall_uid, problem_uid, attempt)
|
inner(wall_uid, problem, attempt)
|
||||||
.await
|
.await
|
||||||
.map_err(error_reporter::Report::new)
|
.map_err(error_reporter::Report::new)
|
||||||
.map_err(ServerFnError::new)
|
.map_err(ServerFnError::new)
|
||||||
|
|||||||
16
notes.txt
Normal file
16
notes.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Random selection: Sample from filtered
|
||||||
|
[patterns with variations that satisfy filter]
|
||||||
|
[variations]
|
||||||
|
|
||||||
|
# Random selection no filter: Sample equally weighted patterns
|
||||||
|
[patterns]
|
||||||
|
[random variation within pattern]
|
||||||
|
|
||||||
|
Normalize: shift left, and use minimum of mirrored pattern pair
|
||||||
|
|
||||||
|
|
||||||
|
# Filter stats:
|
||||||
|
patterns: X
|
||||||
|
pattern variations: Y
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user