Skip to content

Commit 56c40de

Browse files
authored
feat: add articles to home page (#4)
1 parent 4e537f1 commit 56c40de

File tree

11 files changed

+270
-40
lines changed

11 files changed

+270
-40
lines changed

src/components/__tests__/articles.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { render, screen, within } from '@testing-library/preact'
3+
4+
import * as subject from '../articles.tsx'
5+
6+
describe('articles component', () => {
7+
it('should have title', () => {
8+
render(<subject.Articles entries={[]} />)
9+
10+
const result = screen.getByRole('heading', { level: 2 })
11+
12+
expect(result).toHaveTextContent(/thoughts/iu)
13+
})
14+
15+
it('should display links to articles', () => {
16+
const entries: subject.ArticleEntry[] = [
17+
{
18+
title: 'How to fizz buzz',
19+
description: 'Fizz buzz for fun and profit',
20+
href: '/articles/fizz-buzz/',
21+
posted: '2021-01-01',
22+
},
23+
{
24+
title: 'How to foo bar',
25+
description: 'Foo bar for fun and profit',
26+
href: '/articles/foo-bar/',
27+
posted: '2022-02-02',
28+
},
29+
]
30+
31+
render(<subject.Articles entries={entries} />)
32+
33+
const nav = screen.getByRole('navigation')
34+
const list = within(nav).getByRole('list')
35+
const items = within(list).getAllByRole('listitem')
36+
const item0 = items[0]!
37+
const item1 = items[1]!
38+
const link0 = within(item0).getByRole('link')
39+
const link1 = within(item1).getByRole('link')
40+
41+
expect(link0).toHaveTextContent('How to fizz buzz')
42+
expect(link0).toHaveAttribute('href', '/articles/fizz-buzz/')
43+
expect(item0).toHaveTextContent('Fizz buzz for fun and profit')
44+
45+
expect(link1).toHaveTextContent('How to foo bar')
46+
expect(link1).toHaveAttribute('href', '/articles/foo-bar/')
47+
expect(item1).toHaveTextContent('Foo bar for fun and profit')
48+
})
49+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { render, screen, within } from '@testing-library/preact'
3+
4+
import * as subject from '../mdx-layout.tsx'
5+
6+
describe('markdown content layout', () => {
7+
it('should define the main content', () => {
8+
render(
9+
<subject.MDXLayout metadata={{ title: '', description: '' }}>
10+
<span data-testid="contents" />
11+
</subject.MDXLayout>
12+
)
13+
14+
const result = screen.getByRole('main')
15+
16+
expect(within(result).getByTestId('contents')).toBeInTheDocument()
17+
})
18+
19+
it('should include the page title', () => {
20+
render(
21+
<subject.MDXLayout metadata={{ title: 'Cool title', description: '' }} />
22+
)
23+
24+
const main = screen.getByRole('main')
25+
const result = within(main).getByRole('heading', { level: 1 })
26+
27+
expect(result).toHaveTextContent('Cool title')
28+
})
29+
30+
it('should include a link back home', () => {
31+
render(<subject.MDXLayout metadata={{ title: '', description: '' }} />)
32+
33+
const result = screen.getByRole('link', { name: /back/iu })
34+
35+
expect(result).toHaveAttribute('href', '/')
36+
})
37+
})

src/components/articles.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Metadata } from '../renderer/page-context.ts'
2+
import { HoverLink, Heading2 } from './atoms.tsx'
3+
4+
export interface ArticlesProps {
5+
entries: ArticleEntry[]
6+
}
7+
8+
export interface ArticleEntry extends Metadata {
9+
href: string
10+
}
11+
12+
export function Articles(props: ArticlesProps): JSX.Element {
13+
return (
14+
<nav>
15+
<Heading2 class="text-center">assorted thoughts</Heading2>
16+
<ol class="w-full">
17+
{props.entries.map(({ title, description, href, posted }) => (
18+
<li key={href} class="mt-4">
19+
<HoverLink href={href}>
20+
<div class="flex items-baseline">
21+
<h3 class="mt-0 mr-auto text-base">{title}</h3>
22+
<small class="font-light text-base ml-4 shrink-0">
23+
{posted ?? 'DRAFT'}
24+
</small>
25+
</div>
26+
<p class="mt-1 font-light">{description}</p>
27+
</HoverLink>
28+
</li>
29+
))}
30+
</ol>
31+
</nav>
32+
)
33+
}

src/components/atoms.tsx

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,106 +2,126 @@ import type { FunctionComponent, RenderableProps } from 'preact'
22

33
type Atom<E extends HTMLElement> = FunctionComponent<JSX.HTMLAttributes<E>>
44

5+
interface ParsedProps<P> {
6+
id: string
7+
className: string
8+
passProps: Omit<P, 'class'>
9+
}
10+
511
export const parseProps = <
612
P extends RenderableProps<JSX.HTMLAttributes<never>>
713
>(
814
props: P
9-
): { className: string; otherProps: Omit<P, 'class'> } => {
10-
const { class: className, ...otherProps } = props
15+
): ParsedProps<P> => {
16+
const { class: classInput, ...passProps } = props
17+
const id = typeof props.id === 'string' ? props.id : props.id?.value ?? ''
18+
const className =
19+
typeof classInput === 'string' ? classInput : classInput?.value ?? ''
1120

12-
if (!className) return { className: '', otherProps }
13-
if (typeof className === 'string') return { className, otherProps }
14-
return { className: className.value ?? '', otherProps }
21+
return { id, className, passProps }
1522
}
1623

1724
export const Link: Atom<HTMLAnchorElement> = (props) => {
18-
const { className, otherProps } = parseProps(props)
25+
const { className, passProps } = parseProps(props)
1926

2027
return (
2128
<a
2229
class={`text-blue-700 visited:text-purple-700 hover:underline ${className}`}
23-
{...otherProps}
30+
{...passProps}
2431
/>
2532
)
2633
}
2734

2835
export const HoverLink: Atom<HTMLAnchorElement> = (props) => {
29-
const { className, otherProps } = parseProps(props)
36+
const { className, passProps } = parseProps(props)
3037

3138
return (
3239
<a
33-
class={`transition-color hover:text-blue-700 hover:visited:text-purple-700 ${className}`}
34-
{...otherProps}
40+
class={`transition-color hover:text-blue-700 ${className}`}
41+
{...passProps}
3542
/>
3643
)
3744
}
3845

3946
export const Copy: Atom<HTMLParagraphElement> = (props) => {
40-
const { className, otherProps } = parseProps(props)
47+
const { className, passProps } = parseProps(props)
4148

4249
return (
43-
<p class={`text-base leading-relaxed mt4 ${className}`} {...otherProps} />
50+
<p class={`text-base leading-relaxed mt4 ${className}`} {...passProps} />
4451
)
4552
}
4653

4754
export const Heading1: Atom<HTMLHeadingElement> = (props) => {
48-
const { className, otherProps } = parseProps(props)
55+
const { className, passProps } = parseProps(props)
4956

50-
return <h1 class={`mt-8 text-2xl text-center ${className}`} {...otherProps} />
57+
return <h1 class={`mt-8 text-2xl text-center ${className}`} {...passProps} />
5158
}
5259

5360
export const Heading2: Atom<HTMLHeadingElement> = (props) => {
54-
const { className, otherProps } = parseProps(props)
61+
const { id, className, passProps } = parseProps(props)
5562

56-
return <h2 class={`mt-4 text-xl ${className}`} {...otherProps} />
63+
return (
64+
<h2 class={`mt-8 text-xl ${className}`} {...passProps}>
65+
{id ? (
66+
<a
67+
href={`#${id}`}
68+
class="hover:before:content-['#'] before:absolute before:ml--4 before:opacity-50"
69+
>
70+
{passProps.children}
71+
</a>
72+
) : (
73+
passProps.children
74+
)}
75+
</h2>
76+
)
5777
}
5878

5979
export const Heading3: Atom<HTMLHeadingElement> = (props) => {
60-
const { className, otherProps } = parseProps(props)
80+
const { className, passProps } = parseProps(props)
6181

62-
return <h2 class={`mt-4 text-lg ${className}`} {...otherProps} />
82+
return <h2 class={`mt-4 text-lg ${className}`} {...passProps} />
6383
}
6484

6585
export const OrderedList: Atom<HTMLOListElement> = (props) => {
66-
const { className, otherProps } = parseProps(props)
86+
const { className, passProps } = parseProps(props)
6787

6888
return (
69-
<ol class={`mt-4 list-decimal mt-4 pl-8 ${className}`} {...otherProps} />
89+
<ol class={`mt-4 list-decimal mt-4 pl-8 ${className}`} {...passProps} />
7090
)
7191
}
7292
export const UnorderedList: Atom<HTMLUListElement> = (props) => {
73-
const { className, otherProps } = parseProps(props)
93+
const { className, passProps } = parseProps(props)
7494

75-
return <ul class={`mt-4 list-disc mt-4 pl-8 ${className}`} {...otherProps} />
95+
return <ul class={`mt-4 list-disc mt-4 pl-8 ${className}`} {...passProps} />
7696
}
7797

7898
export const ListItem: Atom<HTMLLIElement> = (props) => {
79-
const { className, otherProps } = parseProps(props)
99+
const { className, passProps } = parseProps(props)
80100

81-
return <li class={`text-base leading-relaxed ${className}`} {...otherProps} />
101+
return <li class={`text-base leading-relaxed ${className}`} {...passProps} />
82102
}
83103

84104
export const PreformattedText: Atom<HTMLPreElement> = (props) => {
85-
const { className, otherProps } = parseProps(props)
105+
const { className, passProps } = parseProps(props)
86106

87107
return (
88108
<pre
89-
class={`mt-4 text-xs sm:text-sm w-160 max-w-screen self-center ${className}`}
90-
{...otherProps}
109+
class={`mt-4 text-xs sm:text-sm w-148 max-w-screen self-center ${className}`}
110+
{...passProps}
91111
/>
92112
)
93113
}
94114

95115
export const Code: Atom<HTMLElement> = (props) => {
96-
const { className, otherProps } = parseProps(props)
116+
const { className, passProps } = parseProps(props)
97117
const isBlock = className.includes('hljs')
98118

99119
return (
100120
<code
101121
class={`font-mono ${
102-
isBlock ? 'rounded-lg' : 'rounded px-1 py-0.5 bg-slate-200'
122+
isBlock ? 'rounded-lg !pa-4' : 'rounded px-1 py-0.5 bg-slate-200'
103123
} ${className}`}
104-
{...otherProps}
124+
{...passProps}
105125
/>
106126
)
107127
}

src/components/header.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ export function Header(props: HeaderProps): JSX.Element {
1212
const isHero = mode === HEADER_HERO
1313

1414
return (
15-
<header
16-
class={`flex flex-col items-center text-center mx-auto ${
17-
isHero ? 'mt-32' : 'mt-8'
18-
}`}
19-
>
15+
<header class="flex flex-col items-center text-center mx-auto mt-16">
2016
<a href="/" class="w-32 h-32 border border-current border-2 rounded-full">
2117
<h1 class="pt-8.5 text-2xl leading-tight">
2218
michael

src/components/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import type { LayoutProps } from '../renderer/page-context.tsx'
33
export function Layout(props: LayoutProps) {
44
const { children } = props
55

6-
return <main class="max-w-96 mt-8 mx-auto">{children}</main>
6+
return <main class="max-w-112 mt-8 mx-auto px-4">{children}</main>
77
}

src/components/mdx-layout.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { ComponentProps } from 'preact'
2+
import { MDXProvider } from '@mdx-js/preact'
3+
4+
import type { LayoutProps } from '../renderer/page-context.ts'
5+
import * as atoms from './atoms.tsx'
6+
7+
type MDXComponents = ComponentProps<typeof MDXProvider>['components']
8+
9+
const components: MDXComponents = {
10+
h1: atoms.Heading1,
11+
h2: atoms.Heading2,
12+
h3: atoms.Heading3,
13+
p: atoms.Copy,
14+
a: atoms.Link,
15+
code: atoms.Code,
16+
pre: atoms.PreformattedText,
17+
ul: atoms.UnorderedList,
18+
ol: atoms.OrderedList,
19+
li: atoms.ListItem,
20+
}
21+
22+
export function MDXLayout(props: LayoutProps): JSX.Element {
23+
const { metadata, children } = props
24+
const { title, posted = 'Draft', updated } = metadata
25+
26+
return (
27+
<main class="flex flex-col items-stretch max-w-148 mx-auto px-4">
28+
<h1 class="mt-4 text-center text-xl">{title}</h1>
29+
<small class="mt-1 text-center text-sm leading-relaxed font-light">
30+
<p>Posted: {posted}</p>
31+
{updated ? <p>Updated: {updated}</p> : false}
32+
</small>
33+
<MDXProvider components={components}>{children}</MDXProvider>
34+
<atoms.Link href="/" class="self-center my-8">
35+
Back
36+
</atoms.Link>
37+
</main>
38+
)
39+
}

src/components/socials-nav.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { HoverLink } from './atoms.tsx'
2+
13
const SOCIAL_LINKS: IconLinksProps[] = [
24
{
35
title: 'GitHub',
@@ -31,13 +33,13 @@ function IconLink(props: IconLinksProps): JSX.Element {
3133
const { title, href, icon } = props
3234

3335
return (
34-
<a
35-
class="mx-2 p-3 border border-current border-2 inline-flex items-center justify-center transition-color hover:text-blue-700"
36+
<HoverLink
37+
class="mx-2 p-3 border border-current border-2 inline-flex items-center justify-center"
3638
title={title}
3739
href={href}
3840
>
3941
<div aria-hidden role="img" class={icon} />
40-
</a>
42+
</HoverLink>
4143
)
4244
}
4345

src/pages/articles/_default.page.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import '@fontsource-variable/source-code-pro'
2+
import 'highlight.js/styles/github-dark-dimmed.css'
3+
4+
import { HEADER_MINIMAL, type HeaderProps } from '../../components/header.tsx'
5+
6+
export { MDXLayout as Layout } from '../../components/mdx-layout.tsx'
7+
8+
export const headerProps: HeaderProps = {
9+
mode: HEADER_MINIMAL,
10+
}

0 commit comments

Comments
 (0)