/* ============================================================
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)",
}}>