The source code for our eny.social landing page, which is mirrored in a different repository as part of the CI setup. eny.social
social-network eny local-first
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(Waitlist): add mail submission url

Sam Sauer 0424260b 339ebc4e

+104 -14
+104 -14
app/components/Waitlist.tsx
··· 1 1 "use client"; 2 2 3 3 import { useRef, useState, useEffect } from "react"; 4 - import ButtonCta from "./ui/ButtonCta"; 4 + import { ArrowCircleRightIcon } from "@phosphor-icons/react"; 5 5 import SectionIntroLabel from "./ui/SectionIntroLabel"; 6 6 7 7 const avatars = [ ··· 50 50 "bg-pacific", 51 51 ]; 52 52 53 + const STATUS_MESSAGES: Record<string, string> = { 54 + confirmationSent: "Thank you! Check your inbox to confirm.", 55 + confirmationAlreadySent: 56 + "A confirmation email was already sent — check your inbox.", 57 + alreadySubscribed: "You're already subscribed!", 58 + subscribed: "You're in!", 59 + }; 60 + 61 + async function subscribeToNewsletter(email: string) { 62 + const url = process.env.NEXT_PUBLIC_NEWSLETTER_URL; 63 + if (!url) throw new Error("Newsletter URL not configured"); 64 + 65 + const res = await fetch(`${url}subscribe`, { 66 + method: "POST", 67 + headers: { "Content-Type": "application/x-www-form-urlencoded" }, 68 + body: new URLSearchParams({ email }), 69 + }); 70 + 71 + const html = await res.text(); 72 + const parser = new DOMParser(); 73 + const doc = parser.parseFromString(html, "text/html"); 74 + const title = doc.querySelector("title")?.textContent ?? ""; 75 + 76 + const match = Object.entries(STATUS_MESSAGES).find( 77 + ([, msg]) => msg === title 78 + ); 79 + return match?.[0] ?? "confirmationSent"; 80 + } 81 + 53 82 export default function Waitlist() { 54 83 const sectionRef = useRef<HTMLElement>(null); 55 84 const [isVisible, setIsVisible] = useState(false); 85 + const [email, setEmail] = useState(""); 86 + const [status, setStatus] = useState<string | null>(null); 87 + const [loading, setLoading] = useState(false); 88 + const [error, setError] = useState(false); 89 + 90 + const handleSubmit = async (e: React.FormEvent) => { 91 + e.preventDefault(); 92 + setLoading(true); 93 + setError(false); 94 + setStatus(null); 95 + 96 + try { 97 + const code = await subscribeToNewsletter(email); 98 + setStatus(code); 99 + setEmail(""); 100 + } catch { 101 + setError(true); 102 + } finally { 103 + setLoading(false); 104 + } 105 + }; 56 106 57 107 useEffect(() => { 58 108 const el = sectionRef.current; ··· 80 130 {avatars.map((avatar, i) => ( 81 131 <div 82 132 key={i} 83 - className={`absolute rounded-full ${colors[i]} transition-all duration-700 ease-out ${ 84 - isVisible ? "opacity-40 lg:opacity-90 scale-100" : "opacity-0 scale-0" 133 + className={`absolute rounded-full ${ 134 + colors[i] 135 + } transition-all duration-700 ease-out ${ 136 + isVisible 137 + ? "opacity-40 lg:opacity-90 scale-100" 138 + : "opacity-0 scale-0" 85 139 }`} 86 140 style={{ 87 141 top: avatar.top, ··· 91 145 height: avatar.size, 92 146 transitionDelay: isVisible ? `${i * 120}ms` : "0ms", 93 147 animation: isVisible 94 - ? `${i % 2 === 0 ? "float" : "float-slow"} ${6 + i}s ease-in-out infinite` 148 + ? `${i % 2 === 0 ? "float" : "float-slow"} ${ 149 + 6 + i 150 + }s ease-in-out infinite` 95 151 : "none", 96 152 animationDelay: `${avatar.delay}s`, 97 153 }} ··· 137 193 /> 138 194 </svg> 139 195 140 - <div className={`relative mx-auto max-w-2xl text-center transition-all duration-700 ease-out ${ 141 - isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8" 142 - }`} 196 + <div 197 + className={`relative mx-auto max-w-2xl text-center transition-all duration-700 ease-out ${ 198 + isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8" 199 + }`} 143 200 style={{ transitionDelay: isVisible ? "300ms" : "0ms" }} 144 201 > 145 202 <SectionIntroLabel>Come on in</SectionIntroLabel> 146 203 147 204 <h2 className="mt-6"> 148 - Join the first <span className="italic text-tangerine">1,000</span> 205 + Stay in the <span className="italic text-tangerine">loop</span> 149 206 </h2> 150 207 151 208 <p className="section-copy mx-auto mt-6 max-w-lg"> 152 - The algorithms shaping your reality shouldn&apos;t be controlled by 153 - shareholders in Silicon Valley. At eny.social, you own your feed, your 154 - data, and your voice. 209 + We&apos;re not ready yet, but we&apos;re getting close. Drop your 210 + email and we&apos;ll let you know when eny.social launches. 155 211 </p> 156 212 157 - <ButtonCta href="#" variant="ghost" className="mt-10"> 158 - join the waitlist 159 - </ButtonCta> 213 + {status ? ( 214 + <p className="section-copy mt-10"> 215 + {STATUS_MESSAGES[status] ?? "Thanks for signing up!"} 216 + </p> 217 + ) : ( 218 + <form 219 + onSubmit={handleSubmit} 220 + className={`mx-auto mt-10 flex max-w-md items-center gap-0 rounded-full border-2 border-charcoal bg-transparent pl-5 pr-1 py-1 transition-opacity ${ 221 + loading ? "pointer-events-none opacity-50" : "" 222 + }`} 223 + > 224 + <input 225 + type="email" 226 + required 227 + value={email} 228 + onChange={(e) => setEmail(e.target.value)} 229 + placeholder="your@email.com" 230 + className="flex-1 bg-transparent font-['Instrument_Sans'] text-[18px] font-medium tracking-[-0.6px] text-charcoal placeholder:text-charcoal/40 focus:outline-none" 231 + /> 232 + <button 233 + type="submit" 234 + disabled={loading} 235 + className="group inline-flex items-center gap-2 rounded-full bg-charcoal py-0 pl-[14px] pr-[3px] font-['Instrument_Sans'] text-[18px] font-medium leading-[200%] tracking-[-0.6px] text-linen transition-all hover:bg-transparent hover:text-charcoal" 236 + > 237 + join 238 + <ArrowCircleRightIcon 239 + className="h-8 w-8 transition-transform group-hover:translate-x-0.5" 240 + weight="regular" 241 + /> 242 + </button> 243 + </form> 244 + )} 245 + {error && ( 246 + <p className="section-copy mt-4 text-cotton-candy"> 247 + Something went wrong, please try again later :( 248 + </p> 249 + )} 160 250 </div> 161 251 </section> 162 252 );