Skip to content

Commit 412c42d

Browse files
committed
feat: add created at to table, sort by status
1 parent c260b9d commit 412c42d

8 files changed

Lines changed: 91 additions & 17 deletions

File tree

scripts/fetch-discussions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ async function writeRfd(
685685
const author = comment.author?.login ?? 'unknown';
686686
const authorLink = author === 'unknown'
687687
? '@unknown'
688-
: `<a class="comment-author-link" href="https://github.com/${author}" target="_blank" rel="noopener noreferrer">@${author}</a>`;
688+
: `<a class="comment-author-link" href="https://github.com/${author}" target="_blank" rel="external noopener noreferrer" title="Opens in a new tab" aria-label="@${author} profile (opens in a new tab)">@${author}</a>`;
689689
const createdAtDisplay = formatDisplayDate(comment.createdAt ?? null);
690690
const createdAtFull = formatFullTimestamp(comment.createdAt ?? null);
691691
const createdAt = comment.createdAt
@@ -697,7 +697,9 @@ async function writeRfd(
697697
'',
698698
commentBody,
699699
'',
700-
comment.url ? `<div><sub><a href="${comment.url}" target="_blank" rel="noopener noreferrer">View Comment ↗</a></sub></div>` : null,
700+
comment.url
701+
? `<div><sub><a href="${comment.url}" target="_blank" rel="external noopener noreferrer" title="Opens in a new tab" aria-label="View comment (opens in a new tab)">View Comment ↗</a></sub></div>`
702+
: null,
701703
].filter((line): line is string => line !== null).join('\n'));
702704

703705
return [

src/lib/search-index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// client-side search engine on the index page.
33
export interface SearchIndexItem {
44
number: string;
5+
createdAt: string;
56
updatedAt: string;
67
commentCount: number;
78
state: string;
@@ -15,6 +16,7 @@ export interface SearchIndexItem {
1516
interface SearchableRfd {
1617
data: {
1718
number: string;
19+
createdAt: string;
1820
updatedAt: string;
1921
commentCount: number;
2022
state: string;
@@ -88,6 +90,7 @@ export function buildSearchText(rfd: SearchableRfd): string {
8890
export function toSearchIndexItem(rfd: SearchableRfd): SearchIndexItem {
8991
return {
9092
number: rfd.data.number.toLowerCase(),
93+
createdAt: rfd.data.createdAt,
9194
updatedAt: rfd.data.updatedAt,
9295
commentCount: rfd.data.commentCount ?? 0,
9396
state: rfd.data.state.toLowerCase(),

src/pages/index.astro

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@ const indexHeading = config.indexHeading ?? 'RFD Index';
5050
<div class="sort-control">
5151
<label for="sort-select">Sort by</label>
5252
<select id="sort-select" name="sort">
53+
<option value="created-desc">Created (newest)</option>
54+
<option value="created-asc">Created (oldest)</option>
5355
<option value="updated-desc">Last updated (newest)</option>
5456
<option value="updated-asc">Last updated (oldest)</option>
5557
<option value="comments-desc">Most comments</option>
5658
<option value="comments-asc">Fewest comments</option>
59+
<option value="state-asc">State (A-Z)</option>
60+
<option value="state-desc">State (Z-A)</option>
5761
<option value="number-desc" selected>RFD number (high to low)</option>
5862
<option value="number-asc">RFD number (low to high)</option>
5963
<option value="title-asc">Title (A-Z)</option>
@@ -71,13 +75,27 @@ const indexHeading = config.indexHeading ?? 'RFD Index';
7175
const updatedDate = new Date(rfd.data.updatedAt).toLocaleDateString('en-US', {
7276
year: 'numeric', month: 'short', day: 'numeric'
7377
});
78+
const createdDate = new Date(rfd.data.createdAt).toLocaleDateString('en-US', {
79+
year: 'numeric', month: 'short', day: 'numeric'
80+
});
81+
const createdTimestamp = new Date(rfd.data.createdAt).toLocaleString('en-US', {
82+
year: 'numeric', month: 'long', day: 'numeric',
83+
hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short',
84+
});
85+
const updatedTimestamp = new Date(rfd.data.updatedAt).toLocaleString('en-US', {
86+
year: 'numeric', month: 'long', day: 'numeric',
87+
hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short',
88+
});
89+
const createdHoverText = `Created ${createdTimestamp}`;
90+
const updatedHoverText = `Updated ${updatedTimestamp}`;
7491
return (
7592
<li
7693
class="rfd-row"
7794
data-rfd-row
7895
data-state={rfd.data.state}
7996
data-title={rfd.data.title.toLowerCase()}
8097
data-number={rfd.data.number.toLowerCase()}
98+
data-created={rfd.data.createdAt}
8199
data-updated={rfd.data.updatedAt}
82100
data-comment-count={String(rfd.data.commentCount ?? 0)}
83101
data-labels={rfd.data.labels.join(' ').toLowerCase()}
@@ -108,9 +126,14 @@ const indexHeading = config.indexHeading ?? 'RFD Index';
108126
</Badge>
109127
</div>
110128

129+
<div class="rfd-cell rfd-created">
130+
<span class="rfd-cell-label">Created</span>
131+
<time datetime={rfd.data.createdAt} title={createdHoverText}>{createdDate}</time>
132+
</div>
133+
111134
<div class="rfd-cell rfd-updated">
112135
<span class="rfd-cell-label">Updated</span>
113-
<span>{updatedDate}</span>
136+
<time datetime={rfd.data.updatedAt} title={updatedHoverText}>{updatedDate}</time>
114137
</div>
115138

116139
<div class="rfd-cell rfd-comments">
@@ -125,9 +148,8 @@ const indexHeading = config.indexHeading ?? 'RFD Index';
125148
<div class="label-list">
126149
{rfd.data.labels.map((label, i) => (
127150
<Link
128-
href={`https://github.com/${config.org}/${config.repo}/discussions?label%3A${encodeURIComponent(label)}`}
151+
href={`https://github.com/${config.org}/${config.repo}/discussions?discussions_q=${encodeURIComponent(`is:open label:\"${label}\"`)}`}
129152
isExternal
130-
hideIcon
131153
class="label-chip"
132154
style={rfd.data.labelColors[i] ? `background-color: #${rfd.data.labelColors[i]}; color: ${labelTextColor(rfd.data.labelColors[i])}; border-color: transparent` : ''}
133155
>{label}</Link>

src/pages/rfd/[slug].astro

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,21 @@ const minutesRead = Math.max(1, Math.ceil(wordCount / 200));
5858
{stateConfig?.label ?? rfd.data.state}
5959
</Badge>
6060
<span>
61-
by <Link href={authorProfileUrl} isExternal hideIcon>@{authorLogin}</Link>
61+
by <Link href={authorProfileUrl} isExternal>@{authorLogin}</Link>
6262
</span>
63-
<span>
64-
Created <time datetime={rfd.data.createdAt} title={createdTimestamp}>{createdDate}</time>
63+
<span title={createdTimestamp}>
64+
Created <time datetime={rfd.data.createdAt}>{createdDate}</time>
6565
</span>
66-
<span>
67-
Updated <time datetime={rfd.data.updatedAt} title={updatedTimestamp}>{updatedDate}</time>
66+
<span title={updatedTimestamp}>
67+
Updated <time datetime={rfd.data.updatedAt}>{updatedDate}</time>
6868
</span>
6969
<span>{minutesRead} min read</span>
7070
{rfd.data.labels.length > 0 && (
7171
<div class="label-list">
7272
{rfd.data.labels.map((label, i) => (
7373
<Link
74-
href={`https://github.com/${config.org}/${config.repo}/discussions?label%3A${encodeURIComponent(label)}`}
74+
href={`https://github.com/${config.org}/${config.repo}/discussions?discussions_q=${encodeURIComponent(`is:open label:\"${label}\"`)}`}
7575
isExternal
76-
hideIcon
7776
class="label-chip"
7877
style={rfd.data.labelColors[i] ? `background-color: #${rfd.data.labelColors[i]}; color: ${labelTextColor(rfd.data.labelColors[i])}; border-color: transparent` : ''}
7978
>{label}</Link>
@@ -84,7 +83,7 @@ const minutesRead = Math.max(1, Math.ceil(wordCount / 200));
8483
href={rfd.data.discussionUrl}
8584
isExternal
8685
>
87-
View Discussion
86+
View Discussion
8887
</Link>
8988
</div>
9089
</div>

src/scripts/index-page.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface RowLike {
77

88
export interface IndexSearchItem extends RfdSearchFields {
99
number: string;
10+
createdAt: string;
1011
updatedAt: string;
1112
commentCount: number;
1213
row?: RowLike;
@@ -26,10 +27,14 @@ export type SortKey =
2627
| 'number-asc'
2728
| 'title-asc'
2829
| 'title-desc'
30+
| 'created-desc'
31+
| 'created-asc'
2932
| 'updated-desc'
3033
| 'updated-asc'
3134
| 'comments-desc'
32-
| 'comments-asc';
35+
| 'comments-asc'
36+
| 'state-asc'
37+
| 'state-desc';
3338

3439
export interface CreateIndexControllerParams<RowT extends RowLike> {
3540
rowItems: Array<IndexSearchItem & { row: RowT }>;
@@ -44,7 +49,9 @@ export interface CreateIndexControllerParams<RowT extends RowLike> {
4449
function sortItems<T extends IndexSearchItem>(items: T[], sortKey: SortKey): T[] {
4550
const sorted = [...items];
4651
const cmpNumber = (a: T, b: T) => a.number.localeCompare(b.number, undefined, { numeric: true, sensitivity: 'base' });
52+
const cmpState = (a: T, b: T) => a.state.localeCompare(b.state, undefined, { sensitivity: 'base' });
4753
const cmpTitle = (a: T, b: T) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base' });
54+
const cmpCreated = (a: T, b: T) => (a.createdAt ?? '').localeCompare(b.createdAt ?? '');
4855
const cmpUpdated = (a: T, b: T) => a.updatedAt.localeCompare(b.updatedAt);
4956
const cmpComments = (a: T, b: T) => a.commentCount - b.commentCount;
5057

@@ -64,6 +71,14 @@ function sortItems<T extends IndexSearchItem>(items: T[], sortKey: SortKey): T[]
6471
sorted.sort(cmpUpdated);
6572
return sorted;
6673
}
74+
if (sortKey === 'created-asc') {
75+
sorted.sort(cmpCreated);
76+
return sorted;
77+
}
78+
if (sortKey === 'created-desc') {
79+
sorted.sort((a, b) => cmpCreated(b, a));
80+
return sorted;
81+
}
6782
if (sortKey === 'updated-desc') {
6883
sorted.sort((a, b) => cmpUpdated(b, a));
6984
return sorted;
@@ -76,6 +91,14 @@ function sortItems<T extends IndexSearchItem>(items: T[], sortKey: SortKey): T[]
7691
sorted.sort((a, b) => cmpComments(b, a) || cmpUpdated(b, a));
7792
return sorted;
7893
}
94+
if (sortKey === 'state-asc') {
95+
sorted.sort((a, b) => cmpState(a, b) || cmpUpdated(b, a));
96+
return sorted;
97+
}
98+
if (sortKey === 'state-desc') {
99+
sorted.sort((a, b) => cmpState(b, a) || cmpUpdated(b, a));
100+
return sorted;
101+
}
79102
sorted.sort(cmpTitle);
80103
return sorted;
81104
}
@@ -87,6 +110,7 @@ export function mapRowsToItems<RowT extends RowLike>(rows: RowT[]): Array<IndexS
87110
state: row.getAttribute('data-state') ?? '',
88111
title: row.getAttribute('data-title') ?? '',
89112
number: row.getAttribute('data-number') ?? '',
113+
createdAt: row.getAttribute('data-created') ?? '',
90114
updatedAt: row.getAttribute('data-updated') ?? '',
91115
commentCount: Number.parseInt(row.getAttribute('data-comment-count') ?? '0', 10) || 0,
92116
labels: row.getAttribute('data-labels') ?? '',

src/styles/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878

7979
.index-page .rfd-row {
8080
display: grid;
81-
grid-template-columns: minmax(90px, 0.8fr) minmax(180px, 2fr) minmax(120px, 1fr) minmax(120px, 1fr) minmax(110px, 0.8fr) minmax(180px, 2fr);
81+
grid-template-columns: minmax(90px, 0.8fr) minmax(180px, 2fr) minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 1fr) minmax(110px, 0.8fr) minmax(180px, 2fr);
8282
gap: 0.75rem;
8383
align-items: start;
8484
padding: 0.75rem 0;
@@ -102,6 +102,7 @@
102102
color: var(--color-text-muted);
103103
}
104104

105+
.index-page .rfd-created,
105106
.index-page .rfd-updated {
106107
white-space: nowrap;
107108
color: var(--color-text-muted);

tests/index-page.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ test('mapRowsToItems reads row dataset fields', () => {
3838
'data-state': 'published',
3939
'data-title': 'test title',
4040
'data-number': '0001',
41+
'data-created': '2025-12-01T00:00:00.000Z',
4142
'data-updated': '2026-01-01T00:00:00.000Z',
4243
'data-comment-count': '3',
4344
'data-labels': 'public docs',
@@ -50,6 +51,7 @@ test('mapRowsToItems reads row dataset fields', () => {
5051
assert.equal(items[0].state, 'published');
5152
assert.equal(items[0].title, 'test title');
5253
assert.equal(items[0].number, '0001');
54+
assert.equal(items[0].createdAt, '2025-12-01T00:00:00.000Z');
5355
assert.equal(items[0].updatedAt, '2026-01-01T00:00:00.000Z');
5456
assert.equal(items[0].commentCount, 3);
5557
assert.equal(items[0].labels, 'public docs');
@@ -110,6 +112,7 @@ test('controller loads index for search and maps matches back to rows', async ()
110112
return [
111113
{
112114
number: '0001',
115+
createdAt: '2025-12-01T00:00:00.000Z',
113116
updatedAt: '2026-01-01T00:00:00.000Z',
114117
commentCount: 2,
115118
state: 'discussion',
@@ -196,6 +199,7 @@ test('controller tolerates matched item without resolvable row and noResults nul
196199
loadSearchIndex: async () => [
197200
{
198201
number: '9999',
202+
createdAt: '2025-11-30T00:00:00.000Z',
199203
updatedAt: '2026-01-01T00:00:00.000Z',
200204
commentCount: 1,
201205
state: 'discussion',
@@ -290,8 +294,8 @@ test('controller toggles noResults when zero items match', async () => {
290294

291295
test('controller sorts matched rows by selected sort key', async () => {
292296
const rows = [
293-
makeRow({ 'data-state': 'discussion', 'data-title': 'beta', 'data-number': '0002', 'data-updated': '2026-01-02T00:00:00.000Z', 'data-comment-count': '5', 'data-labels': 'public', 'data-author': 'a' }),
294-
makeRow({ 'data-state': 'discussion', 'data-title': 'alpha', 'data-number': '0001', 'data-updated': '2026-01-03T00:00:00.000Z', 'data-comment-count': '1', 'data-labels': 'public', 'data-author': 'a' }),
297+
makeRow({ 'data-state': 'discussion', 'data-title': 'beta', 'data-number': '0002', 'data-created': '2026-01-03T00:00:00.000Z', 'data-updated': '2026-01-02T00:00:00.000Z', 'data-comment-count': '5', 'data-labels': 'public', 'data-author': 'a' }),
298+
makeRow({ 'data-state': 'committed', 'data-title': 'alpha', 'data-number': '0001', 'data-created': '2026-01-01T00:00:00.000Z', 'data-updated': '2026-01-03T00:00:00.000Z', 'data-comment-count': '1', 'data-labels': 'public', 'data-author': 'a' }),
295299
];
296300
const rowItems = mapRowsToItems(rows);
297301
const appended: string[] = [];
@@ -328,6 +332,22 @@ test('controller sorts matched rows by selected sort key', async () => {
328332
controller.setSortKey('comments-asc');
329333
await controller.applyFilters();
330334
assert.deepEqual(appended.slice(-2), ['0001', '0002']);
335+
336+
controller.setSortKey('created-desc');
337+
await controller.applyFilters();
338+
assert.deepEqual(appended.slice(-2), ['0002', '0001']);
339+
340+
controller.setSortKey('created-asc');
341+
await controller.applyFilters();
342+
assert.deepEqual(appended.slice(-2), ['0001', '0002']);
343+
344+
controller.setSortKey('state-asc');
345+
await controller.applyFilters();
346+
assert.deepEqual(appended.slice(-2), ['0001', '0002']);
347+
348+
controller.setSortKey('state-desc');
349+
await controller.applyFilters();
350+
assert.deepEqual(appended.slice(-2), ['0002', '0001']);
331351
});
332352

333353
test('initIndexPage returns null when root is missing', () => {
@@ -412,6 +432,7 @@ test('initIndexPage wires events and drives controller', async () => {
412432
json: async () => [
413433
{
414434
number: '0001',
435+
createdAt: '2025-12-01T00:00:00.000Z',
415436
updatedAt: '2026-01-01T00:00:00.000Z',
416437
commentCount: 1,
417438
state: 'discussion',

tests/search-index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ test('toSearchIndexItem lowercases fields and includes compact searchText', () =
4747
data: {
4848
title: 'My Title',
4949
number: '0012',
50+
createdAt: '2025-12-31T00:00:00.000Z',
5051
updatedAt: '2026-01-01T00:00:00.000Z',
5152
author: 'SomeUser',
5253
state: 'Published',
@@ -58,6 +59,7 @@ test('toSearchIndexItem lowercases fields and includes compact searchText', () =
5859
const item = toSearchIndexItem(entry);
5960
assert.equal(item.title, 'my title');
6061
assert.equal(item.number, '0012');
62+
assert.equal(item.createdAt, '2025-12-31T00:00:00.000Z');
6163
assert.equal(item.updatedAt, '2026-01-01T00:00:00.000Z');
6264
assert.equal(item.author, 'someuser');
6365
assert.equal(item.state, 'published');

0 commit comments

Comments
 (0)