Add Navbar component, emotion for css and storybook

This commit is contained in:
Charles Bochet 2022-12-04 22:59:30 +01:00
parent eba76274c6
commit 0f2d8a556e
15 changed files with 37849 additions and 4090 deletions

View File

@ -3,23 +3,20 @@ module.exports = {
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
sourceType: 'module'
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:storybook/recommended'],
root: true,
env: {
node: true,
jest: true,
jest: true
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
'@typescript-eslint/no-explicit-any': 'off'
}
};

46
front/.storybook/main.js Normal file
View File

@ -0,0 +1,46 @@
module.exports = {
webpackFinal: (config) => {
config.module.rules.push({
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
require('@babel/preset-typescript').default,
[require('@babel/preset-react').default, { runtime: 'automatic' }],
require('@babel/preset-env').default,
],
},
},
],
})
config.resolve.extensions.push('.ts', '.tsx')
config.module.rules.push({
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
})
config.resolve.extensions.push('.mjs')
return config
},
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/preset-create-react-app"
],
framework: "@storybook/react",
core: {
builder: "@storybook/builder-webpack5"
}
}

View File

@ -0,0 +1,9 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}

41592
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@types/node": "^16.18.4",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
@ -16,14 +18,29 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.stories.*"
],
"rules": {
"import/no-anonymous-default-export": "off"
}
}
]
},
"overrides": {
"react-refresh": "0.14.0"
},
"browserslist": {
"production": [
">0.2%",
@ -37,11 +54,22 @@
]
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-interactions": "^6.5.14",
"@storybook/addon-links": "^6.5.14",
"@storybook/builder-webpack5": "^6.5.14",
"@storybook/manager-webpack5": "^6.5.14",
"@storybook/node-logger": "^6.5.14",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.14",
"@storybook/testing-library": "^0.0.13",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"babel-plugin-named-exports-order": "^0.0.2",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^23.0.0",
@ -50,6 +78,10 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.31.11",
"typescript": "^4.9.3"
"eslint-plugin-storybook": "^0.6.7",
"prettier": "^2.8.0",
"prop-types": "^15.8.1",
"typescript": "^4.9.3",
"webpack": "^5.75.0"
}
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,16 +1,19 @@
import React from 'react';
import Tasks from './pages/Tasks';
import History from './pages/History';
import Performances from './pages/Performances';
import AppLayout from './layout/AppLayout';
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<div className="App">
<AppLayout>
<Routes>
<Route path="/" element={<Tasks />} />
<Route path="/history" element={<History />} />
<Route path="/performances" element={<Performances />} />
</Routes>
</div>
</AppLayout>
);
}

View File

@ -0,0 +1,22 @@
import Navbar from './Navbar';
import styled from '@emotion/styled';
const StyledLayout = styled.div`
display: flex;
flex-direction: column;
`;
type OwnProps = {
children: JSX.Element;
};
function AppLayout({ children }: OwnProps) {
return (
<StyledLayout>
<Navbar />
<div>{children}</div>
</StyledLayout>
);
}
export default AppLayout;

View File

@ -0,0 +1,53 @@
import styled from '@emotion/styled';
import { useNavigate } from 'react-router-dom';
type OwnProps = {
label: string;
to: string;
active?: boolean;
};
type StyledItemProps = {
active?: boolean;
};
const StyledItem = styled.button`
display: flex;
height: 60px;
background: inherit;
align-items: center;
padding-left: 10px;
padding-right: 10px;
margin-left: 10px;
margin-right: 10px;
font-size: 16px;
margin-bottom: -2px;
cursor: pointer;
color: ${(props: StyledItemProps) => (props.active ? 'black' : '#2e3138')};
font-weight: ${(props: StyledItemProps) =>
props.active ? 'bold' : 'inherit'};
border: 0;
border-bottom: ${(props: StyledItemProps) =>
props.active ? '2px solid black' : '2px solid #eaecee'};
&:hover {
border-bottom: 2px solid #2e3138;
}
`;
function NavItem({ label, to, active }: OwnProps) {
const navigate = useNavigate();
return (
<StyledItem
onClick={() => {
navigate(to);
}}
active={active}
aria-selected={active}
>
{label}
</StyledItem>
);
}
export default NavItem;

View File

@ -0,0 +1,53 @@
import styled from '@emotion/styled';
import { useMatch, useResolvedPath } from 'react-router-dom';
import NavItem from './NavItem';
const NavbarContainer = styled.div`
display: flex;
flex-direction: row;
align-items: stretch;
padding-left: 12px;
height: 58px;
border-bottom: 2px solid #eaecee;
`;
function Navbar() {
return (
<>
<NavbarContainer>
<NavItem
label="Tasks"
to="/"
active={
!!useMatch({
path: useResolvedPath('/').pathname,
end: true,
})
}
/>
<NavItem
label="History"
to="/history"
active={
!!useMatch({
path: useResolvedPath('/history').pathname,
end: true,
})
}
/>
<NavItem
label="Performances"
to="/performances"
active={
!!useMatch({
path: useResolvedPath('/performances').pathname,
end: true,
})
}
/>
</NavbarContainer>
</>
);
}
export default Navbar;

View File

@ -0,0 +1,9 @@
function Performances() {
return (
<div>
<h1>This is the performances page</h1>
</div>
);
}
export default Performances;

View File

@ -0,0 +1,20 @@
import { MemoryRouter } from 'react-router-dom';
import NavItem from '../../layout/NavItem';
export default {
title: 'NavItem',
component: NavItem,
};
export const NavItemDefault = () => (
<MemoryRouter>
<NavItem label="Test" to="/test" />
</MemoryRouter>
);
export const NavItemActive = () => (
<MemoryRouter initialEntries={['/test']}>
<NavItem label="Test" to="/test" active={true} />
</MemoryRouter>
);

View File

@ -0,0 +1,14 @@
import { MemoryRouter } from 'react-router-dom';
import Navbar from '../../layout/Navbar';
export default {
title: 'Navbar',
component: Navbar,
};
export const NavbarOnPerformance = () => (
<MemoryRouter initialEntries={['/performances']}>
<Navbar />
</MemoryRouter>
);

View File

@ -0,0 +1,19 @@
import { render, fireEvent } from '@testing-library/react';
import { NavItemDefault } from '../../stories/layout/NavItem.stories'; //👈 Our stories imported here.
const mockedNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
it('Checks the NavItem renders', () => {
const { getByRole } = render(<NavItemDefault />);
const button = getByRole('button');
expect(button).toHaveTextContent('Test');
fireEvent.click(button);
expect(mockedNavigate).toHaveBeenCalledWith('/test');
});

View File

@ -0,0 +1,17 @@
import { render } from '@testing-library/react';
import { NavbarOnPerformance } from '../../stories/layout/Navbar.stories';
it('Checks the NavItem renders', () => {
const { getByRole } = render(<NavbarOnPerformance />);
expect(getByRole('button', { name: 'Performances' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(getByRole('button', { name: 'Tasks' })).toHaveAttribute(
'aria-selected',
'false',
);
});