diff --git a/app/books/devops-survival-guide/client-content.tsx b/app/books/devops-survival-guide/client-content.tsx index 9559d1f71..92a9a5f7b 100644 --- a/app/books/devops-survival-guide/client-content.tsx +++ b/app/books/devops-survival-guide/client-content.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardDescription, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { useNewsletterSubscribe } from '@/hooks/use-newsletter-subscribe'; import { BookOpen, Rocket, @@ -152,6 +153,8 @@ const chapters = [ export function ClientContent() { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [isScrolled, setIsScrolled] = useState(false); + const [bookEmail, setBookEmail] = useState(''); + const { status: bookSubscribeStatus, subscribe: bookSubscribe } = useNewsletterSubscribe(); useEffect(() => { const handleScroll = () => { @@ -524,42 +527,43 @@ export function ClientContent() { {/* Newsletter Signup Form */}
-
- - - {/* Honeypot bot field */} - - -
+ {bookSubscribeStatus === 'error' && ( +

Something went wrong. Please try again.

+ )} + + + + )}

diff --git a/components/book-promotion-popup.tsx b/components/book-promotion-popup.tsx index 51edae2e5..6827b84e4 100644 --- a/components/book-promotion-popup.tsx +++ b/components/book-promotion-popup.tsx @@ -5,6 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { BookOpen, X, Sparkles } from 'lucide-react' import { Button } from '@/components/ui/button' import Confetti from 'react-confetti' +import { EMAIL_RE, BREVO_FORM_URL, submitToBrevo } from '@/lib/newsletter' export function BookPromotionPopup() { const [isVisible, setIsVisible] = useState(false) @@ -60,34 +61,35 @@ export function BookPromotionPopup() { }, 300) } - const handleSubscribe = (e: React.FormEvent) => { - e.preventDefault() + const handleSubscribe = async (e: React.FormEvent) => { + e.preventDefault(); - if (!email) return + if (!email || !EMAIL_RE.test(email)) return; - // Submit to Mailchimp - const form = e.target as HTMLFormElement - const formData = new FormData(form) - - // Open in new window (Mailchimp requirement) - const mailchimpUrl = 'https://devops-daily.us2.list-manage.com/subscribe/post?u=d1128776b290ad8d08c02094f&id=fd76a4e93f&f_id=0022c6e1f0' - const params = new URLSearchParams(formData as any).toString() - window.open(`${mailchimpUrl}&${params}`, '_blank') + try { + await submitToBrevo(email); + } catch (err) { + console.error('[newsletter] Popup subscription error:', err); + const w = window.open(BREVO_FORM_URL, '_blank'); + if (!w) { + console.warn('[newsletter] Popup blocked. Direct user to:', BREVO_FORM_URL); + } + } // Show thank you message - setShowThankYou(true) - localStorage.setItem('book-promo-subscribed', 'true') + setShowThankYou(true); + localStorage.setItem('book-promo-subscribed', 'true'); // Show celebration confetti - setShowConfetti(true) + setShowConfetti(true); // Auto-close after 3 seconds setTimeout(() => { - setShowConfetti(false) - setIsLoaded(false) - setTimeout(() => setIsVisible(false), 300) - }, 3000) - } + setShowConfetti(false); + setIsLoaded(false); + setTimeout(() => setIsVisible(false), 300); + }, 3000); + }; if (!isVisible) return null @@ -160,11 +162,13 @@ export function BookPromotionPopup() { Subscribe for exclusive content & launch updates! ✨

-
+ setEmail(e.target.value)} placeholder="your@email.com" @@ -172,11 +176,6 @@ export function BookPromotionPopup() { className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all" /> - {/* Honeypot */} - -
diff --git a/components/newsletter-inline-form.tsx b/components/newsletter-inline-form.tsx new file mode 100644 index 000000000..783b5a5ca --- /dev/null +++ b/components/newsletter-inline-form.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { CheckCircle } from 'lucide-react'; +import { useNewsletterSubscribe } from '@/hooks/use-newsletter-subscribe'; + +export function NewsletterInlineForm() { + const [email, setEmail] = useState(''); + const { status, subscribe } = useNewsletterSubscribe(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (email) subscribe(email); + }; + + if (status === 'success') { + return ( +
+ +

You're subscribed! Check your inbox.

+
+ ); + } + + return ( +
+ setEmail(e.target.value)} + required + placeholder="you@example.com" + className="w-full px-3 py-2 border border-border bg-background rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary" + /> + + {status === 'error' && ( +

Something went wrong. Please try again.

+ )} + + +
+ ); +} diff --git a/components/sponsor-sidebar.tsx b/components/sponsor-sidebar.tsx index dd248cb9a..4190a547a 100644 --- a/components/sponsor-sidebar.tsx +++ b/components/sponsor-sidebar.tsx @@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'; import { Clock, Sparkles, ExternalLink } from 'lucide-react'; import { CarbonAds } from '@/components/carbon-ads'; import { sponsors } from '@/lib/sponsors'; +import { NewsletterInlineForm } from '@/components/newsletter-inline-form'; interface SponsorSidebarProps { className?: string; @@ -97,40 +98,7 @@ export function SponsorSidebar({ className, relatedPosts = [] }: SponsorSidebarP

Get the latest DevOps tips and tutorials delivered to your inbox.

-
- - - {/* Honeypot bot field */} - - - -
+
{/* Related Posts Section */} diff --git a/hooks/use-newsletter-subscribe.ts b/hooks/use-newsletter-subscribe.ts new file mode 100644 index 000000000..c6c159b9f --- /dev/null +++ b/hooks/use-newsletter-subscribe.ts @@ -0,0 +1,30 @@ +'use client'; + +import { useState } from 'react'; +import { BREVO_FORM_URL, EMAIL_RE, submitToBrevo } from '@/lib/newsletter'; + +export type SubscribeStatus = 'idle' | 'loading' | 'success' | 'error'; + +export function useNewsletterSubscribe() { + const [status, setStatus] = useState('idle'); + + const subscribe = async (email: string) => { + if (!EMAIL_RE.test(email)) { + setStatus('error'); + return; + } + setStatus('loading'); + try { + await submitToBrevo(email); + setStatus('success'); + } catch { + const w = window.open(BREVO_FORM_URL, '_blank'); + if (!w) { + console.warn('[newsletter] Popup blocked. Direct user to:', BREVO_FORM_URL); + } + setStatus('error'); + } + }; + + return { status, subscribe }; +} diff --git a/lib/newsletter.ts b/lib/newsletter.ts new file mode 100644 index 000000000..54f286fcd --- /dev/null +++ b/lib/newsletter.ts @@ -0,0 +1,24 @@ +/** Brevo subscription form URL. Public — safe to commit, no env var needed. */ +export const BREVO_FORM_URL = + 'https://66ce6dcc.sibforms.com/serve/MUIFABg_VUzhY-5kln8REbgjz0epYq6FtPckqwqsIG_s4FKBiVUqR9Q5SakKep9c2cHa2NEC1J02ps4tMUbaxssoB7MvwSggRvWktJJ7-LM9oWVRG3h0KhFHXsNOgoCSEo9OTB_CIp8JyRlALoSmEQOGpRoVYIYEq2LD0ikQ6T56zXVF8ZNc3tFBykMZGOxtLEfD7tap75LwptWPxg=='; + +export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +/** + * Submit an email to the Brevo hosted subscription form. + * Uses mode:'no-cors' because Brevo's endpoint doesn't emit CORS headers. + * The response is opaque, so any non-throwing fetch is treated as success. + */ +export async function submitToBrevo(email: string): Promise { + const body = new URLSearchParams({ + EMAIL: email.trim(), + email_address_check: '', + locale: 'en', + }); + await fetch(BREVO_FORM_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + mode: 'no-cors', + }); +}