Archived
MapleBrief: production Canadian news aggregator
Next.js 14 + TypeScript + Tailwind + Prisma/SQLite. - LLM synthesis abstraction (OpenAI/Anthropic/Ollama) + admin settings UI - RSS ingestion pipeline (parser + cheerio content) + node-cron worker - AdSense AdUnit placements (header/in-feed/in-article/sidebar) - Sources analysis attribution, nofollow links, canonical/OG, sitemap/robots - Admin auth (ADMIN_API_KEY, timingSafeEqual, fail-closed 401) - Multi-cell lady-ga-ga marker, sitemap, robots, legal pages - Comprehensive README (setup, LLM config, AdSense, migrations, deploy)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ContactForm() {
|
||||
const [state, setState] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.currentTarget);
|
||||
setState('sending');
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: data.get('email'),
|
||||
topic: data.get('topic'),
|
||||
url: data.get('url'),
|
||||
message: data.get('message'),
|
||||
}),
|
||||
});
|
||||
setState(res.ok ? 'sent' : 'error');
|
||||
} catch {
|
||||
setState('error');
|
||||
}
|
||||
}
|
||||
|
||||
if (state === 'sent') {
|
||||
return (
|
||||
<div className="mt-8 rounded-xl border border-emerald-300 bg-emerald-50 p-6 text-[15px] text-emerald-900">
|
||||
Thanks — your message has been queued. For time-sensitive legal
|
||||
notices, also email the address listed on the site.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="mt-8 space-y-4">
|
||||
<label className="block text-sm">
|
||||
<span className="font-medium text-ink-800">Your email</span>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 text-sm focus:border-maple-600 focus:outline-none"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-sm">
|
||||
<span className="font-medium text-ink-800">Topic</span>
|
||||
<select name="topic" className="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 text-sm" defaultValue="correction">
|
||||
<option value="correction">Correction</option>
|
||||
<option value="dmca">Copyright / DMCA notice</option>
|
||||
<option value="advertising">AdSense / advertising</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="block text-sm">
|
||||
<span className="font-medium text-ink-800">Affected brief URL (if any)</span>
|
||||
<input
|
||||
name="url"
|
||||
type="url"
|
||||
className="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 text-sm focus:border-maple-600 focus:outline-none"
|
||||
placeholder="https://…"
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-sm">
|
||||
<span className="font-medium text-ink-800">Message</span>
|
||||
<textarea
|
||||
name="message"
|
||||
required
|
||||
rows={6}
|
||||
className="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 text-sm focus:border-maple-600 focus:outline-none"
|
||||
placeholder="…"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={state === 'sending'}
|
||||
className="rounded-lg bg-maple-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-maple-700 disabled:opacity-50"
|
||||
>
|
||||
{state === 'sending' ? 'Sending…' : 'Send message'}
|
||||
</button>
|
||||
{state === 'error' && (
|
||||
<p className="text-sm text-red-700">Could not send — please try again.</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user