Upcoming releases render as a grid of per-release cards
This commit is contained in:
1 parent
34e1b5150a
commit
71d5c68550
3 files changed
+60
-50
No files matched your search
@@ -260,12 +260,14 @@ in three kinds:
|
||||
- `severity` is notice-only: `urgent`/`heads-up`/`info` tints the card edge, the
|
||||
chip, and the icon on Home. Setting it on a live widget is a validation error
|
||||
(live rows have no severity to sort by).
|
||||
- Home renders one merged calendar card (every `calendar` widget, sorted by
|
||||
date), one card per `recent` widget, then notices - urgent first, then heads-up
|
||||
then info, each group in creation order. The next upcoming release that carries
|
||||
artwork gets a full-width hero tile above the timeline (its relayed thumb plus
|
||||
title and date); the timeline row keeps its own small thumb. Each widget also
|
||||
gets its own sidebar button for a single-card view.
|
||||
- Home renders one merged release group (every `calendar` widget, ascending by
|
||||
date) and one group per `recent` widget (newest first) - each group is a header
|
||||
(title, refresh stamp, per-widget errors) followed by one card per release,
|
||||
laid out as a wrapping grid - then notices, urgent first, then heads-up then
|
||||
info, each group in creation order. The next upcoming release that carries
|
||||
artwork also gets a full-width hero tile above the groups (its relayed thumb
|
||||
plus title and date). Each widget gets its own sidebar button for a
|
||||
single-widget view.
|
||||
- Writes are admin-only when OIDC is on, reads any session - same gate as
|
||||
services.
|
||||
|
||||
|
||||
@@ -233,30 +233,21 @@ function noticeCard(widget) {
|
||||
);
|
||||
}
|
||||
|
||||
function itemRow(item) {
|
||||
function releaseCard(item) {
|
||||
const thumb = item.thumb
|
||||
? `<img class="widget-thumb" src="${esc(item.thumb)}" alt="${esc(item.label)}" loading="lazy" />`
|
||||
: "";
|
||||
return (
|
||||
`<div class="widget-item">` +
|
||||
`<div class="release-card">` +
|
||||
thumb +
|
||||
`<span class="widget-label">${esc(item.label)}</span>` +
|
||||
(item.sub ? `<span class="widget-sub">${esc(item.sub)}</span>` : "") +
|
||||
`<span class="widget-when">${esc(dayLabel(item.date))}</span>` +
|
||||
`<span class="widget-src">${esc(item.source || "")}</span>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function groupByDate(items) {
|
||||
const days = [];
|
||||
for (const item of items) {
|
||||
const last = days[days.length - 1];
|
||||
if (last && last.date === item.date) last.items.push(item);
|
||||
else days.push({ date: item.date, items: [item] });
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
function dayLabel(date) {
|
||||
const parts = (date || "").split("-").map(Number);
|
||||
if (parts.length !== 3) return date || "";
|
||||
@@ -278,11 +269,14 @@ function clockOf(iso) {
|
||||
return new Date(iso).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
// One card per live widget group: every calendar widget merges into a single
|
||||
// sorted timeline, each "recently added" widget gets its own card.
|
||||
function liveCard(widgets, title) {
|
||||
// A live group is a header plus one card per release: every calendar widget
|
||||
// merges into one ascending timeline, each "recently added" widget keeps its
|
||||
// own newest-first group (upstream order, no re-sorting).
|
||||
function liveCard(widgets, title, ascending) {
|
||||
const items = widgets.flatMap((widget) => (widget.data || {}).items || []);
|
||||
const days = groupByDate(items);
|
||||
const rows = ascending
|
||||
? items.slice().sort((a, b) => ((a.date || "") < (b.date || "") ? -1 : 1))
|
||||
: items;
|
||||
const notes = widgets
|
||||
.map((widget) => {
|
||||
const data = widget.data || {};
|
||||
@@ -293,24 +287,22 @@ function liveCard(widgets, title) {
|
||||
const stamp = refreshed.length ? `refreshed ${clockOf(refreshed.sort()[0])}` : "";
|
||||
const meta = [...notes, stamp].filter(Boolean).join(" · ");
|
||||
return (
|
||||
`<div class="widget-card live">` +
|
||||
`<div class="live-group">` +
|
||||
`<div class="live-head">` +
|
||||
`<span class="icon">${widgetIcon(widgets[0])}</span>` +
|
||||
`<div class="widget-title">${esc(title)}</div>` +
|
||||
`<div class="widget-meta">${esc(meta)}</div>` +
|
||||
days.map(
|
||||
(day) =>
|
||||
`<div class="widget-day">` +
|
||||
`<div class="widget-day-head">${esc(dayLabel(day.date))}</div>` +
|
||||
day.items.map(itemRow).join("") +
|
||||
`</div>`
|
||||
).join("") +
|
||||
(days.length ? "" : `<div class="widget-empty">${esc(meta || "nothing in the window")}</div>`) +
|
||||
`</div>` +
|
||||
(rows.length ? `<div class="release-cards">${rows.map(releaseCard).join("")}</div>` : "") +
|
||||
(rows.length ? "" : `<div class="widget-empty">${esc(meta || "nothing in the window")}</div>`) +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function widgetCard(widget) {
|
||||
return widget.kind === "notice" ? noticeCard(widget) : liveCard([widget], widget.name);
|
||||
return widget.kind === "notice"
|
||||
? noticeCard(widget)
|
||||
: liveCard([widget], widget.name, widget.kind === "calendar");
|
||||
}
|
||||
|
||||
const SEVERITY_RANK = { urgent: 0, "heads-up": 1, info: 2 };
|
||||
@@ -348,8 +340,8 @@ function renderHome() {
|
||||
const notices = state.widgets.filter((widget) => widget.kind === "notice");
|
||||
notices.sort((a, b) => SEVERITY_RANK[a.severity || "info"] - SEVERITY_RANK[b.severity || "info"]);
|
||||
const cards = [];
|
||||
if (calendars.length) cards.push(liveCard(calendars, "Upcoming"));
|
||||
for (const widget of recents) cards.push(liveCard([widget], widget.name));
|
||||
if (calendars.length) cards.push(liveCard(calendars, "Upcoming", true));
|
||||
for (const widget of recents) cards.push(liveCard([widget], widget.name, false));
|
||||
for (const widget of notices) cards.push(noticeCard(widget));
|
||||
const banner =
|
||||
`<div class="home-banner">` +
|
||||
|
||||
@@ -331,23 +331,39 @@ body {
|
||||
.widget-card.notice.heads-up .icon { color: var(--warning); }
|
||||
.widget-card.notice.urgent .icon { color: var(--error); }
|
||||
|
||||
/* live widgets: one merged timeline for calendars, own card for recents */
|
||||
.widget-card.live { border-left: 3px solid var(--accent); }
|
||||
.widget-card.live .widget-meta { color: var(--muted); font-size: 12px; }
|
||||
.widget-card.live .widget-day { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
|
||||
.widget-card.live .widget-day-head { font-size: 12px; font-weight: 700; color: var(--muted); }
|
||||
.widget-card.live .widget-item { display: flex; align-items: center; gap: 10px; }
|
||||
.widget-card.live .widget-thumb {
|
||||
width: 34px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
/* live widgets: a header plus one card per release */
|
||||
.live-group { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; }
|
||||
.live-head { display: flex; align-items: center; gap: 10px; }
|
||||
.live-head .icon { font-size: 16px; }
|
||||
.live-head .widget-title { font-weight: 700; }
|
||||
.live-head .widget-meta { color: var(--muted); font-size: 12px; }
|
||||
.release-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.widget-card.live .widget-label { font-weight: 600; font-size: 14px; }
|
||||
.widget-card.live .widget-sub { color: var(--muted); font-size: 12px; }
|
||||
.widget-card.live .widget-src { font-size: 11px; color: var(--accent); }
|
||||
.widget-card.live .widget-empty { color: var(--muted); font-size: 13px; margin-top: 8px; }
|
||||
.release-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.release-card .widget-thumb {
|
||||
width: 56px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.release-card .widget-label { font-weight: 600; font-size: 14px; }
|
||||
.release-card .widget-sub { color: var(--muted); font-size: 12px; }
|
||||
.release-card .widget-when { font-size: 12px; font-weight: 700; color: var(--muted); }
|
||||
.release-card .widget-src { font-size: 11px; color: var(--accent); }
|
||||
.widget-empty { color: var(--muted); font-size: 13px; }
|
||||
|
||||
.home-empty { color: var(--muted); font-size: 14px; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user