Spaces:
Running
Running
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; |