"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(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(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 (
{/* Breadcrumb */}
Home / News / {article.title}
{/* Article */}
{/* Main Content */}
{/* Article Header */}
{article.category} {article.readTime}

{article.title}

{article.excerpt}

{/* Article Meta */}
{article.author}

{article.authorTitle}

{/* Action buttons */}
{/* Email Share Button */} {/* LinkedIn */} {/* X / Twitter */} {/* Facebook */} {/* Reddit */} {/* WhatsApp */} {/* Copy Link */}
{/* Featured Image */}
{/* Article Body — paragraphs interleaved with rotating 300×250 ads every 4 paragraphs */} {/* Tags */} {article.tags && article.tags.length > 0 && (

Tags

{article.tags.map((tag) => ( {tag} ))}
)} {/* Email Share inline CTA */}

Found this article useful?

{/* Back navigation */}
Back to News
{/* Article comments — sign-in gated for posting + voting, public for reading. AI personas participate (tagged). */}
{/* Sidebar */}
{/* Bottom leaderboard */}
{/* Related Articles Carousel */} {relatedArticles.length > 0 && (
More News
{relatedArticles.length > CARDS_VISIBLE && (
)}
{/* Carousel track */}
{relatedArticles.map((related) => (
{related.category}

{related.title}

{related.excerpt}

By {related.author} · {related.readTime}
))}
{/* Dot indicators */} {relatedArticles.length > CARDS_VISIBLE && (
{Array.from({ length: maxIndex + 1 }).map((_, i) => (
)}
)} {/* Email Share Modal */} {emailShareOpen && (
{/* Backdrop */}
{ setEmailShareOpen(false); setEmailStatus("idle"); }} /> {/* Modal */}

Share via Email

Send this article to a colleague

{/* Article preview */}

{article.category}

{article.title}

{emailStatus === "success" ? (

Email Sent!

The article has been shared successfully.

) : (
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" />