Compare commits
9 Commits
b37386b9e8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d609118de | |||
| d11bf28625 | |||
| 9bbe1dd214 | |||
| e3ef695069 | |||
| 5bdfd6835d | |||
| 27716c5ec0 | |||
| dea8c45939 | |||
| 22367f45f2 | |||
| e5853268de |
844
Cargo.lock
generated
844
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ publish = false
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7", optional = true }
|
axum = { version = "0.8", optional = true }
|
||||||
camino = { version = "1.1", optional = true }
|
camino = { version = "1.1", optional = true }
|
||||||
chrono = { version = "0.4.39", features = ["now", "serde"] }
|
chrono = { version = "0.4.39", features = ["now", "serde"] }
|
||||||
clap = { version = "4.5.7", features = ["derive"] }
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
@@ -23,18 +23,17 @@ derive_more = { version = "2", features = [
|
|||||||
] }
|
] }
|
||||||
http = "1"
|
http = "1"
|
||||||
image = { version = "0.25", optional = true }
|
image = { version = "0.25", optional = true }
|
||||||
leptos = { version = "0.7.7", features = ["tracing"] }
|
leptos = { version = "0.8", features = ["tracing"] }
|
||||||
leptos_axum = { version = "0.7", optional = true }
|
leptos_axum = { version = "0.8", optional = true }
|
||||||
leptos_meta = { version = "0.7" }
|
leptos_meta = { version = "0.8" }
|
||||||
leptos_router = { version = "0.7.0" }
|
leptos_router = { version = "0.8" }
|
||||||
moonboard-parser = { workspace = true, optional = true }
|
moonboard-parser = { workspace = true, optional = true }
|
||||||
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
rand = { version = "0.9", default-features = false, features = ["thread_rng"] }
|
||||||
ron = { version = "0.8" }
|
ron = { version = "0.8" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
server_fn = { version = "0.7.4", features = ["cbor"] }
|
server_fn = { version = "0.8", features = ["cbor"] }
|
||||||
smart-default = "0.7.1"
|
smart-default = "0.7.1"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
tower = { version = "0.4", optional = true }
|
|
||||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
tracing = { version = "0.1" }
|
tracing = { version = "0.1" }
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
@@ -46,7 +45,6 @@ xdg = { version = "2.5", optional = true }
|
|||||||
uuid = { version = "1.12", features = ["serde", "v4"] }
|
uuid = { version = "1.12", features = ["serde", "v4"] }
|
||||||
redb = { version = "2.4", optional = true }
|
redb = { version = "2.4", optional = true }
|
||||||
bincode = { version = "1.3", optional = true }
|
bincode = { version = "1.3", optional = true }
|
||||||
serde_json = { version = "1" }
|
|
||||||
codee = { version = "0.3" }
|
codee = { version = "0.3" }
|
||||||
error_reporter = { version = "1" }
|
error_reporter = { version = "1" }
|
||||||
getrandom = { version = "0.3.1" }
|
getrandom = { version = "0.3.1" }
|
||||||
@@ -54,9 +52,6 @@ getrandom = { version = "0.3.1" }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
test-try = "0.1"
|
test-try = "0.1"
|
||||||
|
|
||||||
[dev-dependencies.serde_json]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
|
hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
|
||||||
ssr = [
|
ssr = [
|
||||||
@@ -65,7 +60,6 @@ ssr = [
|
|||||||
"dep:image",
|
"dep:image",
|
||||||
"dep:bincode",
|
"dep:bincode",
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
"dep:tower",
|
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:confik",
|
"dep:confik",
|
||||||
@@ -77,6 +71,11 @@ ssr = [
|
|||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[package.metadata.cargo-shear]
|
||||||
|
# getrandom is depended on in order to turn on its wasm feature indirectly
|
||||||
|
ignored = ["getrandom"]
|
||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
output-name = "ascend"
|
output-name = "ascend"
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ pub mod ron {
|
|||||||
|
|
||||||
use codee::Decoder;
|
use codee::Decoder;
|
||||||
use codee::Encoder;
|
use codee::Encoder;
|
||||||
|
use leptos::prelude::FromServerFnError;
|
||||||
|
use leptos::prelude::ServerFnErrorErr;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use server_fn::ServerFnError;
|
use server_fn::ContentType;
|
||||||
use server_fn::codec::Encoding;
|
use server_fn::codec::Encoding;
|
||||||
use server_fn::codec::FromReq;
|
use server_fn::codec::FromReq;
|
||||||
use server_fn::codec::FromRes;
|
use server_fn::codec::FromRes;
|
||||||
use server_fn::codec::IntoReq;
|
use server_fn::codec::IntoReq;
|
||||||
use server_fn::codec::IntoRes;
|
use server_fn::codec::IntoRes;
|
||||||
|
use server_fn::error::IntoAppError;
|
||||||
use server_fn::request::ClientReq;
|
use server_fn::request::ClientReq;
|
||||||
use server_fn::request::Req;
|
use server_fn::request::Req;
|
||||||
use server_fn::response::ClientRes;
|
use server_fn::response::ClientRes;
|
||||||
use server_fn::response::Res;
|
use server_fn::response::TryRes;
|
||||||
|
|
||||||
pub struct Ron;
|
pub struct Ron;
|
||||||
|
|
||||||
@@ -44,10 +47,13 @@ pub mod ron {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Encoding for Ron {
|
impl Encoding for Ron {
|
||||||
const CONTENT_TYPE: &'static str = "application/ron";
|
|
||||||
const METHOD: http::Method = http::Method::POST;
|
const METHOD: http::Method = http::Method::POST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ContentType for Ron {
|
||||||
|
const CONTENT_TYPE: &'static str = "application/ron";
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RonEncoded<T>(pub T);
|
pub struct RonEncoded<T>(pub T);
|
||||||
|
|
||||||
@@ -74,9 +80,10 @@ pub mod ron {
|
|||||||
where
|
where
|
||||||
Request: ClientReq<Err>,
|
Request: ClientReq<Err>,
|
||||||
T: Serialize,
|
T: Serialize,
|
||||||
|
Err: FromServerFnError,
|
||||||
{
|
{
|
||||||
fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<Err>> {
|
fn into_req(self, path: &str, accepts: &str) -> Result<Request, Err> {
|
||||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
let data = Ron::encode(&self.0).map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
|
||||||
Request::try_new_post(path, Ron::CONTENT_TYPE, accepts, data)
|
Request::try_new_post(path, Ron::CONTENT_TYPE, accepts, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,21 +93,25 @@ pub mod ron {
|
|||||||
where
|
where
|
||||||
Request: Req<Err> + Send,
|
Request: Req<Err> + Send,
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
|
Err: FromServerFnError,
|
||||||
{
|
{
|
||||||
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
|
async fn from_req(req: Request) -> Result<Self, Err> {
|
||||||
let data = req.try_into_string().await?;
|
let data = req.try_into_string().await?;
|
||||||
Ron::decode(&data).map(RonEncoded).map_err(|e| ServerFnError::Args(e.to_string()))
|
Ron::decode(&data)
|
||||||
|
.map(RonEncoded)
|
||||||
|
.map_err(|e| ServerFnErrorErr::Args(e.to_string()).into_app_error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntoRes
|
// IntoRes
|
||||||
impl<CustErr, T, Response> IntoRes<Ron, Response, CustErr> for RonEncoded<T>
|
impl<Err, T, Response> IntoRes<Ron, Response, Err> for RonEncoded<T>
|
||||||
where
|
where
|
||||||
Response: Res<CustErr>,
|
Response: TryRes<Err>,
|
||||||
T: Serialize + Send,
|
T: Serialize + Send,
|
||||||
|
Err: FromServerFnError,
|
||||||
{
|
{
|
||||||
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
|
async fn into_res(self) -> Result<Response, Err> {
|
||||||
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
let data = Ron::encode(&self.0).map_err(|e| ServerFnErrorErr::Serialization(e.to_string()).into_app_error())?;
|
||||||
Response::try_from_string(Ron::CONTENT_TYPE, data)
|
Response::try_from_string(Ron::CONTENT_TYPE, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,12 +121,13 @@ pub mod ron {
|
|||||||
where
|
where
|
||||||
Response: ClientRes<Err> + Send,
|
Response: ClientRes<Err> + Send,
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
|
Err: FromServerFnError,
|
||||||
{
|
{
|
||||||
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
|
async fn from_res(res: Response) -> Result<Self, Err> {
|
||||||
let data = res.try_into_string().await?;
|
let data = res.try_into_string().await?;
|
||||||
Ron::decode(&data)
|
Ron::decode(&data)
|
||||||
.map(RonEncoded)
|
.map(RonEncoded)
|
||||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
.map_err(|e| ServerFnErrorErr::Deserialization(e.to_string()).into_app_error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use leptos::prelude::*;
|
|||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub fn Attempt(#[prop(into)] date: Signal<DateTime<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");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
let s = time_ago(date.get());
|
let s = time_ago(date.get());
|
||||||
|
|
||||||
@@ -19,9 +19,7 @@ pub fn Attempt(#[prop(into)] date: Signal<DateTime<Utc>>, #[prop(into)] attempt:
|
|||||||
};
|
};
|
||||||
|
|
||||||
let text_color = match attempt.get() {
|
let text_color = match attempt.get() {
|
||||||
Some(models::Attempt::Flash) => "text-cyan-500",
|
Some(attempt) => attempt.gradient().class_text(),
|
||||||
Some(models::Attempt::Send) => "text-teal-500",
|
|
||||||
Some(models::Attempt::Attempt) => "text-pink-500",
|
|
||||||
None => "",
|
None => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,34 +19,40 @@ pub fn Button(
|
|||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let margin = "mx-2 my-1 sm:mx-5 sm:my-2.5";
|
let margin = "mx-2 my-1 sm:mx-5 sm:my-2.5";
|
||||||
|
|
||||||
let icon_view = icon.get().map(|i| {
|
let icon_view = move || {
|
||||||
let icon_view = i.into_view();
|
icon.get().map(|i| {
|
||||||
let mut classes = "self-center".to_string();
|
let icon_view = i.into_view();
|
||||||
classes.push(' ');
|
let mut classes = "self-center".to_string();
|
||||||
classes.push_str(margin);
|
classes.push(' ');
|
||||||
classes.push(' ');
|
classes.push_str(margin);
|
||||||
classes.push_str(color.class_text());
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_text());
|
||||||
|
|
||||||
view! { <div class=classes>{icon_view}</div> }
|
view! { <div class=classes>{icon_view}</div> }
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let separator = (icon.read().is_some() && text.read().is_some()).then(|| {
|
let separator = move || {
|
||||||
let mut classes = "w-0.5 bg-linear-to-br min-w-0.5".to_string();
|
(icon.read().is_some() && text.read().is_some()).then(|| {
|
||||||
classes.push(' ');
|
let mut classes = "w-0.5 bg-linear-to-br min-w-0.5".to_string();
|
||||||
classes.push_str(color.class_from());
|
classes.push(' ');
|
||||||
classes.push(' ');
|
classes.push_str(color.class_from());
|
||||||
classes.push_str(color.class_to());
|
classes.push(' ');
|
||||||
|
classes.push_str(color.class_to());
|
||||||
|
|
||||||
view! { <div class=classes /> }
|
view! { <div class=classes /> }
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let text_view = text.get().map(|text| {
|
let text_view = move || {
|
||||||
let mut classes = "self-center uppercase w-full text-sm sm:text-base md:text-lg font-thin".to_string();
|
text.get().map(|text| {
|
||||||
classes.push(' ');
|
let mut classes = "self-center uppercase w-full text-sm sm:text-base md:text-lg font-thin".to_string();
|
||||||
classes.push_str(margin);
|
classes.push(' ');
|
||||||
|
classes.push_str(margin);
|
||||||
|
|
||||||
view! { <div class=classes>{text}</div> }
|
view! { <div class=classes>{text}</div> }
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let class = move || {
|
let class = move || {
|
||||||
let mut classes = vec![];
|
let mut classes = vec![];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub enum Icon {
|
|||||||
WrenchSolid,
|
WrenchSolid,
|
||||||
ForwardSolid,
|
ForwardSolid,
|
||||||
Check,
|
Check,
|
||||||
|
Heart,
|
||||||
HeartOutline,
|
HeartOutline,
|
||||||
ArrowPath,
|
ArrowPath,
|
||||||
PaperAirplaneSolid,
|
PaperAirplaneSolid,
|
||||||
@@ -26,6 +27,7 @@ impl Icon {
|
|||||||
Icon::WrenchSolid => view! { <WrenchSolid /> }.into_any(),
|
Icon::WrenchSolid => view! { <WrenchSolid /> }.into_any(),
|
||||||
Icon::ForwardSolid => view! { <ForwardSolid /> }.into_any(),
|
Icon::ForwardSolid => view! { <ForwardSolid /> }.into_any(),
|
||||||
Icon::Check => view! { <Check /> }.into_any(),
|
Icon::Check => view! { <Check /> }.into_any(),
|
||||||
|
Icon::Heart => view! { <Heart /> }.into_any(),
|
||||||
Icon::HeartOutline => view! { <HeartOutline /> }.into_any(),
|
Icon::HeartOutline => view! { <HeartOutline /> }.into_any(),
|
||||||
Icon::ArrowPath => view! { <ArrowPath /> }.into_any(),
|
Icon::ArrowPath => view! { <ArrowPath /> }.into_any(),
|
||||||
Icon::PaperAirplaneSolid => view! { <PaperAirplaneSolid /> }.into_any(),
|
Icon::PaperAirplaneSolid => view! { <PaperAirplaneSolid /> }.into_any(),
|
||||||
@@ -123,6 +125,20 @@ pub fn Check() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Heart() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-6"
|
||||||
|
>
|
||||||
|
<path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HeartOutline() -> impl IntoView {
|
pub fn HeartOutline() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ pub fn OutlinedBox(children: Children, color: Gradient, #[prop(optional)] highli
|
|||||||
let mut c = "py-1.5 rounded-md".to_string();
|
let mut c = "py-1.5 rounded-md".to_string();
|
||||||
if highlight() {
|
if highlight() {
|
||||||
let bg = match color {
|
let bg = match color {
|
||||||
Gradient::PinkOrange => "bg-pink-900",
|
Gradient::PinkOrange => "bg-rose-900",
|
||||||
Gradient::CyanBlue => "bg-cyan-800",
|
Gradient::CyanBlue => "bg-cyan-800",
|
||||||
Gradient::TealLime => "bg-teal-700",
|
Gradient::TealLime => "bg-emerald-700",
|
||||||
Gradient::PurplePink => "bg-purple-900",
|
Gradient::PurplePink => "bg-fuchsia-950",
|
||||||
Gradient::PurpleBlue => "bg-purple-900",
|
Gradient::PurpleBlue => "bg-purple-900",
|
||||||
Gradient::Orange => "bg-orange-900",
|
Gradient::Orange => "bg-orange-900",
|
||||||
|
Gradient::Pink => "bg-pink-900",
|
||||||
|
Gradient::PinkRed => "bg-fuchsia-950",
|
||||||
};
|
};
|
||||||
|
|
||||||
c.push(' ');
|
c.push(' ');
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ pub enum Gradient {
|
|||||||
PurplePink,
|
PurplePink,
|
||||||
#[default]
|
#[default]
|
||||||
Orange,
|
Orange,
|
||||||
|
Pink,
|
||||||
|
PinkRed,
|
||||||
}
|
}
|
||||||
impl Gradient {
|
impl Gradient {
|
||||||
pub fn class_from(&self) -> &str {
|
pub fn class_from(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Gradient::PinkOrange => "from-pink-500",
|
Gradient::PinkOrange => "from-pink-500",
|
||||||
Gradient::CyanBlue => "from-cyan-500",
|
Gradient::CyanBlue => "from-cyan-500",
|
||||||
@@ -17,10 +19,12 @@ impl Gradient {
|
|||||||
Gradient::PurplePink => "from-purple-500",
|
Gradient::PurplePink => "from-purple-500",
|
||||||
Gradient::PurpleBlue => "from-purple-600",
|
Gradient::PurpleBlue => "from-purple-600",
|
||||||
Gradient::Orange => "from-orange-400",
|
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 {
|
match self {
|
||||||
Gradient::PinkOrange => "to-orange-400",
|
Gradient::PinkOrange => "to-orange-400",
|
||||||
Gradient::CyanBlue => "to-blue-500",
|
Gradient::CyanBlue => "to-blue-500",
|
||||||
@@ -28,17 +32,21 @@ impl Gradient {
|
|||||||
Gradient::PurplePink => "to-pink-500",
|
Gradient::PurplePink => "to-pink-500",
|
||||||
Gradient::PurpleBlue => "to-blue-500",
|
Gradient::PurpleBlue => "to-blue-500",
|
||||||
Gradient::Orange => "to-orange-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 {
|
match self {
|
||||||
Gradient::PinkOrange => "text-pink-500",
|
Gradient::PinkOrange => "text-rose-400",
|
||||||
Gradient::CyanBlue => "text-cyan-500",
|
Gradient::CyanBlue => "text-cyan-500",
|
||||||
Gradient::TealLime => "text-teal-300",
|
Gradient::TealLime => "text-emerald-300",
|
||||||
Gradient::PurplePink => "text-purple-500",
|
Gradient::PurplePink => "text-fuchsia-500",
|
||||||
Gradient::PurpleBlue => "text-purple-600",
|
Gradient::PurpleBlue => "text-purple-600",
|
||||||
Gradient::Orange => "text-orange-400",
|
Gradient::Orange => "text-orange-400",
|
||||||
|
Gradient::Pink => "text-pink-400",
|
||||||
|
Gradient::PinkRed => "text-pink-400",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::gradient::Gradient;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -23,7 +24,7 @@ impl Pattern {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn canonicalize(&self) -> Self {
|
pub fn canonicalize(&self) -> Self {
|
||||||
let mut pattern = self.clone();
|
let mut pattern = self.clone();
|
||||||
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
let min_col = pattern.pattern.keys().map(|hold_position| hold_position.col).min().unwrap_or(0);
|
||||||
pattern.pattern = pattern
|
pattern.pattern = pattern
|
||||||
.pattern
|
.pattern
|
||||||
.iter()
|
.iter()
|
||||||
@@ -41,7 +42,7 @@ impl Pattern {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn shift_left(&self, shift: u64) -> Option<Self> {
|
pub fn shift_left(&self, shift: u64) -> Option<Self> {
|
||||||
// Out of bounds check
|
// Out of bounds check
|
||||||
if let Some(min_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).min() {
|
if let Some(min_col) = self.pattern.keys().map(|hold_position| hold_position.col).min() {
|
||||||
if shift > min_col {
|
if shift > min_col {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ impl Pattern {
|
|||||||
.pattern
|
.pattern
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(hold_position, hold_role)| {
|
.map(|(hold_position, hold_role)| {
|
||||||
let mut hold_position = hold_position.clone();
|
let mut hold_position = *hold_position;
|
||||||
hold_position.col -= shift;
|
hold_position.col -= shift;
|
||||||
(hold_position, *hold_role)
|
(hold_position, *hold_role)
|
||||||
})
|
})
|
||||||
@@ -63,7 +64,7 @@ impl Pattern {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn shift_right(&self, wall_dimensions: WallDimensions, shift: u64) -> Option<Self> {
|
pub fn shift_right(&self, wall_dimensions: WallDimensions, shift: u64) -> Option<Self> {
|
||||||
// Out of bounds check
|
// Out of bounds check
|
||||||
if let Some(max_col) = self.pattern.iter().map(|(hold_position, _)| hold_position.col).max() {
|
if let Some(max_col) = self.pattern.keys().map(|hold_position| hold_position.col).max() {
|
||||||
if max_col + shift >= wall_dimensions.cols {
|
if max_col + shift >= wall_dimensions.cols {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -73,7 +74,7 @@ impl Pattern {
|
|||||||
.pattern
|
.pattern
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(hold_position, hold_role)| {
|
.map(|(hold_position, hold_role)| {
|
||||||
let mut hold_position = hold_position.clone();
|
let mut hold_position = *hold_position;
|
||||||
hold_position.col += shift;
|
hold_position.col += shift;
|
||||||
(hold_position, *hold_role)
|
(hold_position, *hold_role)
|
||||||
})
|
})
|
||||||
@@ -86,8 +87,8 @@ impl Pattern {
|
|||||||
pub fn mirror(&self) -> Self {
|
pub fn mirror(&self) -> Self {
|
||||||
let mut pattern = self.clone();
|
let mut pattern = self.clone();
|
||||||
|
|
||||||
let min_col = pattern.pattern.iter().map(|(hold_position, _)| hold_position.col).min().unwrap_or(0);
|
let min_col = pattern.pattern.keys().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);
|
let max_col = pattern.pattern.keys().map(|hold_position| hold_position.col).max().unwrap_or(0);
|
||||||
|
|
||||||
pattern.pattern = pattern
|
pattern.pattern = pattern
|
||||||
.pattern
|
.pattern
|
||||||
@@ -217,6 +218,14 @@ impl Attempt {
|
|||||||
Attempt::Flash => Icon::BoltSolid,
|
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 {
|
impl std::str::FromStr for Problem {
|
||||||
|
|||||||
@@ -29,20 +29,18 @@ pub fn Settings() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
// #[component]
|
||||||
#[tracing::instrument(skip_all)]
|
// #[tracing::instrument(skip_all)]
|
||||||
fn Import(wall_uid: WallUid) -> impl IntoView {
|
// fn Import(wall_uid: WallUid) -> impl IntoView {
|
||||||
let import_from_mini_moonboard = ServerAction::<ImportFromMiniMoonboard>::new();
|
// let import_from_mini_moonboard = ServerAction::<ImportFromMiniMoonboard>::new();
|
||||||
|
// let onclick = move |_mouse_event| {
|
||||||
let onclick = move |_mouse_event| {
|
// import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
||||||
import_from_mini_moonboard.dispatch(ImportFromMiniMoonboard { wall_uid });
|
// };
|
||||||
};
|
// view! {
|
||||||
|
// <p>"Import problems from"</p>
|
||||||
view! {
|
// <button on:click=onclick>"Mini Moonboard"</button>
|
||||||
<p>"Import problems from"</p>
|
// }
|
||||||
<button on:click=onclick>"Mini Moonboard"</button>
|
// }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::gradient::Gradient;
|
|||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::HoldRole;
|
use crate::models::HoldRole;
|
||||||
use crate::server_functions;
|
use crate::server_functions;
|
||||||
|
use crate::server_functions::SetIsFavorite;
|
||||||
use leptos::Params;
|
use leptos::Params;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_router::params::Params;
|
use leptos_router::params::Params;
|
||||||
@@ -71,9 +72,7 @@ struct Context {
|
|||||||
cb_next_problem: Callback<()>,
|
cb_next_problem: Callback<()>,
|
||||||
cb_set_problem: Callback<models::Problem>,
|
cb_set_problem: Callback<models::Problem>,
|
||||||
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
|
||||||
|
cb_set_is_favorite: Callback<bool>,
|
||||||
#[expect(dead_code)]
|
|
||||||
filtered_problems: Signal<BTreeSet<models::Problem>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -99,9 +98,6 @@ fn Controller(
|
|||||||
let user_interaction = signals::user_interaction(user_interactions.into(), problem.into());
|
let user_interaction = signals::user_interaction(user_interactions.into(), problem.into());
|
||||||
let problem_transformations = signals::problem_transformations(wall);
|
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 filtered_problem_transformations = signals::filtered_problem_transformations(problem_transformations.into(), 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);
|
||||||
@@ -112,6 +108,20 @@ fn Controller(
|
|||||||
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
upsert_todays_attempt.dispatch(RonEncoded(attempt));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set favorite
|
||||||
|
let set_is_favorite = ServerAction::<RonEncoded<server_functions::SetIsFavorite>>::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
|
// Callback: Set specific problem
|
||||||
let cb_set_problem: Callback<models::Problem> = Callback::new(move |problem| {
|
let cb_set_problem: Callback<models::Problem> = Callback::new(move |problem| {
|
||||||
set_problem.set(Some(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 {
|
provide_context(Context {
|
||||||
wall,
|
wall,
|
||||||
problem: problem.into(),
|
problem: problem.into(),
|
||||||
@@ -176,9 +196,9 @@ fn Controller(
|
|||||||
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,
|
cb_set_problem,
|
||||||
|
cb_set_is_favorite,
|
||||||
todays_attempt,
|
todays_attempt,
|
||||||
filter_holds: filter_holds.into(),
|
filter_holds: filter_holds.into(),
|
||||||
filtered_problems: filtered_problems.into(),
|
|
||||||
filtered_problem_transformations: filtered_problem_transformations.into(),
|
filtered_problem_transformations: filtered_problem_transformations.into(),
|
||||||
user_interactions: user_interactions.into(),
|
user_interactions: user_interactions.into(),
|
||||||
});
|
});
|
||||||
@@ -205,7 +225,7 @@ fn View() -> impl IntoView {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div class="flex flex-row justify-around">
|
<div class="flex flex-row justify-between">
|
||||||
<NextProblemButton />
|
<NextProblemButton />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -214,12 +234,14 @@ 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 /> <Transformations /> <Separator /><AttemptRadioGroup />
|
<Separator /> <div class="flex flex-row gap-2 justify-between">
|
||||||
<Separator /> <History />
|
<Transformations />
|
||||||
|
<FavoriteButton />
|
||||||
|
</div> <Separator /> <AttemptRadioGroup /> <Separator /> <History />
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row flex-auto justify-end items-start px-2 pt-3">
|
<div class="flex flex-col px-2 pt-3 gap-4">
|
||||||
<HoldsButton />
|
<HoldsButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,6 +302,16 @@ fn Transformations() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn LikedButton() -> impl IntoView {
|
||||||
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
|
let _ctx = use_context::<Context>().unwrap();
|
||||||
|
|
||||||
|
view! { <Button text="Saved" icon=Icon::HeartOutline color=Gradient::PinkRed /> }
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn HoldsButton() -> impl IntoView {
|
fn HoldsButton() -> impl IntoView {
|
||||||
@@ -296,6 +328,25 @@ fn HoldsButton() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn FavoriteButton() -> impl IntoView {
|
||||||
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
|
let ctx = use_context::<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 });
|
||||||
|
let text = Signal::derive(move || if ui_toggle.get() { "Saved" } else { "Save" }.to_string());
|
||||||
|
view! { <Button text icon on_click color=Gradient::PinkRed /> }
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
fn NextProblemButton() -> impl IntoView {
|
fn NextProblemButton() -> impl IntoView {
|
||||||
@@ -635,6 +686,7 @@ mod signals {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(dead_code)]
|
||||||
pub(crate) fn filtered_problems(
|
pub(crate) fn filtered_problems(
|
||||||
wall: Signal<models::Wall>,
|
wall: Signal<models::Wall>,
|
||||||
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
|
||||||
@@ -645,7 +697,7 @@ mod signals {
|
|||||||
wall.problems
|
wall.problems
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
||||||
.map(|problem| problem.clone())
|
.cloned()
|
||||||
.collect::<BTreeSet<models::Problem>>()
|
.collect::<BTreeSet<models::Problem>>()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -665,7 +717,7 @@ mod signals {
|
|||||||
problem_set
|
problem_set
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
.filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
|
||||||
.map(|problem| problem.clone())
|
.cloned()
|
||||||
.collect::<HashSet<models::Problem>>()
|
.collect::<HashSet<models::Problem>>()
|
||||||
})
|
})
|
||||||
.filter(|set| !set.is_empty())
|
.filter(|set| !set.is_empty())
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Err
|
|||||||
if is_at_version(db, 3).await? {
|
if is_at_version(db, 3).await? {
|
||||||
migrate_to_v4(db).await?;
|
migrate_to_v4(db).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +70,13 @@ pub async fn migrate_to_v4(db: &Database) -> Result<(), Box<dyn std::error::Erro
|
|||||||
.map(|problem_uid| {
|
.map(|problem_uid| {
|
||||||
let old_prob = &problems_dump[&(wall_uid, problem_uid)];
|
let old_prob = &problems_dump[&(wall_uid, problem_uid)];
|
||||||
let method = old_prob.method;
|
let method = old_prob.method;
|
||||||
let problem = models::Problem {
|
|
||||||
|
models::Problem {
|
||||||
pattern: models::Pattern {
|
pattern: models::Pattern {
|
||||||
pattern: old_prob.holds.clone(),
|
pattern: old_prob.holds.clone(),
|
||||||
},
|
},
|
||||||
method,
|
method,
|
||||||
};
|
}
|
||||||
problem
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let wall = models::Wall {
|
let wall = models::Wall {
|
||||||
|
|||||||
@@ -43,10 +43,8 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Database
|
|||||||
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
||||||
};
|
};
|
||||||
|
|
||||||
let problem = models::Problem {
|
let pattern = models::Pattern { pattern }.canonicalize();
|
||||||
pattern: models::Pattern { pattern },
|
let problem = models::Problem { pattern, method };
|
||||||
method,
|
|
||||||
};
|
|
||||||
problems.push(problem);
|
problems.push(problem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
|
|||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
use redb::ReadableTable;
|
use redb::ReadableTable;
|
||||||
tracing::trace!("Enter");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
let db = expect_context::<Database>();
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ pub(crate) async fn get_wall_by_uid(wall_uid: models::WallUid) -> Result<RonEnco
|
|||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
tracing::trace!("Enter");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -84,7 +84,7 @@ pub(crate) async fn get_user_interaction(
|
|||||||
use crate::server::db::Database;
|
use crate::server::db::Database;
|
||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
tracing::trace!("Enter");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -129,7 +129,7 @@ pub(crate) async fn get_user_interactions_for_wall(
|
|||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
use redb::ReadableTable;
|
use redb::ReadableTable;
|
||||||
tracing::trace!("Enter");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -188,7 +188,7 @@ pub(crate) async fn upsert_todays_attempt(
|
|||||||
use crate::server::db::DatabaseOperationError;
|
use crate::server::db::DatabaseOperationError;
|
||||||
use leptos::prelude::expect_context;
|
use leptos::prelude::expect_context;
|
||||||
|
|
||||||
tracing::trace!("Enter");
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
#[derive(Debug, Error, Display, From)]
|
#[derive(Debug, Error, Display, From)]
|
||||||
enum Error {
|
enum Error {
|
||||||
@@ -244,3 +244,62 @@ pub(crate) async fn upsert_todays_attempt(
|
|||||||
.map_err(ServerFnError::new)
|
.map_err(ServerFnError::new)
|
||||||
.map(RonEncoded::new)
|
.map(RonEncoded::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets is_favorite field for a problem
|
||||||
|
#[server(
|
||||||
|
input = Ron,
|
||||||
|
output = Ron,
|
||||||
|
custom = RonEncoded
|
||||||
|
)]
|
||||||
|
#[tracing::instrument(err(Debug))]
|
||||||
|
pub(crate) async fn set_is_favorite(
|
||||||
|
wall_uid: models::WallUid,
|
||||||
|
problem: models::Problem,
|
||||||
|
is_favorite: bool,
|
||||||
|
) -> Result<RonEncoded<models::UserInteraction>, ServerFnError> {
|
||||||
|
use crate::server::db::Database;
|
||||||
|
use crate::server::db::DatabaseOperationError;
|
||||||
|
use leptos::prelude::expect_context;
|
||||||
|
|
||||||
|
crate::tracing::on_enter!();
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Display, From)]
|
||||||
|
enum Error {
|
||||||
|
#[display("Wall not found: {_0:?}")]
|
||||||
|
WallNotFound(#[error(not(source))] models::WallUid),
|
||||||
|
|
||||||
|
DatabaseOperation(DatabaseOperationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner(wall_uid: models::WallUid, problem: models::Problem, is_favorite: bool) -> Result<UserInteraction, Error> {
|
||||||
|
let db = expect_context::<Database>();
|
||||||
|
|
||||||
|
let user_interaction = db
|
||||||
|
.write(|txn| {
|
||||||
|
let mut user_table = txn.open_table(crate::server::db::current::TABLE_USER)?;
|
||||||
|
|
||||||
|
let key = (wall_uid, problem.clone());
|
||||||
|
|
||||||
|
// Pop or default
|
||||||
|
let mut user_interaction = user_table
|
||||||
|
.remove(&key)?
|
||||||
|
.map(|guard| guard.value())
|
||||||
|
.unwrap_or_else(|| models::UserInteraction::new(wall_uid, problem));
|
||||||
|
|
||||||
|
user_interaction.is_favorite = is_favorite;
|
||||||
|
|
||||||
|
user_table.insert(key, user_interaction.clone())?;
|
||||||
|
|
||||||
|
Ok(user_interaction)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user_interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner(wall_uid, problem, is_favorite)
|
||||||
|
.await
|
||||||
|
.map_err(error_reporter::Report::new)
|
||||||
|
.map_err(ServerFnError::new)
|
||||||
|
.map(RonEncoded::new)
|
||||||
|
}
|
||||||
|
|||||||
BIN
docs/ascend.jpg
BIN
docs/ascend.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 392 KiB |
18
flake.lock
generated
18
flake.lock
generated
@@ -10,11 +10,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1741078851,
|
"lastModified": 1746696290,
|
||||||
"narHash": "sha256-1Qu/Uu+yPUDhHM2XjTbwQqpSrYhhHu7TpHHrT7UO/0o=",
|
"narHash": "sha256-YokYinNgGIu80OErVMuFoIELhetzb45aWKTiKYNXvWA=",
|
||||||
"owner": "plul",
|
"owner": "plul",
|
||||||
"repo": "basecamp",
|
"repo": "basecamp",
|
||||||
"rev": "3e4579d8b4400506e5f53069448b3471608b5281",
|
"rev": "108ef2874fd8f934602cda5bfdc0e58a541c6b4a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1742456341,
|
"lastModified": 1746576598,
|
||||||
"narHash": "sha256-yvdnTnROddjHxoQqrakUQWDZSzVchczfsuuMOxg476c=",
|
"narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7344a3b78128f7b1765dba89060b015fb75431a7",
|
"rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -53,11 +53,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1742524367,
|
"lastModified": 1746671794,
|
||||||
"narHash": "sha256-KzTwk/5ETJavJZYV1DEWdCx05M4duFCxCpRbQSKWpng=",
|
"narHash": "sha256-V+mpk2frYIEm85iYf+KPDmCGG3zBRAEhbv0E3lHdG2U=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "70bf752d176b2ce07417e346d85486acea9040ef",
|
"rev": "ceec434b8741c66bb8df5db70d7e629a9d9c598f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
Reference in New Issue
Block a user