initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,782 @@
"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 { createClient } from "@/lib/supabase/client";
import type { Article } from "@/lib/articles/sampleArticles";
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>
{/* 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.date}</span>
<span className="text-xs text-[#555]">·</span>
<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 */}
<div
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"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
{/* 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">
{/* 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 */}
<div className="ad-placeholder w-full h-[250px] flex items-center justify-center">
<div className="text-center">
<p className="text-[#444] text-xs font-body">Advertisement</p>
<p className="text-[#333] text-[10px] font-body mt-1">300×250</p>
</div>
</div>
</aside>
</div>
</article>
{/* 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>
<span className="text-[#444] text-[10px]">·</span>
<span className="text-[#555] font-body text-[11px]">{related.date}</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>
);
}