"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 SidebarAdStack from "@/components/SidebarAdStack"; 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"; // DO NOT OVERRIDE — ArticleDetailClient interface and session tracking interface ArticleDetailClientProps { article: Article; relatedArticles: Article[]; relatedForumThreads?: RelatedForumThread[]; } 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 ArticleDetailClient({ article, relatedArticles, relatedForumThreads = [] }: ArticleDetailClientProps) { 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 articleStartTime = useRef(Date.now()); const hasTrackedView = useRef(false); const supabase = createClient(); // Get current user useEffect(() => { supabase?.auth?.getUser().then(({ data }) => { setCurrentUser(data?.user ?? null); }); }, []); // Track article view on mount 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 — analytics should not break the page } }; 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 already 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]); // Breadcrumb section label const sectionLabel = article.section === "gear" ? "Gear & Reviews" : article.section === "show-coverage" ? "Show Coverage" : article.section === "technology"? "Technology" :"News"; const sectionHref = article.section === "gear" ? "/gear" : article.section === "show-coverage" ? "/show-coverage" : article.section === "technology"? "/technology" :"/news"; return (
{/* Breadcrumb navigation */} {/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
Home / {sectionLabel} / {article.title}
{/* Article Content */}
{/* Main Content */}
{/* Article Header */}
{article.category} {article.date} · {article.readTime}

{article.title}

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

{article.authorTitle}

{(() => { const articleUrl = typeof window !== "undefined" ? window.location.href : `https://avbeat.com/articles/${article.slug}`; return ( <>
{/* Featured Image — editorial standard: cap at 480px high. */}
{/* Article Body */} {/* DO NOT OVERRIDE — article body prose styles */}
{/* Tags */} {article.tags && article.tags.length > 0 && (

Tags

{article.tags.map((tag) => ( {tag} ))}
)} {/* Back navigation */}
Back to {sectionLabel}
{/* Article comments — anonymous read, signed-in members can post + vote + reply. */}
{/* Sidebar — every right-rail block (ads, Related Articles, Related Forum Posts) shares the same 300px column width on lg+ so left + right edges align cleanly with the ad inventory below. */}
{/* Toast Notification */} {saveToast && (
{saveToast}
)}
); }