diff --git a/shsf/contact-api/main.py b/shsf/contact-api/main.py index b81709e..d8c3969 100644 --- a/shsf/contact-api/main.py +++ b/shsf/contact-api/main.py @@ -11,6 +11,7 @@ RATE_LIMIT_MAX_REQUESTS = 5 MESSAGES_STORAGE = "portfolio_contact_messages" ALLOWED_ORIGINS = { "https://luna.reversed.dev", + "https://luna.spaceistyping.com", "http://localhost:5173", "http://localhost:4173", } @@ -18,7 +19,7 @@ EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$") def _cors_headers(origin=""): - allowed_origin = origin if origin in ALLOWED_ORIGINS else "https://luna.reversed.dev" + allowed_origin = origin if origin in ALLOWED_ORIGINS else "https://luna.spaceistyping.com" return { "Access-Control-Allow-Origin": allowed_origin, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", diff --git a/src/App.jsx b/src/App.jsx index a7d0ca1..acc48eb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; const REDIRECT_HOST = 'luna.reversed.dev'; const REDIRECT_TARGET = 'https://luna.spaceistyping.com'; const REDIRECT_DELAY_SECONDS = 5; +const CONTACT_API_URL = import.meta.env.VITE_CONTACT_API_URL || 'https://shsf-api.reversed.dev/api/exec/17/cba6645c-2ca2-4e7a-ad94-e6114cbde761'; const capabilities = [ { @@ -49,8 +50,8 @@ const principles = [ const contactCards = [ { label: 'email', - value: 'space@reversed.dev', - href: 'mailto:space@reversed.dev', + value: 'clawy@reversed.dev', + href: 'mailto:clawy@reversed.dev', }, { label: 'gitea', @@ -59,8 +60,17 @@ const contactCards = [ }, ]; +const initialForm = { + username: '', + email: '', + message: '', +}; + function App() { const [secondsLeft, setSecondsLeft] = useState(REDIRECT_DELAY_SECONDS); + const [form, setForm] = useState(initialForm); + const [status, setStatus] = useState({ type: '', message: '' }); + const [isSubmitting, setIsSubmitting] = useState(false); const shouldRedirect = typeof window !== 'undefined' && window.location.hostname === REDIRECT_HOST; useEffect(() => { @@ -84,6 +94,47 @@ function App() { }; }, [shouldRedirect]); + const handleChange = (event) => { + const { name, value } = event.target; + setForm((current) => ({ ...current, [name]: value })); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + setIsSubmitting(true); + setStatus({ type: '', message: '' }); + + try { + const response = await fetch(CONTACT_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(form), + }); + + const data = await response.json().catch(() => ({})); + const payload = data?._res && typeof data._res === 'object' ? data._res : data; + + if (!response.ok) { + throw new Error(payload?.error || payload?.message || 'Something went wrong while sending the message.'); + } + + setStatus({ + type: 'success', + message: payload?.message || 'Message received. It made it through.', + }); + setForm(initialForm); + } catch (error) { + setStatus({ + type: 'error', + message: error.message || 'Something went wrong while sending the message.', + }); + } finally { + setIsSubmitting(false); + } + }; + return (
{shouldRedirect && ( @@ -217,22 +268,90 @@ function App() {
-
-
-

contact

-

want to reach me?

-

- easiest route is still a direct message, but these work too. -

+
+
+
+

contact

+

want to reach me?

+

+ send a message straight from the site, or just email me if that’s easier. +

+
+ +
+ {contactCards.map((item) => ( + + {item.label} + {item.value} + + ))} +
-
- {contactCards.map((item) => ( - - {item.label} - {item.value} - - ))} +
+
+

contact form

+

messages go straight into the inbox backend, no crusty third-party form junk.

+
+ +
+ + + + +