Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading