feat: header on main page
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod app;
|
||||
pub mod pages {
|
||||
pub mod edit_wall;
|
||||
pub mod routes;
|
||||
pub mod wall;
|
||||
}
|
||||
pub mod components {
|
||||
|
||||
@@ -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>
|
||||
|
||||
85
crates/ascend/src/pages/routes.rs
Normal file
85
crates/ascend/src/pages/routes.rs
Normal 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(())
|
||||
}
|
||||
@@ -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> }
|
||||
|
||||
@@ -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>),
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
5
justfile
5
justfile
@@ -30,3 +30,8 @@ reset-state:
|
||||
# Open firewall port for development
|
||||
open-firewall:
|
||||
sudo nixos-firewall-tool open tcp 1337
|
||||
|
||||
# Remove local datastore and copy prod datastore
|
||||
cp-prod-datastore:
|
||||
rm -r datastore
|
||||
rsync --human-readable --recursive --info=stats1,progress2 --archive 192.168.1.3:/home/ascend/datastore .
|
||||
|
||||
Reference in New Issue
Block a user