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
31 changes: 31 additions & 0 deletions src/util/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,37 @@ export function groupTimelineByYearMonth(
return groupByYearMonthGeneric(entries);
}

/**
* 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.
*/
export function groupTimelineByYearDay(
entries: TimelineEntry[]
): Record<string, Record<string, TimelineEntry[]>> {
return entries.reduce(
(acc, entry) => {
const date = new Date(entry.date);
const year = date.getFullYear().toString();
const day = formatDate(entry.date);
Comment on lines +128 to +130

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").
*/
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' });
Comment on lines +143 to +146
}

/**
* Format month and year (e.g., "January 2025")
*/
Expand Down
Loading