diff --git a/site/app/page.tsx b/site/app/page.tsx index 8b1c842..90b4af5 100644 --- a/site/app/page.tsx +++ b/site/app/page.tsx @@ -1,6 +1,7 @@ import Image from "next/image"; import { EmailReveal } from "./email-reveal"; import { NewsletterForm } from "./newsletter-form"; +import { SubscribeModal } from "./subscribe-modal"; import { TerminalCascade } from "./terminal-cascade"; import { SearchIcon, @@ -92,6 +93,7 @@ function FeatureBlock({ export default function Home() { return (
+ {/* ══════ HERO ══════ */}
{/* Grid background */} diff --git a/site/app/subscribe-modal.tsx b/site/app/subscribe-modal.tsx new file mode 100644 index 0000000..79c0537 --- /dev/null +++ b/site/app/subscribe-modal.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; + +const API_URL = "https://alezmad-nuc.tail58f5ad.ts.net"; + +type Phase = "boot" | "prompt" | "sending" | "done"; + +const BOOT_LINES = [ + { text: "$ cladm --subscribe", delay: 0 }, + { text: "Scanning newsletters...", delay: 400, dim: true }, + { text: "Found: cladm releases, new tools, project updates", delay: 800, dim: true }, + { text: "", delay: 1100 }, + { text: "Ready. Enter your email to subscribe.", delay: 1200, accent: true }, +]; + +export function SubscribeModal() { + const [open, setOpen] = useState(false); + const [phase, setPhase] = useState("boot"); + const [bootIndex, setBotIndex] = useState(0); + const [email, setEmail] = useState(""); + const [error, setError] = useState(""); + const inputRef = useRef(null); + + // Show modal after 15s or on scroll past 60% + useEffect(() => { + if (sessionStorage.getItem("cladm-subscribed")) return; + + const timer = setTimeout(() => setOpen(true), 15000); + + function onScroll() { + const pct = window.scrollY / (document.body.scrollHeight - window.innerHeight); + if (pct > 0.6) { + setOpen(true); + window.removeEventListener("scroll", onScroll); + } + } + window.addEventListener("scroll", onScroll, { passive: true }); + + return () => { + clearTimeout(timer); + window.removeEventListener("scroll", onScroll); + }; + }, []); + + // Boot sequence + useEffect(() => { + if (!open || phase !== "boot") return; + if (bootIndex >= BOOT_LINES.length) { + setPhase("prompt"); + return; + } + const t = setTimeout( + () => setBotIndex((i) => i + 1), + (BOOT_LINES[bootIndex]?.delay ?? 0) - (bootIndex > 0 ? BOOT_LINES[bootIndex - 1]?.delay ?? 0 : 0) || 300 + ); + return () => clearTimeout(t); + }, [open, phase, bootIndex]); + + // Focus input when prompt phase starts + useEffect(() => { + if (phase === "prompt") inputRef.current?.focus(); + }, [phase]); + + const dismiss = useCallback(() => { + setOpen(false); + setPhase("boot"); + setBotIndex(0); + setEmail(""); + setError(""); + }, []); + + // Esc to close + useEffect(() => { + if (!open) return; + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") dismiss(); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, dismiss]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!email.trim()) return; + setPhase("sending"); + setError(""); + try { + const res = await fetch(`${API_URL}/subscribe`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: email.trim(), project: "cladm" }), + }); + const data = await res.json(); + if (data.ok) { + setPhase("done"); + sessionStorage.setItem("cladm-subscribed", "1"); + } else { + setError(data.error || "failed"); + setPhase("prompt"); + } + } catch { + setError("network error"); + setPhase("prompt"); + } + } + + if (!open) return null; + + return ( +
e.target === e.currentTarget && dismiss()} + > + {/* Backdrop */} +
+ + {/* Terminal */} +
+ {/* Title bar */} +
+
+ ); +}