diff --git a/src/components/pages/ChangelogPage.astro b/src/components/pages/ChangelogPage.astro index c82cf6d..03f68a8 100644 --- a/src/components/pages/ChangelogPage.astro +++ b/src/components/pages/ChangelogPage.astro @@ -1,16 +1,18 @@ --- // The /changelog page: the releases of ThinkWatch Enterprise, ThinkWatch Lite -// and ThinkWatch Core, newest first, filterable by product. Each release shows -// its version and date and links to its page on GitHub; the Enterprise releases -// written up on this site also show their notes. Copy lives in +// and ThinkWatch Core, newest first and grouped by month, filterable by +// product, under the latest release of each. Each release shows its version +// and date and links to its page on GitHub; the Enterprise releases written up +// on this site also show their notes. Copy lives in // src/i18n/pages/changelog.ts, the list in src/lib/changelog.ts. import { render } from "astro:content"; import Base from "~/layouts/Base.astro"; import SiteHeader from "~/components/SiteHeader.astro"; import SiteFooter from "~/components/SiteFooter.astro"; +import NeonMotion from "~/components/NeonMotion.astro"; import { getLang } from "~/i18n"; import { changelogCopy } from "~/i18n/pages/changelog"; -import { getChangelog, releaseProductName } from "~/lib/changelog"; +import { getChangelog, releaseProductName, type ChangelogEntry } from "~/lib/changelog"; import { breadcrumbs, webPage } from "~/lib/structured-data"; const lang = getLang(Astro); @@ -22,171 +24,476 @@ const jsonLd = [ breadcrumbs(lang, "/changelog", c.meta.breadcrumbHome, c.meta.breadcrumbPage), ]; -const typeStyles: Record = { - release: "border-brand-1/40 bg-brand-1/10 text-brand-1", - feature: "border-white/15 bg-white/5 text-[var(--color-text)]", - fix: "border-emerald-500/30 bg-emerald-500/10 text-emerald-300", - security: "border-accent/40 bg-accent/10 text-accent", -}; - // Release times are UTC; format them in UTC so the date does not depend on where the site is built. const fmt = new Intl.DateTimeFormat(c.dateLocale, { ...c.dateFormat, timeZone: "UTC" }); +const fmtFull = new Intl.DateTimeFormat(c.dateLocale, { ...c.fullDateFormat, timeZone: "UTC" }); +const fmtMonth = new Intl.DateTimeFormat(c.dateLocale, { ...c.monthFormat, timeZone: "UTC" }); -const filters = [ - { id: "all", label: c.all }, - ...(["enterprise", "lite", "core"] as const).map((id) => ({ id, label: c.filters[id] })), -]; +const products = ["lite", "enterprise", "core"] as const; +const count = (id: string) => (id === "all" ? entries.length : entries.filter((e) => e.product === id).length); +const filters = [{ id: "all", label: c.all }, ...products.map((id) => ({ id, label: c.filters[id] }))]; + +const latest = products + .map((id) => entries.find((e) => e.product === id)) + .filter((e): e is ChangelogEntry => Boolean(e)); + +// Newest month first; releases keep their newest-first order inside it. +const months: { key: string; label: string; items: ChangelogEntry[] }[] = []; +for (const e of entries) { + const key = `${e.date.getUTCFullYear()}-${e.date.getUTCMonth()}`; + if (months.at(-1)?.key !== key) months.push({ key, label: fmtMonth.format(e.date), items: [] }); + months.at(-1)!.items.push(e); +} --- -
-
-
- {c.eyebrow} -
-

- {c.titleA}{c.titleHighlight} -

-

- {c.intro}{c.rss}{c.introEnd} -

-
- -
- {filters.map((f, i) => ( - - ))} -
- -
    - {entries.map(async (entry) => { - const name = releaseProductName(entry.product, lang); - const notes = entry.notes; - const Content = notes ? (await render(notes)).Content : null; - return ( -
  1. - {/* The anchor the release had while the page listed Enterprise alone (#v0.4.0). */} - {entry.legacyId && } - - -
    - - {name} +
    +
    + + +
    +

    {c.eyebrow}

    +

    {c.titleA}{c.titleHighlight}

    +

    + {c.intro}{c.rss}{c.introEnd} +

    + +

    {c.latest}

    + - - {notes && ( - -

    {notes.data.title}

    - - {notes.data.highlights && ( -
      - {notes.data.highlights.map((h) => ( -
    • - - {h} -
    • - ))} -
    - )} - -
    - {Content && } -
    -
    - )} -
  2. - ); - })} -
+ + ))} + + + + +
+
+ {filters.map((f, i) => ( + + ))} +
+ +
+ {months.map((m) => ( +
+

{m.label}

+
    + {m.items.map(async (entry) => { + const name = releaseProductName(entry.product, lang); + const notes = entry.notes; + const Content = notes ? (await render(notes)).Content : null; + return ( +
  1. + {/* The anchor the release had while the page listed Enterprise alone (#v0.4.0). */} + {entry.legacyId && } +
    + + {name} + {entry.tag} + {entry.url ? ( + + {c.github} ↗ + + ) : } +
    + + {notes && ( +
    +

    {notes.data.title}

    + {notes.data.highlights && ( +
      + {notes.data.highlights.map((h) =>
    • {h}
    • )} +
    + )} + {Content && ( +
    + {c.readNotes} +
    +
    + )} +
    + )} +
  2. + ); + })} +
+
+ ))} +
+
+