← 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>&nbsp;live now</span>
        <span class="chip"><strong id="totalCount">0</strong>&nbsp;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>