Skip to content

Commit

Permalink
feat(structure): Add groundwork for routing and navigation of content (
Browse files Browse the repository at this point in the history
…#5)

* feat(structure): Add groundwork for routing and navigation of content

* feat(navigation): convert tests back to Jest, add RTL and nav tests
  • Loading branch information
wise-king-sullyman authored Dec 16, 2024
1 parent 3888dc6 commit 665c4d2
Show file tree
Hide file tree
Showing 31 changed files with 11,040 additions and 5,481 deletions.
4 changes: 4 additions & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
presets: ['@babel/preset-typescript', '@babel/preset-react'],
plugins: ['@babel/plugin-transform-modules-commonjs']
};
7 changes: 4 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ export default [
'react-compiler/react-compiler': 'warn',
'react-hooks/exhaustive-deps': 'warn',
'react/no-unescaped-entities': ['error', { forbid: ['>', '}'] }],
"react/no-unknown-property": ["error", { ignore: ["class"] }],
"react/no-unknown-property": ["error", { ignore: ["class", "transition:animate"] }],
'spaced-comment': 'error',
'use-isnan': 'error',
'valid-typeof': 'off'
'valid-typeof': 'off',
'spaced-comment': 'off',
}
}
},
];
26 changes: 26 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from 'jest'

const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
'^.+\\.m?jsx?$': 'babel-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
},
setupFilesAfterEnv: ['<rootDir>/test.setup.ts'],
transformIgnorePatterns: [
'/node_modules/(?!(change-case|@?nanostores)/)',
],
}

export default config
15,339 changes: 9,978 additions & 5,361 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"astro": "astro",
"prettier": "prettier --write ./src",
"lint": "eslint . --cache --cache-strategy content",
"test": "vitest --config ./tsconfig.vitest.json",
"test:watch": "vitest --watch"
"test": "jest",
"test:watch": "jest --watch"
},
"prettier": {
"plugins": [
Expand All @@ -38,18 +38,27 @@
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"astro": "^5.0.4",
"change-case": "5.4.4",
"nanostores": "^0.11.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.81.0",
"typescript": "^5.6.3"
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@eslint/js": "^9.16.0",
"@semantic-release/git": "^10.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.9.1",
"@typescript-eslint/eslint-plugin": "^8.17.0",
"@typescript-eslint/parser": "^8.17.0",
"babel-jest": "^29.7.0",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-astro": "^1.3.1",
Expand All @@ -59,11 +68,14 @@
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^5.1.0",
"globals": "^15.12.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^25.0.1",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
"semantic-release": "^24.2.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript-eslint": "^8.15.0",
"vitest": "^2.1.8"
"typescript-eslint": "^8.15.0"
}
}
Binary file added public/content/typography/line-height.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions src/components/NavEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NavItem } from '@patternfly/react-core'

export interface TextContentEntry {
id: string
data: {
id: string
section: string
}
collection: string
}

interface NavEntryProps {
entry: TextContentEntry
isActive: boolean
}

export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
const { id } = entry
const { id: entryTitle, section } = entry.data

return (
<NavItem itemId={id} to={`/${section}/${id}`} isActive={isActive} id={`nav-entry-${id}`}>
{entryTitle}
</NavItem>
)
}
38 changes: 38 additions & 0 deletions src/components/NavSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NavExpandable } from '@patternfly/react-core'
import { sentenceCase } from 'change-case'
import { NavEntry, type TextContentEntry } from './NavEntry'

interface NavSectionProps {
entries: TextContentEntry[]
sectionId: string
activeItem: string
}

export const NavSection = ({
entries,
sectionId,
activeItem,
}: NavSectionProps) => {
const isExpanded = window.location.pathname.includes(sectionId)

const sortedNavEntries = entries.sort((a, b) =>
a.data.id.localeCompare(b.data.id),
)

const isActive = sortedNavEntries.some((entry) => entry.id === activeItem)

const items = sortedNavEntries.map((entry) => (
<NavEntry key={entry.id} entry={entry} isActive={activeItem === entry.id} />
))

return (
<NavExpandable
title={sentenceCase(sectionId)}
isActive={isActive}
isExpanded={isExpanded}
id={`nav-section-${sectionId}`}
>
{items}
</NavExpandable>
)
}
4 changes: 2 additions & 2 deletions src/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getCollection } from 'astro:content'
import { Navigation as ReactNav } from './Navigation.tsx'
const navEntries = await getCollection('test')
const navEntries = await getCollection('textContent')
---

<ReactNav client:idle navEntries={navEntries} />
<ReactNav client:only="react" navEntries={navEntries} transition:animate="fade" />
60 changes: 24 additions & 36 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,56 @@
import React, { useState } from 'react'
import { useEffect, useState } from 'react'
import {
Nav,
NavList,
NavItem,
PageSidebar,
PageSidebarBody,
} from '@patternfly/react-core'
import { useStore } from '@nanostores/react'
import { isNavOpen } from '../stores/navStore'

interface NavOnSelectProps {
groupId: number | string
itemId: number | string
to: string
}

interface NavEntry {
id: string
data: {
title: string
}
collection: string
}
import { NavSection } from './NavSection'
import { type TextContentEntry } from './NavEntry'

interface NavigationProps {
navEntries: NavEntry[]
navEntries: TextContentEntry[]
}

export const Navigation: React.FunctionComponent<NavigationProps> = ({
navEntries,
}: NavigationProps) => {
const $isNavOpen = useStore(isNavOpen)
const [activeItem, setActiveItem] = useState('')

useEffect(() => {
setActiveItem(window.location.pathname.split('/').reverse()[0])
}, [])

const onNavSelect = (
_event: React.FormEvent<HTMLInputElement>,
selectedItem: NavOnSelectProps,
selectedItem: { itemId: string | number },
) => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
typeof selectedItem.itemId === 'string' &&
setActiveItem(selectedItem.itemId)
setActiveItem(selectedItem.itemId.toString())
}

const $isNavOpen = useStore(isNavOpen)
const sections = new Set(navEntries.map((entry) => entry.data.section))

const sortedNavEntries = navEntries.sort((a, b) =>
a.data.title.localeCompare(b.data.title),
)
const navSections = Array.from(sections).map((section) => {
const entries = navEntries.filter((entry) => entry.data.section === section)

const navItems = sortedNavEntries.map((entry) => (
<NavItem
key={entry.id}
itemId={entry.id}
isActive={activeItem === entry.id}
to={`/${entry.collection}/${entry.id}`}
>
{entry.data.title}
</NavItem>
))
return (
<NavSection
key={section}
entries={entries}
sectionId={section}
activeItem={activeItem}
/>
)
})

return (
<PageSidebar isSidebarOpen={$isNavOpen}>
<PageSidebarBody>
<Nav onSelect={onNavSelect}>
<NavList>{navItems}</NavList>
<NavList>{navSections}</NavList>
</Nav>
</PageSidebarBody>
</PageSidebar>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Page.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Content, PageSection } from '@patternfly/react-core'
<slot name="sidebar" />
<div class={styles.pageMainContainer}>
<main class={styles.pageMain}>
<PageSection>
<PageSection transition:animate="none">
<Content>
<slot />
</Content>
Expand Down
36 changes: 36 additions & 0 deletions src/components/__tests__/NavEntry.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

import { render, screen } from '@testing-library/react';
import { NavEntry, TextContentEntry } from '../NavEntry';

const mockEntry: TextContentEntry = {
id: 'entry1',
data: { id: 'Entry1', section: 'section1' },
collection: 'textContent',
};

describe('NavEntry', () => {
it('renders without crashing', () => {
render(<NavEntry entry={mockEntry} isActive={false} />);
expect(screen.getByText('Entry1')).toBeInTheDocument();
});

it('renders the correct link', () => {
render(<NavEntry entry={mockEntry} isActive={false} />);
expect(screen.getByRole('link')).toHaveAttribute('href', '/section1/entry1');
});

it('marks the entry as active if isActive is true', () => {
render(<NavEntry entry={mockEntry} isActive={true} />);
expect(screen.getByRole('link')).toHaveClass('pf-m-current');
});

it('does not mark the entry as active if isActive is false', () => {
render(<NavEntry entry={mockEntry} isActive={false} />);
expect(screen.getByRole('link')).not.toHaveClass('pf-m-current');
});

it('matches snapshot', () => {
const { asFragment } = render(<NavEntry entry={mockEntry} isActive={false} />);
expect(asFragment()).toMatchSnapshot();
});
});
Loading

0 comments on commit 665c4d2

Please sign in to comment.