Source Code
src/pages/contact.rs
use leptos::*;
use leptos_meta::{Meta, Title};
use leptos_router::ActionForm;
#[server(SendContactEmail, "/api")]
pub async fn send_contact_email(
name: String,
email: String,
message: String,
) -> Result<(), ServerFnError> {
use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials,
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
};
let name = name.replace(['\r', '\n'], " ");
let smtp_user =
std::env::var("SMTP_USERNAME").map_err(|_| ServerFnError::new("SMTP_USERNAME not set"))?;
let smtp_pass =
std::env::var("SMTP_PASSWORD").map_err(|_| ServerFnError::new("SMTP_PASSWORD not set"))?;
let to_address = std::env::var("CONTACT_TO_EMAIL")
.map_err(|_| ServerFnError::new("CONTACT_TO_EMAIL not set"))?;
let email_msg = Message::builder()
.from(
smtp_user
.parse()
.map_err(|e: lettre::address::AddressError| ServerFnError::new(e))?,
)
.reply_to(
format!("{name} <{email}>")
.parse()
.map_err(|e: lettre::address::AddressError| ServerFnError::new(e))?,
)
.to(to_address
.parse()
.map_err(|e: lettre::address::AddressError| ServerFnError::new(e))?)
.subject(format!("Portfolio contact from {name}"))
.header(ContentType::TEXT_PLAIN)
.body(format!("From: {name} <{email}>\n\n{message}"))
.map_err(|e| ServerFnError::new(e))?;
let creds = Credentials::new(smtp_user, smtp_pass);
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.protonmail.ch")
.map_err(|e| ServerFnError::new(e))?
.credentials(creds)
.port(587)
.build();
mailer
.send(email_msg)
.await
.map_err(|e| ServerFnError::new(format!("SMTP send failed: {e}")))?;
Ok(())
}
#[component]
pub fn Contact() -> impl IntoView {
let send_email = Action::<SendContactEmail, _>::server();
let response = send_email.value();
let pending = send_email.pending();
let submitted = move || matches!(response.get(), Some(Ok(())));
let error_msg = move || -> Option<String> {
match response.get() {
Some(Err(e)) => Some(e.to_string()),
_ => None,
}
};
view! {
<Title text="Contact – Peter Pinto"/>
<Meta name="description" content="Get in touch with Peter Pinto. Available for project inquiries, questions, or just a conversation."/>
<div class="page">
<span class="eyebrow">"Contact"</span>
<h1>"Let's " <em style="font-style:italic; color: var(--accent)">"Talk"</em></h1>
<p class="lead">
"Whether you're hiring, building something interesting, or just want to
connect — I'd love to hear from you."
</p>
<div class="contact-layout">
<div class="contact-info">
<h3>"Direct"</h3>
<p>"Prefer email? Reach me directly at any time."</p>
<div class="contact-links">
<a href="mailto:contact@peterpinto.dev" class="contact-link">
<div>
<span class="link-label">"Email"</span>
"contact@peterpinto.dev"
</div>
</a>
<a href="https://www.linkedin.com/in/peterpintodev/" target="_blank" rel="noopener noreferrer" class="contact-link">
<div>
<span class="link-label">"LinkedIn"</span>
"linkedin.com/in/peterpintodev/"
</div>
</a>
<a href="https://github.com/petergpinto" target="_blank" rel="noopener noreferrer" class="contact-link">
<div>
<span class="link-label">"GitHub"</span>
"github.com/petergpinto"
</div>
</a>
</div>
</div>
<div>
<Show
when=submitted
fallback=move || view! {
<ActionForm action=send_email class="contact-form">
<div class="form-group">
<label for="name">"Name"</label>
<input
id="name"
type="text"
name="name"
placeholder="Jane Smith"
required
/>
</div>
<div class="form-group">
<label for="email">"Email"</label>
<input
id="email"
type="email"
name="email"
placeholder="jane@example.com"
required
/>
</div>
<div class="form-group">
<label for="message">"Message"</label>
<textarea
id="message"
name="message"
placeholder="Tell me what's on your mind…"
required
/>
</div>
{move || error_msg().map(|e| view! {
<p class="form-error">"Error sending message: " {e}</p>
})}
<button
type="submit"
class="btn btn-primary"
disabled=move || pending.get()
>
<Show
when=move || pending.get()
fallback=|| view! { "Send Message →" }
>
<span class="spinner" />
"Sending…"
</Show>
</button>
</ActionForm>
}
>
<div class="form-success">
<p>"✓ Message received — I'll be in touch soon."</p>
</div>
</Show>
</div>
</div>
</div>
}
}