feat(webserver): Vendor and reimplement main pages in leptos

This commit is contained in:
Tristan Daniël Maat 2025-11-24 03:29:18 +08:00
parent aeba7301b0
commit 59fdb37222
Signed by: tlater
GPG key ID: 02E935006CF2E8E7
25 changed files with 4862 additions and 176 deletions

View file

@ -0,0 +1,70 @@
use leptos::prelude::*;
use leptos_meta::{Meta, Title};
use markdown_view_leptos::markdown_view;
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<Meta name="description" content="tlater.net homepage" />
<Title text="Welcome to tlater.net!" />
<section class="section">
<div class="container">
<h1 class="title has-text-weight-normal is-family-monospace">
"$ "<span id="typed-welcome" />
</h1>
<hr />
<div class="columns">
<div class="column content">
{markdown_view!(
r#"
### About Me
Looks like you found my website. I suppose introductions are in order.
My name's Tristan, I'm an avid Dutch-South African software
engineer. You probably either met me at an open source conference, a
hackathon, a badminton session or at a roleplaying table.
"#
)}
</div>
<div class="column content">
{markdown_view!(
r#"### My Work
I'm interested in a variety of things in the open source
world. Perhaps thanks to my pursuit of the perfect Linux desktop, this
has revolved a lot around reproducible build and deployment systems
for the last few years, initially starting with
[BuildStream](https://buildstream.build/) back in ~2017. I gave a
couple of talks on it at build meetups in the UK in subsequent years,
though sadly most evidence of that appears to have disappeared.
Since then this has culminated in a strong fondness for
[NixOS](https://nixos.org/) and Nix, as its active community makes
private use cases much more feasible. As such, I have a vested
interest in making this community as large as possible - I post a lot
on the NixOS [discourse](https://discourse.nixos.org/) trying to help
newcomers out where I can.
I also just enjoy Programming, my core languages for personal work are
currently probably Rust and Python, although I have a very varied
background. This is in part due to my former work as a consultant,
which required new languages every few months. I have experience from
JavaScript over Elm to Kotlin, but eventually I hope I might only need
to write Rust ;)
If you're interested in seeing these things for yourself, visit my
[Gitlab](https://gitlab.com/tlater) and
[GitHub](https://github.com/tlater) pages.
"#
)}
</div>
</div>
</div>
</section>
}
}

View file

@ -0,0 +1,217 @@
use leptos::{html::Form, logging};
use leptos_meta::{Meta, Title};
use leptos::{prelude::*, server_fn::codec::JsonEncoding};
use markdown_view_leptos::markdown_view;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[server]
async fn submit_mail(mail: String, subject: String, message: String) -> Result<String, MailError> {
use crate::AppState;
const NTFY_TOPIC_CREDENTIAL_NAME: &str = "ntfy-topic";
let mail = format!("From: {}\nSubject: {}\n{}", mail, subject, message);
logging::log!("{}", mail);
let state = use_context::<AppState>().unwrap();
let ntfy_topic = std::fs::read_to_string(
state
.config
.credentials_directory
.join(NTFY_TOPIC_CREDENTIAL_NAME),
)?;
let ntfy_url = state.config.ntfy_instance.join(ntfy_topic.trim())?;
state
.http_client
.post(ntfy_url)
.body(mail)
.send()
.await
.map_err(|err| err.without_url())?
.error_for_status()
.map_err(|err| err.without_url())?;
leptos_axum::redirect("/mail");
Ok("Mail successfully sent!".to_owned())
}
#[component]
fn MailForm() -> impl IntoView {
let form: NodeRef<Form> = NodeRef::new();
let submit_mail = ServerAction::<SubmitMail>::new();
let pending = submit_mail.pending();
let flash_message = submit_mail.value();
Effect::new(move ||
if flash_message.get().is_some_and(|m| m.is_ok()) {
if let Some(form) = form.get() {
form.reset();
} else {
logging::warn!("Failed to reset form");
}
}
);
view! {
<Show when=move || { flash_message.get().is_some() }>
<div
class="notification is-light"
class=("is-success", move || flash_message.get().is_some_and(|m| m.is_ok()))
class=("is-danger", move || flash_message.get().is_some_and(|m| m.is_err()))
>
<button
class="delete"
aria-label="Close"
on:click=move |_| {
*flash_message.write() = None;
}
/>
<span role="alert">
{move || match flash_message.get() {
None => "".to_owned(),
Some(Ok(message)) => message,
Some(Err(error)) => format!("{}", error),
}}
</span>
</div>
</Show>
<ActionForm node_ref=form action=submit_mail>
<fieldset disabled=move || pending.get()>
<div class="field">
<label class="label" for="mail">
Email address
</label>
<div class="control">
<input
id="mail"
class="input"
type="email"
placeholder="Your address"
name="mail"
required
/>
</div>
</div>
<div class="field">
<label class="label" for="subject">
Subject
</label>
<div class="control">
<input
id="subject"
class="input"
type="text"
placeholder="E.g. There's a typo on your home page!"
name="subject"
autocomplete="off"
required
/>
</div>
</div>
<div class="field">
<label class="label" for="message">
Message
</label>
<textarea
id="message"
class="textarea"
type="text"
rows="6"
name="message"
autocomplete="off"
required
/>
</div>
<div class="control">
<div class="field">
<button
type="submit"
class="button is-link"
class=("is-loading", move || pending.get())
>
Send
</button>
</div>
</div>
</fieldset>
</ActionForm>
}
}
#[component]
pub fn Mail() -> impl IntoView {
view! {
<Meta name="description" content="tlater.net mail submission" />
<Title text="Mail submission" />
<section class="section">
<div class="container">
<h1 class="title has-text-weight-normal">Contact Me</h1>
<div class="columns">
<div class="column">
<MailForm />
</div>
<div class="column content">
{markdown_view!(
r#"
Any messages you enter here are directly forwarded to me. I aim to
respond within a day.
Don't be upset about the form, I want to avoid the spam publishing
your email address brings with it... And minimize the amount of mail
that doesn't reach me, this form is an exception in all my spam
filters, you see ;)
"#
)}
</div>
</div>
</div>
</section>
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub enum MailError {
#[error("This form appears to currently be broken :(")]
Permanent,
#[error("Mail service appears to be down; please try again later")]
Temporary,
#[error("Server error: {0}")]
ServerFnError(ServerFnErrorErr),
}
impl From<url::ParseError> for MailError {
fn from(error: url::ParseError) -> Self {
logging::error!("Invalid ntfy URL: {error}");
Self::Permanent
}
}
impl From<std::io::Error> for MailError {
fn from(error: std::io::Error) -> Self {
logging::error!("Couldn't read ntfy topic secret: {error}");
Self::Permanent
}
}
impl From<reqwest::Error> for MailError {
fn from(error: reqwest::Error) -> Self {
logging::error!("Failed to connect to ntfy: {error}");
Self::Temporary
}
}
impl FromServerFnError for MailError {
type Encoder = JsonEncoding;
fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
Self::ServerFnError(value)
}
}