feat: build index page with sorted post listing and ArticleCard components
This replaces the smoke test index with a real page that: - Imports getCollection, BaseLayout, NavBar, ArticleCard, getReadingTime - Fetches all posts and sorts descending by date - Maps posts to ArticleCard components with title, date, type, summary, slug, readingTime - Styles .post-list with prose max-width, space-6/space-12 margins, flex column, space-2 gap Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
+32
-21
@@ -1,28 +1,39 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import NavBar from '../components/NavBar.astro';
|
||||
import PostHeader from '../components/PostHeader.astro';
|
||||
import ArticleCard from '../components/ArticleCard.astro';
|
||||
import { getReadingTime } from '../utils/reading-time';
|
||||
|
||||
const posts = await getCollection('posts');
|
||||
const sortedPosts = posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
---
|
||||
|
||||
<BaseLayout title="Crash Test Dev" description="Foundation smoke test">
|
||||
<BaseLayout title='Crash Test Dev'>
|
||||
<NavBar slot="nav" />
|
||||
<PostHeader
|
||||
title="Speeding Up Webpack Typescript Incremental Builds by 7x"
|
||||
date={new Date('2019-08-16')}
|
||||
type="post"
|
||||
readingTime={12}
|
||||
/>
|
||||
<div style="max-width: var(--width-prose); margin-left: auto; margin-right: auto; display: flex; flex-direction: column; gap: var(--space-4);">
|
||||
<p style="font-family: var(--font-prose); font-size: 18px; color: var(--text-primary); line-height: var(--leading-prose);">
|
||||
This is the prose voice — Source Serif 4 at 18px. It carries the reader through long-form
|
||||
content with a warm, readable serif that feels like ink on parchment.
|
||||
</p>
|
||||
<p style="font-family: var(--font-structure); font-size: 14px; color: var(--text-secondary); line-height: var(--leading-normal);">
|
||||
This is the structure voice — Inter at 14px in secondary color. It handles navigation,
|
||||
labels, metadata, and UI chrome with a clean sans-serif that stays out of the way.
|
||||
</p>
|
||||
<code style="display: block; font-family: var(--font-evidence); font-size: var(--type-sm); color: var(--code-text); background: var(--surface-code); padding: var(--space-2); border-radius: var(--rounded-md); border-left: 3px solid var(--accent-primary);">
|
||||
const buildTime = 7 * previousBuildTime; // evidence voice — JetBrains Mono
|
||||
</code>
|
||||
</div>
|
||||
<section class="post-list" aria-label="All posts">
|
||||
{sortedPosts.map((post) => (
|
||||
<ArticleCard
|
||||
title={post.data.title}
|
||||
date={post.data.date}
|
||||
type={post.data.type}
|
||||
summary={post.data.summary}
|
||||
slug={post.id}
|
||||
readingTime={post.body ? getReadingTime(post.body) : undefined}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.post-list {
|
||||
max-width: var(--width-prose);
|
||||
margin-top: var(--space-6);
|
||||
margin-bottom: var(--space-12);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const pagePath = resolve('src/pages/index.astro');
|
||||
|
||||
function readPage() {
|
||||
return readFileSync(pagePath, 'utf-8');
|
||||
}
|
||||
|
||||
describe('Index page', () => {
|
||||
it('should exist at src/pages/index.astro', () => {
|
||||
assert.ok(existsSync(pagePath), 'Index page file should exist');
|
||||
});
|
||||
|
||||
describe('Imports', () => {
|
||||
it('should import getCollection from astro:content', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /import\s+.*getCollection.*from\s+['"]astro:content['"]/, 'should import getCollection');
|
||||
});
|
||||
|
||||
it('should import BaseLayout', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /import\s+BaseLayout\s+from\s+['"].*BaseLayout\.astro['"]/, 'should import BaseLayout');
|
||||
});
|
||||
|
||||
it('should import NavBar', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /import\s+NavBar\s+from\s+['"].*NavBar\.astro['"]/, 'should import NavBar');
|
||||
});
|
||||
|
||||
it('should import ArticleCard', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /import\s+ArticleCard\s+from\s+['"].*ArticleCard\.astro['"]/, 'should import ArticleCard');
|
||||
});
|
||||
|
||||
it('should import getReadingTime', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /import\s+.*getReadingTime.*from\s+['"].*reading-time['"]/, 'should import getReadingTime');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data fetching', () => {
|
||||
it('should fetch all posts with getCollection', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /getCollection\s*\(\s*['"]posts['"]\s*\)/, 'should call getCollection("posts")');
|
||||
});
|
||||
|
||||
it('should sort posts descending by date', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /sort\s*\(/, 'should call sort');
|
||||
assert.match(content, /b\.data\.date\.getTime\(\)\s*-\s*a\.data\.date\.getTime\(\)/, 'should sort descending by date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template', () => {
|
||||
it('should use BaseLayout with title Crash Test Dev', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /<BaseLayout\s+title\s*=\s*['"]Crash Test Dev['"]/, 'should have BaseLayout with title');
|
||||
});
|
||||
|
||||
it('should include NavBar in nav slot', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /<NavBar\s+slot\s*=\s*["']nav["']/, 'should have NavBar with slot="nav"');
|
||||
});
|
||||
|
||||
it('should have a section with class post-list and aria-label', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /<section\s+class\s*=\s*["']post-list["']/, 'should have section.post-list');
|
||||
assert.match(content, /aria-label\s*=\s*["']All posts["']/, 'should have aria-label="All posts"');
|
||||
});
|
||||
|
||||
it('should map sortedPosts to ArticleCard components', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /sortedPosts\.map/, 'should map sortedPosts');
|
||||
assert.match(content, /<ArticleCard/, 'should render ArticleCard');
|
||||
});
|
||||
|
||||
it('should pass title from post.data.title', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /title\s*=\s*\{post\.data\.title\}/, 'should pass title');
|
||||
});
|
||||
|
||||
it('should pass date from post.data.date', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /date\s*=\s*\{post\.data\.date\}/, 'should pass date');
|
||||
});
|
||||
|
||||
it('should pass type from post.data.type', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /type\s*=\s*\{post\.data\.type\}/, 'should pass type');
|
||||
});
|
||||
|
||||
it('should pass summary from post.data.summary', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /summary\s*=\s*\{post\.data\.summary\}/, 'should pass summary');
|
||||
});
|
||||
|
||||
it('should pass slug as post.id', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /slug\s*=\s*\{post\.id\}/, 'should pass slug as post.id');
|
||||
});
|
||||
|
||||
it('should compute and pass readingTime from post.body', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /getReadingTime\s*\(\s*post\.body/, 'should call getReadingTime(post.body...)');
|
||||
assert.match(content, /readingTime\s*=\s*\{/, 'should pass readingTime prop');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Style', () => {
|
||||
it('should have .post-list with prose max-width', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /\.post-list\s*\{/, 'should have .post-list selector');
|
||||
assert.match(content, /max-width\s*:\s*var\(--width-prose\)/, 'should have prose max-width');
|
||||
});
|
||||
|
||||
it('should have space-6 top and space-12 bottom margins', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /margin-top\s*:\s*var\(--space-6\)/, 'should have space-6 top margin');
|
||||
assert.match(content, /margin-bottom\s*:\s*var\(--space-12\)/, 'should have space-12 bottom margin');
|
||||
});
|
||||
|
||||
it('should have auto left and right margins', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /margin-left\s*:\s*auto/, 'should have auto left margin');
|
||||
assert.match(content, /margin-right\s*:\s*auto/, 'should have auto right margin');
|
||||
});
|
||||
|
||||
it('should have flex column display', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /display\s*:\s*flex/, 'should have flex display');
|
||||
assert.match(content, /flex-direction\s*:\s*column/, 'should have column direction');
|
||||
});
|
||||
|
||||
it('should have space-2 gap', () => {
|
||||
const content = readPage();
|
||||
assert.match(content, /gap\s*:\s*var\(--space-2\)/, 'should have space-2 gap');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user