/* CARDS — renders poster cards, rows (carousels) and grids. Items can come straight from the API ({tmdb_id, type, name, year, poster}) or from storage records; normalizeItem() handles both. */ (function () { 'use strict'; const { escapeHtml: esc, tmdbResize, formatSeconds } = SR.util; function normalizeItem(r) { return { tmdbId: r.tmdb_id != null ? r.tmdb_id : r.tmdbId, type: r.type, season: r.season, episode: r.episode, name: r.name || r.title || '', year: r.year || '', poster: r.poster || r.poster_url || '', backdrop: r.backdrop || r.backdrop_url || '', rating: r.rating || (r.vote_average ? Number(r.vote_average).toFixed(1) : ''), overview: r.overview || '', }; } function watchHref(item) { const p = new URLSearchParams(); p.set('id', item.tmdbId); p.set('type', item.type); if (item.type === 'tv') { if (item.season != null) p.set('s', item.season); if (item.episode != null) p.set('e', item.episode); } return `watch.html?${p.toString()}`; } function card(item, opts = {}) { const el = document.createElement('a'); el.className = 'card'; el.href = opts.href || watchHref(item); const poster = tmdbResize(item.poster, 'w342'); const progress = opts.progress != null ? `` : ''; const meta = item.meta || (item.year ? `${item.year} · ${item.type === 'tv' ? 'TV' : 'Movie'}` : item.type === 'tv' ? 'TV' : 'Movie'); const metaHtml = item.metaHtml || `${esc(meta)}`; el.innerHTML = ` ${poster ? `` : '?'} ${progress} ${esc(item.name)} ${metaHtml}`; return el; } function row(title, items) { const section = document.createElement('section'); section.className = 'row'; section.innerHTML = `

${esc(title)}

`; const scroll = document.createElement('div'); scroll.className = 'row__scroll'; for (const item of items) scroll.appendChild(card(item)); section.appendChild(scroll); return section; } function grid(items) { const el = document.createElement('div'); el.className = 'grid'; for (const item of items) el.appendChild(card(item)); return el; } function emptyState(title, hint) { const el = document.createElement('div'); el.className = 'empty'; el.innerHTML = `

${esc(title)}

${hint ? `

${esc(hint)}

` : ''}`; return el; } // "Continue watching" row: latest record per show, with progress bar function continueRow() { const recs = SR.storage.latestPerShow(); if (!recs.length) return null; const items = recs.map((r) => ({ tmdbId: r.tmdbId, type: r.type, season: r.season, episode: r.episode, name: r.show || r.title || '', poster: r.poster || '', meta: r.type === 'tv' ? `S${r.season} · E${r.episode} · ${formatSeconds(Math.max(0, r.d - r.t))} left` : `Movie · ${formatSeconds(Math.max(0, r.d - r.t))} left`, metaHtml: r.type === 'tv' ? `S${r.season} · E${r.episode}·${formatSeconds(Math.max(0, r.d - r.t))} left` : `Movie · ${formatSeconds(Math.max(0, r.d - r.t))} left`, progress: r.d ? r.t / r.d : 0, })); return row('Continue watching', items); } function heroCarousel(items) { const data = (items || []).filter((item) => item && item.backdrop && item.name); if (!data.length) return null; const section = document.createElement('section'); section.className = 'hero'; section.setAttribute('aria-roledescription', 'carousel'); section.setAttribute('aria-label', 'Featured titles'); const wrap = document.createElement('div'); wrap.className = 'hero__slides'; const slideEls = []; for (let i = 0; i < data.length; i += 1) { const item = data[i]; const slide = document.createElement('article'); slide.className = i === 0 ? 'hero__slide is-active' : 'hero__slide'; slide.setAttribute('role', 'group'); slide.setAttribute('aria-roledescription', 'slide'); slide.setAttribute('aria-label', `${i + 1} of ${data.length}: ${item.name}`); const typeLabel = item.type === 'tv' ? 'TV' : 'Movie'; const metaBits = [ item.rating ? `★ ${esc(item.rating)}` : '', item.year ? `${esc(item.year)}` : '', `${esc(typeLabel)}`, ] .filter(Boolean) .join('·'); slide.innerHTML = `
Trending now

${esc(item.name)}

${metaBits}
${item.overview ? `

${esc(item.overview)}

` : ''}
`; wrap.appendChild(slide); slideEls.push(slide); } section.appendChild(wrap); if (slideEls.length > 1) { const prev = document.createElement('button'); prev.type = 'button'; prev.className = 'hero__arrow hero__arrow--prev'; prev.setAttribute('aria-label', 'Previous slide'); prev.innerHTML = ''; const next = document.createElement('button'); next.type = 'button'; next.className = 'hero__arrow hero__arrow--next'; next.setAttribute('aria-label', 'Next slide'); next.innerHTML = ''; const dots = document.createElement('div'); dots.className = 'hero__dots'; const dotEls = []; for (let i = 0; i < slideEls.length; i += 1) { const dot = document.createElement('button'); dot.type = 'button'; dot.className = i === 0 ? 'hero__dot is-active' : 'hero__dot'; dot.setAttribute('aria-label', `Go to slide ${i + 1}`); dots.appendChild(dot); dotEls.push(dot); } section.append(prev, next, dots); let current = 0; let timer = null; function goTo(index) { current = (index + slideEls.length) % slideEls.length; slideEls.forEach((slide, i) => slide.classList.toggle('is-active', i === current)); dotEls.forEach((dot, i) => dot.classList.toggle('is-active', i === current)); } function start() { stop(); if (!document.hidden) timer = setInterval(() => goTo(current + 1), 7000); } function stop() { if (timer) { clearInterval(timer); timer = null; } } prev.addEventListener('click', () => { goTo(current - 1); start(); }); next.addEventListener('click', () => { goTo(current + 1); start(); }); dotEls.forEach((dot, i) => dot.addEventListener('click', () => { goTo(i); start(); })); section.addEventListener('mouseenter', stop); section.addEventListener('mouseleave', start); section.addEventListener('focusin', stop); section.addEventListener('focusout', (e) => { if (!section.contains(e.relatedTarget)) start(); }); document.addEventListener('visibilitychange', () => { if (document.hidden) stop(); else start(); }); start(); } return section; } window.SR = window.SR || {}; SR.cards = { normalizeItem, card, row, grid, emptyState, continueRow, watchHref, heroCarousel }; })();