Files
avbeat-com/src/app/news/[slug]/NewsArticleDetailClient.tsx
Ryan Salazar 9c5b0e4959 wire real ad rotation: Blackmagic 300x600 fixed + rotating 300x250s
Replace the placeholder Unsplash banner array with the live AdSanity
inventory served from /legacy/ads/ (same 27 active creatives currently
on broadcastbeat.com).

New:
- src/lib/ads.ts: ad inventory (1 fixed Blackmagic 300x600, 22 rotating
  300x250s, 4 leaderboard 728x90s) + Fisher-Yates shuffle + pickAds()
  helper that draws N unique ads excluding any caller-supplied src set.
- src/components/AdImage.tsx: renders one ad as <a target=_blank rel=
  sponsored noopener noreferrer><AppImage>.
- src/components/SidebarAdStack.tsx: Blackmagic 300x600 fixed on top,
  then count rotating 300x250s. Memoizes the rotation per render so a
  refresh re-shuffles. Accepts excludeSrcs to coordinate with other
  slots on the same page.
- src/components/ArticleBody.tsx: splits article HTML at </p>
  boundaries, splices a 300x250 ad after every Nth paragraph (default
  4). Same exclude-set pattern.

Wiring:
- ArticleFeed.tsx: remove the inline sidebarAds array (was 5 Unsplash
  placeholders + 1 LiveU 300x250). Sidebar now <SidebarAdStack count=5/>.
- NewsArticleDetailClient.tsx: sidebar 300x600 + 300x250 AdSlot
  placeholders replaced with <SidebarAdStack count=4/>. Article body
  innerHTML replaced with <ArticleBody html=... everyN=4/> so
  300x250 ads slot in between paragraphs.

Same banners as the live broadcastbeat.com (sourced from the existing
WP AdSanity install we cached locally during Phase 2). Each <a> opens
in a new tab and uses rel='sponsored noopener noreferrer'.
2026-05-08 18:16:07 +00:00

790 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import AppImage from "@/components/ui/AppImage";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import AdSlot from "@/components/AdSlot";
import SidebarAdStack from "@/components/SidebarAdStack";
import ArticleBody from "@/components/ArticleBody";
import { createClient } from "@/lib/supabase/client";
import type { Article } from "@/lib/articles/types";
interface NewsArticleDetailClientProps {
article: Article;
relatedArticles: Article[];
}
function getSessionId(): string {
if (typeof window === "undefined") return "";
let sid = sessionStorage.getItem("bb_session_id");
if (!sid) {
sid = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
sessionStorage.setItem("bb_session_id", sid);
}
return sid;
}
export default function NewsArticleDetailClient({
article,
relatedArticles,
}: NewsArticleDetailClientProps) {
const [isSaved, setIsSaved] = useState(false);
const [savingState, setSavingState] = useState<"idle" | "saving" | "removing">("idle");
const [saveToast, setSaveToast] = useState<string | null>(null);
const [currentUser, setCurrentUser] = useState<{ id: string } | null>(null);
const [emailShareOpen, setEmailShareOpen] = useState(false);
const [emailTo, setEmailTo] = useState("");
const [emailNote, setEmailNote] = useState("");
const [emailSending, setEmailSending] = useState(false);
const [emailStatus, setEmailStatus] = useState<"idle" | "success" | "error">("idle");
const [carouselIndex, setCarouselIndex] = useState(0);
const articleStartTime = useRef<number>(Date.now());
const hasTrackedView = useRef(false);
const supabase = createClient();
const CARDS_VISIBLE = 3;
const maxIndex = Math.max(0, relatedArticles.length - CARDS_VISIBLE);
// Get current user
useEffect(() => {
supabase?.auth?.getUser().then(({ data }) => {
setCurrentUser(data?.user ?? null);
});
}, []);
// Track article view
useEffect(() => {
if (hasTrackedView.current || !article?.slug) return;
hasTrackedView.current = true;
articleStartTime.current = Date.now();
const trackView = async () => {
try {
const { data: userData } = await supabase?.auth?.getUser();
await supabase?.from("article_analytics")?.insert({
article_slug: article.slug,
article_title: article.title,
user_id: userData?.user?.id ?? null,
session_id: getSessionId(),
event_type: "view",
read_time_seconds: 0,
scroll_depth: 0,
referrer: typeof document !== "undefined" ? document.referrer : "",
});
// Track reading history and topic preferences for authenticated users
if (userData?.user?.id) {
// Upsert reading history
await supabase?.from("reading_history")?.upsert(
{
user_id: userData.user.id,
article_slug: article.slug,
article_title: article.title,
article_excerpt: article.excerpt ?? null,
article_image: article.image ?? null,
article_image_alt: article.alt ?? null,
article_category: article.category ?? null,
article_author: article.author ?? null,
article_read_time: article.readTime ?? null,
article_date: article.date ?? null,
viewed_at: new Date().toISOString(),
},
{ onConflict: "user_id,article_slug" }
);
// Upsert topic preference
if (article.category) {
const topic = article.category.toLowerCase().trim();
const { data: existing } = await supabase
?.from("user_topic_preferences")
?.select("id, view_count")
?.eq("user_id", userData.user.id)
?.eq("topic", topic)
?.maybeSingle();
if (existing) {
await supabase
?.from("user_topic_preferences")
?.update({
view_count: (existing.view_count || 0) + 1,
last_viewed_at: new Date().toISOString(),
})
?.eq("id", existing.id);
} else {
await supabase?.from("user_topic_preferences")?.insert({
user_id: userData.user.id,
topic,
view_count: 1,
last_viewed_at: new Date().toISOString(),
});
}
}
}
} catch {
// Silent fail
}
};
trackView();
}, [article?.slug]);
// Track read completion on unmount
useEffect(() => {
return () => {
if (!article?.slug) return;
const readSeconds = Math.round((Date.now() - articleStartTime.current) / 1000);
if (readSeconds < 5) return;
const trackRead = async () => {
try {
const { data: userData } = await supabase?.auth?.getUser();
await supabase?.from("article_analytics")?.insert({
article_slug: article.slug,
article_title: article.title,
user_id: userData?.user?.id ?? null,
session_id: getSessionId(),
event_type: "read",
read_time_seconds: readSeconds,
scroll_depth: 0,
referrer: typeof document !== "undefined" ? document.referrer : "",
});
} catch {
// Silent fail
}
};
trackRead();
};
}, [article?.slug]);
// Check if article is saved
useEffect(() => {
if (!currentUser || !article?.slug) return;
const checkSaved = async () => {
try {
const { data } = await supabase
?.from("reading_list")
?.select("id")
?.eq("user_id", currentUser.id)
?.eq("article_slug", article.slug)
?.maybeSingle();
setIsSaved(!!data);
} catch {
// Silent fail
}
};
checkSaved();
}, [currentUser, article?.slug]);
const handleSaveToReadingList = useCallback(async () => {
if (!currentUser) {
setSaveToast("Sign in to save articles to your reading list");
setTimeout(() => setSaveToast(null), 3000);
return;
}
if (isSaved) {
setSavingState("removing");
try {
await supabase
?.from("reading_list")
?.delete()
?.eq("user_id", currentUser.id)
?.eq("article_slug", article.slug);
setIsSaved(false);
setSaveToast("Removed from reading list");
} catch {
setSaveToast("Failed to remove article");
} finally {
setSavingState("idle");
setTimeout(() => setSaveToast(null), 2500);
}
} else {
setSavingState("saving");
try {
await supabase?.from("reading_list")?.insert({
user_id: currentUser.id,
article_slug: article.slug,
article_title: article.title,
article_excerpt: article.excerpt,
article_image: article.image,
article_image_alt: article.alt,
article_category: article.category,
article_author: article.author,
article_read_time: article.readTime,
article_date: article.date,
});
setIsSaved(true);
setSaveToast("Saved to reading list");
} catch {
setSaveToast("Failed to save article");
} finally {
setSavingState("idle");
setTimeout(() => setSaveToast(null), 2500);
}
}
}, [currentUser, isSaved, article]);
const handleEmailShare = useCallback(async () => {
if (!emailTo.trim()) return;
setEmailSending(true);
setEmailStatus("idle");
try {
const articleUrl = `${process.env.NEXT_PUBLIC_SITE_URL || "https://broadcastb5322.builtwithrocket.new"}/news/${article.slug}`;
const res = await fetch("/api/news/share-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: emailTo.trim(),
articleTitle: article.title,
articleUrl,
articleExcerpt: article.excerpt,
note: emailNote.trim(),
}),
});
if (res.ok) {
setEmailStatus("success");
setEmailTo("");
setEmailNote("");
setTimeout(() => {
setEmailShareOpen(false);
setEmailStatus("idle");
}, 2500);
} else {
setEmailStatus("error");
}
} catch {
setEmailStatus("error");
} finally {
setEmailSending(false);
}
}, [emailTo, emailNote, article]);
const prevCarousel = () => setCarouselIndex((i) => Math.max(0, i - 1));
const nextCarousel = () => setCarouselIndex((i) => Math.min(maxIndex, i + 1));
const articleUrl =
typeof window !== "undefined"
? window.location.href
: `https://broadcastb5322.builtwithrocket.new/news/${article.slug}`;
return (
<div className="min-h-screen bg-background">
<Header />
{/* Breadcrumb */}
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]">
<div className="max-w-container mx-auto px-4 py-2.5 flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">
Home
</Link>
<span>/</span>
<Link href="/news" className="hover:text-[#3b82f6] transition-colors">
News
</Link>
<span>/</span>
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
</div>
</div>
{/* Top leaderboard */}
<div className="max-w-container mx-auto px-4 pt-6">
<AdSlot zone="article-top" size="728x90" />
</div>
{/* Article */}
<article className="max-w-container mx-auto px-4 py-8 md:py-12">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Main Content */}
<div className="lg:col-span-8">
{/* Article Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-4">
<Link
href="/news"
className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline">
{article.category}
</Link>
<span className="text-xs text-[#777]">{article.readTime}</span>
</div>
<h1 className="font-heading text-3xl md:text-4xl font-bold mb-4 leading-tight text-[#f0f0f0]">
{article.title}
</h1>
<p className="font-body text-lg text-[#999] mb-6 leading-relaxed">
{article.excerpt}
</p>
{/* Article Meta */}
<div className="flex items-center justify-between py-4 border-t border-b border-[#222]">
<div className="flex items-center gap-3">
<AppImage
src={article.authorAvatar}
alt={`${article.author}${article.authorTitle}`}
width={40}
height={40}
className="w-10 h-10 rounded-full object-cover"
/>
<div>
<Link
href={`/authors/${article.authorSlug}`}
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
{article.author}
</Link>
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-3">
{/* Email Share Button */}
<button
onClick={() => setEmailShareOpen(true)}
aria-label="Share via email"
className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-[#888] hover:text-[#3b82f6] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
Share
</button>
{/* Save Button */}
<button
onClick={handleSaveToReadingList}
disabled={savingState !== "idle"}
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
{savingState === "saving" ?"Saving..."
: savingState === "removing" ?"Removing..."
: isSaved
? "✓ Saved" :"Save"}
</button>
</div>
</div>
</div>
{/* Featured Image */}
<div className="mb-8 overflow-hidden rounded-sm">
<AppImage
src={article.image}
alt={article.alt}
width={800}
height={450}
className="w-full h-auto object-cover"
priority
/>
</div>
{/* Article Body — paragraphs interleaved with rotating 300×250 ads every 4 paragraphs */}
<ArticleBody
html={article.content}
everyN={4}
className="prose prose-invert max-w-none mb-8 font-body text-[#aaa] leading-relaxed
[&_h2]:font-heading [&_h2]:text-[#e0e0e0] [&_h2]:text-xl [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-4 [&_h2]:border-b [&_h2]:border-[#222] [&_h2]:pb-2
[&_p]:mb-4 [&_p]:leading-relaxed
[&_ul]:mb-4 [&_ul]:pl-5 [&_ul]:space-y-2
[&_li]:text-[#aaa]
[&_strong]:text-[#e0e0e0] [&_strong]:font-bold
[&_a]:text-[#3b82f6] [&_a]:hover:underline"
/>
{/* Tags */}
{article.tags && article.tags.length > 0 && (
<div className="py-6 border-t border-[#222]">
<p className="font-body text-xs text-[#555] uppercase tracking-wider mb-3">Tags</p>
<div className="flex flex-wrap gap-2">
{article.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6] transition-colors rounded-sm cursor-default">
{tag}
</span>
))}
</div>
</div>
)}
{/* Email Share inline CTA */}
<div className="py-6 border-t border-[#222]">
<div className="flex items-center justify-between">
<p className="font-body text-sm text-[#777]">Found this article useful?</p>
<button
onClick={() => setEmailShareOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] text-[#888] hover:text-[#3b82f6] text-xs font-bold uppercase tracking-widest transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
Share via Email
</button>
</div>
</div>
{/* Back navigation */}
<div className="pt-6 border-t border-[#222]">
<Link
href="/news"
className="inline-flex items-center gap-2 text-[#3b82f6] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<polyline points="15 18 9 12 15 6" />
</svg>
Back to News
</Link>
</div>
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* Blackmagic 300x600 fixed at top + rotating 300x250 banners */}
<SidebarAdStack count={4} />
{/* Related Articles Sidebar */}
<div className="bg-[#111] border border-[#222] p-5">
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
Related Articles
</h3>
<div className="space-y-4">
{relatedArticles.slice(0, 3).map((related) => (
<Link
key={related.slug}
href={`/news/${related.slug}`}
className="flex gap-3 group">
<div className="flex-shrink-0 w-[80px] h-[55px] relative overflow-hidden rounded-sm">
<AppImage
src={related.image}
alt={related.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="80px"
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
{related.category}
</p>
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
{related.title}
</p>
</div>
</Link>
))}
</div>
</div>
{/* Newsletter Signup */}
<div className="bg-[#0d1520] border border-[#1e3a5f] p-5">
<h3 className="font-heading text-[#e0e0e0] font-bold mb-2">Stay Updated</h3>
<p className="font-body text-[#777] text-xs mb-4">
Get the latest broadcast engineering news delivered to your inbox.
</p>
<Link
href="/home-page#newsletter"
className="btn-subscribe text-xs py-2 px-4 inline-block w-full text-center">
Subscribe Free
</Link>
</div>
{/* Ad placeholder */}
</aside>
</div>
</article>
{/* Bottom leaderboard */}
<div className="max-w-container mx-auto px-4 pb-8">
<AdSlot zone="article-bottom" size="728x90" />
</div>
{/* Related Articles Carousel */}
{relatedArticles.length > 0 && (
<section className="border-t border-[#1a1a1a] bg-[#0d0d0d] py-10">
<div className="max-w-container mx-auto px-4">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<span className="section-label">More News</span>
<div className="h-px w-16 bg-[#2a2a2a]" />
</div>
{relatedArticles.length > CARDS_VISIBLE && (
<div className="flex items-center gap-2">
<button
onClick={prevCarousel}
disabled={carouselIndex === 0}
aria-label="Previous articles"
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<button
onClick={nextCarousel}
disabled={carouselIndex >= maxIndex}
aria-label="Next articles"
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
</div>
)}
</div>
{/* Carousel track */}
<div className="overflow-hidden">
<div
className="flex gap-5 transition-transform duration-300 ease-in-out"
style={{
transform: `translateX(calc(-${carouselIndex} * (100% / ${CARDS_VISIBLE} + 20px / ${CARDS_VISIBLE})))`,
}}>
{relatedArticles.map((related) => (
<Link
key={related.slug}
href={`/news/${related.slug}`}
className="group flex-shrink-0 w-[calc(33.333%-14px)]"
style={{ minWidth: "calc(33.333% - 14px)" }}>
<div className="relative overflow-hidden rounded-sm mb-3 aspect-[16/9]">
<AppImage
src={related.image}
alt={related.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-300"
sizes="(max-width: 768px) 100vw, 33vw"
/>
</div>
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">
{related.category}
</span>
</div>
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug group-hover:text-[#3b82f6] transition-colors line-clamp-2 mb-1.5">
{related.title}
</h3>
<p className="text-[#666] font-body text-xs line-clamp-2 leading-relaxed">
{related.excerpt}
</p>
<div className="flex items-center gap-2 mt-2">
<span className="text-[#555] font-body text-[11px]">By {related.author}</span>
<span className="text-[#444] text-[10px]">·</span>
<span className="text-[#555] font-body text-[11px]">{related.readTime}</span>
</div>
</Link>
))}
</div>
</div>
{/* Dot indicators */}
{relatedArticles.length > CARDS_VISIBLE && (
<div className="flex justify-center gap-1.5 mt-6">
{Array.from({ length: maxIndex + 1 }).map((_, i) => (
<button
key={i}
onClick={() => setCarouselIndex(i)}
aria-label={`Go to slide ${i + 1}`}
className={`w-1.5 h-1.5 rounded-full transition-colors focus:outline-none ${
i === carouselIndex ? "bg-[#3b82f6]" : "bg-[#333]"
}`}
/>
))}
</div>
)}
</div>
</section>
)}
{/* Email Share Modal */}
{emailShareOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-label="Share article via email">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70"
onClick={() => {
setEmailShareOpen(false);
setEmailStatus("idle");
}}
/>
{/* Modal */}
<div className="relative bg-[#111] border border-[#2a2a2a] rounded-sm w-full max-w-md p-6 shadow-2xl">
<button
onClick={() => {
setEmailShareOpen(false);
setEmailStatus("idle");
}}
aria-label="Close share dialog"
className="absolute top-4 right-4 text-[#555] hover:text-[#e0e0e0] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<div className="flex items-center gap-3 mb-5">
<div className="w-9 h-9 rounded-sm bg-[#1a2a3a] border border-[#1e3a5f] flex items-center justify-center flex-shrink-0">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#3b82f6"
strokeWidth="2"
aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
</div>
<div>
<h2 className="font-heading text-[#e0e0e0] font-bold text-base">Share via Email</h2>
<p className="font-body text-[#666] text-xs mt-0.5">Send this article to a colleague</p>
</div>
</div>
{/* Article preview */}
<div className="bg-[#0d0d0d] border border-[#1a1a1a] rounded-sm p-3 mb-5">
<p className="font-body text-[#888] text-[10px] uppercase tracking-wider mb-1">
{article.category}
</p>
<p className="font-heading text-[#e0e0e0] text-sm font-bold line-clamp-2 leading-snug">
{article.title}
</p>
</div>
{emailStatus === "success" ? (
<div className="text-center py-6">
<div className="w-12 h-12 rounded-full bg-[#0d2a1a] border border-[#1a5a2a] flex items-center justify-center mx-auto mb-3">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#22c55e"
strokeWidth="2.5"
aria-hidden="true">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<p className="font-heading text-[#e0e0e0] font-bold mb-1">Email Sent!</p>
<p className="font-body text-[#777] text-sm">The article has been shared successfully.</p>
</div>
) : (
<div className="space-y-4">
<div>
<label
htmlFor="email-to"
className="block font-body text-xs text-[#888] uppercase tracking-wider mb-1.5">
Recipient Email <span className="text-[#3b82f6]">*</span>
</label>
<input
id="email-to"
type="email"
value={emailTo}
onChange={(e) => setEmailTo(e.target.value)}
placeholder="colleague@example.com"
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors"
/>
</div>
<div>
<label
htmlFor="email-note"
className="block font-body text-xs text-[#888] uppercase tracking-wider mb-1.5">
Personal Note <span className="text-[#555]">(optional)</span>
</label>
<textarea
id="email-note"
value={emailNote}
onChange={(e) => setEmailNote(e.target.value)}
placeholder="Thought you might find this interesting..."
rows={3}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors resize-none"
/>
</div>
{emailStatus === "error" && (
<p className="text-xs font-body text-red-400">
Failed to send email. Please try again.
</p>
)}
<button
onClick={handleEmailShare}
disabled={emailSending || !emailTo.trim()}
className="w-full btn-subscribe py-2.5 text-sm font-bold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
{emailSending ? (
<>
<svg
className="animate-spin"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true">
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
Sending...
</>
) : (
"Send Article"
)}
</button>
</div>
)}
</div>
</div>
)}
{/* Toast Notification */}
{saveToast && (
<div
role="status"
aria-live="polite"
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
{saveToast}
</div>
)}
<Footer />
</div>
);
}