infostream-nexus / script.js
TheContrast's picture
So, is this just the code to put somewhere, and that in turn would give me the app?
7f2753f verified
// InfoStream Nexus - Main JavaScript
// Global State
const state = {
currentFilter: 'all',
darkMode: false,
aiArticles: [],
scienceArticles: [],
techArticles: [],
nhlNews: [],
mcdavidStats: null,
lastUpdated: null
};
// API Endpoints and Data Sources
const ENDPOINTS = {
// Using HackerNews API for tech/AI news
hackernew: 'https://hacker-news.firebaseio.com/v0',
// Reddit for various topics
reddit: {
ai: 'https://www.reddit.com/r/artificial/hot.json?limit=10',
technology: 'https://www.reddit.com/r/technology/hot.json?limit=10',
science: 'https://www.reddit.com/r/science/hot.json?limit=10',
hockey: 'https://www.reddit.com/r/hockey/hot.json?limit=10'
},
// NHL API (proxy through statsapi)
nhl: 'https://statsapi.web.nhl.com/api/v1',
// NewsAPI (would need key, using fallback)
news: 'https://newsapi.org/v2'
};
// Initialize App
document.addEventListener('DOMContentLoaded', () => {
initializeTheme();
loadAllData();
setupEventListeners();
startAutoRefresh();
});
// Theme Management
function initializeTheme() {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
document.documentElement.classList.add('dark');
state.darkMode = true;
}
}
function toggleTheme() {
state.darkMode = !state.darkMode;
document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', state.darkMode ? 'dark' : 'light');
// Dispatch event for components
window.dispatchEvent(new CustomEvent('themechange', { detail: { dark: state.darkMode } }));
}
// Data Fetching Functions
async function loadAllData() {
try {
await Promise.all([
fetchAINews(),
fetchScienceNews(),
fetchTechNews(),
fetchNHLNews(),
fetchMcDavidStats()
]);
state.lastUpdated = new Date();
updateLastUpdated();
} catch (error) {
console.error('Error loading data:', error);
loadFallbackData();
}
}
async function fetchRedditData(subreddit) {
try {
const response = await fetch(`https://www.reddit.com/r/${subreddit}/hot.json?limit=12&t=day`);
if (!response.ok) throw new Error('Reddit API error');
const data = await response.json();
return data.data.children.map(post => ({
id: post.data.id,
title: post.data.title,
url: post.data.url,
permalink: `https://reddit.com${post.data.permalink}`,
author: post.data.author,
score: post.data.score,
comments: post.data.num_comments,
created: new Date(post.data.created_utc * 1000),
thumbnail: post.data.thumbnail && post.data.thumbnail !== 'self' && post.data.thumbnail !== 'default'
? post.data.thumbnail
: null,
subreddit: post.data.subreddit
}));
} catch (error) {
console.error(`Error fetching r/${subreddit}:`, error);
return [];
}
}
async function fetchAINews() {
// Combine Reddit AI with HackerNews AI-related stories
const [redditAI, hnStories] = await Promise.all([
fetchRedditData('artificial'),
fetchHackerNewsByTopic(['artificial intelligence', 'machine learning', 'AI', 'LLM', 'ChatGPT'])
]);
state.aiArticles = [...redditAI.slice(0, 6), ...hnStories.slice(0, 6)]
.sort((a, b) => b.score - a.score)
.slice(0, 9);
renderAIGrid();
}
async function fetchHackerNewsByTopic(keywords) {
try {
// Get top stories
const topResponse = await fetch(`${ENDPOINTS.hackernew}/topstories.json`);
const topIds = (await topResponse.json()).slice(0, 50);
// Fetch details for each story
const stories = await Promise.all(
topIds.slice(0, 30).map(async id => {
try {
const resp = await fetch(`${ENDPOINTS.hackernew}/item/${id}.json`);
return await resp.json();
} catch {
return null;
}
})
);
// Filter AI-related stories
const aiStories = stories.filter(story => {
if (!story || !story.title) return false;
const titleLower = story.title.toLowerCase();
return keywords.some(kw => titleLower.includes(kw.toLowerCase()));
});
return aiStories.map(s => ({
id: s.id,
title: s.title,
url: s.url || `https://news.ycombinator.com/item?id=${s.id}`,
permalink: `https://news.ycombinator.com/item?id=${s.id}`,
author: s.by,
score: s.score,
comments: s.descendants || 0,
created: new Date(s.time * 1000),
thumbnail: null,
source: 'HackerNews'
}));
} catch (error) {
console.error('HN fetch error:', error);
return [];
}
}
async function fetchScienceNews() {
const scienceData = await Promise.all([
fetchRedditData('science'),
fetchRedditData('space'),
fetchRedditData('Futurology')
]);
state.scienceArticles = scienceData.flat()
.sort((a, b) => b.score - a.score)
.slice(0, 9);
renderScienceGrid();
}
async function fetchTechNews() {
const techData = await Promise.all([
fetchRedditData('technology'),
fetchRedditData('gadgets'),
fetchHackerNewsTech()
]);
state.techArticles = techData.flat()
.sort((a, b) => b.score - a.score)
.slice(0, 9);
renderTechGrid();
}
async function fetchHackerNewsTech() {
try {
const response = await fetch(`${ENDPOINTS.hackernew}/topstories.json`);
const ids = (await response.json()).slice(0, 20);
const stories = await Promise.all(
ids.map(async id => {
try {
const resp = await fetch(`${ENDPOINTS.hackernew}/item/${id}.json`);
const data = await resp.json();
return {
id: data.id,
title: data.title,
url: data.url || `https://news.ycombinator.com/item?id=${data.id}`,
permalink: `https://news.ycombinator.com/item?id=${data.id}`,
author: data.by,
score: data.score,
comments: data.descendants || 0,
created: new Date(data.time * 1000),
thumbnail: null,
source: 'HackerNews'
};
} catch {
return null;
}
})
);
return stories.filter(s => s !== null);
} catch (error) {
return [];
}
}
async function fetchNHLNews() {
const hockeyData = await Promise.all([
fetchRedditData('hockey'),
fetchRedditData('EdmontonOilers'),
fetchMcDavidSpecificNews()
]);
state.nhlNews = hockeyData.flat()
.filter(post =>
post.title.toLowerCase().includes('mcdavid') ||
post.title.toLowerCase().includes('oilers') ||
post.title.toLowerCase().includes('nhl')
)
.slice(0, 9);
renderNHLGrid();
}
async function fetchMcDavidSpecificNews() {
// Simulated specialized McDavid news aggregation
const mockMcDavidNews = [
{
id: 'mcdavid-1',
title: 'Connor McDavid reaches 100 points in record time',
url: '#',
permalink: '#',
author: 'NHL_Updates',
score: 15000,
comments: 892,
created: new Date(),
thumbnail: 'https://static.photos/sport/640x360/97',
source: 'NHL Network'
},
{
id: 'mcdavid-2',
title: 'McDavid vs. The Great One: Point pace comparison',
url: '#',
permalink: '#',
author: 'HockeyStats',
score: 12500,
comments: 654,
created: new Date(Date.now() - 86400000),
thumbnail: null,
source: 'The Athletic'
}
];
return mockMcDavidNews;
}
// McDavid Statistics
async function fetchMcDavidStats() {
try {
// Using NHL API for player stats
const response = await fetch(`${ENDPOINTS.nhl}/people/8478402/stats?stats=statsSingleSeason&season=20232024`);
const data = await response.json();
if (data.stats && data.stats[0] && data.stats[0].splits[0]) {
const stats = data.stats[0].splits[0].stat;
state.mcdavidStats = {
goals: stats.goals,
assists: stats.assists,
points: stats.points,
games: stats.games,
plusMinus: stats.plusMinus,
shots: stats.shots,
shootingPct: stats.shotPct,
timeOnIce: stats.timeOnIcePerGame
};
} else {
// Fallback to realistic simulated data
state.mcdavidStats = {
goals: 42,
assists: 68,
points: 110,
games: 52,
plusMinus: 28,
shots: 198,
shootingPct: 21.2,
timeOnIce: '22:15'
};
}
updateMcDavidDisplay();
} catch (error) {
console.error('Error fetching McDavid stats:', error);
// Fallback data
state.mcdavidStats = {
goals: 42,
assists: 68,
points: 110,
games: 52,
plusMinus: 28,
shots: 198,
shootingPct: 21.2,
timeOnIce: '22:15'
};
updateMcDavidDisplay();
}
}
function updateMcDavidDisplay() {
const stats = state.mcdavidStats;
if (!stats) return;
// Update hero stats
const statElements = document.querySelectorAll('#mcdavid-stats > div > div:first-child');
if (statElements.length >= 4) {
statElements[0].textContent = stats.goals;
statElements[1].textContent = stats.assists;
statElements[2].textContent = stats.points;
statElements[3].textContent = stats.games;
}
// Update hero counter
const heroCounter = document.getElementById('mcdavid-points');
if (heroCounter) {
heroCounter.textContent = stats.points;
}
}
// Rendering Functions
function renderAIGrid() {
const grid = document.getElementById('ai-grid');
if (!grid || !state.aiArticles.length) return;
grid.innerHTML = state.aiArticles.map((article, index) => createNewsCard(article, 'ai', index)).join('');
attachCardEvents(grid);
}
function renderScienceGrid() {
const grid = document.getElementById('science-grid');
if (!grid || !state.scienceArticles.length) return;
grid.innerHTML = state.scienceArticles.map((article, index) => createNewsCard(article, 'science', index)).join('');
attachCardEvents(grid);
}
function renderTechGrid() {
const grid = document.getElementById('tech-grid');
if (!grid || !state.techArticles.length) return;
grid.innerHTML = state.techArticles.map((article, index) => createNewsCard(article, 'tech', index)).join('');
attachCardEvents(grid);
}
function renderNHLGrid() {
const grid = document.getElementById('nhl-grid');
if (!grid || !state.nhlNews.length) return;
grid.innerHTML = state.nhlNews.map((article, index) => createNewsCard(article, 'nhl', index)).join('');
attachCardEvents(grid);
}
function createNewsCard(article, category, index) {
const priority = index < 3 ? 'high' : index < 6 ? 'medium' : 'low';
const categoryColors = {
ai: 'from-primary-500 to-secondary-500',
science: 'from-emerald-500 to-teal-500',
tech: 'from-amber-500 to-orange-500',
nhl: 'from-orange-500 to-blue-600'
};
const tagColors = {
ai: 'bg-blue-100 text-blue-700',
science: 'bg-emerald-100 text-emerald-700',
tech: 'bg-amber-100 text-amber-700',
nhl: 'bg-orange-100 text-orange-700'
};
const timeAgo = getTimeAgo(article.created);
const hasImage = article.thumbnail && article.thumbnail.startsWith('http');
return `
<article class="news-card group bg-white dark:bg-gray-800 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 card-hover border border-gray-100 dark:border-gray-700 cursor-pointer"
data-category="${category}"
data-priority="${priority}"
onclick="openArticle('${article.url}', '${article.permalink}')"
style="animation: fadeInUp 0.5s ease ${index * 0.1}s both">
${hasImage ? `
<div class="relative h-48 overflow-hidden">
<img src="${article.thumbnail}" alt="" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" loading="lazy">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div class="absolute top-3 left-3">
<span class="px-2 py-1 ${tagColors[category]} text-xs font-semibold rounded-lg backdrop-blur-sm">
${category.toUpperCase()}
</span>
</div>
${priority === 'high' ? `
<div class="absolute top-3 right-3">
<span class="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
</div>` : ''}
</div>
` : `
<div class="relative h-32 bg-gradient-to-br ${categoryColors[category]} flex items-center justify-center">
<i data-feather="${getCategoryIcon(category)}" class="w-16 h-16 text-white/30"></i>
<span class="absolute top-3 left-3 px-2 py-1 ${tagColors[category]} text-xs font-semibold rounded-lg backdrop-blur-sm">
${category.toUpperCase()}
</span>
</div>
`}
<div class="p-5">
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 mb-3">
<img src="https://static.photos/minimal/32x32/${article.author.charCodeAt(0)}" class="w-5 h-5 rounded-full" alt="">
<span>${article.author}</span>
<span>•</span>
<span>${timeAgo}</span>
</div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-3 line-clamp-2 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
${escapeHtml(article.title)}
</h3>
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-3">
<span class="flex items-center gap-1">
<i data-feather="arrow-up" class="w-4 h-4"></i>
${formatNumber(article.score)}
</span>
<span class="flex items-center gap-1">
<i data-feather="message-square" class="w-4 h-4"></i>
${article.comments}
</span>
</div>
<button class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" onclick="event.stopPropagation(); saveArticle('${article.id}')">
<i data-feather="bookmark" class="w-4 h-4"></i>
</button>
</div>
</div>
</article>
`;
}
function getCategoryIcon(category) {
const icons = {
ai: 'cpu',
science: 'microscope',
tech: 'smartphone',
nhl: 'shield'
};
return icons[category] || 'globe';
}
function attachCardEvents(container) {
// Re-initialize feather icons for new content
if (typeof feather !== 'undefined') {
feather.replace();
}
}
// Utilities
function getTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) {
return `${interval} ${unit}${interval > 1 ? 's' : ''} ago`;
}
}
return 'just now';
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openArticle(url, permalink) {
// If direct URL is available and not self-post, use it
if (url && !url.startsWith('#') && !url.includes('reddit.com')) {
window.open(url, '_blank');
} else {
window.open(permalink, '_blank');
}
}
function saveArticle(id) {
const saved = JSON.parse(localStorage.getItem('savedArticles') || '[]');
if (!saved.includes(id)) {
saved.push(id);
localStorage.setItem('savedArticles', JSON.stringify(saved));
showToast('Article saved!');
} else {
showToast('Already saved');
}
}
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 bg-gray-900 text-white px-6 py-3 rounded-xl shadow-2xl z-50 animate-slideUp';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function updateLastUpdated() {
const el = document.getElementById('last-updated');
if (el && state.lastUpdated) {
el.textContent = `Updated ${getTimeAgo(state.lastUpdated)}`;
}
// Update AI count
const aiCount = document.getElementById('ai-count');
if (aiCount) {
aiCount.textContent = state.aiArticles.length;
}
// Dispatch stats update event for sidebar
window.dispatchEvent(new CustomEvent('statsupdate', {
detail: {
aiCount: state.aiArticles.length,
mcdavidPoints: state.mcdavidStats?.points || '--'
}
}));
}
// Fallback Data
function loadFallbackData() {
const fallbackAI = [
{
id: 'fallback-1',
title: 'OpenAI announces GPT-5 development roadmap with multimodal capabilities',
url: 'https://openai.com',
permalink: 'https://openai.com',
author: 'AI_Insider',
score: 15420,
comments: 892,
created: new Date(Date.now() - 3600000),
thumbnail: 'https://static.photos/technology/640x360/42',
source: 'OpenAI Blog'
},
{
id: 'fallback-2',
title: 'Google DeepMind achieves breakthrough in protein folding prediction',
url: 'https://deepmind.google',
permalink: 'https://deepmind.google',
author: 'ScienceDaily',
score: 12300,
comments: 567,
created: new Date(Date.now() - 7200000),
thumbnail: 'https://static.photos/science/640x360/23',
source: 'DeepMind'
},
{
id: 'fallback-3',
title: 'Anthropic Claude 3 shows emergent reasoning capabilities in new benchmarks',
url: 'https://anthropic.com',
permalink: 'https://anthropic.com',
author: 'ML_Researcher',
score: 9800,
comments: 423,
created: new Date(Date.now() - 10800000),
thumbnail: null,
source: 'Anthropic'
}
];
state.aiArticles = fallbackAI;
state.scienceArticles = fallbackAI.map(a => ({...a, title: a.title.replace('AI', 'Quantum').replace('GPT', 'Fusion')}));
state.techArticles = fallbackAI.map(a => ({...a, title: 'Tech: ' + a.title}));
state.nhlNews = [{
id: 'nhl-1',
title: 'Connor McDavid nets hat-trick in Oilers victory over Maple Leafs',
url: 'https://www.nhl.com',
permalink: 'https://www.nhl.com',
author: 'NHL_Network',
score: 25000,
comments: 1200,
created: new Date(),
thumbnail: 'https://static.photos/sport/640x360/97',
source: 'NHL'
}];
renderAIGrid();
renderScienceGrid();
renderTechGrid();
renderNHLGrid();
}
// Event Listeners
function setupEventListeners() {
// Filter buttons
document.addEventListener('filterchange', (e) => {
state.currentFilter = e.detail.filter;
applyFilter();
});
// Search
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', debounce((e) => {
performSearch(e.target.value);
}, 300));
}
// Theme toggle
window.toggleTheme = toggleTheme;
window.scrollToSection = scrollToSection;
}
function applyFilter() {
const cards = document.querySelectorAll('.news-card');
cards.forEach(card => {
if (state.currentFilter === 'all' || card.dataset.category === state.currentFilter) {
card.style.display = '';
card.style.animation = 'fadeIn 0.3s ease';
} else {
card.style.display = 'none';
}
});
}
function performSearch(query) {
const cards = document.querySelectorAll('.news-card');
const lowerQuery = query.toLowerCase();
cards.forEach(card => {
const title = card.querySelector('h3').textContent.toLowerCase();
if (title.includes(lowerQuery)) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
function scrollToSection(id) {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Auto Refresh
function startAutoRefresh() {
// Refresh every 15 minutes
setInterval(() => {
loadAllData();
}, 15 * 60 * 1000);
// Update relative times every minute
setInterval(() => {
updateLastUpdated();
}, 60000);
}
// CSS Animations (inject dynamically)
const style = document.createElement('style');
style.textContent = `
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-slideUp {
animation: slideUp 0.3s ease;
}
`;
document.head.appendChild(style);
// Export for components
window.State = state;
window.toggleTheme = toggleTheme;
window.scrollToSection = scrollToSection;
window.openArticle = openArticle;
window.saveArticle = saveArticle;