Files
luna-portfolio/src/sections/Contact.jsx
2026-04-14 18:46:17 +02:00

210 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { SectionWrapper } from '../components/SectionWrapper';
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 socialLinks = [
{ name: 'Email', url: 'mailto:space@reversed.dev', icon: '📧', desc: 'space@reversed.dev' },
{ name: 'GitHub / Gitea', url: 'https://gitea.reversed.dev/luna', icon: '⌨️', desc: 'gitea.reversed.dev/luna' },
];
const initialForm = {
username: '',
email: '',
message: '',
};
export function Contact() {
const [form, setForm] = useState(initialForm);
const [status, setStatus] = useState({ type: '', message: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
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. A reply might take a bit, but 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 (
<section id="contact" className="py-24 px-4 bg-darker">
<div className="max-w-5xl mx-auto">
<SectionWrapper>
<div className="text-center mb-16">
<h2 className="text-4xl font-bold mb-4">
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Get in Touch
</span>
</h2>
<p className="text-slate-400 max-w-2xl mx-auto">
Send a message directly from the site, or use one of the links below.
</p>
</div>
</SectionWrapper>
<div className="grid gap-8 lg:grid-cols-[1.15fr_0.85fr] items-start">
<SectionWrapper delay={100}>
<div className="bg-dark border border-slate-800 rounded-2xl p-6 md:p-8 shadow-xl shadow-black/20">
<div className="mb-6">
<h3 className="text-2xl font-semibold text-slate-100 mb-2">Contact form</h3>
<p className="text-slate-400 text-sm">
Messages go straight into the inbox backend. No weird third-party form service nonsense.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-slate-300 mb-2">
Name
</label>
<input
id="username"
name="username"
type="text"
value={form.username}
onChange={handleChange}
minLength={2}
maxLength={80}
required
className="w-full rounded-xl border border-slate-700 bg-darker px-4 py-3 text-slate-100 placeholder:text-slate-500 focus:border-primary focus:outline-none"
placeholder="What should this site call you?"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-300 mb-2">
Email
</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleChange}
maxLength={200}
required
className="w-full rounded-xl border border-slate-700 bg-darker px-4 py-3 text-slate-100 placeholder:text-slate-500 focus:border-primary focus:outline-none"
placeholder="name@example.com"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-slate-300 mb-2">
Message
</label>
<textarea
id="message"
name="message"
value={form.message}
onChange={handleChange}
minLength={10}
maxLength={5000}
required
rows={6}
className="w-full rounded-xl border border-slate-700 bg-darker px-4 py-3 text-slate-100 placeholder:text-slate-500 focus:border-primary focus:outline-none resize-y"
placeholder="Say something good. Or interesting. Ideally both."
/>
</div>
{status.message ? (
<div
className={`rounded-xl border px-4 py-3 text-sm ${
status.type === 'success'
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
: 'border-rose-500/30 bg-rose-500/10 text-rose-300'
}`}
>
{status.message}
</div>
) : null}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-slate-500 max-w-sm">
Rate limited per IP to keep spam bots from being annoying.
</p>
<button
type="submit"
disabled={isSubmitting}
className="inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-primary to-secondary px-5 py-3 font-medium text-white transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
>
{isSubmitting ? 'Sending...' : 'Send message'}
</button>
</div>
</form>
</div>
</SectionWrapper>
<SectionWrapper delay={200}>
<div className="space-y-4">
<div className="p-4 bg-dark rounded-lg border border-amber-500/30 mb-6">
<div className="flex items-start gap-3">
<span className="text-2xl"></span>
<div>
<div className="font-medium text-amber-400 mb-1">Email Allowlist</div>
<p className="text-sm text-slate-400">
Direct email is still allowlisted. The form is the easier route if that whole dance is annoying.
</p>
</div>
</div>
</div>
{socialLinks.map((link) => (
<a
key={link.name}
href={link.url}
target={link.url.startsWith('mailto') ? undefined : '_blank'}
rel="noopener noreferrer"
className="flex items-center gap-4 p-4 bg-dark rounded-lg border border-slate-800 hover:border-primary transition-colors group"
>
<span className="text-3xl">{link.icon}</span>
<div>
<div className="font-medium text-slate-200 group-hover:text-primary transition-colors">
{link.name}
</div>
<div className="text-sm text-slate-500">{link.desc}</div>
</div>
</a>
))}
</div>
</SectionWrapper>
</div>
</div>
</section>
);
}