← back to Govarbitrage
Public /newsletter landing: digest pitch, subscribe form with confirm-your-email state, CAN-SPAM small print; nav link
e626b08d4a469e8dc23f948f6d3aecde4a54b271 · 2026-07-13 01:19:22 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
A src/app/newsletter/SubscribeForm.tsxA src/app/newsletter/page.tsxM src/app/page.tsx
Diff
commit e626b08d4a469e8dc23f948f6d3aecde4a54b271
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 01:19:22 2026 -0700
Public /newsletter landing: digest pitch, subscribe form with confirm-your-email state, CAN-SPAM small print; nav link
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
src/app/newsletter/SubscribeForm.tsx | 75 ++++++++++++++++++++++++++++++++++++
src/app/newsletter/page.tsx | 45 ++++++++++++++++++++++
src/app/page.tsx | 3 ++
3 files changed, 123 insertions(+)
diff --git a/src/app/newsletter/SubscribeForm.tsx b/src/app/newsletter/SubscribeForm.tsx
new file mode 100644
index 0000000..0b0e869
--- /dev/null
+++ b/src/app/newsletter/SubscribeForm.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { useState } from "react";
+
+type State = { phase: "idle" | "busy" | "done" | "error"; message?: string };
+
+export default function SubscribeForm() {
+ const [email, setEmail] = useState("");
+ const [website, setWebsite] = useState(""); // honeypot — humans never see it
+ const [state, setState] = useState<State>({ phase: "idle" });
+
+ async function submit(e: React.FormEvent) {
+ e.preventDefault();
+ setState({ phase: "busy" });
+ try {
+ const res = await fetch("/api/newsletter/subscribe", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, website }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) {
+ setState({ phase: "error", message: data.error || "Something went wrong. Please try again." });
+ return;
+ }
+ setState({ phase: "done", message: data.message });
+ } catch {
+ setState({ phase: "error", message: "Network error. Please try again." });
+ }
+ }
+
+ if (state.phase === "done") {
+ return (
+ <div style={{ background: "#f0fdf4", border: "1px solid #bbf7d0", borderRadius: 12, padding: "22px 24px", textAlign: "center" }}>
+ <div style={{ fontSize: 20, fontWeight: 700, color: "#166534", marginBottom: 6 }}>Almost there — confirm your email</div>
+ <p style={{ color: "#15803d", fontSize: 14, margin: 0 }}>{state.message}</p>
+ </div>
+ );
+ }
+
+ return (
+ <form onSubmit={submit} style={{ display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" }}>
+ <input
+ type="email"
+ required
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ placeholder="you@example.com"
+ aria-label="Email address"
+ style={{ flex: "1 1 260px", maxWidth: 340, padding: "11px 14px", fontSize: 15, border: "1px solid #cbd5e1", borderRadius: 10, background: "#fff", color: "#0f172a" }}
+ />
+ {/* Honeypot: visually hidden, tab-skipped. Bots that fill it are dropped. */}
+ <input
+ type="text"
+ name="website"
+ value={website}
+ onChange={(e) => setWebsite(e.target.value)}
+ tabIndex={-1}
+ autoComplete="off"
+ aria-hidden="true"
+ style={{ position: "absolute", left: -9999, width: 1, height: 1, opacity: 0 }}
+ />
+ <button
+ type="submit"
+ disabled={state.phase === "busy"}
+ style={{ padding: "11px 22px", fontSize: 15, fontWeight: 700, background: "#111827", color: "#fff", border: "none", borderRadius: 10, cursor: state.phase === "busy" ? "wait" : "pointer" }}
+ >
+ {state.phase === "busy" ? "Subscribing…" : "Get the digest"}
+ </button>
+ {state.phase === "error" && (
+ <div style={{ flexBasis: "100%", textAlign: "center", color: "#b91c1c", fontSize: 13 }}>{state.message}</div>
+ )}
+ </form>
+ );
+}
diff --git a/src/app/newsletter/page.tsx b/src/app/newsletter/page.tsx
new file mode 100644
index 0000000..454481d
--- /dev/null
+++ b/src/app/newsletter/page.tsx
@@ -0,0 +1,45 @@
+import SubscribeForm from "./SubscribeForm";
+
+export const dynamic = "force-dynamic";
+
+export const metadata = {
+ title: "Deal Digest Newsletter — GovArbitrage",
+ description: "Twice-daily Top-10 government-surplus arbitrage deals, honesty-gated and confidence-labeled.",
+};
+
+export default function NewsletterPage() {
+ return (
+ <main style={{ maxWidth: 640, margin: "56px auto", fontFamily: "system-ui", padding: "0 20px", color: "#0f172a" }}>
+ <div style={{ textAlign: "center", marginBottom: 8 }}>
+ <h1 style={{ fontSize: 34, margin: 0 }}>The Top-10 Deals Digest</h1>
+ <p style={{ color: "#475569", fontSize: 16 }}>
+ Twice a day (6am & 5pm PT), the ten best government-surplus arbitrage
+ opportunities across 8 auction networks — de-duped, scored, and
+ honesty-gated: every figure is confidence-discounted and capped, never inflated.
+ </p>
+ </div>
+
+ <ul style={{ listStyle: "none", padding: 0, margin: "22px 0", fontSize: 14, lineHeight: 1.8, color: "#334155" }}>
+ {[
+ "Top-10 opportunities ranked by conservative expected net profit",
+ "Recommended max bid with all-in cost math (premium, tax, freight, fees)",
+ "Confidence labels on every estimate — verify comps before bidding",
+ "One-click unsubscribe in every email, effective immediately",
+ ].map((f) => (
+ <li key={f}>
+ <span style={{ color: "#16a34a", marginRight: 8 }}>✓</span>{f}
+ </li>
+ ))}
+ </ul>
+
+ <SubscribeForm />
+
+ <p style={{ textAlign: "center", color: "#94a3b8", fontSize: 12, marginTop: 26, lineHeight: 1.6 }}>
+ Double opt-in: we only email you after you click the confirmation link.
+ Every digest carries a single-click unsubscribe link — no login, no questions,
+ effective immediately — plus our mailing address, per CAN-SPAM. We never
+ share or sell your address. Estimates are heuristic; always verify before bidding.
+ </p>
+ </main>
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 8f709d7..f2319bc 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -27,6 +27,9 @@ export default async function DashboardPage() {
<a href="/selling-avenues" className="text-primary underline-offset-4 hover:underline">
Selling avenues →
</a>
+ <a href="/newsletter" className="text-primary underline-offset-4 hover:underline">
+ Newsletter →
+ </a>
{user && (
<span className="flex items-center gap-2 text-muted-foreground">
{user.email} · {user.role}
← d242730 Newsletter API: subscribe (honeypot+rate-limit, enumeration-
·
back to Govarbitrage
·
Contrarian fixes: digest send-to-list engine (TEST-logged), 8d98349 →