feat: add base app

This commit is contained in:
2025-01-11 22:45:44 +01:00
commit 11f8cccde4
30 changed files with 4317 additions and 0 deletions

37
crates/app/Cargo.toml Normal file
View File

@@ -0,0 +1,37 @@
[package]
name = "app"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
server_fn.workspace = true
axum = { workspace = true, optional = true, features = ["macros"] }
leptos_axum = { workspace = true, optional = true }
#tokio = { workspace = true, optional = true }
serde.workspace = true
http.workspace = true
cfg-if.workspace = true
thiserror.workspace = true
reqwest = { version = "0.12.11", optional = true, features = ["json"] }
serde_json = { version = "1.0.134", optional = true }
[features]
default = []
hydrate = ["leptos/hydrate"]
ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:leptos_axum",
"dep:serde_json",
"dep:axum",
]
reqwest = ["dep:reqwest"]
axum = ["dep:axum"]
serde_json = ["dep:serde_json"]

View File

@@ -0,0 +1,74 @@
use cfg_if::cfg_if;
use http::status::StatusCode;
use leptos::prelude::*;
use thiserror::Error;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
#[derive(Clone, Debug, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
}
}
}
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying the error.
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => RwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get_untracked();
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
view! {
<h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=move || { errors.clone().into_iter().enumerate() }
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
let error_string = error.1.to_string();
let error_code = error.1.status_code();
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

61
crates/app/src/lib.rs Normal file
View File

@@ -0,0 +1,61 @@
use crate::error_template::{AppError, ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, StaticSegment};
pub mod error_template;
#[cfg(feature = "ssr")]
pub mod state;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
<Stylesheet id="leptos" href="/pkg/client.css" />
// sets the document title
<Title text="client" />
// content for this welcome page
<Router>
<main class="">
<Routes fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! { <ErrorTemplate outside_errors /> }.into_view()
}>
<Route path=StaticSegment("") view=HomePage />
</Routes>
</main>
</Router>
}
}
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<h1> "client" </h1>
}
}

14
crates/app/src/state.rs Normal file
View File

@@ -0,0 +1,14 @@
use axum::extract::FromRef;
use leptos::prelude::expect_context;
use server_fn::ServerFnError;
#[derive(FromRef, Clone)]
pub struct State {}
pub async fn get_state() -> Result<State, ServerFnError> {
let state = expect_context::<crate::state::State>();
let axum::extract::State(state): axum::extract::State<crate::state::State> =
leptos_axum::extract_with_state(&state).await?;
Ok(state)
}

19
crates/client/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
app = { path = "../app", default-features = false, features = ["ssr"] }
leptos = { workspace = true, features = ["ssr"] }
leptos_axum.workspace = true
axum.workspace = true
simple_logger.workspace = true
tokio.workspace = true
tower.workspace = true
tower-http.workspace = true
log.workspace = true
dotenvy = "0.15.7"

78
crates/client/src/main.rs Normal file
View File

@@ -0,0 +1,78 @@
use app::*;
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use state::State;
use tokio::signal;
#[tokio::main]
async fn main() {
let _ = dotenvy::dotenv();
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// 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>
// Alternately a file can be specified such as Some("Cargo.toml")
// The file would need to be included with the executable when moved to deployment
let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options;
let state = State {};
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.leptos_routes_with_context(
&leptos_options,
routes,
{
let state = state.clone();
move || {
provide_context(state.clone());
}
},
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log::info!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}

View File

@@ -0,0 +1,18 @@
[package]
name = "frontend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
app = { path = "../app", default-features = false, features = ["hydrate"] }
leptos = { workspace = true, features = [ "hydrate" ] }
console_error_panic_hook.workspace = true
console_log.workspace = true
log.workspace = true
wasm-bindgen.workspace = true

View File

@@ -0,0 +1,10 @@
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
// initializes logging using the `log` crate
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}