210 lines
8.2 KiB
JavaScript
210 lines
8.2 KiB
JavaScript
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>
|
||
);
|
||
}
|