191 lines
18 KiB
JavaScript
191 lines
18 KiB
JavaScript
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const n of r)if(n.type==="childList")for(const d of n.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&s(d)}).observe(document,{childList:!0,subtree:!0});function l(r){const n={};return r.integrity&&(n.integrity=r.integrity),r.referrerPolicy&&(n.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?n.credentials="include":r.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function s(r){if(r.ep)return;r.ep=!0;const n=l(r);fetch(r.href,n)}})();const x=document.querySelector("#app"),t={articles:[],groups:[],loading:!0,error:null,filter:"all",viewMode:"regular",carMode:!1,carModeIndex:0,carModeInterval:null};function M(e){const o=new Date(e),s=new Date-o,r=Math.floor(s/(1e3*60*60)),n=Math.floor(s/(1e3*60));return n<60?n+"m ago":r<24?r+"h ago":o.toLocaleDateString("en-US",{month:"short",day:"numeric"})}function b(e,o=150){return!e||e.length<=o?e||"":e.slice(0,o).trim()+"..."}const f={abc:{name:"ABC News",color:"bg-blue-100 text-blue-800"},npr:{name:"NPR",color:"bg-indigo-100 text-indigo-800"},cnn:{name:"CNN",color:"bg-orange-100 text-orange-800"},nbc:{name:"NBC News",color:"bg-purple-100 text-purple-800"},cbs:{name:"CBS News",color:"bg-emerald-100 text-emerald-800"},nytimes:{name:"NY Times",color:"bg-slate-100 text-slate-800"}};function u(e){return f[e]?.color||"bg-gray-100 text-gray-800"}function c(e){return f[e]?.name||e}const w={politics:"bg-red-100 text-red-800",business:"bg-green-100 text-green-800",technology:"bg-blue-100 text-blue-800",sports:"bg-orange-100 text-orange-800",entertainment:"bg-pink-100 text-pink-800",health:"bg-teal-100 text-teal-800",science:"bg-purple-100 text-purple-800",world:"bg-indigo-100 text-indigo-800",other:"bg-gray-100 text-gray-800"};function y(e){return w[e]||w.other}function k(e){const o=e.image?`<div class="aspect-video overflow-hidden bg-gray-100"><img src="${e.image}" alt="" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" onerror="this.parentElement.style.display='none'" /></div>`:"",l=e.sources.slice(0,3).map(r=>`<span class="px-2 py-0.5 text-xs font-medium rounded-full ${u(r)}">${c(r)}</span>`).join(""),s=e.sources.length>3?`<span class="text-xs text-gray-500">+${e.sources.length-3}</span>`:"";return`<article class="group bg-white rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden border border-gray-100 border-l-4 border-l-blue-500">
|
|
${o}
|
|
<div class="p-5">
|
|
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
|
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${y(e.category)}">
|
|
${e.category}
|
|
</span>
|
|
<span class="text-xs text-gray-500">${e.articleCount} articles</span>
|
|
<span class="ml-auto px-2 py-0.5 text-xs font-medium rounded-full bg-blue-600 text-white">AI Summary</span>
|
|
</div>
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-3">
|
|
${e.title}
|
|
</h2>
|
|
<p class="text-sm text-gray-600 line-clamp-[10] mb-4">
|
|
${e.summary}
|
|
</p>
|
|
<div class="flex items-center gap-2 flex-wrap mb-3">
|
|
${l}${s}
|
|
</div>
|
|
<details class="text-sm">
|
|
<summary class="cursor-pointer text-blue-600 hover:text-blue-800 font-medium">View source articles</summary>
|
|
<ul class="mt-2 space-y-1 pl-4">
|
|
${e.articles.map(r=>`<li><a href="${r.link}" target="_blank" rel="noopener noreferrer" class="text-gray-600 hover:text-blue-600 hover:underline">${b(r.title,60)} <span class="text-gray-400">(${c(r.source)})</span></a></li>`).join("")}
|
|
</ul>
|
|
</details>
|
|
</div>
|
|
</article>`}function C(e){const o=e.image?`<div class="aspect-video overflow-hidden bg-gray-100"><img src="${e.image}" alt="" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" onerror="this.parentElement.style.display='none'" /></div>`:"";return`<article class="group bg-white rounded-xl shadow-sm hover:shadow-lg transition-all duration-300 overflow-hidden border border-gray-100">
|
|
<a href="${e.link}" target="_blank" rel="noopener noreferrer" class="block">
|
|
${o}
|
|
<div class="p-5">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${u(e.source)}">
|
|
${c(e.source)}
|
|
</span>
|
|
<span class="text-xs text-gray-500">${M(e.pubDate)}</span>
|
|
</div>
|
|
<h2 class="text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors line-clamp-2 mb-2">
|
|
${e.title}
|
|
</h2>
|
|
<p class="text-sm text-gray-600 line-clamp-[10]">
|
|
${b(e.content,500)}
|
|
</p>
|
|
</div>
|
|
</a>
|
|
</article>`}function $(){return`<div class="col-span-full flex flex-col items-center justify-center py-20">
|
|
<div class="w-12 h-12 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin mb-4"></div>
|
|
<p class="text-gray-600">${t.viewMode==="ai"?"AI is analyzing and grouping news...":"Loading latest news..."}</p>
|
|
</div>`}function j(e){return`<div class="col-span-full flex flex-col items-center justify-center py-20 text-center">
|
|
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
</svg></div>
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">Failed to load news</h3>
|
|
<p class="text-gray-600 mb-4">${e}</p>
|
|
<button onclick="fetchNews()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">Try Again</button>
|
|
</div>`}function v(){return`<div class="col-span-full flex flex-col items-center justify-center py-20 text-center">
|
|
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
|
|
</svg></div>
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">No articles found</h3>
|
|
<p class="text-gray-600">Try selecting a different source filter.</p>
|
|
</div>`}function N(e,o,l){const s=e.sources.slice(0,4).map(n=>`<span class="px-3 py-1 text-sm font-medium rounded-full ${u(n)} bg-opacity-90">${c(n)}</span>`).join(""),r=e.sources.length>4?`<span class="text-sm text-gray-300">+${e.sources.length-4} more</span>`:"";return`
|
|
<div class="car-mode-card max-w-4xl w-full bg-white/95 backdrop-blur-sm rounded-3xl shadow-2xl overflow-hidden">
|
|
${e.image?`
|
|
<div class="h-64 sm:h-80 overflow-hidden">
|
|
<img src="${e.image}" alt="" class="w-full h-full object-cover" onerror="this.parentElement.style.display='none'" />
|
|
</div>
|
|
`:""}
|
|
<div class="p-8 sm:p-10">
|
|
<div class="flex items-center gap-3 mb-4 flex-wrap">
|
|
<span class="px-4 py-1.5 text-sm font-semibold rounded-full ${y(e.category)}">
|
|
${e.category}
|
|
</span>
|
|
<span class="text-gray-500">${e.articleCount} articles</span>
|
|
<span class="ml-auto px-3 py-1 text-sm font-medium rounded-full bg-blue-600 text-white">AI Summary</span>
|
|
</div>
|
|
<h2 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4 leading-tight">
|
|
${e.title}
|
|
</h2>
|
|
<p class="text-lg text-gray-600 leading-relaxed mb-6 line-clamp-6">
|
|
${e.summary}
|
|
</p>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
${s}${r}
|
|
</div>
|
|
</div>
|
|
<!-- Progress bar -->
|
|
<div class="h-1 bg-gray-200">
|
|
<div class="car-mode-progress h-full bg-blue-600"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Card counter -->
|
|
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 flex items-center gap-2">
|
|
${Array.from({length:l},(n,d)=>`
|
|
<div class="w-2 h-2 rounded-full transition-all duration-300 ${d===o?"bg-white w-6":"bg-white/40"}"></div>
|
|
`).join("")}
|
|
</div>
|
|
`}function I(){if(t.groups.length===0)return`
|
|
<div class="fixed inset-0 z-50 car-mode-bg flex items-center justify-center">
|
|
<div class="car-mode-blur-1"></div>
|
|
<div class="car-mode-blur-2"></div>
|
|
<div class="car-mode-blur-3"></div>
|
|
<div class="relative z-10 text-center text-white">
|
|
<div class="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-6"></div>
|
|
<p class="text-xl">Loading AI grouped news...</p>
|
|
</div>
|
|
<button onclick="exitCarMode()" class="absolute top-6 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Exit Car Mode (Esc)">
|
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`;const e=t.groups[t.carModeIndex];return`
|
|
<div class="fixed inset-0 z-50 car-mode-bg flex items-center justify-center p-6">
|
|
<div class="car-mode-blur-1"></div>
|
|
<div class="car-mode-blur-2"></div>
|
|
<div class="car-mode-blur-3"></div>
|
|
|
|
<!-- Exit button -->
|
|
<button onclick="exitCarMode()" class="absolute top-6 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Exit Car Mode (Esc)">
|
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Navigation buttons -->
|
|
<button onclick="carModePrev()" class="absolute left-6 top-1/2 -translate-y-1/2 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Previous">
|
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
</svg>
|
|
</button>
|
|
<button onclick="carModeNext()" class="absolute right-6 top-1/2 -translate-y-1/2 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Next">
|
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Card -->
|
|
<div class="relative z-10 flex items-center justify-center w-full">
|
|
${N(e,t.carModeIndex,t.groups.length)}
|
|
</div>
|
|
|
|
<!-- Car mode indicator -->
|
|
<div class="absolute top-6 left-6 z-20 flex items-center gap-2 text-white/70">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"></path>
|
|
</svg>
|
|
<span class="text-sm font-medium">Car Mode</span>
|
|
</div>
|
|
</div>
|
|
`}function a(){if(t.carMode){x.innerHTML=I();return}const e=t.filter==="all"?t.articles:t.articles.filter(i=>i.source===t.filter),o=t.loading?"animate-spin":"",l=[...new Set(t.articles.map(i=>i.source))];let s="";t.loading?s=$():t.error?s=j(t.error):t.viewMode==="ai"?t.groups.length===0?s=v():s=t.groups.map(k).join(""):e.length===0?s=v():s=e.map(C).join("");const r=t.viewMode==="regular"?"bg-gray-900 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200",n=t.viewMode==="ai"?"bg-blue-600 text-white":"bg-blue-100 text-blue-700 hover:bg-blue-200",d=t.viewMode==="regular"?`
|
|
<button onclick="setFilter('all')" class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${t.filter==="all"?"bg-gray-900 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}">All</button>
|
|
${l.map(i=>`<button onclick="setFilter('${i}')" class="px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${t.filter===i?"bg-gray-900 text-white":u(i)}">${f[i]?.name||i}</button>`).join("")}
|
|
`:"";x.innerHTML=`<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
|
<header class="bg-white shadow-sm sticky top-0 z-10">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">News Feed</h1>
|
|
<p class="text-sm text-gray-500">${t.viewMode==="ai"?"AI-grouped summaries from multiple sources":"Latest headlines from multiple sources"}</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<div class="flex items-center gap-1 mr-2 p-1 bg-gray-100 rounded-lg">
|
|
<button onclick="setViewMode('regular')" class="px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${r}">Regular</button>
|
|
<button onclick="setViewMode('ai')" class="px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${n}">AI Grouped</button>
|
|
</div>
|
|
${d}
|
|
${t.viewMode==="ai"?'<button onclick="clearCacheAndRefresh()" class="ml-2 px-3 py-1.5 text-sm font-medium text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors" title="Clear cache and fetch fresh AI groupings">Clear Cache</button>':""}
|
|
<button onclick="enterCarMode()" class="ml-2 px-3 py-1.5 text-sm font-medium bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded-lg transition-colors flex items-center gap-1.5" title="Enter Car Mode - Auto-cycling news display">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0"></path>
|
|
</svg>
|
|
Car Mode
|
|
</button>
|
|
<button onclick="refresh()" class="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors" title="Refresh">
|
|
<svg class="w-5 h-5 ${o}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
</svg></button>
|
|
</div></div></div></header>
|
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
${s}
|
|
</div></main>
|
|
<footer class="bg-white border-t border-gray-200 mt-auto">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 text-center text-sm text-gray-500">
|
|
News aggregated from ABC, NPR, CNN, NBC, CBS & NY Times
|
|
</div></footer></div>`}async function g(){t.loading=!0,t.error=null,a();try{const e=await fetch("/api/news");if(!e.ok)throw new Error("Failed to fetch news");const o=await e.json(),l=o.feeds.flatMap(s=>s.items).sort((s,r)=>new Date(r.pubDate)-new Date(s.pubDate));t.articles=l,t.loading=!1,o.errors&&o.errors.length>0&&console.warn("Some feeds failed:",o.errors)}catch(e){t.error=e.message,t.loading=!1}a()}async function p(){t.loading=!0,t.error=null,a();try{const e=await fetch("/api/grouped-news");if(!e.ok)throw new Error("Failed to fetch grouped news");const o=await e.json();t.groups=o.groups,t.loading=!1}catch(e){t.error=e.message,t.loading=!1}a()}window.setFilter=function(e){t.filter=e,a()};window.setViewMode=function(e){t.viewMode=e,e==="ai"&&t.groups.length===0?p():a()};window.refresh=function(){t.viewMode==="ai"?p():g()};window.clearCacheAndRefresh=async function(){t.loading=!0,t.error=null,a();try{const e=await fetch("/api/grouped-news?refresh=true");if(!e.ok)throw new Error("Failed to fetch grouped news");const o=await e.json();t.groups=o.groups,t.loading=!1}catch(e){t.error=e.message,t.loading=!1}a()};window.fetchNews=g;window.fetchGroupedNews=p;window.enterCarMode=async function(){if(t.carMode=!0,t.carModeIndex=0,a(),t.groups.length===0)try{const e=await fetch("/api/grouped-news");if(!e.ok)throw new Error("Failed to fetch grouped news");const o=await e.json();t.groups=o.groups,a()}catch(e){console.error("Failed to load grouped news for car mode:",e)}h()};window.exitCarMode=function(){t.carMode=!1,m(),a()};window.carModeNext=function(){t.groups.length!==0&&(t.carModeIndex=(t.carModeIndex+1)%t.groups.length,m(),a(),h())};window.carModePrev=function(){t.groups.length!==0&&(t.carModeIndex=(t.carModeIndex-1+t.groups.length)%t.groups.length,m(),a(),h())};function h(){m(),t.carModeInterval=setInterval(()=>{t.groups.length>0&&(t.carModeIndex=(t.carModeIndex+1)%t.groups.length,a())},5e3)}function m(){t.carModeInterval&&(clearInterval(t.carModeInterval),t.carModeInterval=null)}document.addEventListener("keydown",e=>{if(t.carMode)switch(e.key){case"Escape":window.exitCarMode();break;case"ArrowRight":case" ":window.carModeNext();break;case"ArrowLeft":window.carModePrev();break}});g();setInterval(()=>{t.viewMode==="ai"?p():g()},300*1e3);
|