← back to Fleet Tv

public/widget/spotlight.html

197 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fleet Spotlight</title>
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #0c0d10;
    --fg: #f4f1eb;
    --dim: rgba(244,241,235,0.55);
    --accent: #c9a14b;
    --rule: rgba(255,255,255,0.07);
  }
  html, body { height: 100%; background: var(--bg); color: var(--fg); font-family: -apple-system, "Helvetica Neue", "Segoe UI", system-ui, sans-serif; overflow: hidden; }

  .card {
    height: 100%;
    display: grid; grid-template-rows: 1fr auto;
    background: var(--bg);
    position: relative; overflow: hidden;
  }
  .image-wrap {
    position: relative; overflow: hidden; min-height: 0;
    background: #1a1c20;
  }
  .image-wrap img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover;
    opacity: 0;
    transform: scale(1.04);
    transition: opacity 0.7s ease, transform 8s ease-out;
  }
  .image-wrap img.live { opacity: 1; transform: scale(1.0); }

  /* Vignette overlay so caption reads on bright wallpaper images */
  .vignette {
    position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(to bottom, rgba(0,0,0,0) 60%, rgba(0,0,0,0.55) 100%);
  }

  .pill {
    position: absolute; top: 0.65rem; left: 0.65rem;
    padding: 0.22rem 0.6rem 0.24rem;
    font-size: 0.6rem; letter-spacing: 0.2em; text-transform: uppercase;
    background: rgba(0,0,0,0.65);
    border: 1px solid currentColor;
    border-radius: 999px;
    color: #fff;
    backdrop-filter: blur(4px);
    z-index: 2;
  }

  .meta {
    padding: 0.85rem 1rem 1rem;
    border-top: 1px solid var(--rule);
    background: linear-gradient(180deg, rgba(0,0,0,0.18), var(--bg));
    display: grid; gap: 0.4rem;
  }
  .meta .title {
    font-size: 0.85rem; line-height: 1.3; max-height: 2.6em; overflow: hidden;
    color: var(--fg);
  }
  .meta .row {
    display: flex; justify-content: space-between; align-items: baseline;
    color: var(--dim);
    font-size: 0.62rem; letter-spacing: 0.2em; text-transform: uppercase;
  }
  .meta .row .vendor { color: var(--fg); font-weight: 500; }
  .meta .row .site { font-weight: 500; }
  .meta a { color: inherit; text-decoration: none; }
  .meta a.cta {
    display: inline-block; margin-top: 0.25rem; color: var(--accent);
    border-bottom: 1px solid currentColor; padding-bottom: 1px;
    font-size: 0.65rem; letter-spacing: 0.22em; text-transform: uppercase;
  }
  .meta a.cta:hover { color: var(--fg); }

  /* Loading state */
  .splash {
    position: absolute; inset: 0; display: grid; place-items: center;
    background: var(--bg); z-index: 5;
    color: var(--accent);
    transition: opacity 0.4s ease;
    font-size: 0.7rem; letter-spacing: 0.32em; text-transform: uppercase;
  }
  .splash.hide { opacity: 0; pointer-events: none; }
  .splash::after { content: 'Fleet Spotlight'; animation: pulse 1.6s ease-in-out infinite; }
  @keyframes pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }

  /* Compact rotation indicator (very subtle) */
  .progress {
    position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    z-index: 1;
    background: rgba(255,255,255,0.04);
  }
  .progress > div {
    height: 100%; width: 0; background: var(--accent);
    transition: width 0.2s linear;
  }
</style>
</head>
<body>
<div class="card">
  <div class="image-wrap">
    <span class="pill" id="pill" style="display:none">—</span>
    <img id="img-a" alt="" referrerpolicy="no-referrer">
    <img id="img-b" alt="" referrerpolicy="no-referrer">
    <div class="vignette"></div>
    <div class="splash" id="splash"></div>
    <div class="progress"><div id="bar"></div></div>
  </div>
  <div class="meta">
    <a id="cta-link" target="_blank" rel="noopener noreferrer">
      <div class="title" id="title">—</div>
    </a>
    <div class="row">
      <span class="vendor" id="vendor">—</span>
      <span class="site" id="site">—</span>
    </div>
    <a id="cta" class="cta" target="_blank" rel="noopener noreferrer">view product →</a>
  </div>
</div>

<script>
(() => {
  const ROTATE_MS = 30_000;
  const params = new URLSearchParams(location.search);
  const customRotate = parseInt(params.get('rotate') || '', 10);
  const interval = (Number.isFinite(customRotate) && customRotate >= 5_000) ? customRotate : ROTATE_MS;

  const $ = (id) => document.getElementById(id);
  const imgA = $('img-a'), imgB = $('img-b');
  const splash = $('splash'), pill = $('pill'), bar = $('bar');
  let active = 'a', tickStart = 0, raf;

  async function next() {
    try {
      const r = await fetch('/api/random?count=1', { cache: 'no-store' });
      const j = await r.json();
      const p = (j.items || [])[0];
      if (!p) return;

      const incoming = active === 'a' ? imgB : imgA;
      const outgoing = active === 'a' ? imgA : imgB;

      // Preload to avoid empty flash
      const tmp = new Image();
      tmp.onload = () => {
        incoming.src = p.image;
        incoming.classList.add('live');
        outgoing.classList.remove('live');
        active = active === 'a' ? 'b' : 'a';

        $('title').textContent = p.title || '(untitled)';
        $('vendor').textContent = p.vendor || '—';
        $('site').textContent = p.site;
        pill.style.display = 'inline-block';
        pill.textContent = p.site.replace(/wallpaper$|wallcoverings?$|walls$/i, m => '·' + m);
        pill.style.color = p.accent || '#c9a14b';
        $('cta-link').href = p.productUrl;
        $('cta').href = p.productUrl;
        splash.classList.add('hide');
        bar.style.background = p.accent || '#c9a14b';
      };
      tmp.onerror = () => { setTimeout(next, 1500); }; // try another product if image fails
      tmp.referrerPolicy = 'no-referrer';
      tmp.src = p.image;

      tickStart = Date.now();
    } catch (e) {
      console.error(e);
    }
  }

  function tick() {
    const elapsed = Date.now() - tickStart;
    const pct = Math.min(100, (elapsed / interval) * 100);
    bar.style.width = pct + '%';
    raf = requestAnimationFrame(tick);
  }

  next();
  setInterval(next, interval);
  tick();

  // Pause rotation when document is hidden (saves cycles when tab is in background)
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) cancelAnimationFrame(raf);
    else { tickStart = Date.now(); tick(); }
  });
})();
</script>
</body>
</html>