feat: header on main page

This commit is contained in:
2025-01-23 19:25:14 +01:00
parent 71de1b1f64
commit 52c5d9f1ed
10 changed files with 205 additions and 110790 deletions

View File

@@ -1,5 +1,4 @@
use crate::pages::edit_wall::EditWall;
use crate::pages::wall::Wall;
use crate::pages;
use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;
@@ -42,8 +41,9 @@ pub fn App() -> impl leptos::IntoView {
<Router>
<main>
<Routes fallback=|| "Not found">
<Route path=path!("/wall") view=Wall />
<Route path=path!("/wall/edit") view=EditWall />
<Route path=path!("/") view=pages::wall::Wall />
<Route path=path!("/wall/edit") view=pages::edit_wall::EditWall />
<Route path=path!("/wall/routes") view=pages::routes::Routes />
</Routes>
</main>
</Router>

View File

@@ -11,6 +11,52 @@ pub struct HeaderItem {
pub link: Option<String>,
}
/// Header with background color etc.
#[component]
pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
view! {
<div class="flex">
// Left gradient chunk
<div class="flex-grow">
<div class="h-2/5" style="background: #eaac53" />
<div
class="h-3/5"
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
<div class="flex-none container mx-auto text-black" style="background: #eaac53">
<Header items />
</div>
// Right gradient chunk
<div class="flex-grow" style="background: #eaac53" />
</div>
<div class="flex">
// Left gradient chunk
<div class="flex-grow" />
<div class="flex-none container mx-auto">
// Background color gradient
<div
class="h-6"
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
// Right gradient chunk
<div class="flex-grow">
<div class="h-4/5" style="background: #eaac53" />
<div
class="h-1/5"
style="background: linear-gradient(to bottom right, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
</div>
}
}
/// Function header without styling
#[component]
pub fn Header(items: HeaderItems) -> impl IntoView {
let HeaderItems { left, middle, right } = items;
@@ -18,17 +64,17 @@ pub fn Header(items: HeaderItems) -> impl IntoView {
view! {
<div class="grid grid-cols-[1fr_3fr_1fr] text-xl font-semibold p-4">
// Left side of header
<div>
<div class="justify-self-start">
<Items items=left />
</div>
// Expanding space in the middle
<div class="w-fit mx-auto font-semibold">
<div class="justify-self-center font-semibold">
<Items items=middle />
</div>
// Right side of header
<div>
<div class="justify-self-end">
<Items items=right />
</div>
</div>
@@ -37,7 +83,12 @@ pub fn Header(items: HeaderItems) -> impl IntoView {
#[component]
fn Items(items: Vec<HeaderItem>) -> impl IntoView {
items.into_iter().map(|item| view! { <Item item /> }).collect_view()
let items = items.into_iter().map(|item| view! { <Item item /> }).collect_view();
view! {
<div class="flex gap-4">
{ items }
</div>
}
}
#[component]

View File

@@ -1,6 +1,7 @@
pub mod app;
pub mod pages {
pub mod edit_wall;
pub mod routes;
pub mod wall;
}
pub mod components {

View File

@@ -1,7 +1,7 @@
use crate::codec::ron::RonCodec;
use crate::components::header::Header;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::models;
use crate::models::HoldPosition;
use crate::models::Wall;
@@ -26,7 +26,7 @@ pub fn EditWall() -> impl leptos::IntoView {
let header_items = HeaderItems {
left: vec![HeaderItem {
text: "← Ascend".to_string(),
link: Some("/wall".to_string()),
link: Some("/".to_string()),
}],
middle: vec![HeaderItem {
text: "EDIT WALL".to_string(),
@@ -37,44 +37,7 @@ pub fn EditWall() -> impl leptos::IntoView {
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<div class="flex">
// Left gradient chunk
<div class="flex-grow">
<div class="h-2/5" style="background: #eaac53" />
<div
class="h-3/5"
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
<div class="flex-none container mx-auto text-black" style="background: #eaac53">
<Header items=header_items />
</div>
// Right gradient chunk
<div class="flex-grow" style="background: #eaac53" />
</div>
<div class="flex">
// Left gradient chunk
<div class="flex-grow" />
<div class="flex-none container mx-auto">
// Background color gradient
<div
class="h-6"
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
// Right gradient chunk
<div class="flex-grow">
<div class="h-4/5" style="background: #eaac53" />
<div
class="h-1/5"
style="background: linear-gradient(to bottom right, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
/>
</div>
</div>
<StyledHeader items=header_items />
<div class="container mx-auto mt-2">
<Await future=load let:data>
@@ -94,7 +57,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
holds.push(view! { <Hold hold /> });
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-2", data.wall.rows, data.wall.cols);
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols);
view! {
<div>

View File

@@ -0,0 +1,85 @@
use crate::codec::ron::RonCodec;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use leptos::prelude::*;
use serde::Deserialize;
use serde::Serialize;
use std::ops::Deref;
use std::path::PathBuf;
#[component]
pub fn Routes() -> impl leptos::IntoView {
let load = async move {
// TODO: What to do about this unwrap?
load_initial_data().await.unwrap()
};
let header_items = HeaderItems {
left: vec![HeaderItem {
text: "← Ascend".to_string(),
link: Some("/".to_string()),
}],
middle: vec![HeaderItem {
text: "ROUTES".to_string(),
link: None,
}],
right: vec![],
};
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<StyledHeader items=header_items />
<div class="container mx-auto mt-2">
<Await future=load let:data>
<Ready data=data.deref().to_owned() />
</Await>
</div>
</div>
}
}
#[component]
fn Ready(data: InitialData) -> impl leptos::IntoView {
tracing::debug!("ready");
// let import_from_mini_moonboard = Action::from(ServerAction::<ImportFromMiniMoonboard>::new());
view! {
// <p>"Import problems from"</p>
// <button on:click=import_from_mini_moonboard>"Mini Moonboard"</button>
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InitialData {}
#[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
// use crate::server::state::State;
// let state = expect_context::<State>();
Ok(RonCodec::new(InitialData {}))
}
#[server(name = ImportFromMiniMoonboard)]
#[tracing::instrument]
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
tracing::info!("Importing mini moonboard problems");
let file_path: PathBuf = todo!();
let problems = crate::server::import_mini_moonboard_problems(&file_path).await?;
use crate::server::state::State;
let state = expect_context::<State>();
state
.persistent
.update(|s| {
s.problems.problems.extend(problems);
})
.await?;
Ok(())
}

View File

@@ -1,7 +1,7 @@
use crate::codec::ron::RonCodec;
use crate::components::header::Header;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::models;
use crate::models::HoldRole;
use leptos::prelude::*;
@@ -23,16 +23,23 @@ pub fn Wall() -> impl leptos::IntoView {
text: "Ascend".to_string(),
link: None,
}],
right: vec![HeaderItem {
text: "Edit wall".to_string(),
link: Some("/wall/edit".to_string()),
}],
right: vec![
HeaderItem {
text: "Routes".to_string(),
link: Some("/wall/routes".to_string()),
},
HeaderItem {
text: "Holds".to_string(),
link: Some("/wall/edit".to_string()),
},
],
};
leptos::view! {
<div class="min-w-screen min-h-screen bg-slate-900">
<div class="container mx-auto">
<Header items=header_items />
<StyledHeader items=header_items />
<div class="container mx-auto mt-2">
<Await future=load let:data>
<Ready data=data.deref().to_owned() />
</Await>
@@ -64,7 +71,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
cells.push(cell);
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-2", data.wall.rows, data.wall.cols);
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", data.wall.rows, data.wall.cols);
view! {
<div class=move || { grid_classes.clone() }>{cells}</div>
@@ -83,7 +90,7 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp
// None => Some("brightness-50"),
None => None,
};
let mut s = "bg-indigo-100 aspect-square rounded".to_string();
let mut s = "bg-sky-100 aspect-square rounded".to_string();
if let Some(c) = role_classes {
s.push(' ');
s.push_str(c);
@@ -93,7 +100,7 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp
let img = hold.image.map(|img| {
let src = format!("/files/holds/{}", img.filename);
view! { <img class="hover:object-scale-down object-cover w-full h-full" src=src /> }
view! { <img class="object-cover w-full h-full" src=src /> }
});
view! { <div class=class>{img}</div> }

View File

@@ -76,7 +76,7 @@ pub mod state {
pub mod persistence;
pub const STATE_FILE: &str = "state.ron";
pub const STATE_FILE: &str = "datastore/private/state.ron";
#[tracing::instrument]
pub async fn main() {
@@ -127,6 +127,8 @@ async fn serve() -> Result<(), Error> {
use leptos_axum::LeptosRoutes;
use leptos_axum::generate_route_list;
run_migrations().await.map_err(self::Error::Migration)?;
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
// For deployment these variables are:
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
@@ -166,10 +168,11 @@ async fn load_state() -> Result<State, Error> {
tracing::info!("Loading state");
let p = PathBuf::from(STATE_FILE);
let persistent = if p.try_exists()? {
tracing::info!("No state found at {STATE_FILE}, creating default state");
Persistent::<PersistentState>::load(&p).await?
} else {
tracing::info!("No state found at {STATE_FILE}, creating default state");
Persistent::<PersistentState>::new(PersistentState::default(), p)
};
@@ -177,7 +180,7 @@ async fn load_state() -> Result<State, Error> {
}
#[tracing::instrument]
async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> {
pub(crate) async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> {
let mut problems = Vec::new();
tracing::info!("Parsing mini moonboard problems from {}", file_path.display());
@@ -203,10 +206,28 @@ async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>
Ok(problems)
}
async fn run_migrations() -> Result<(), Box<dyn std::error::Error>> {
// State file moved to datastore/private
{
let m = PathBuf::from("state.ron");
if m.try_exists()? {
tracing::warn!("MIGRATING STATE FILE");
let p = PathBuf::from(STATE_FILE);
tokio::fs::create_dir_all(p.parent().unwrap()).await?;
tokio::fs::rename(m, &p).await?;
}
}
Ok(())
}
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
#[display("Server crash")]
#[display("Server crash: {_variant}")]
pub enum Error {
Io(std::io::Error),
Persistence(persistence::Error),
Parser(moonboard_parser::Error),
#[display("Failed migration")]
Migration(Box<dyn std::error::Error>),
}

View File

@@ -100,6 +100,12 @@ impl<T> Persistent<T> {
{
tracing::debug!("Persisting state");
let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?;
if let Some(parent) = file_path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|source| Error::CreateDir {
source,
dir: parent.to_owned(),
})?;
}
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
file_path: file_path.to_owned(),
source,
@@ -122,4 +128,7 @@ pub enum Error {
#[display("Failed to write file: {}", file_path.display())]
Write { file_path: PathBuf, source: std::io::Error },
#[display("Failed to create directory: {}", dir.display())]
CreateDir { dir: PathBuf, source: std::io::Error },
}