Files
2026-10-16 05:20:52 +02:00

169 lines
8.9 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Games Planning Preview</title>
<style>
:root {
--bg: #1d232a; --surface: #2a323c; --text: #a6adbb; --muted: #6b7280; --line: #ffffff10;
--hour: 104px; --col: 150px; --gap: 10px; --break: 34px;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; }
.page { max-width: 1280px; margin: 0 auto; padding: 48px 16px; }
h1 { text-align: center; font-size: 3rem; margin: 0 0 16px; color: #e5e7eb; }
.sub { text-align: center; font-size: 1.25rem; margin: 0 0 32px; }
.toolbar { display: flex; justify-content: flex-end; gap: 8px; align-items: center; margin-bottom: 24px; }
.toggle { width: 44px; height: 24px; border-radius: 999px; border: 2px solid var(--muted); position: relative; }
.toggle::after { content: ""; position: absolute; left: 2px; top: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--muted); }
.scroller { overflow-x: auto; }
.planning { display: grid; gap: var(--gap); width: max-content; margin: 0 auto; }
.axis, .column { position: relative; }
.axis { width: 56px; }
.tick { position: absolute; right: 8px; font-size: 12px; color: var(--muted); transform: translateY(-50%); }
.day { position: absolute; left: 0; right: calc(-1 * (var(--col) + var(--gap)) * 6); border-top: 1px solid #ffffff30;
font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: #e5e7eb; padding-top: 2px; }
.hourline { position: absolute; left: 56px; right: 0; border-top: 1px dashed var(--line); }
.break { position: absolute; left: 0; right: 0; display: flex; align-items: center; justify-content: center;
font-size: 11px; color: var(--muted); background: repeating-linear-gradient(45deg, #ffffff06 0 6px, transparent 6px 12px); border-radius: 4px; }
.column { width: var(--col); }
.card { position: absolute; left: 0; right: 0; border-radius: 6px; overflow: hidden; background: var(--surface);
box-shadow: 0 20px 25px -5px #0006; transition: all .15s; cursor: pointer; }
.card:hover { transform: translateY(-8px) scale(1.05); box-shadow: 0 25px 50px -12px #000a; z-index: 5; }
.card img.cover { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.card .time { position: absolute; top: 6px; left: 6px; font-size: 11px; padding: 1px 6px; border-radius: 4px;
background: #000a; color: #fff; opacity: 0; transition: opacity .15s; }
.card:hover .time { opacity: 1; }
.fallback { height: 100%; display: flex; flex-direction: column; justify-content: center; gap: 6px; padding: 10px;
border-left: 4px solid var(--c); background: linear-gradient(160deg, color-mix(in srgb, var(--c) 35%, var(--surface)), var(--surface)); }
.fallback .title { color: #f3f4f6; font-weight: 600; line-height: 1.2; }
.fallback .when { font-size: 12px; color: #d1d5db; }
/* multi-anims: one long card, its games stacked & sticky while you scroll */
.multi { background: linear-gradient(180deg, color-mix(in srgb, var(--c) 30%, var(--surface)), var(--surface)); padding: 8px; overflow: visible; }
.multi:hover { transform: none; }
.multi .head { color: #f3f4f6; font-weight: 700; text-transform: uppercase; font-size: 13px; margin-bottom: 8px; letter-spacing: .05em; }
.multi .sub-card { position: sticky; top: 8px; aspect-ratio: 2/3; border-radius: 4px; overflow: hidden; margin-bottom: 12px;
box-shadow: 0 10px 15px -3px #0008; background: var(--surface); }
.multi .sub-card .fallback { border-left: none; }
</style>
</head>
<body>
<div class="page">
<h1>Get the games</h1>
<p class="sub">Don't own them yet? Grab them on Steam</p>
<div class="toolbar"><span>Show prices</span><span class="toggle"></span></div>
<div class="scroller"><div class="planning" id="planning"></div></div>
</div>
<script src="data.js"></script>
<script>
// Preview only: in the app this comes from Game.name matching + library_image
const STEAM = {
"factorio": 427520, "fall guys": 1097150, "lethal company": 1966720, "rocket league": 252950,
"r.e.p.o.": 3241660, "tricky towers": 437920, "rounds": 1557740, "cosmoteer": 799600,
"golf it!": 571740, "towerfall: ascension": 251470, "puyo puyo — tetris": 546050,
"peak": 3527290, "straftat": 2386720, "trackmania": 2225070, "slay the spire 2": 2868840,
};
const coverOf = (name) => {
const id = STEAM[name.toLowerCase()];
return id && `https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/${id}/library_600x900.jpg`;
};
const HOUR = 3600e3;
const css = getComputedStyle(document.documentElement);
const HOUR_PX = parseFloat(css.getPropertyValue("--hour"));
const BREAK_PX = parseFloat(css.getPropertyValue("--break"));
const TZ = "Europe/Zurich";
const fmtTime = new Intl.DateTimeFormat("en-GB", { timeZone: TZ, hour: "2-digit", minute: "2-digit" });
const fmtDay = new Intl.DateTimeFormat("en-GB", { timeZone: TZ, weekday: "long", day: "numeric", month: "short" });
const slots = PLANNING.list.map((s) => ({ ...s, b: Date.parse(s.begin), e: Date.parse(s.end) }));
const singles = slots.filter((s) => !s.is_multi_anims);
// Hours with something going on; long empty runs (nights) get squashed into a thin break
const start = Math.floor(Math.min(...slots.map((s) => s.b)) / HOUR) * HOUR;
const end = Math.ceil(Math.max(...slots.map((s) => s.e)) / HOUR) * HOUR;
const busy = (t) => singles.some((s) => s.b < t + HOUR && s.e > t);
const segments = []; // {from, to, y, h, kind}
let y = 0;
for (let t = start; t < end;) {
if (busy(t)) { segments.push({ from: t, to: t + HOUR, y, h: HOUR_PX, kind: "hour" }); y += HOUR_PX; t += HOUR; continue; }
let u = t; while (u < end && !busy(u)) u += HOUR;
const short = (u - t) / HOUR < 3;
const h = short ? HOUR_PX * (u - t) / HOUR : BREAK_PX;
segments.push({ from: t, to: u, y, h, kind: short ? "idle" : "break" }); y += h; t = u;
}
const total = y;
const yOf = (t) => {
const s = segments.find((s) => t >= s.from && t <= s.to) || segments.at(-1);
return s.y + s.h * Math.min(1, (t - s.from) / (s.to - s.from));
};
const cols = [...new Set(slots.map((s) => s.colonneS))].sort((a, b) => a - b); // drop unused columns
const root = document.getElementById("planning");
root.style.gridTemplateColumns = `56px repeat(${cols.length}, var(--col))`;
// Time axis + day headers + hour lines
const axis = document.createElement("div");
axis.className = "axis"; axis.style.height = total + "px";
root.appendChild(axis);
let lastDay = null;
for (const s of segments) {
if (s.kind === "break") {
const el = document.createElement("div");
el.className = "break"; el.style.top = s.y + "px"; el.style.height = s.h + "px";
el.style.right = `calc(-1 * (var(--col) + var(--gap)) * ${cols.length})`;
el.textContent = `${fmtTime.format(s.from)} – ${fmtTime.format(s.to)}`;
axis.appendChild(el);
continue;
}
for (let t = s.from; t < s.to; t += HOUR) {
const ty = yOf(t), day = fmtDay.format(t);
if (day !== lastDay) {
const d = document.createElement("div");
d.className = "day"; d.style.top = ty + "px"; d.textContent = day;
d.style.right = `calc(-1 * (var(--col) + var(--gap)) * ${cols.length})`;
axis.appendChild(d); lastDay = day;
} else {
const tick = document.createElement("div");
tick.className = "tick"; tick.style.top = ty + "px"; tick.textContent = fmtTime.format(t);
axis.appendChild(tick);
}
}
}
const fallback = (title, when, color) =>
`<div class="fallback" style="--c:${color}"><div class="title">${title}</div>${when ? `<div class="when">${when}</div>` : ""}</div>`;
const coverOrFallback = (name, when, color) => {
const src = coverOf(name);
if (!src) return fallback(name, when, color);
// Not every game has a library capsule, swap to the colored card if it 404s
return `<img class="cover" src="${src}" alt="${name}" title="${name}"
onerror="this.outerHTML = decodeURIComponent('${encodeURIComponent(fallback(name, when, color))}')">`;
};
for (const c of cols) {
const col = document.createElement("div");
col.className = "column"; col.style.height = total + "px";
for (const s of slots.filter((s) => s.colonneS === c)) {
const top = yOf(s.b) + 3, height = yOf(s.e) - yOf(s.b) - 6;
const when = `${fmtTime.format(s.b)} – ${fmtTime.format(s.e)}`;
const card = document.createElement("div");
card.style.top = top + "px"; card.style.height = height + "px";
if (s.is_multi_anims) {
card.className = "card multi"; card.style.setProperty("--c", s.color_code);
card.innerHTML = `<div class="head">${s.title}</div>` +
s.aandts.map((a) => `<div class="sub-card">${coverOrFallback(a.name, "", s.color_code)}</div>`).join("");
} else {
card.className = "card";
card.innerHTML = coverOrFallback(s.title, when, s.color_code) + `<span class="time">${when}</span>`;
}
col.appendChild(card);
}
root.appendChild(col);
}
</script>
</body>
</html>