[object Object]

← 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

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 &amp; 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 →