This commit is contained in:
2025-02-27 22:52:23 +01:00
parent abe52b2b66
commit 1280e1fc20
8 changed files with 178 additions and 62 deletions

View File

@@ -6,7 +6,11 @@ language-servers = ["rust-analyzer", "tailwindcss-ls"]
# procMacro = { ignored = { leptos_macro = ["server"] } } # procMacro = { ignored = { leptos_macro = ["server"] } }
cargo = { features = ["ssr", "hydrate"] } cargo = { features = ["ssr", "hydrate"] }
check = { command = "clippy" } check = { command = "clippy" }
rustfmt = { overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"] } rustfmt = { overrideCommand = [
"sh",
"-c",
"rustfmt --emit stdout --edition 2024 | leptosfmt --stdin",
] }
[language-server.tailwindcss-ls] [language-server.tailwindcss-ls]
config = { userLanguages = { rust = "html", "*.rs" = "html" } } config = { userLanguages = { rust = "html", "*.rs" = "html" } }

View File

@@ -67,3 +67,23 @@ pub fn Check() -> impl IntoView {
</svg> </svg>
} }
} }
#[component]
pub fn Heart() -> impl IntoView {
view! {
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
/>
</svg>
}
}

View File

@@ -18,45 +18,8 @@ pub mod components {
pub mod problem_info; pub mod problem_info;
} }
pub mod resources { pub mod resources;
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models::{self};
use leptos::prelude::Get;
use leptos::prelude::Signal;
use leptos::server::Resource;
use server_fn::ServerFnError;
type RonResource<T> = Resource<Result<T, ServerFnError>, Ron>;
pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wall> {
Resource::new_with_options(
move || wall_uid.get(),
move |wall_uid| async move { crate::server_functions::get_wall(wall_uid).await.map(RonEncoded::into_inner) },
false,
)
}
pub fn problem_by_uid(wall_uid: Signal<models::WallUid>, problem_uid: Signal<models::ProblemUid>) -> RonResource<models::Problem> {
Resource::new_with_options(
move || (wall_uid.get(), problem_uid.get()),
move |(wall_uid, problem_uid)| async move {
crate::server_functions::get_problem(wall_uid, problem_uid)
.await
.map(RonEncoded::into_inner)
},
false,
)
}
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<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,
)
}
}
pub mod codec; pub mod codec;
pub mod models; pub mod models;
pub mod server_functions; pub mod server_functions;

View File

@@ -14,11 +14,14 @@ pub use v2::Root;
pub use v2::Wall; pub use v2::Wall;
pub use v2::WallDimensions; pub use v2::WallDimensions;
pub use v2::WallUid; pub use v2::WallUid;
pub use v3::UserInteraction;
pub mod v3 { pub mod v3 {
use super::v2; use super::v2;
use chrono::NaiveDate;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use std::collections::BTreeSet;
/// Registers user interaction with a problem /// Registers user interaction with a problem
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -27,26 +30,26 @@ pub mod v3 {
pub problem_uid: v2::ProblemUid, pub problem_uid: v2::ProblemUid,
/// Climbed problem /// Climbed problem
pub is_completed: bool, pub completed_on: BTreeSet<NaiveDate>,
/// Flashed problem /// Flashed problem
pub is_flashed: bool, pub flashed_on: BTreeSet<NaiveDate>,
/// Is among favorite problems /// Is among favorite problems
pub is_favorite: bool, pub is_favorite: bool,
/// Added to personal challenges /// Added to personal challenges
pub is_challenge: bool, pub is_saved: bool,
} }
impl UserInteraction { impl UserInteraction {
pub fn new(wall_uid: v2::WallUid, problem_uid: v2::ProblemUid) -> Self { pub fn new(wall_uid: v2::WallUid, problem_uid: v2::ProblemUid) -> Self {
Self { Self {
wall_uid, wall_uid,
problem_uid, problem_uid,
is_completed: false,
is_flashed: false,
is_favorite: false, is_favorite: false,
is_challenge: false, completed_on: BTreeSet::new(),
flashed_on: BTreeSet::new(),
is_saved: false,
} }
} }
} }

View File

@@ -78,6 +78,8 @@ pub fn Wall() -> impl IntoView {
} }
}); });
let user_interaction = crate::resources::user_interaction(wall_uid, problem_uid.into());
let header_items = move || HeaderItems { let header_items = move || HeaderItems {
left: vec![], left: vec![],
middle: vec![HeaderItem { middle: vec![HeaderItem {
@@ -98,6 +100,7 @@ pub fn Wall() -> impl IntoView {
let foo = RwSignal::new(false); let foo = RwSignal::new(false);
let bar = RwSignal::new(false); let bar = RwSignal::new(false);
let baz = RwSignal::new(false);
leptos::view! { leptos::view! {
<div class="min-w-screen min-h-screen bg-neutral-950"> <div class="min-w-screen min-h-screen bg-neutral-950">
@@ -153,7 +156,7 @@ pub fn Wall() -> impl IntoView {
</span> </span>
</div> </div>
// <icons::Bolt /> // <icons::Bolt />
<div class="w-full text-lg font-semibold">Flash!</div> <div class="w-full text-lg font-semibold">Flash</div>
</div> </div>
</label> </label>
</div> </div>
@@ -169,18 +172,48 @@ pub fn Wall() -> impl IntoView {
/> />
<label for="climbed-option" class="cursor-pointer"> <label for="climbed-option" class="cursor-pointer">
<div <div
class="flex items-center gap-2 relative px-5 py-3.5 transition-all ease-in duration-75 bg-gray-900 rounded-md" class="flex items-center gap-2 relative px-5 py-3.5 transition-all ease-in duration-75 bg-gray-900 rounded-md text-white"
class:bg-transparent=move || bar.get() class:bg-transparent=move || bar.get()
> >
<div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500 text-white"> <div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500">
<span class=("text-teal-300", move || bar.get())> <span class=("text-black", move || bar.get())>
<icons::Check /> <icons::Check />
</span> </span>
</div> </div>
<div class="w-full text-lg font-semibold">Climbed!</div> <div
class="w-full text-lg font-semibold"
class=("text-black", move || bar.get())
>
Climbed
</div>
</div> </div>
</label> </label>
</div> </div>
<div class="relative inline-flex items-center justify-center p-0.5 mb-2 me-2 overflow-hidden text-sm font-medium rounded-lg group bg-gradient-to-br from-pink-500 to-orange-400 text-white hover:brightness-125">
<input
type="checkbox"
id="favorite-option"
value=""
class="hidden peer"
required=""
bind:checked=baz
/>
<label for="favorite-option" class="cursor-pointer">
<div
class="flex items-center gap-2 relative px-5 py-3.5 transition-all ease-in duration-75 bg-gray-900 rounded-md"
class:bg-transparent=move || baz.get()
>
<div class="aspect-square rounded-sm ring-offset-gray-700 border-gray-500 text-pink-500">
<span class=("text-white", move || baz.get())>
<icons::Heart />
</span>
</div>
<div class="w-full text-lg font-semibold">Favorite</div>
</div>
</label>
</div>
</div> </div>
</div> </div>
}; };
@@ -227,7 +260,6 @@ fn Hold(hold: models::Hold, role: Signal<Option<HoldRole>>) -> impl IntoView {
Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"), Some(HoldRole::Zone) => Some("outline outline-offset-2 outline-amber-500"),
Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"), Some(HoldRole::End) => Some("outline outline-offset-2 outline-red-500"),
None => Some("brightness-50"), None => Some("brightness-50"),
// None => None,
}; };
let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string(); let mut s = "bg-sky-100 aspect-square rounded hover:brightness-125".to_string();
if let Some(c) = role_classes { if let Some(c) = role_classes {

View File

@@ -0,0 +1,55 @@
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::models::{self};
use leptos::prelude::Get;
use leptos::prelude::Signal;
use leptos::server::Resource;
use server_fn::ServerFnError;
type RonResource<T> = Resource<Result<T, ServerFnError>, Ron>;
pub fn wall_by_uid(wall_uid: Signal<models::WallUid>) -> RonResource<models::Wall> {
Resource::new_with_options(
move || wall_uid.get(),
move |wall_uid| async move { crate::server_functions::get_wall(wall_uid).await.map(RonEncoded::into_inner) },
false,
)
}
pub fn problem_by_uid(wall_uid: Signal<models::WallUid>, problem_uid: Signal<models::ProblemUid>) -> RonResource<models::Problem> {
Resource::new_with_options(
move || (wall_uid.get(), problem_uid.get()),
move |(wall_uid, problem_uid)| async move {
crate::server_functions::get_problem(wall_uid, problem_uid)
.await
.map(RonEncoded::into_inner)
},
false,
)
}
pub fn problems_for_wall(wall_uid: Signal<models::WallUid>) -> RonResource<Vec<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,
)
}
pub fn user_interaction(
wall_uid: Signal<models::WallUid>,
problem_uid: Signal<Option<models::ProblemUid>>,
) -> RonResource<Option<models::UserInteraction>> {
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_user_interaction(wall_uid, problem_uid)
.await
.map(RonEncoded::into_inner)
},
false,
)
}

View File

@@ -1,6 +1,7 @@
use crate::codec::ron::Ron; use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded; use crate::codec::ron::RonEncoded;
use crate::models; use crate::models;
use crate::models::UserInteraction;
use leptos::server; use leptos::server;
use server_fn::ServerFnError; use server_fn::ServerFnError;
@@ -14,7 +15,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::debug!("Enter"); tracing::trace!("Enter");
let db = expect_context::<Database>(); let db = expect_context::<Database>();
@@ -26,8 +27,6 @@ pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError>
}) })
.await?; .await?;
tracing::debug!("Exit");
Ok(RonEncoded::new(walls)) Ok(RonEncoded::new(walls))
} }
@@ -41,7 +40,7 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<mod
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::debug!("Enter"); tracing::trace!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)] #[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error { enum Error {
@@ -63,8 +62,6 @@ pub(crate) async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<mod
}) })
.await?; .await?;
tracing::debug!("ok");
Ok(RonEncoded::new(wall)) Ok(RonEncoded::new(wall))
} }
@@ -78,7 +75,7 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<R
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::debug!("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)]
enum Error { enum Error {
@@ -122,11 +119,53 @@ pub(crate) async fn get_problems_for_wall(wall_uid: models::WallUid) -> Result<R
let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?; let problems = inner(wall_uid).await.map_err(error_reporter::Report::new).map_err(ServerFnError::new)?;
tracing::debug!("ok");
Ok(RonEncoded::new(problems)) Ok(RonEncoded::new(problems))
} }
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(err(Debug))]
pub(crate) async fn get_user_interaction(
wall_uid: models::WallUid,
problem_uid: models::ProblemUid,
) -> Result<RonEncoded<Option<models::UserInteraction>>, 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, problem_uid: models::ProblemUid) -> Result<Option<UserInteraction>, Error> {
let db = expect_context::<Database>();
let user_interaction = db
.read(|txn| {
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());
Ok(user_interaction)
})
.await?;
Ok(user_interaction)
}
let user_interaction = inner(wall_uid, problem_uid)
.await
.map_err(error_reporter::Report::new)
.map_err(ServerFnError::new)?;
Ok(RonEncoded::new(user_interaction))
}
#[server( #[server(
input = Ron, input = Ron,
output = Ron, output = Ron,
@@ -137,7 +176,7 @@ pub(crate) async fn get_problem(wall_uid: models::WallUid, problem_uid: models::
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::debug!("Enter"); tracing::trace!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)] #[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error { enum Error {

View File

@@ -142,7 +142,7 @@
pkgs.binaryen pkgs.binaryen
]; ];
env.RUST_LOG = "info,ascend=trace"; env.RUST_LOG = "info,ascend=debug";
env.MOONBOARD_PROBLEMS = "moonboard-problems"; env.MOONBOARD_PROBLEMS = "moonboard-problems";
}; };
}; };