← back to Allnewsdaily
public/index.html
299 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>All News Daily — global newsroom directory</title>
<meta name="description" content="Every major news outlet in the world — logo, link, and a red LIVE pill that glows when the network is broadcasting live." />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23dc2626'/%3E%3Ctext x='50%25' y='52%25' dominant-baseline='middle' text-anchor='middle' font-family='ui-sans-serif' font-weight='800' font-size='30' fill='white'%3EN%3C/text%3E%3C/svg%3E" />
<style>
:root {
--bg: #0b0c10;
--bg-card: #14161c;
--bg-card-hover: #1c1f28;
--fg: #e7e9ee;
--fg-dim: #8b91a0;
--border: #232733;
--accent: #dc2626;
--accent-glow: rgba(220, 38, 38, .55);
--cols: 6;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f7f9;
--bg-card: #ffffff;
--bg-card-hover: #fbfbfc;
--fg: #14161c;
--fg-dim: #5b6273;
--border: #e3e5ec;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Inter", "SF Pro Text", system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
header { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: saturate(180%) blur(12px); border-bottom: 1px solid var(--border); }
.bar { display: flex; align-items: center; gap: 14px; padding: 12px 20px; flex-wrap: wrap; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; letter-spacing: -.01em; font-size: 18px; }
.brand .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent); animation: pulse 1.4s ease-in-out infinite; }
.brand small { font-weight: 500; color: var(--fg-dim); letter-spacing: 0; margin-left: 4px; font-size: 12px; }
.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-left: auto; }
.controls select, .controls input[type="search"], .controls input[type="range"] { background: var(--bg-card); color: var(--fg); border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
.controls label { font-size: 12px; color: var(--fg-dim); display: flex; align-items: center; gap: 6px; }
.controls input[type="search"] { width: 200px; }
.controls input[type="range"] { padding: 0; width: 120px; vertical-align: middle; }
.controls .chip { padding: 5px 9px; border-radius: 999px; border: 1px solid var(--border); font-size: 12px; color: var(--fg-dim); }
.controls .chip strong { color: var(--fg); font-weight: 700; }
.controls .chip .live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); display: inline-block; vertical-align: middle; margin-right: 6px; box-shadow: 0 0 0 3px var(--accent-glow); animation: pulse 1.4s ease-in-out infinite; }
main { padding: 20px; }
.grid { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 14px; }
.card {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 18px 14px 14px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
text-align: center;
gap: 10px;
transition: transform .12s ease, background .12s ease, border-color .12s ease, box-shadow .12s ease;
min-height: 160px;
}
.card:hover { background: var(--bg-card-hover); transform: translateY(-2px); border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); }
.card a.outlet-link { color: inherit; text-decoration: none; display: contents; }
.card .logo-wrap {
width: 88px; height: 88px;
border-radius: 14px;
background: #fff;
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
overflow: hidden;
box-shadow: 0 1px 0 rgba(255,255,255,.04) inset;
}
.card .logo-wrap img { max-width: 78%; max-height: 78%; object-fit: contain; }
.card .logo-wrap.placeholder { background: linear-gradient(135deg, #1c1f28, #2a2e3a); color: #fff; font-weight: 800; font-size: 26px; letter-spacing: -.02em; }
.card .meta { display: flex; flex-direction: column; gap: 2px; align-items: center; min-height: 38px; }
.card .name { font-weight: 700; font-size: 14px; letter-spacing: -.01em; line-height: 1.15; }
.card .sub { color: var(--fg-dim); font-size: 11.5px; display: flex; align-items: center; gap: 5px; }
.card .flag { font-size: 14px; line-height: 1; }
.card .badge-row { position: absolute; top: 8px; left: 8px; right: 8px; display: flex; justify-content: space-between; align-items: center; pointer-events: none; }
/* The LIVE pill — RED, GLOWING when on live */
.live-pill {
pointer-events: auto;
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 8px 3px 7px;
font-size: 10.5px; font-weight: 800; letter-spacing: .08em;
color: #fff; background: var(--accent);
border-radius: 999px;
text-transform: uppercase; text-decoration: none;
box-shadow: 0 0 0 3px var(--accent-glow), 0 0 18px 4px var(--accent-glow);
animation: pulse 1.4s ease-in-out infinite;
}
.live-pill::before {
content: ""; width: 6px; height: 6px; border-radius: 50%; background: #fff;
box-shadow: 0 0 6px #fff;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 3px var(--accent-glow), 0 0 18px 4px var(--accent-glow); }
50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 45%, transparent), 0 0 26px 10px var(--accent-glow); }
}
.card.is-live { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.card.is-live::after {
content: ""; position: absolute; inset: -1px; border-radius: 13px; pointer-events: none;
box-shadow: 0 0 22px 1px color-mix(in srgb, var(--accent) 35%, transparent);
}
.empty { padding: 60px 20px; text-align: center; color: var(--fg-dim); }
footer { color: var(--fg-dim); font-size: 12px; padding: 24px 20px 40px; text-align: center; }
footer a { color: var(--fg-dim); }
@media (max-width: 1100px) { :root { --cols: 5; } }
@media (max-width: 900px) { :root { --cols: 4; } }
@media (max-width: 680px) { :root { --cols: 3; } .controls input[type="search"] { width: 140px; } }
@media (max-width: 460px) { :root { --cols: 2; } }
</style>
</head>
<body>
<h1 style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">All News Daily</h1>
<header>
<div class="bar">
<div class="brand"><span class="dot"></span>All News Daily<small>global newsroom directory</small></div>
<div class="controls">
<input id="q" type="search" placeholder="Search outlet / country…" autocomplete="off" />
<select id="region"><option value="">All regions</option></select>
<select id="lang"><option value="">All languages</option></select>
<select id="category"><option value="">All categories</option></select>
<select id="sort">
<option value="default">Sort: Default</option>
<option value="live">Live first</option>
<option value="name">Name A→Z</option>
<option value="country">Country A→Z</option>
<option value="region">Region A→Z</option>
</select>
<label>Density <input id="cols" type="range" min="3" max="10" value="6" /></label>
<span class="chip"><span class="live-dot"></span><strong id="liveCount">0</strong> live now</span>
<span class="chip"><strong id="totalCount">0</strong> outlets</span>
</div>
</div>
</header>
<main>
<section id="grid" class="grid"></section>
<div id="empty" class="empty" hidden>No outlets match those filters.</div>
</main>
<footer>
Live status refreshes every ~90s · click a logo to visit the outlet · click <span class="live-pill" style="animation:none">LIVE</span> to watch the live stream
</footer>
<script>
const grid = document.getElementById('grid');
const empty = document.getElementById('empty');
const $ = id => document.getElementById(id);
const state = {
outlets: [],
filters: { q: '', region: '', lang: '', category: '', sort: 'default' },
cols: parseInt(localStorage.getItem('and.cols') || '6', 10),
};
function applyCols(n) {
n = Math.max(3, Math.min(10, Number(n) || 6));
state.cols = n;
document.documentElement.style.setProperty('--cols', String(n));
localStorage.setItem('and.cols', String(n));
const slider = $('cols');
if (slider && Number(slider.value) !== n) slider.value = String(n);
}
function initials(name) {
return name
.replace(/\(.*?\)/g, '')
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(s => s[0])
.join('')
.toUpperCase();
}
function logoHtml(o) {
if (!o.logo) return `<div class="logo-wrap placeholder">${initials(o.name)}</div>`;
return `<div class="logo-wrap"><img loading="lazy" decoding="async" alt="${o.name} logo" src="${o.logo}" onerror="this.parentElement.classList.add('placeholder');this.parentElement.innerHTML='${initials(o.name)}';"></div>`;
}
function cardHtml(o) {
const liveHref = o.liveVideoId
? `https://www.youtube.com/watch?v=${o.liveVideoId}`
: o.liveStream || o.href;
const livePill = o.isLive
? `<a class="live-pill" href="${liveHref}" target="_blank" rel="noopener noreferrer" title="Watch ${o.name} live">Live</a>`
: '';
return `
<article class="card ${o.isLive ? 'is-live' : ''}" data-id="${o.id}">
<div class="badge-row">
<span></span>
${livePill}
</div>
<a class="outlet-link" href="${o.href}" target="_blank" rel="noopener noreferrer" title="Visit ${o.name}">
${logoHtml(o)}
<div class="meta">
<div class="name">${o.name}</div>
<div class="sub"><span class="flag">${o.flag || ''}</span><span>${o.country} · ${o.region}</span></div>
</div>
</a>
</article>
`;
}
function render() {
const f = state.filters;
let list = state.outlets.slice();
if (f.q) {
const q = f.q.toLowerCase();
list = list.filter(o =>
o.name.toLowerCase().includes(q) ||
(o.country && o.country.toLowerCase().includes(q)) ||
(o.region && o.region.toLowerCase().includes(q)) ||
(o.lang && o.lang.toLowerCase().includes(q))
);
}
if (f.region) list = list.filter(o => o.region === f.region);
if (f.lang) list = list.filter(o => o.lang === f.lang);
if (f.category) list = list.filter(o => o.category === f.category);
if (f.sort === 'live') {
list.sort((a, b) => (b.isLive - a.isLive) || a.name.localeCompare(b.name));
} else if (f.sort === 'name') {
list.sort((a, b) => a.name.localeCompare(b.name));
} else if (f.sort === 'country') {
list.sort((a, b) => a.country.localeCompare(b.country) || a.name.localeCompare(b.name));
} else if (f.sort === 'region') {
list.sort((a, b) => a.region.localeCompare(b.region) || a.name.localeCompare(b.name));
} else {
// default — live first, then by name
list.sort((a, b) => (b.isLive - a.isLive) || a.name.localeCompare(b.name));
}
grid.innerHTML = list.map(cardHtml).join('');
empty.hidden = list.length > 0;
const liveCount = state.outlets.filter(o => o.isLive).length;
$('liveCount').textContent = liveCount;
$('totalCount').textContent = state.outlets.length;
}
function populateSelect(id, values, label) {
const el = $(id);
const cur = el.value;
el.innerHTML = `<option value="">${label}</option>` + values.map(v => `<option value="${v}">${v}</option>`).join('');
el.value = cur;
}
async function loadOutlets() {
try {
const r = await fetch('/api/outlets', { cache: 'no-store' });
const j = await r.json();
state.outlets = j.outlets || [];
const regions = [...new Set(state.outlets.map(o => o.region).filter(Boolean))].sort();
const langs = [...new Set(state.outlets.map(o => o.lang).filter(Boolean))].sort();
const cats = [...new Set(state.outlets.map(o => o.category).filter(Boolean))].sort();
populateSelect('region', regions, 'All regions');
populateSelect('lang', langs, 'All languages');
populateSelect('category', cats, 'All categories');
render();
} catch (e) {
console.error('loadOutlets failed', e);
}
}
function bindControls() {
$('q').addEventListener('input', e => { state.filters.q = e.target.value.trim(); render(); });
$('region').addEventListener('change', e => { state.filters.region = e.target.value; render(); });
$('lang').addEventListener('change', e => { state.filters.lang = e.target.value; render(); });
$('category').addEventListener('change', e => { state.filters.category = e.target.value; render(); });
$('sort').addEventListener('change', e => {
state.filters.sort = e.target.value;
localStorage.setItem('and.sort', e.target.value);
render();
});
$('cols').addEventListener('input', e => applyCols(e.target.value));
const savedSort = localStorage.getItem('and.sort');
if (savedSort) { $('sort').value = savedSort; state.filters.sort = savedSort; }
applyCols(state.cols);
}
bindControls();
loadOutlets();
setInterval(loadOutlets, 30000); // refresh live status every 30s on the client
</script>
</body>
</html>