This commit is contained in:
Kristofers Solo 2025-01-30 08:23:18 +02:00
parent 74b9b00de1
commit b626e0a7d7
12 changed files with 212 additions and 75 deletions

1
Cargo.lock generated
View File

@ -871,6 +871,7 @@ dependencies = [
"leptos_router",
"reqwest",
"serde",
"thiserror 2.0.11",
"tokio",
"wasm-bindgen",
]

View File

@ -20,5 +20,6 @@ codegen-units = 1
[workspace.lints.clippy]
nursery = "warn"
pedantic = "warn"
unwrap_used = "warn"
style = "warn"
perf = "warn"

View File

@ -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<Settings, config::ConfigError> {
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<String> for Environment {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
Self::from_str(&value)
}
}
impl FromStr for Environment {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"production" => Ok(Self::Production),
other => Err(format!(

View File

@ -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"]

View File

@ -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! {
<!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! {
// 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 />
</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>
}
}

View File

@ -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>
}
}

View File

@ -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>
}
}

View File

@ -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>
}
}

View File

@ -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>
}
}

View File

@ -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);
}

View File

@ -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};

View File

@ -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