diff --git a/Cargo.lock b/Cargo.lock index b06f0b8..7f724b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -871,6 +871,7 @@ dependencies = [ "leptos_router", "reqwest", "serde", + "thiserror 2.0.11", "tokio", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 9752738..22bdd56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,5 +20,6 @@ codegen-units = 1 [workspace.lints.clippy] nursery = "warn" -pedantic = "warn" unwrap_used = "warn" +style = "warn" +perf = "warn" diff --git a/backend/src/config.rs b/backend/src/config.rs index b75de84..4287f05 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; @@ -66,10 +66,10 @@ impl DatabaseSettings { pub fn get_config() -> Result { let base_path = std::env::current_dir().expect("Failed to determine current directory"); let config_directory = base_path.join("backend").join("config"); - let env: Environment = std::env::var("APP_ENVIRONMENT") + let env = std::env::var("APP_ENVIRONMENT") .unwrap_or_else(|_| "local".into()) - .try_into() - .expect("Failed to parse APP_ENVIRONMENT"); + .parse() + .unwrap_or(Environment::Local); let env_filename = format!("{}.toml", &env); @@ -97,7 +97,14 @@ impl Display for Environment { impl TryFrom for Environment { type Error = String; fn try_from(value: String) -> Result { - match value.to_lowercase().as_str() { + Self::from_str(&value) + } +} + +impl FromStr for Environment { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { "local" => Ok(Self::Local), "production" => Ok(Self::Production), other => Err(format!( diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index b530614..efac00b 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -7,16 +7,18 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -leptos = { version = "0.7.0", features = ["nightly"] } -leptos_router = { version = "0.7.0", features = ["nightly"] } +leptos = { version = "0.7", features = ["nightly"] } +leptos_router = { version = "0.7", features = ["nightly"] } axum = { version = "0.7", optional = true } console_error_panic_hook = { version = "0.1", optional = true } -leptos_axum = { version = "0.7.0", optional = true } -leptos_meta = { version = "0.7.0" } +leptos_axum = { version = "0.7", optional = true } +leptos_meta = { version = "0.7" } tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } wasm-bindgen = { version = "=0.2.100", optional = true } reqwest = { version = "0.12", features = ["json"] } serde.workspace = true +thiserror.workspace = true + [features] hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"] diff --git a/frontend/src/app.rs b/frontend/src/app.rs deleted file mode 100644 index 7c31dc8..0000000 --- a/frontend/src/app.rs +++ /dev/null @@ -1,61 +0,0 @@ -use leptos::prelude::*; -use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; -use leptos_router::{ - components::{Route, Router, Routes}, - StaticSegment, -}; - -pub fn shell(options: LeptosOptions) -> impl IntoView { - view! { - - - - - - - - - - - - - - } -} - -#[component] -pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - // injects a stylesheet into the document - // id=leptos means cargo-leptos will hot-reload this stylesheet - - - // sets the document title - - - // content for this welcome page - <Router> - <main> - <Routes fallback=|| "Page not found.".into_view()> - <Route path=StaticSegment("") view=HomePage /> - </Routes> - </main> - </Router> - } -} - -/// Renders the home page of your application. -#[component] -fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let count = RwSignal::new(0); - let on_click = move |_| *count.write() += 1; - - view! { - <h1>"Welcome to Leptos!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> - } -} diff --git a/frontend/src/components/app.rs b/frontend/src/components/app.rs new file mode 100644 index 0000000..b1e67cb --- /dev/null +++ b/frontend/src/components/app.rs @@ -0,0 +1,33 @@ +use leptos::prelude::*; +use leptos_meta::{provide_meta_context, Stylesheet, Title}; +use leptos_router::{ + components::{Route, Router, Routes}, + StaticSegment, +}; + +use crate::components::{homepage::HomePage, register::RegisterForm}; + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document <head> + // id=leptos means cargo-leptos will hot-reload this stylesheet + <Stylesheet id="leptos" href="/pkg/frontend.css" /> + + // sets the document title + <Title text="Welcome to Leptos" /> + + // content for this welcome page + <Router> + <main> + <Routes fallback=|| "Page not found.".into_view()> + <Route path=StaticSegment("") view=HomePage /> + <Route path=StaticSegment("/register") view=RegisterForm /> + </Routes> + </main> + </Router> + } +} diff --git a/frontend/src/components/homepage.rs b/frontend/src/components/homepage.rs new file mode 100644 index 0000000..140458b --- /dev/null +++ b/frontend/src/components/homepage.rs @@ -0,0 +1,14 @@ +use leptos::prelude::*; + +/// Renders the home page of your application. +#[component] +pub fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let count = RwSignal::new(0); + let on_click = move |_| *count.write() += 1; + + view! { + <h1>"Welcome to Leptos!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs new file mode 100644 index 0000000..cde9b45 --- /dev/null +++ b/frontend/src/components/mod.rs @@ -0,0 +1,25 @@ +mod app; +mod homepage; +mod register; +pub use app::App; + +use leptos::prelude::*; +use leptos_meta::MetaTags; + +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> + } +} diff --git a/frontend/src/components/register.rs b/frontend/src/components/register.rs new file mode 100644 index 0000000..e6c380b --- /dev/null +++ b/frontend/src/components/register.rs @@ -0,0 +1,115 @@ +use leptos::{ev::SubmitEvent, prelude::*}; +use reqwest::{Client, StatusCode}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FormData { + username: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResponseData { + username: String, + code: String, +} + +#[server] +async fn register(payload: FormData) -> Result<ResponseData, ServerFnError> { + let client = Client::new(); + let response = client + .post("http://localhost:8000/api/v1/register") + .json(&payload) + .send() + .await?; + + match response.status() { + StatusCode::CREATED => { + let response_data = response.json::<ResponseData>().await?; + Ok(response_data) + } + status => { + let error_msg = response.text().await?; + Err(ServerFnError::ServerError(format!( + "Registration failed: {} - {}", + status, error_msg + ))) + } + } +} + +#[component] +pub fn RegisterForm() -> impl IntoView { + let username = RwSignal::new(String::new()); + let register_action = Action::new(|input: &FormData| { + let input = input.clone(); + async move { register(input).await } + }); + let on_submit = move |ev: SubmitEvent| { + ev.prevent_default(); + let form_data = FormData { + username: username.get(), + }; + register_action.dispatch(form_data); + }; + let is_submitting = move || register_action.pending().get(); + + view! { + <form on:submit=on_submit> + <div> + <label>"Username"</label> + <input + type="text" + on:input=move |ev| username.set(event_target_value(&ev)) + prop:value=username + /> + </div> + <button type="submit" disabled=is_submitting> + {move || { if is_submitting() { "Registering..." } else { "Register" } }} + </button> + <ErrorBoundary fallback=move |errors| { + view! { + <div class="error-container"> + <p class="error-title">"Registration Error:"</p> + <ul class="error-list"> + {move || { + errors + .get() + .into_iter() + .map(|(_, e)| { + view! { <li class="error-item">{e.to_string()}</li> } + }) + .collect_view() + }} + </ul> + </div> + } + }> + {move || { + register_action + .value() + .get() + .map(|result| { + match result { + Ok(response) => { + view! { + <div class="success-container"> + <p class="success-message">"Registration successful!"</p> + <div class="registration-details"> + <p>"Username: " {response.username}</p> + <p>"Your Code: " {response.code}</p> + <p class="code-notice"> + "Please save this code. You will need it to login." + </p> + </div> + </div> + } + .into_any() + } + Err(e) => view! { <span>{e.to_string()}</span> }.into_any(), + } + }) + }} + </ErrorBoundary> + </form> + } +} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 151489d..ab638fe 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,9 +1,9 @@ -pub mod app; +pub mod components; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { - use crate::app::*; + use crate::components::*; console_error_panic_hook::set_once(); leptos::mount::hydrate_body(App); } diff --git a/frontend/src/main.rs b/frontend/src/main.rs index e8a999d..7ca865d 100644 --- a/frontend/src/main.rs +++ b/frontend/src/main.rs @@ -2,7 +2,7 @@ #[tokio::main] async fn main() { use axum::Router; - use frontend::app::*; + use frontend::components::*; use leptos::logging::log; use leptos::prelude::*; use leptos_axum::{generate_route_list, LeptosRoutes}; diff --git a/scripts/init_db b/scripts/init_db index c9c4cbc..8b47958 100755 --- a/scripts/init_db +++ b/scripts/init_db @@ -43,4 +43,4 @@ done DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" export DATABASE_URL sqlx database create -sqlx migrate run +sqlx migrate run --source backend/migrations