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:
2026-08-15 16:07:07 -04:00
commit 1010f27f44
76 changed files with 7177 additions and 0 deletions
+90
View File
@@ -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>
);
}