Files
avbeat-com/src/app/news/[slug]/NewsArticleDetailClient.tsx
Claude d43f78b161 av: clone broadcastbeat sources + AV Beat rebrand (Phase 1)
- Replaces the prior Rocket scaffold (saved on pre-bb-clone-rollback branch)
- Domain: broadcastbeat.com -> avbeat.com
- Brand: Broadcast Beat -> AV Beat
- Slug: broadcastbeat -> avbeat (package, identifiers)
- Schema fallback: bb -> av (env var name unchanged)
- Placeholder AV BEAT logo (gold->red gradient) at /assets/logos/av.svg
- All BB/RMP logo references repointed

Outstanding before public DNS swap:
  - Supabase av schema bootstrap (mirror of bb tables)
  - WordPress archive import (avbeat-com -> av.articles)
  - Coolify env vars
  - dev-avbeat.onsethost.com staging deploy + visual review
2026-06-02 15:32:56 +00:00

902 lines
41 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 StarRating from "@/components/StarRating";
import { createClient } from "@/lib/supabase/client";
import type { Article } from "@/lib/articles/types";
import type { RelatedForumThread } from "@/lib/articles/legacy-source";
import ArticleComments from "@/components/ArticleComments";
interface NewsArticleDetailClientProps {
article: Article;
relatedArticles: Article[];
relatedForumThreads?: RelatedForumThread[];
companiesInStory?: React.ReactNode;
}
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,
relatedForumThreads = [],
companiesInStory,
}: 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://avbeat.com"}/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://avbeat.com/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.readTime}</span>
</div>
<h1 className="font-heading text-3xl md:text-4xl font-bold mb-4 leading-tight text-[#f0f0f0]">
{article.title}
</h1>
<div className="mb-4">
<StarRating seed={article.slug} publishedAt={article.publishedAt || article.date} size="md" />
</div>
<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-2 flex-wrap">
{/* Email Share Button */}
<button
onClick={() => setEmailShareOpen(true)}
aria-label="Share via email"
title="Email"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg width="15" height="15" 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>
</button>
{/* LinkedIn */}
<a
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer"
aria-label="Share on LinkedIn"
title="LinkedIn"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#0a66c2]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/>
</svg>
</a>
{/* X / Twitter */}
<a
href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`}
target="_blank" rel="noopener noreferrer"
aria-label="Share on X"
title="X (Twitter)"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-white">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
{/* Facebook */}
<a
href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer"
aria-label="Share on Facebook"
title="Facebook"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1877f2] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#1877f2]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
{/* Reddit */}
<a
href={`https://www.reddit.com/submit?url=${encodeURIComponent(articleUrl)}&title=${encodeURIComponent(article.title)}`}
target="_blank" rel="noopener noreferrer"
aria-label="Share on Reddit"
title="Reddit"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ff4500] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ff4500]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>
</a>
{/* WhatsApp */}
<a
href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + ' ' + articleUrl)}`}
target="_blank" rel="noopener noreferrer"
aria-label="Share on WhatsApp"
title="WhatsApp"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#25d366] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#25d366]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.711.306 1.265.489 1.697.626.713.226 1.362.194 1.875.118.572-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413z"/>
</svg>
</a>
{/* Copy Link */}
<button
onClick={async () => {
try { await navigator.clipboard.writeText(articleUrl); }
catch { /* swallow */ }
}}
aria-label="Copy article link"
title="Copy link"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</button>
<span className="w-px h-5 bg-[#252525] mx-1" aria-hidden="true" />
{/* 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="bb-article-prose prose prose-invert max-w-none mb-8"
/>
{/* 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>
{/* Article comments — sign-in gated for posting + voting,
public for reading. AI personas participate (tagged). */}
<ArticleComments articleSlug={article.slug} />
</div>
{/* Sidebar */}
<aside className="lg:col-span-4 space-y-6">
{/* Blackmagic 300x600 fixed at top + every 300x250 banner stacked */}
<SidebarAdStack />
{/* Companies referenced in the article body */}
{companiesInStory}
{/* 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>
{/* Related Forum Posts — same widget as /articles/[slug] */}
{relatedForumThreads.length > 0 && (
<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 Forum Posts
</h3>
<ul className="space-y-3.5">
{relatedForumThreads.map((t) => (
<li key={t.id}>
<Link href={`/forum/thread/${t.id}`} className="group block">
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
{t.title}
</p>
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
{t.category_name && (
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider">
{t.category_name}
</span>
)}
<span>·</span>
<span>{t.reply_count} {t.reply_count === 1 ? "reply" : "replies"}</span>
{t.vote_score > 0 && <><span>·</span><span> {t.vote_score}</span></>}
</p>
</Link>
</li>
))}
</ul>
<Link
href="/forum"
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#3b82f6] hover:underline font-body font-semibold uppercase tracking-wider"
>
Browse all forum threads
</Link>
</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>
);
}