← 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>