/* ============================================================ CoinFeeds — News + YouTube tabs for the coin detail page ------------------------------------------------------------ Data is mocked but shaped like real API payloads so it's trivial to swap in the live calls later: - News → CryptoPanic public API: https://cryptopanic.com/api/v1/posts/?auth_token=KEY ¤cies=BTC&kind=news&public=true - Video → YouTube Data API v3 search.list: https://www.googleapis.com/youtube/v3/search?part=snippet &q=Bitcoin+BTC&type=video&order=date&key=KEY ============================================================ */ const NEWS_SOURCES = ["CoinDesk", "The Block", "Decrypt", "Cointelegraph", "Bloomberg", "Reuters", "CryptoSlate", "Bankless"]; /* ----- helpers ----- */ const fmtRel = (mins) => { if (mins < 1) return "just now"; if (mins < 60) return `${Math.round(mins)}m ago`; if (mins < 1440) return `${Math.round(mins / 60)}h ago`; return `${Math.round(mins / 1440)}d ago`; }; const fmtViews = (n) => { if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M"; if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K"; return String(n); }; const fmtDuration = (s) => { const m = Math.floor(s / 60), sec = s % 60; if (m >= 60) return `${Math.floor(m/60)}:${String(m%60).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; return `${m}:${String(sec).padStart(2,"0")}`; }; /* deterministic pseudo-random from a seed */ const rng = (seed) => { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; }; /* ----- mock news (CryptoPanic-shaped) ----- */ const NEWS_TEMPLATES = [ { headline: "{name} ETF inflows hit new record as institutional demand builds", sentiment: "positive", topic: "ETF" }, { headline: "{name} network upgrade lands on mainnet, fees drop 40%", sentiment: "positive", topic: "Tech" }, { headline: "Analyst: {symbol} approaching key resistance level, breakout possible", sentiment: "neutral", topic: "Markets" }, { headline: "SEC delays decision on spot {symbol} ETF application", sentiment: "negative", topic: "Regulation" }, { headline: "{name} developer activity reaches all-time high in Q1 2026", sentiment: "positive", topic: "Tech" }, { headline: "Whale wallet moves $42M of {symbol} to exchange, traders watch closely", sentiment: "negative", topic: "On-chain" }, { headline: "Major payments processor adds {symbol} support across 12 countries", sentiment: "positive", topic: "Adoption" }, { headline: "{name} foundation announces $50M ecosystem grant program", sentiment: "positive", topic: "Ecosystem" }, { headline: "Why {symbol} could outperform this cycle, according to three analysts", sentiment: "neutral", topic: "Markets" }, { headline: "{name} protocol exploit drains $8M, team confirms patch deployed", sentiment: "negative", topic: "Security" }, { headline: "On-chain data shows long-term {symbol} holders accumulating", sentiment: "positive", topic: "On-chain" }, { headline: "{name} sees 3x growth in active addresses month-over-month", sentiment: "positive", topic: "On-chain" }, { headline: "Trader sentiment on {symbol} hits 6-month high — what it means", sentiment: "neutral", topic: "Sentiment" }, { headline: "{name} integration goes live on three major DeFi platforms", sentiment: "positive", topic: "DeFi" }, ]; const buildNews = (coin) => { const r = rng(coin.seed * 13); const items = NEWS_TEMPLATES.map((t, i) => { const src = NEWS_SOURCES[Math.floor(r() * NEWS_SOURCES.length)]; const minsAgo = Math.round(r() * 1440 * 2 + i * 8); const votes = Math.round(r() * 380 + 12); const comments = Math.round(r() * 80 + 2); return { id: `${coin.symbol}-news-${i}`, headline: t.headline.replace("{name}", coin.name).replace("{symbol}", coin.symbol), source: src, topic: t.topic, sentiment: t.sentiment, minsAgo, votes, comments, url: `https://cryptopanic.com/news/${coin.symbol.toLowerCase()}/${i}`, }; }); // sort newest first return items.sort((a, b) => a.minsAgo - b.minsAgo); }; /* ----- mock videos (YouTube Data API shaped) ----- */ const VIDEO_TEMPLATES = [ { title: "{name} price analysis — what the charts are telling us", channel: "Coin Bureau", verified: true, durSec: 842, vGrad: ["#FF6B6B","#FFD93D"] }, { title: "Why I'm buying more {symbol} this week", channel: "Benjamin Cowen", verified: true, durSec: 1210, vGrad: ["#3A7BD5","#00D2FF"] }, { title: "{name} explained in 10 minutes — the 2026 update", channel: "Whiteboard Crypto", verified: false, durSec: 624, vGrad: ["#11998e","#38ef7d"] }, { title: "{symbol} technical analysis: bull flag forming on daily?", channel: "InvestAnswers", verified: true, durSec: 1825, vGrad: ["#1f1c2c","#928DAB"] }, { title: "Top 3 reasons {name} outperforms in 2026", channel: "Altcoin Daily", verified: true, durSec: 522, vGrad: ["#FC466B","#3F5EFB"] }, { title: "I bought $10,000 of {symbol} — here's why", channel: "DataDash", verified: false, durSec: 1402, vGrad: ["#373B44","#4286f4"] }, { title: "{name} vs the rest: a deep dive", channel: "Bankless HQ", verified: true, durSec: 2890, vGrad: ["#000428","#004e92"] }, { title: "Live: {symbol} market open — Q&A", channel: "CoinDesk TV", verified: true, durSec: 4322, vGrad: ["#C33764","#1D2671"] }, ]; const buildVideos = (coin) => { const r = rng(coin.seed * 29); return VIDEO_TEMPLATES.map((v, i) => ({ id: `${coin.symbol}-vid-${i}`, title: v.title.replace("{name}", coin.name).replace("{symbol}", coin.symbol), channel: v.channel, verified: v.verified, durSec: v.durSec, views: Math.round(r() * 480000 + 4000), minsAgo: Math.round(r() * 1440 * 5 + i * 90), vGrad: v.vGrad, url: `https://www.youtube.com/results?search_query=${encodeURIComponent(coin.name + " " + coin.symbol)}`, })).sort((a, b) => a.minsAgo - b.minsAgo); }; /* ----- sentiment dot ----- */ const SentimentDot = ({ s }) => { const map = { positive: { c: "var(--up)", l: "Bullish" }, negative: { c: "var(--down)", l: "Bearish" }, neutral: { c: "var(--fg-3)", l: "Neutral" }, }; const m = map[s]; return ( {m.l} ); }; /* ----- News row ----- */ const NewsRow = ({ item }) => { const [hover, setHover] = useState(false); return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 16, alignItems: "start", padding: "16px 20px", borderBottom: "1px solid var(--line-1)", textDecoration: "none", color: "inherit", background: hover ? "var(--bg-2)" : "transparent", transition: "background 120ms var(--ease-out)", }}>
{item.source} {item.topic} {fmtRel(item.minsAgo)}
{item.headline}
▲ {item.votes} {item.comments} comments
); }; /* ----- Video card ----- */ const VideoCard = ({ v }) => { const [hover, setHover] = useState(false); return ( setHover(true)} onMouseLeave={() => setHover(false)} style={{ textDecoration: "none", color: "inherit", display: "block" }}>
{/* faux play button */}
{/* duration chip */}
{fmtDuration(v.durSec)}
{v.title}
{v.channel} {v.verified && ( )}
{fmtViews(v.views)} views · {fmtRel(v.minsAgo)}
); }; /* ----- main feeds component ----- */ const CoinFeeds = ({ coin }) => { const [tab, setTab] = useState("news"); const [filter, setFilter] = useState("all"); const news = useMemo(() => buildNews(coin), [coin.symbol]); const videos = useMemo(() => buildVideos(coin), [coin.symbol]); const filteredNews = filter === "all" ? news : news.filter(n => n.sentiment === filter); return (
{/* tab header */}
setTab("news")} label="News" count={news.length}/> setTab("videos")} label="Videos" count={videos.length}/>
{tab === "news" && (
{[ { id: "all", label: "All" }, { id: "positive", label: "Bullish" }, { id: "neutral", label: "Neutral" }, { id: "negative", label: "Bearish" }, ].map(f => ( ))}
)}
Live
{/* tab content */} {tab === "news" ? (
{filteredNews.length === 0 ? (
No {filter} stories for {coin.symbol} right now.
) : filteredNews.map(item => )}
Source: CryptoPanic API View all news
) : (
{videos.map(v => )}
Source: YouTube Data API v3 View all videos
)}
); }; const FeedTab = ({ active, onClick, label, count }) => ( ); window.CoinFeeds = CoinFeeds;