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