wip: v3
This commit is contained in:
@@ -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" } }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
55
crates/ascend/src/resources.rs
Normal file
55
crates/ascend/src/resources.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user