feat(webserver): Vendor and reimplement main pages in leptos
This commit is contained in:
parent
aeba7301b0
commit
59fdb37222
25 changed files with 4862 additions and 176 deletions
217
pkgs/packages/webserver/src/app/mail.rs
Normal file
217
pkgs/packages/webserver/src/app/mail.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue