Skip to content

Commit 665c4d2

Browse files
feat(structure): Add groundwork for routing and navigation of content (#5)
* feat(structure): Add groundwork for routing and navigation of content * feat(navigation): convert tests back to Jest, add RTL and nav tests
1 parent 3888dc6 commit 665c4d2

31 files changed

+11040
-5481
lines changed

babel.config.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
presets: ['@babel/preset-typescript', '@babel/preset-react'],
3+
plugins: ['@babel/plugin-transform-modules-commonjs']
4+
};

eslint.config.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,11 @@ export default [
121121
'react-compiler/react-compiler': 'warn',
122122
'react-hooks/exhaustive-deps': 'warn',
123123
'react/no-unescaped-entities': ['error', { forbid: ['>', '}'] }],
124-
"react/no-unknown-property": ["error", { ignore: ["class"] }],
124+
"react/no-unknown-property": ["error", { ignore: ["class", "transition:animate"] }],
125125
'spaced-comment': 'error',
126126
'use-isnan': 'error',
127-
'valid-typeof': 'off'
127+
'valid-typeof': 'off',
128+
'spaced-comment': 'off',
128129
}
129-
}
130+
},
130131
];

jest.config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* For a detailed explanation regarding each configuration property, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
import type { Config } from 'jest'
6+
7+
const config: Config = {
8+
preset: 'ts-jest',
9+
testEnvironment: 'jsdom',
10+
roots: ['<rootDir>/src'],
11+
transform: {
12+
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
13+
'^.+\\.m?jsx?$': 'babel-jest',
14+
},
15+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
16+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
17+
moduleNameMapper: {
18+
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
19+
},
20+
setupFilesAfterEnv: ['<rootDir>/test.setup.ts'],
21+
transformIgnorePatterns: [
22+
'/node_modules/(?!(change-case|@?nanostores)/)',
23+
],
24+
}
25+
26+
export default config

package-lock.json

Lines changed: 9978 additions & 5361 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"astro": "astro",
1111
"prettier": "prettier --write ./src",
1212
"lint": "eslint . --cache --cache-strategy content",
13-
"test": "vitest --config ./tsconfig.vitest.json",
14-
"test:watch": "vitest --watch"
13+
"test": "jest",
14+
"test:watch": "jest --watch"
1515
},
1616
"prettier": {
1717
"plugins": [
@@ -38,18 +38,27 @@
3838
"@types/react": "^18.3.12",
3939
"@types/react-dom": "^18.3.1",
4040
"astro": "^5.0.4",
41+
"change-case": "5.4.4",
4142
"nanostores": "^0.11.3",
4243
"react": "^18.3.1",
4344
"react-dom": "^18.3.1",
4445
"sass": "^1.81.0",
4546
"typescript": "^5.6.3"
4647
},
4748
"devDependencies": {
49+
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
50+
"@babel/preset-react": "^7.26.3",
51+
"@babel/preset-typescript": "^7.26.0",
4852
"@eslint/js": "^9.16.0",
4953
"@semantic-release/git": "^10.0.1",
54+
"@testing-library/jest-dom": "^6.6.3",
55+
"@testing-library/react": "^16.1.0",
56+
"@testing-library/user-event": "^14.5.2",
57+
"@types/jest": "^29.5.14",
5058
"@types/node": "^22.9.1",
5159
"@typescript-eslint/eslint-plugin": "^8.17.0",
5260
"@typescript-eslint/parser": "^8.17.0",
61+
"babel-jest": "^29.7.0",
5362
"eslint": "^9.16.0",
5463
"eslint-config-prettier": "^9.1.0",
5564
"eslint-plugin-astro": "^1.3.1",
@@ -59,11 +68,14 @@
5968
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
6069
"eslint-plugin-react-hooks": "^5.1.0",
6170
"globals": "^15.12.0",
71+
"jest": "^29.7.0",
72+
"jest-environment-jsdom": "^29.7.0",
73+
"jsdom": "^25.0.1",
6274
"prettier": "^3.4.2",
6375
"prettier-plugin-astro": "^0.14.1",
6476
"semantic-release": "^24.2.0",
77+
"ts-jest": "^29.2.5",
6578
"ts-node": "^10.9.2",
66-
"typescript-eslint": "^8.15.0",
67-
"vitest": "^2.1.8"
79+
"typescript-eslint": "^8.15.0"
6880
}
6981
}
56.6 KB
Loading

src/components/NavEntry.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NavItem } from '@patternfly/react-core'
2+
3+
export interface TextContentEntry {
4+
id: string
5+
data: {
6+
id: string
7+
section: string
8+
}
9+
collection: string
10+
}
11+
12+
interface NavEntryProps {
13+
entry: TextContentEntry
14+
isActive: boolean
15+
}
16+
17+
export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
18+
const { id } = entry
19+
const { id: entryTitle, section } = entry.data
20+
21+
return (
22+
<NavItem itemId={id} to={`/${section}/${id}`} isActive={isActive} id={`nav-entry-${id}`}>
23+
{entryTitle}
24+
</NavItem>
25+
)
26+
}

src/components/NavSection.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NavExpandable } from '@patternfly/react-core'
2+
import { sentenceCase } from 'change-case'
3+
import { NavEntry, type TextContentEntry } from './NavEntry'
4+
5+
interface NavSectionProps {
6+
entries: TextContentEntry[]
7+
sectionId: string
8+
activeItem: string
9+
}
10+
11+
export const NavSection = ({
12+
entries,
13+
sectionId,
14+
activeItem,
15+
}: NavSectionProps) => {
16+
const isExpanded = window.location.pathname.includes(sectionId)
17+
18+
const sortedNavEntries = entries.sort((a, b) =>
19+
a.data.id.localeCompare(b.data.id),
20+
)
21+
22+
const isActive = sortedNavEntries.some((entry) => entry.id === activeItem)
23+
24+
const items = sortedNavEntries.map((entry) => (
25+
<NavEntry key={entry.id} entry={entry} isActive={activeItem === entry.id} />
26+
))
27+
28+
return (
29+
<NavExpandable
30+
title={sentenceCase(sectionId)}
31+
isActive={isActive}
32+
isExpanded={isExpanded}
33+
id={`nav-section-${sectionId}`}
34+
>
35+
{items}
36+
</NavExpandable>
37+
)
38+
}

src/components/Navigation.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getCollection } from 'astro:content'
33
44
import { Navigation as ReactNav } from './Navigation.tsx'
55
6-
const navEntries = await getCollection('test')
6+
const navEntries = await getCollection('textContent')
77
---
88

9-
<ReactNav client:idle navEntries={navEntries} />
9+
<ReactNav client:only="react" navEntries={navEntries} transition:animate="fade" />

src/components/Navigation.tsx

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,56 @@
1-
import React, { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import {
33
Nav,
44
NavList,
5-
NavItem,
65
PageSidebar,
76
PageSidebarBody,
87
} from '@patternfly/react-core'
98
import { useStore } from '@nanostores/react'
109
import { isNavOpen } from '../stores/navStore'
11-
12-
interface NavOnSelectProps {
13-
groupId: number | string
14-
itemId: number | string
15-
to: string
16-
}
17-
18-
interface NavEntry {
19-
id: string
20-
data: {
21-
title: string
22-
}
23-
collection: string
24-
}
10+
import { NavSection } from './NavSection'
11+
import { type TextContentEntry } from './NavEntry'
2512

2613
interface NavigationProps {
27-
navEntries: NavEntry[]
14+
navEntries: TextContentEntry[]
2815
}
2916

3017
export const Navigation: React.FunctionComponent<NavigationProps> = ({
3118
navEntries,
3219
}: NavigationProps) => {
20+
const $isNavOpen = useStore(isNavOpen)
3321
const [activeItem, setActiveItem] = useState('')
3422

23+
useEffect(() => {
24+
setActiveItem(window.location.pathname.split('/').reverse()[0])
25+
}, [])
26+
3527
const onNavSelect = (
3628
_event: React.FormEvent<HTMLInputElement>,
37-
selectedItem: NavOnSelectProps,
29+
selectedItem: { itemId: string | number },
3830
) => {
39-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
40-
typeof selectedItem.itemId === 'string' &&
41-
setActiveItem(selectedItem.itemId)
31+
setActiveItem(selectedItem.itemId.toString())
4232
}
4333

44-
const $isNavOpen = useStore(isNavOpen)
34+
const sections = new Set(navEntries.map((entry) => entry.data.section))
4535

46-
const sortedNavEntries = navEntries.sort((a, b) =>
47-
a.data.title.localeCompare(b.data.title),
48-
)
36+
const navSections = Array.from(sections).map((section) => {
37+
const entries = navEntries.filter((entry) => entry.data.section === section)
4938

50-
const navItems = sortedNavEntries.map((entry) => (
51-
<NavItem
52-
key={entry.id}
53-
itemId={entry.id}
54-
isActive={activeItem === entry.id}
55-
to={`/${entry.collection}/${entry.id}`}
56-
>
57-
{entry.data.title}
58-
</NavItem>
59-
))
39+
return (
40+
<NavSection
41+
key={section}
42+
entries={entries}
43+
sectionId={section}
44+
activeItem={activeItem}
45+
/>
46+
)
47+
})
6048

6149
return (
6250
<PageSidebar isSidebarOpen={$isNavOpen}>
6351
<PageSidebarBody>
6452
<Nav onSelect={onNavSelect}>
65-
<NavList>{navItems}</NavList>
53+
<NavList>{navSections}</NavList>
6654
</Nav>
6755
</PageSidebarBody>
6856
</PageSidebar>

0 commit comments

Comments
 (0)