Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions src/components/changelog/ChangelogCard.astro
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
---
import type { CollectionEntry } from 'astro:content';
import { formatDate, getProductAccent, renderInlineMarkdown } from '~/util/changelog';
import { getProductAccent, renderInlineMarkdown } from '~/util/changelog';

interface Props {
entry: CollectionEntry<'changelog'>;
}

const { entry } = Astro.props;
const { title, description, date, tags } = entry.data;
const { title, description, tags } = entry.data;
const primaryTag = tags[0];
const accent = getProductAccent(primaryTag);

const monthDay = new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
---

<article
Expand All @@ -29,14 +24,13 @@ const monthDay = new Date(date).toLocaleDateString('en-US', {
</h3>
{description && <p class="card-desc">{description}</p>}
</div>
<div class="card-aside">
<time datetime={formatDate(date)} class="card-date">{monthDay}</time>
{tags.length > 0 && (
{tags.length > 0 && (
<div class="card-aside">
<div class="pills-stack">
{tags.map((t) => <span class="card-pill">{t}</span>)}
</div>
)}
</div>
</div>
)}
</article>

<style>
Expand Down Expand Up @@ -114,13 +108,6 @@ const monthDay = new Date(date).toLocaleDateString('en-US', {
align-items: flex-end;
padding-top: 3px;
}
.card-date {
font-size: 0.7rem;
font-weight: 600;
color: var(--theme-text-muted);
white-space: nowrap;
letter-spacing: 0.02em;
}
.pills-stack {
display: flex;
flex-direction: column;
Expand Down
26 changes: 2 additions & 24 deletions src/components/changelog/ChangelogMarquee.astro
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
---
import type { CollectionEntry } from 'astro:content';
import { formatDate, getProductAccent, renderInlineMarkdown } from '~/util/changelog';
import { getProductAccent, renderInlineMarkdown } from '~/util/changelog';

interface Props {
entry: CollectionEntry<'changelog'>;
}

const { entry } = Astro.props;
const { title, description, date, tags, image } = entry.data;
const { title, description, tags, image } = entry.data;
const primaryTag = tags[0];
const accent = getProductAccent(primaryTag);

const monthDay = new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
---

<article
Expand All @@ -34,7 +29,6 @@ const monthDay = new Date(date).toLocaleDateString('en-US', {
</h3>
{description && <p class="marquee-desc">{description}</p>}
</div>
<time datetime={formatDate(date)} class="marquee-date">{monthDay}</time>
</div>
</article>

Expand Down Expand Up @@ -74,10 +68,6 @@ const monthDay = new Date(date).toLocaleDateString('en-US', {
}
.marquee-body {
padding: 24px 26px 24px 28px;
display: grid;
grid-template-columns: 1fr auto;
gap: 22px;
align-items: start;
}
.marquee-content {
min-width: 0;
Expand Down Expand Up @@ -136,23 +126,11 @@ const monthDay = new Date(date).toLocaleDateString('en-US', {
font-size: 0.92em;
font-family: var(--font-mono);
}
.marquee-date {
font-size: 0.75rem;
font-weight: 600;
color: var(--theme-text-muted);
white-space: nowrap;
letter-spacing: 0.02em;
padding-top: 3px;
}

@media (max-width: 36rem) {
.marquee-hero {
height: 160px;
}
.marquee-body {
grid-template-columns: 1fr;
gap: 8px;
}
.marquee-title {
font-size: 1.25rem;
}
Expand Down
60 changes: 28 additions & 32 deletions src/pages/changelog/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ChangelogSubscribePopover from '../../components/changelog/ChangelogSubsc
import enterpriseReleases from '../../data/enterprise-releases.json';
import MainLayout from '../../layouts/MainLayout.astro';
import type { TimelineEntry } from '../../util/changelog';
import { formatDate, groupTimelineByYearMonth, sortChangelog } from '../../util/changelog';
import { formatDayLabel, groupTimelineByYearDay, sortChangelog } from '../../util/changelog';

const ENTERPRISE_TAG = 'Enterprise';
const entries = sortChangelog(await getCollection('changelog'));
Expand All @@ -26,7 +26,7 @@ const changelogTimeline: TimelineEntry[] = entries.map((entry) => ({
const timelineEntries: TimelineEntry[] = [...changelogTimeline, ...releaseMarkers].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
const groupedTimeline = groupTimelineByYearMonth(timelineEntries);
const groupedTimeline = groupTimelineByYearDay(timelineEntries);
const years = Object.keys(groupedTimeline).sort((a, b) => b.localeCompare(a));

const title = 'Changelog';
Expand Down Expand Up @@ -60,19 +60,17 @@ const description = 'Latest product updates from Mergify.';
</nav>

{years.map((year) => {
const months = Object.keys(groupedTimeline[year]).sort((a, b) => {
const da = new Date(`${a} 1, ${year}`);
const db = new Date(`${b} 1, ${year}`);
return db.getTime() - da.getTime();
});
const days = Object.keys(groupedTimeline[year]).sort((a, b) => b.localeCompare(a));
return (
<section class="cl-year" data-year={year}>
<h2 class="cl-year-heading" id={`year-${year}`}>{year}</h2>
{months.map((month) => (
<div class="cl-month-group">
<h3 class="cl-month-heading">{month}</h3>
{days.map((day) => (
<div class="cl-day-group">
<h3 class="cl-day-heading">
<time datetime={day}>{formatDayLabel(day)}</time>
</h3>
<div class="cl-feed">
{groupedTimeline[year][month].map((item) => (
{groupedTimeline[year][day].map((item) => (
item.kind === 'release' ? (
<article
class="cl-release"
Expand All @@ -84,9 +82,6 @@ const description = 'Latest product updates from Mergify.';
<strong>Enterprise {item.version}</strong>
<span class="release-sub">released for self-hosted</span>
</span>
<time datetime={formatDate(item.date)} class="release-date">
{new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</time>
</article>
) : (
item.entry.data.image
Expand Down Expand Up @@ -127,15 +122,15 @@ const description = 'Latest product updates from Mergify.';
});
noResults.hidden = visible !== 0;

Array.from(document.querySelectorAll('.cl-month-group')).forEach(function (g) {
var any = Array.from(g.querySelectorAll('[data-tags]')).some(function (e) {
Array.from(document.querySelectorAll('.cl-day-group')).forEach(function (d) {
var any = Array.from(d.querySelectorAll('[data-tags]')).some(function (e) {
return e.style.display !== 'none';
});
g.style.display = activeTag || q ? (any ? '' : 'none') : '';
d.style.display = activeTag || q ? (any ? '' : 'none') : '';
});
Array.from(document.querySelectorAll('.cl-year')).forEach(function (y) {
var any = Array.from(y.querySelectorAll('.cl-month-group')).some(function (g) {
return g.style.display !== 'none';
var any = Array.from(y.querySelectorAll('.cl-day-group')).some(function (d) {
return d.style.display !== 'none';
});
y.style.display = activeTag || q ? (any ? '' : 'none') : '';
});
Expand Down Expand Up @@ -295,13 +290,20 @@ const description = 'Latest product updates from Mergify.';
padding-bottom: 10px;
border-bottom: 1px solid var(--theme-border);
}
.cl-month-heading {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--theme-text-muted);
margin: 28px 0 14px;
.cl-day-group {
margin-top: 28px;
}
.cl-day-group:first-child {
margin-top: 18px;
}
.cl-day-heading {
font-size: 0.8125rem;
font-weight: 600;
color: var(--theme-text-secondary);
margin: 0 0 12px;
}
.cl-day-heading time {
color: inherit;
}

/* Feed */
Expand Down Expand Up @@ -336,12 +338,6 @@ const description = 'Latest product updates from Mergify.';
.release-sub {
color: var(--theme-text-muted);
}
.release-date {
margin-left: auto;
font-weight: 600;
color: var(--theme-text-muted);
font-size: 0.7rem;
}

.cl-no-results {
text-align: center;
Expand Down
65 changes: 37 additions & 28 deletions src/util/changelog.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
import type { CollectionEntry } from 'astro:content';
import { escape } from './html-entities';

type DateCarrier = {
date: Date | string;
};

function groupByYearMonthGeneric<T extends DateCarrier>(
entries: T[]
): Record<string, Record<string, T[]>> {
return entries.reduce(
(acc, entry) => {
const date = new Date(entry.date);
const year = date.getFullYear().toString();
const month = date.toLocaleDateString('en-US', { month: 'long' });

if (!acc[year]) {
acc[year] = {};
}
if (!acc[year][month]) {
acc[year][month] = [];
}
acc[year][month].push(entry);
return acc;
},
{} as Record<string, Record<string, T[]>>
);
}

export type TimelineEntry =
| { kind: 'changelog'; date: Date | string; entry: CollectionEntry<'changelog'> }
| { kind: 'release'; date: Date | string; version: string };
Expand Down Expand Up @@ -109,10 +83,45 @@ export function groupByYearMonth(
);
}

export function groupTimelineByYearMonth(
/**
* Group timeline entries by year and day. The day key is the ISO
* `YYYY-MM-DD` string, which sorts lexicographically the same as
* chronologically and is trivial to re-format for display. Year and day
* are both derived in UTC so an entry never lands under a year that
* disagrees with its day key (e.g., a December 31 release published at
* UTC midnight in a westerly zone).
*/
export function groupTimelineByYearDay(
entries: TimelineEntry[]
): Record<string, Record<string, TimelineEntry[]>> {
return groupByYearMonthGeneric(entries);
return entries.reduce(
(acc, entry) => {
const date = new Date(entry.date);
const year = date.getUTCFullYear().toString();
const day = formatDate(entry.date);
Comment thread
jd marked this conversation as resolved.

if (!acc[year]) acc[year] = {};
if (!acc[year][day]) acc[year][day] = [];
acc[year][day].push(entry);
return acc;
},
{} as Record<string, Record<string, TimelineEntry[]>>
);
}

/**
* Format a date as a short, human-readable day label (e.g., "May 6").
* Formatted in UTC to match the UTC-based day key used for grouping —
* otherwise zones west of UTC would render an ISO `YYYY-MM-DD` (UTC
* midnight) as the previous day.
*/
export function formatDayLabel(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
timeZone: 'UTC',
});
}

/**
Expand Down
Loading