diff --git a/app.css b/app.css index 2425b2c..24745d8 100644 --- a/app.css +++ b/app.css @@ -921,6 +921,198 @@ textarea.inline-edit { font-size: .82rem; } +/* ── Per-type bespoke: TVSeries (Netflix-style streaming page) ── */ +.card.streaming-page { + background: #0b0b0f; + border-color: rgba(255,255,255,.06); + overflow: hidden; +} +.card.streaming-page .hd.hidden { display: none; } + +.streaming-hero { + position: relative; + width: 100%; + height: 420px; + overflow: hidden; + cursor: pointer; +} +.streaming-hero-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + filter: brightness(.82); + transition: transform .6s cubic-bezier(.16,1,.3,1), filter .4s; +} +.streaming-hero:hover .streaming-hero-img { + transform: scale(1.04); + filter: brightness(.9); +} +.streaming-scrim { + position: absolute; + inset: 0; + background: + linear-gradient(180deg, + rgba(11,11,15,.1) 0%, + rgba(11,11,15,.3) 30%, + rgba(11,11,15,.7) 65%, + rgba(11,11,15,.97) 100%), + linear-gradient(90deg, + rgba(11,11,15,.5) 0%, + transparent 50%); +} +.streaming-overlay { + position: absolute; + bottom: 0; left: 0; right: 0; + padding: 1.5rem 2.5rem 2rem; + color: #fff; + max-width: 680px; +} +.streaming-badge { + display: inline-block; + padding: .28rem .7rem; + border-radius: 4px; + background: rgba(255,255,255,.12); + border: 1px solid rgba(255,255,255,.15); + font-size: .6rem; + font-weight: 700; + letter-spacing: .12em; + text-transform: uppercase; + color: rgba(255,255,255,.85); + margin-bottom: .75rem; +} +.streaming-title { + margin: 0 0 .5rem; + font-size: 2.6rem; + font-weight: 900; + letter-spacing: -.03em; + line-height: 1.05; + text-shadow: 0 2px 16px rgba(0,0,0,.6); +} +.streaming-meta { + font-size: .88rem; + font-weight: 500; + color: rgba(255,255,255,.7); + margin-bottom: 1rem; + letter-spacing: .01em; +} +.streaming-play-btn { + display: inline-flex; + align-items: center; + gap: .5rem; + padding: .65rem 1.8rem; + border: none; + border-radius: 6px; + background: #fff; + color: #0b0b0f; + font-size: .95rem; + font-weight: 700; + cursor: pointer; + transition: background .2s, transform .15s; +} +.streaming-play-btn:hover { + background: rgba(255,255,255,.85); + transform: scale(1.03); +} +.streaming-play-btn:active { + transform: scale(.98); +} +.streaming-synopsis { + margin: 1rem 0 0; + font-size: .88rem; + line-height: 1.6; + color: rgba(255,255,255,.72); +} + +/* Body */ +.card.streaming-page .bd { + background: #0b0b0f; + padding: 1.5rem 2.5rem 2rem; +} +.card.streaming-page .bd::before { display: none; } +.card.streaming-page .row .k { color: rgba(255,255,255,.4); } +.card.streaming-page .row .v { color: rgba(255,255,255,.85); } +.card.streaming-page .row { border-bottom-color: rgba(255,255,255,.04); } + +/* Director credit */ +.streaming-credit { + display: flex; + align-items: center; + gap: .6rem; + margin-bottom: 1rem; +} +.streaming-credit-label { + font-size: .65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + color: rgba(255,255,255,.35); +} +.streaming-credit-name { + font-size: .95rem; + font-weight: 600; + color: rgba(255,255,255,.9); +} + +/* Genre tags */ +.streaming-tags { + display: flex; + flex-wrap: wrap; + gap: .4rem; + margin-bottom: 1.25rem; +} +.streaming-tag { + padding: .3rem .7rem; + border-radius: 100px; + background: rgba(255,255,255,.08); + border: 1px solid rgba(255,255,255,.08); + font-size: .72rem; + font-weight: 600; + color: rgba(255,255,255,.7); +} + +/* Episode info bar */ +.streaming-info-bar { + display: flex; + flex-wrap: wrap; + gap: .75rem; + margin-bottom: 1rem; +} +.streaming-info-chip { + display: flex; + flex-direction: column; + align-items: center; + padding: .65rem 1.1rem; + border-radius: 10px; + background: rgba(255,255,255,.06); + border: 1px solid rgba(255,255,255,.06); +} +.streaming-info-num { + font-size: 1.4rem; + font-weight: 800; + color: #fff; + line-height: 1.1; +} +.streaming-info-label { + font-size: .58rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + color: rgba(255,255,255,.4); + margin-top: .15rem; +} + +@media (max-width: 600px) { + .streaming-hero { height: 320px; } + .streaming-overlay { padding: 1rem 1.25rem 1.25rem; max-width: 100%; } + .streaming-title { font-size: 1.7rem; } + .streaming-synopsis { font-size: .82rem; } + .card.streaming-page .bd { padding: 1.25rem; } + .streaming-info-bar { gap: .5rem; } + .streaming-info-chip { padding: .5rem .8rem; } + .streaming-info-num { font-size: 1.1rem; } +} + /* ── Per-type bespoke: Product — Apple Showcase ── */ .card.product-showcase { diff --git a/app.js b/app.js index 654ff0f..c4d8abe 100644 --- a/app.js +++ b/app.js @@ -573,6 +573,154 @@ } } + // ── TVSERIES (Netflix-style streaming page) ── + if (cfg.type === "TVSeries") { + const card = document.querySelector(".card"); + if (card) card.classList.add("streaming-page"); + + const heroMount = document.getElementById("hero-mount"); + const typeLabel = document.getElementById("type-label"); + const iconEl = document.getElementById("icon"); + + // Remove generic hero so we can build our own + heroMount.querySelector(".hero")?.remove(); + + // ── Dark immersive hero ── + const imageData = data["image"]; + const hero = document.createElement("div"); hero.className = "streaming-hero"; + hero.setAttribute("data-prop", "image"); + hero.setAttribute("title", "Click to change image URL"); + hero.setAttribute("tabindex", "0"); + + if (imageData) { + const img = document.createElement("img"); + img.className = "streaming-hero-img"; + img.src = imageData; + img.alt = data["name"] || "Series art"; + img.loading = "lazy"; + hero.appendChild(img); + } + + // Dark gradient scrim + const scrim = document.createElement("div"); scrim.className = "streaming-scrim"; + hero.appendChild(scrim); + + // Overlay content + const overlay = document.createElement("div"); overlay.className = "streaming-overlay"; + + // Type badge + const badge = document.createElement("span"); badge.className = "streaming-badge"; + badge.textContent = "\u{1F4FA}\u{00A0}" + (typeLabel ? typeLabel.textContent : "TV Series"); + overlay.appendChild(badge); + + // Title — huge + const h1 = document.createElement("h1"); h1.className = "streaming-title"; + h1.textContent = data["name"] || ""; + overlay.appendChild(h1); + + // Meta row: year · seasons · episodes · genre + const metaParts = []; + const start = data["startDate"]; + if (start) { const yr = start.match(/(\d{4})/); if (yr) metaParts.push(yr[1]); } + const seasons = data["numberOfSeasons"]; + if (seasons) metaParts.push(seasons + " Season" + (seasons !== "1" ? "s" : "")); + const episodes = data["numberOfEpisodes"]; + if (episodes) metaParts.push(episodes + " Episode" + (episodes !== "1" ? "s" : "")); + const genre = data["genre"]; + if (genre) metaParts.push(genre.split(",")[0].trim()); + if (metaParts.length) { + const meta = document.createElement("div"); meta.className = "streaming-meta"; + meta.textContent = metaParts.join(" \u{00B7} "); + overlay.appendChild(meta); + } + + // Play button + const url = data["url"]; + const playBtn = document.createElement("button"); + playBtn.className = "streaming-play-btn"; + playBtn.innerHTML = "\u{25B6}\u{FE0F} Play"; + if (url) { + playBtn.addEventListener("click", () => window.open(url, "_blank")); + } + overlay.appendChild(playBtn); + + // Synopsis + const desc = data["description"]; + if (desc) { + const synopsis = document.createElement("p"); synopsis.className = "streaming-synopsis"; + synopsis.textContent = desc; + overlay.appendChild(synopsis); + } + + hero.appendChild(overlay); + heroMount.appendChild(hero); + + // Hide normal header + const hd = document.querySelector(".hd"); + if (hd) hd.classList.add("hidden"); + + // Mark all handled props + handled.add("name"); + handled.add("image"); + handled.add("description"); + handled.add("genre"); + handled.add("numberOfSeasons"); + handled.add("numberOfEpisodes"); + handled.add("startDate"); + handled.add("url"); + + // ── Body: director + genre tags + episodes overview ── + + // Director credit + const director = data["director"]; + if (director) { + const credit = document.createElement("div"); credit.className = "streaming-credit"; + credit.innerHTML = "Directed by" + director + ""; + view.appendChild(credit); + handled.add("director"); + } + + // Genre tags + if (genre) { + const tags = document.createElement("div"); tags.className = "streaming-tags"; + genre.split(",").forEach(function(g) { + var tag = document.createElement("span"); + tag.className = "streaming-tag"; + tag.textContent = g.trim(); + tags.appendChild(tag); + }); + view.appendChild(tags); + } + + // Episodes overview card + if (seasons || episodes) { + const infoBar = document.createElement("div"); + infoBar.className = "streaming-info-bar"; + if (seasons) { + const chip = document.createElement("div"); chip.className = "streaming-info-chip"; + chip.innerHTML = "" + seasons + "Season" + (seasons !== "1" ? "s" : "") + ""; + infoBar.appendChild(chip); + } + if (episodes) { + const chip = document.createElement("div"); chip.className = "streaming-info-chip"; + chip.innerHTML = "" + episodes + "Episode" + (episodes !== "1" ? "s" : "") + ""; + infoBar.appendChild(chip); + } + var endD = data["endDate"]; + if (start) { + var dateText = formatDate(start); + if (endD) dateText = dateText + " \u{2013} " + formatDate(endD); + else dateText = dateText + " \u{2013} Present"; + var dateChip = document.createElement("div"); + dateChip.className = "streaming-info-chip"; + dateChip.innerHTML = "" + dateText + ""; + infoBar.appendChild(dateChip); + } + view.appendChild(infoBar); + handled.add("endDate"); + } + } + // ── PRODUCT (Apple product page) ── if (cfg.type === "Product") { const card = document.querySelector(".card");