Compare commits

..

4 Commits

Author SHA1 Message Date
abe52b2b66 wip: v3 2025-02-27 17:51:07 +01:00
3afd9cd8b2 feat: UserInteraction table 2025-02-27 17:03:43 +01:00
4ebd49a31e wip 2025-02-27 16:06:46 +01:00
54bf4ddaec wip: flash button 2025-02-26 23:41:55 +01:00
13 changed files with 264 additions and 26 deletions

View File

@@ -5,9 +5,8 @@ language-servers = ["rust-analyzer", "tailwindcss-ls"]
[language-server.rust-analyzer.config] [language-server.rust-analyzer.config]
# procMacro = { ignored = { leptos_macro = ["server"] } } # procMacro = { ignored = { leptos_macro = ["server"] } }
cargo = { features = ["ssr", "hydrate"] } cargo = { features = ["ssr", "hydrate"] }
check = { command = "clippy" }
[language-server.rust-analyzer.config.check] rustfmt = { overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"] }
command = "clippy"
[language-server.tailwindcss-ls] [language-server.tailwindcss-ls]
config = { userLanguages = { rust = "html", "*.rs" = "html" } } config = { userLanguages = { rust = "html", "*.rs" = "html" } }

View File

@@ -0,0 +1,69 @@
use leptos::prelude::*;
#[component]
pub fn Bolt() -> impl IntoView {
view! {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
fill-rule="evenodd"
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z"
clip-rule="evenodd"
/>
</svg>
}
}
#[component]
pub fn Wrench() -> impl IntoView {
view! {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
fill-rule="evenodd"
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
clip-rule="evenodd"
/>
</svg>
}
}
#[component]
pub fn Forward() -> impl IntoView {
view! {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path d="M5.055 7.06C3.805 6.347 2.25 7.25 2.25 8.69v8.122c0 1.44 1.555 2.343 2.805 1.628L12 14.471v2.34c0 1.44 1.555 2.343 2.805 1.628l7.108-4.061c1.26-.72 1.26-2.536 0-3.256l-7.108-4.061C13.555 6.346 12 7.249 12 8.689v2.34L5.055 7.061Z" />
</svg>
}
}
#[component]
pub fn Check() -> impl IntoView {
view! {
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
fill-rule="evenodd"
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
}
}

View File

@@ -13,9 +13,11 @@ pub mod components {
pub mod button; pub mod button;
pub mod header; pub mod header;
pub mod icons;
pub mod problem; pub mod problem;
pub mod problem_info; pub mod problem_info;
} }
pub mod resources { pub mod resources {
use crate::codec::ron::Ron; use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded; use crate::codec::ron::RonEncoded;

View File

@@ -15,6 +15,43 @@ pub use v2::Wall;
pub use v2::WallDimensions; pub use v2::WallDimensions;
pub use v2::WallUid; pub use v2::WallUid;
pub mod v3 {
use super::v2;
use serde::Deserialize;
use serde::Serialize;
/// Registers user interaction with a problem
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserInteraction {
pub wall_uid: v2::WallUid,
pub problem_uid: v2::ProblemUid,
/// Climbed problem
pub is_completed: bool,
/// Flashed problem
pub is_flashed: bool,
/// Is among favorite problems
pub is_favorite: bool,
/// Added to personal challenges
pub is_challenge: bool,
}
impl UserInteraction {
pub fn new(wall_uid: v2::WallUid, problem_uid: v2::ProblemUid) -> Self {
Self {
wall_uid,
problem_uid,
is_completed: false,
is_flashed: false,
is_favorite: false,
is_challenge: false,
}
}
}
}
pub mod v2 { pub mod v2 {
use super::v1; use super::v1;
use derive_more::Display; use derive_more::Display;

View File

@@ -30,10 +30,10 @@ pub fn Routes() -> 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 problems = crate::resources::problems_for_wall(wall_uid);
let header_items = HeaderItems { let header_items = move || HeaderItems {
left: vec![HeaderItem { left: vec![HeaderItem {
text: "← Ascend".to_string(), text: "← Ascend".to_string(),
link: Some("/".to_string()), link: Some(format!("/wall/{}", wall_uid.get())),
}], }],
middle: vec![HeaderItem { middle: vec![HeaderItem {
text: "Routes".to_string(), text: "Routes".to_string(),
@@ -63,7 +63,10 @@ pub fn Routes() -> impl IntoView {
each=problems_sample each=problems_sample
key=|problem| problem.uid key=|problem| problem.uid
children=move |problem: models::Problem| { children=move |problem: models::Problem| {
view! { <Problem dim=wall_dimensions problem /> <hr class="h-px my-8 border-0 bg-gray-700" />} view! {
<Problem dim=wall_dimensions problem />
<hr class="h-px my-8 border-0 bg-gray-700" />
}
} }
/> />
</div> </div>
@@ -76,7 +79,7 @@ pub fn Routes() -> impl IntoView {
view! { view! {
<div class="min-w-screen min-h-screen bg-neutral-950"> <div class="min-w-screen min-h-screen bg-neutral-950">
<StyledHeader items=header_items /> <StyledHeader items=Signal::derive(header_items) />
<div class="container mx-auto mt-6"> <div class="container mx-auto mt-6">
<Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense> <Suspense fallback=|| view! { <p>"loading"</p> }>{suspend}</Suspense>

View File

@@ -23,9 +23,8 @@ pub fn Settings() -> impl IntoView {
<div class="min-w-screen min-h-screen bg-neutral-950"> <div class="min-w-screen min-h-screen bg-neutral-950">
<StyledHeader items=header_items /> <StyledHeader items=header_items />
<div class="container mx-auto mt-2">
// {move || view! { <Import wall_uid=wall_uid.get() /> }} // {move || view! { <Import wall_uid=wall_uid.get() /> }}
</div> <div class="container mx-auto mt-2"></div>
</div> </div>
} }
} }

View File

@@ -4,6 +4,7 @@ use crate::components::button::Button;
use crate::components::header::HeaderItem; use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems; use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader; use crate::components::header::StyledHeader;
use crate::components::icons;
use crate::models; use crate::models;
use crate::models::HoldRole; use crate::models::HoldRole;
use leptos::Params; use leptos::Params;
@@ -95,6 +96,9 @@ pub fn Wall() -> impl IntoView {
], ],
}; };
let foo = RwSignal::new(false);
let bar = 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">
<StyledHeader items=Signal::derive(header_items) /> <StyledHeader items=Signal::derive(header_items) />
@@ -115,12 +119,68 @@ pub fn Wall() -> impl IntoView {
<div> <div>
<Button <Button
onclick=move |_| fn_next_problem(&wall) onclick=move |_| fn_next_problem(&wall)
// TODO: use forward icon
text="➤ Next problem" text="➤ Next problem"
/> />
<div class="m-4"/> <div class="m-4" />
{move || problem_signal.get().map(|problem| view! { <ProblemInfo problem /> })} {move || {
problem_signal
.get()
.map(|problem| view! { <ProblemInfo problem /> })
}}
<div class="m-4" />
<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-cyan-500 to-blue-500 text-white hover:brightness-125">
<input
type="checkbox"
id="flash-option"
value=""
class="hidden peer"
required=""
bind:checked=foo
/>
<label for="flash-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 || foo.get()
>
<div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500 text-white">
<span class=("text-cyan-500", move || foo.get())>
<icons::Check />
</span>
</div>
// <icons::Bolt />
<div class="w-full text-lg font-semibold">Flash!</div>
</div>
</label>
</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-teal-300 to-lime-300 text-white hover:brightness-125">
<input
type="checkbox"
id="climbed-option"
value=""
class="hidden peer"
required=""
bind:checked=bar
/>
<label for="climbed-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 || bar.get()
>
<div class="aspect-square rounded-sm ring-offset-gray-700 bg-white border-gray-500 text-white">
<span class=("text-teal-300", move || bar.get())>
<icons::Check />
</span>
</div>
<div class="w-full text-lg font-semibold">Climbed!</div>
</div>
</label>
</div>
</div> </div>
</div> </div>
}; };

View File

@@ -56,6 +56,15 @@ impl Database {
self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into)) self.read(|dbtx| dbtx.open_table(TABLE_VERSION)?.get(()).map(|o| o.map(|v| v.value())).map_err(Into::into))
.await .await
} }
#[tracing::instrument(skip_all)]
pub async fn set_version(&self, version: Version) -> Result<(), DatabaseOperationError> {
self.write(|txn| {
let mut table = txn.open_table(TABLE_VERSION)?;
table.insert((), version)?;
Ok(())
}).await
}
} }
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
@@ -75,7 +84,7 @@ impl DatabaseOperationError {
} }
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version"); pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
#[derive(Serialize, Deserialize, Debug, derive_more::Display)] #[derive(Serialize, Deserialize, Debug, derive_more::Display, PartialEq, Eq, PartialOrd, Ord)]
#[display("{version}")] #[display("{version}")]
pub struct Version { pub struct Version {
pub version: u64, pub version: u64,
@@ -86,6 +95,7 @@ impl Version {
} }
} }
// TODO: implement test
#[tracing::instrument(skip_all, err)] #[tracing::instrument(skip_all, err)]
pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> { pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperationError> {
db.write(|txn| { db.write(|txn| {
@@ -116,6 +126,13 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
let table = txn.open_table(current::TABLE_PROBLEMS)?; let table = txn.open_table(current::TABLE_PROBLEMS)?;
assert!(table.is_empty()?); assert!(table.is_empty()?);
} }
// User table
{
// Opening the table creates the table
let table = txn.open_table(current::TABLE_USER)?;
assert!(table.is_empty()?);
}
} }
Ok(()) Ok(())
@@ -126,7 +143,26 @@ pub async fn init_at_current_version(db: &Database) -> Result<(), DatabaseOperat
} }
use crate::models; use crate::models;
pub use v2 as current; pub mod current {
use super::v2;
use super::v3;
pub use v2::TABLE_PROBLEMS;
pub use v2::TABLE_ROOT;
pub use v2::TABLE_WALLS;
pub use v3::TABLE_USER;
pub use v3::VERSION;
}
pub mod v3 {
use crate::models;
use crate::server::db::bincode::Bincode;
use redb::TableDefinition;
pub const VERSION: u64 = 3;
pub const TABLE_USER: TableDefinition<Bincode<(models::v2::WallUid, models::v2::ProblemUid)>, Bincode<models::v3::UserInteraction>> =
TableDefinition::new("user");
}
pub mod v2 { pub mod v2 {
use crate::models; use crate::models;

View File

@@ -1,6 +1,34 @@
use super::db::Database; use super::db::Database;
use super::db::DatabaseOperationError;
use super::db::{self};
#[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? {
migrate_to_v3(db).await?;
}
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all, err)]
pub async fn migrate_to_v3(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
use redb::ReadableTableMetadata;
tracing::warn!("MIGRATING TO VERSION 3");
db.write(|txn| {
// Opening the table creates the table
let table = txn.open_table(db::current::TABLE_USER)?;
assert!(table.is_empty()?);
Ok(())
})
.await?;
db.set_version(db::Version {version: db::v3::VERSION}).await?;
Ok(())
}
async fn is_at_version(db: &Database, version: u64) -> Result<bool, DatabaseOperationError> {
let v = db.get_version().await?;
Ok(v == Some(db::Version { version }))
}

View File

@@ -6,6 +6,9 @@
}, },
// https://tailwindcss.com/docs/content-configuration#using-regular-expressions // https://tailwindcss.com/docs/content-configuration#using-regular-expressions
safelist: [ safelist: [
{
pattern: /bg-transparent/,
},
{ {
pattern: /grid-cols-.+/, pattern: /grid-cols-.+/,
}, },

18
flake.lock generated
View File

@@ -10,11 +10,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1740139153, "lastModified": 1740523129,
"narHash": "sha256-Xa1wCQBbsFHCaXgVBjtraZcWywuXBN+YhdqGle4nLVc=", "narHash": "sha256-q/k/T9Hf+aCo8/xQnqyw+E7dYx8Nq1u7KQ2ylORcP+M=",
"owner": "plul", "owner": "plul",
"repo": "basecamp", "repo": "basecamp",
"rev": "0a29da733dc2f7b386dd3667b63a51c55238fbfd", "rev": "0882906c106ab0bf193b3417c845c5accbec2419",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -25,11 +25,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1740396192, "lastModified": 1740547748,
"narHash": "sha256-ATMHHrg3sG1KgpQA5x8I+zcYpp5Sf17FaFj/fN+8OoQ=", "narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d9b69c3ec2a2e2e971c534065bdd53374bd68b97", "rev": "3a05eebede89661660945da1f151959900903b6a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -53,11 +53,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1740450604, "lastModified": 1740623427,
"narHash": "sha256-T/lqASXzCzp5lJISCUw+qwfRmImVUnhKgAhn8ymRClI=", "narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "5961ca311c85c31fc5f51925b4356899eed36221", "rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -53,3 +53,6 @@ leptos-discord:
leptos-issues: leptos-issues:
xdg-open "https://github.com/leptos-rs/leptos/issues" xdg-open "https://github.com/leptos-rs/leptos/issues"
icons:
xdg-open "https://heroicons.com/"

View File

@@ -9,5 +9,4 @@
- decide on holds vs wall-edit terminology - decide on holds vs wall-edit terminology
- clock - clock
- hotkeys (enter =next problem, arrow = shift left/right up/down) - hotkeys (enter =next problem, arrow = shift left/right up/down)
- remove brightness reduction when mousing over holds
- impl `sizes` hint next to `srcset` - impl `sizes` hint next to `srcset`