Initial commit AB#12
This commit is contained in:
22
app/.babelrc
Normal file
22
app/.babelrc
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic"
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"regenerator": true
|
||||
},
|
||||
],
|
||||
"istanbul"
|
||||
],
|
||||
|
||||
}
|
||||
2
app/.eslintignore
Normal file
2
app/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
cypress
|
||||
14
app/.eslintrc
Normal file
14
app/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
]
|
||||
}
|
||||
4
app/.gitignore
vendored
Normal file
4
app/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.nyc_output
|
||||
coverage-cy
|
||||
dist
|
||||
8
app/.nycrc
Normal file
8
app/.nycrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"all": true,
|
||||
"excludeAfterRemap": true,
|
||||
"report-dir": "coverage-cy",
|
||||
"reporter": ["text", "json", "html"],
|
||||
"extension": [".js"],
|
||||
"include": "src/**/*.js"
|
||||
}
|
||||
4
app/.prettierignore
Normal file
4
app/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.eslintrc
|
||||
package-lock.json
|
||||
package.json
|
||||
tsconfig.json
|
||||
5
app/.prettierrc
Normal file
5
app/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"jsxSingleQuote": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
0
app/README.md
Normal file
0
app/README.md
Normal file
69
app/cypress.config.ts
Normal file
69
app/cypress.config.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// return Object.assign({}, config, require('@bahmutov/cypress-code-coverage/plugin')(on, config));
|
||||
require('@bahmutov/cypress-code-coverage/plugin')(on, config);
|
||||
|
||||
return config;
|
||||
},
|
||||
// baseUrl: 'http://localhost:3000',
|
||||
// specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}'
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
// here are the additional settings from Gleb's instructions
|
||||
webpackConfig: {
|
||||
mode: "development",
|
||||
devtool: false,
|
||||
module: {
|
||||
rules: [
|
||||
// application and Cypress files are bundled like React components
|
||||
// and instrumented using the babel-plugin-istanbul
|
||||
{
|
||||
test: /\.(ts|js)x?$/,
|
||||
exclude: /node_modules/,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
},
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
'@babel/preset-typescript'
|
||||
],
|
||||
plugins: [
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"regenerator": true
|
||||
},
|
||||
],
|
||||
"istanbul"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(s(a|c)ss)$/,
|
||||
use: ['css-loader', 'sass-loader'],
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
setupNodeEvents(on, config) {
|
||||
// return Object.assign({}, config, require('@bahmutov/cypress-code-coverage/plugin')(on, config))
|
||||
require('@bahmutov/cypress-code-coverage/plugin')(on, config)
|
||||
|
||||
return config
|
||||
},
|
||||
specPattern: 'src/**/**/*.cy.{js,jsx,ts,tsx}'
|
||||
},
|
||||
});
|
||||
5
app/cypress/fixtures/example.json
Normal file
5
app/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
37
app/cypress/support/commands.ts
Normal file
37
app/cypress/support/commands.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
12
app/cypress/support/component-index.html
Normal file
12
app/cypress/support/component-index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
41
app/cypress/support/component.ts
Normal file
41
app/cypress/support/component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
// import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import { mount } from 'cypress/react18'
|
||||
|
||||
import '@bahmutov/cypress-code-coverage/support';
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
1
app/cypress/support/e2e.ts
Normal file
1
app/cypress/support/e2e.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@bahmutov/cypress-code-coverage/support';
|
||||
BIN
app/cypress/videos/Venues.cy.tsx.mp4
Normal file
BIN
app/cypress/videos/Venues.cy.tsx.mp4
Normal file
Binary file not shown.
1
app/declaration.d.ts
vendored
Normal file
1
app/declaration.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module '*.scss';
|
||||
12
app/index.html
Normal file
12
app/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<html>
|
||||
<head lang="en">
|
||||
<title>MLB Gameday</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
<body>
|
||||
<div id="app-root"></div>
|
||||
</body>
|
||||
26331
app/package-lock.json
generated
Normal file
26331
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
app/package.json
Normal file
74
app/package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "mlb-gameday",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --port 3000",
|
||||
"build": "NODE_ENV=production webpack",
|
||||
"cy:run": "npx cypress run --component",
|
||||
"test": "start-server-and-test start http://localhost:3000 cy:run",
|
||||
"lint": "eslint ./src --fix",
|
||||
"format": "prettier --write ./src"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/noahspannbauer/react-mlb-schedule-js.git"
|
||||
},
|
||||
"author": "Noah Spannbauer",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/noahspannbauer/react-mlb-schedule-js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/noahspannbauer/react-mlb-schedule-js#readme",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pretty-quick --staged && npx lint-staged"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@bahmutov/cypress-code-coverage": "^1.3.2",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"css-loader": "^6.7.3",
|
||||
"cypress": "^12.3.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"mini-css-extract-plugin": "^2.7.2",
|
||||
"node-sass": "^8.0.0",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.8.3",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass-loader": "^13.2.0",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"start-server-and-test": "^1.15.3",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.3",
|
||||
"@fluentui/react": "^8.104.2",
|
||||
"classnames": "^2.3.2",
|
||||
"graphql": "^16.6.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-grid-system": "^8.1.6",
|
||||
"react-router-dom": "^6.6.1"
|
||||
}
|
||||
}
|
||||
39
app/src/components/app/App.tsx
Normal file
39
app/src/components/app/App.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, useLocation } from 'react-router-dom';
|
||||
import { Container, Row, Col } from 'react-grid-system';
|
||||
import { initializeIcons } from '@fluentui/font-icons-mdl2';
|
||||
import Menu from '../menu/Menu';
|
||||
import Venues from '../venues/Venues';
|
||||
|
||||
initializeIcons();
|
||||
|
||||
const App: React.FC<unknown> = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Container fluid={true} style={{ padding: '0' }}>
|
||||
<Row>
|
||||
<Col sm={6}>Goodbye!</Col>
|
||||
<Col sm={6}>Hello!</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={3}>
|
||||
<Menu
|
||||
selectedKey={
|
||||
location.pathname === '/'
|
||||
? 'home'
|
||||
: location.pathname.replace('/', '')
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Routes>
|
||||
<Route path='venues' element={<Venues />} />
|
||||
</Routes>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
3
app/src/components/menu/IMenuProps.ts
Normal file
3
app/src/components/menu/IMenuProps.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IMenuProps {
|
||||
selectedKey: string;
|
||||
}
|
||||
29
app/src/components/menu/Menu.tsx
Normal file
29
app/src/components/menu/Menu.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IMenuProps } from './IMenuProps';
|
||||
import {
|
||||
Nav,
|
||||
INavLinkGroup
|
||||
} from '@fluentui/react/lib/Nav';
|
||||
|
||||
const Menu: React.FC<IMenuProps> = (props: IMenuProps) => {
|
||||
const navLinkGroups: INavLinkGroup[] = [
|
||||
{
|
||||
links: [
|
||||
{
|
||||
name: 'Home',
|
||||
url: 'http://localhost:3000',
|
||||
key: 'home'
|
||||
},
|
||||
{
|
||||
name: 'Venues',
|
||||
url: 'http://localhost:3000/venues',
|
||||
key: 'venues'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return <Nav groups={navLinkGroups} selectedKey={props.selectedKey} />;
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
6
app/src/components/paginator/IPaginatorProps.ts
Normal file
6
app/src/components/paginator/IPaginatorProps.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IPaginatorProps {
|
||||
totalPageCount: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
onPageChange: (newCurrentPage: number) => void;
|
||||
}
|
||||
23
app/src/components/paginator/Paginator.module.scss
Normal file
23
app/src/components/paginator/Paginator.module.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.paginator {
|
||||
.paginatorContainer {
|
||||
display: flex;
|
||||
direction: row;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.paginatorLink {
|
||||
display: flex;
|
||||
direction: row;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
margin: 4px 4px 0 4px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.paginatorButton {
|
||||
button {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
160
app/src/components/paginator/Paginator.tsx
Normal file
160
app/src/components/paginator/Paginator.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React from 'react';
|
||||
import { IPaginatorProps } from './IPaginatorProps';
|
||||
import styles from './Paginator.module.scss';
|
||||
import {
|
||||
IconButton
|
||||
} from '@fluentui/react/lib/Button';
|
||||
import { Link } from '@fluentui/react/lib/Link';
|
||||
|
||||
const range = (start: number, end: number) => {
|
||||
return [...Array(end).keys()].map((element) => element + start);
|
||||
};
|
||||
|
||||
const Paginator: React.FC<IPaginatorProps> = ({
|
||||
totalPageCount,
|
||||
currentPage,
|
||||
pageSize,
|
||||
onPageChange
|
||||
}: IPaginatorProps) => {
|
||||
const pages: number[] = range(1, totalPageCount);
|
||||
|
||||
return (
|
||||
<div className={styles.paginator}>
|
||||
<div className={styles.paginatorContainer}>
|
||||
<div className={styles.paginatorButton}>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'DoubleChevronLeft' }}
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onPageChange(1)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.paginatorButton}>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'ChevronLeft' }}
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
/>
|
||||
</div>
|
||||
{pages.map((page) => {
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
className={styles.paginatorLink}
|
||||
disabled={page === currentPage}
|
||||
onClick={() => onPageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={styles.paginatorButton}>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'ChevronRight' }}
|
||||
disabled={currentPage === totalPageCount}
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.paginatorButton}>
|
||||
<IconButton
|
||||
iconProps={{ iconName: 'DoubleChevronRight' }}
|
||||
disabled={currentPage === totalPageCount}
|
||||
onClick={() => onPageChange(totalPageCount)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// const ellipses = '...';
|
||||
// const paginationRange: (string | number)[] | undefined = useMemo(() => {
|
||||
// const siblingCount = 2;
|
||||
// const totalPageNumbers = 1;
|
||||
// const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
|
||||
// console.log(leftSiblingIndex)
|
||||
// const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPageCount);
|
||||
// console.log(rightSiblingIndex)
|
||||
// const shouldShowLeftDots = leftSiblingIndex > 2;
|
||||
// const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
|
||||
// const firstPageIndex = 1;
|
||||
// const lastPageIndex = totalPageCount;
|
||||
// const range = (start: number, end: number) => {
|
||||
// let length = end - start + 1;
|
||||
// return Array.from({ length }, (_, idx) => idx + start);
|
||||
// };
|
||||
|
||||
// if (totalPageNumbers >= totalPageCount) {
|
||||
// return range(1, totalPageCount)
|
||||
// }
|
||||
|
||||
// if (!shouldShowLeftDots && shouldShowRightDots) {
|
||||
// let leftItemCount = 3 + 2 * siblingCount;
|
||||
// let leftRange = range(1, leftItemCount);
|
||||
|
||||
// return [...leftRange, ellipses, totalPageCount]
|
||||
// }
|
||||
|
||||
// if (shouldShowLeftDots && !shouldShowRightDots) {
|
||||
// let rightItemCount = 3 + 2 * siblingCount;
|
||||
// let rightRange = range(
|
||||
// totalPageCount - rightItemCount + 1,
|
||||
// totalPageCount
|
||||
// );
|
||||
|
||||
// return [firstPageIndex, ellipses, ...rightRange];
|
||||
// }
|
||||
|
||||
// if (shouldShowLeftDots && shouldShowRightDots) {
|
||||
// let middleRange = range(leftSiblingIndex, rightSiblingIndex);
|
||||
|
||||
// return [firstPageIndex, ellipses, ...middleRange, ellipses, lastPageIndex];
|
||||
// }
|
||||
// }, [totalPageCount, currentPage, pageSize]);
|
||||
// // let lastPage: string | number = paginationRange ? paginationRange[paginationRange.length - 1] :
|
||||
|
||||
// const onPrevious = () => {
|
||||
// onPageChange(currentPage - 1);
|
||||
// }
|
||||
|
||||
// const onNext = () => {
|
||||
// onPageChange(currentPage + 1);
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(paginationRange)
|
||||
// }, [paginationRange])
|
||||
|
||||
// return (
|
||||
// <Container fluid={true} style={{ padding: '0' }}>
|
||||
// <Row>
|
||||
// <Col>
|
||||
// <IconButton
|
||||
// iconProps={{ iconName: 'ChevronLeft' }}
|
||||
// disabled={currentPage === 1}
|
||||
// onClick={onPrevious}
|
||||
// />
|
||||
// </Col>
|
||||
// {paginationRange?.map((pageNumber: any) => {
|
||||
// if (pageNumber === ellipses) {
|
||||
// return <div>dots</div>
|
||||
// } else {
|
||||
// return <Link
|
||||
// onClick={() => onPageChange(parseInt(pageNumber))}
|
||||
// underline
|
||||
// >
|
||||
// {pageNumber}
|
||||
// </Link>
|
||||
// }
|
||||
// })}
|
||||
// <Col>
|
||||
// <IconButton
|
||||
// iconProps={{ iconName: 'ChevronRight' }}
|
||||
// // disabled={currentPage === lastPage}
|
||||
// onClick={onNext}
|
||||
// />
|
||||
// </Col>
|
||||
// </Row>
|
||||
// </Container>
|
||||
// )
|
||||
};
|
||||
|
||||
export default Paginator;
|
||||
13
app/src/components/venues/IVenuesState.ts
Normal file
13
app/src/components/venues/IVenuesState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ICommandBarItemProps } from '@fluentui/react/lib/CommandBar';
|
||||
import { IVenue } from '../../models/IVenue';
|
||||
|
||||
export interface IVenuesState {
|
||||
items: IVenue[];
|
||||
totalItemCount: number | null;
|
||||
totalPageCount: number | null;
|
||||
currentPageNumber: number;
|
||||
pageSize: number;
|
||||
selectedItems: IVenue[];
|
||||
commandBarItems: ICommandBarItemProps[];
|
||||
isPanelOpen: boolean;
|
||||
}
|
||||
104
app/src/components/venues/Venues.cy.tsx
Normal file
104
app/src/components/venues/Venues.cy.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import AppContextProvider from '../../context/AppContextProvider';
|
||||
import Venues from './Venues';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { GET_VENUES } from '../../constants/graphQL';
|
||||
|
||||
const mocks: any[] = [
|
||||
{
|
||||
request: {
|
||||
query: GET_VENUES,
|
||||
variables: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
venues: {
|
||||
currentPage: 1,
|
||||
errors: null,
|
||||
message: null,
|
||||
pageSize: 10,
|
||||
succeeded: true,
|
||||
totalItemCount: 55,
|
||||
totalPageCount: 6,
|
||||
results: [
|
||||
{
|
||||
active: 'True',
|
||||
id: 32,
|
||||
link: '/api/v1/venues/32',
|
||||
name: 'American Family Field'
|
||||
},
|
||||
{
|
||||
active: 'True',
|
||||
id: 2518,
|
||||
link: '/api/v1/venues/2518',
|
||||
name: 'American Family Fields of Phoenix'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 1,
|
||||
link: '/api/v1/venues/1',
|
||||
name: 'Angel Stadium'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 2700,
|
||||
link: '/api/v1/venues/2700',
|
||||
name: 'BayCare Ballpark'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 2889,
|
||||
link: '/api/v1/venues/2889',
|
||||
name: 'Busch Stadium'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 3809,
|
||||
link: '/api/v1/venues/3809',
|
||||
name: 'Camelback Ranch'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 2534,
|
||||
link: '/api/v1/venues/2534',
|
||||
name: 'Charlotte Sports Park'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 15,
|
||||
link: '/api/v1/venues/15',
|
||||
name: 'Chase Field'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 3289,
|
||||
link: '/api/v1/venues/3289',
|
||||
name: 'Citi Field'
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
id: 2681,
|
||||
link: '/api/v1/venues/2681',
|
||||
name: 'Citizens Bank Park'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
describe('Venues', () => {
|
||||
it('renders', () => {
|
||||
cy.mount(
|
||||
<AppContextProvider>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<Venues />
|
||||
</MockedProvider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
});
|
||||
});
|
||||
177
app/src/components/venues/Venues.tsx
Normal file
177
app/src/components/venues/Venues.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import {
|
||||
CommandBar,
|
||||
ICommandBarItemProps
|
||||
} from '@fluentui/react/lib/CommandBar';
|
||||
import {
|
||||
DetailsList,
|
||||
IColumn,
|
||||
Selection,
|
||||
SelectionMode
|
||||
} from '@fluentui/react/lib/DetailsList';
|
||||
import { MessageBar, MessageBarType } from '@fluentui/react/lib/MessageBar';
|
||||
import { Panel } from '@fluentui/react/lib/Panel';
|
||||
import Paginator from '../paginator/Paginator';
|
||||
import { Spinner, SpinnerSize } from '@fluentui/react/lib/Spinner';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import { GET_VENUES } from '../../constants/graphQL';
|
||||
import { INotification } from '../../models/INotification';
|
||||
import { IVenue } from '../../models/IVenue';
|
||||
|
||||
const Venues: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const appContext = useAppContext();
|
||||
const currentPageNumber = state.currentPageNumber;
|
||||
const pageSize = state.pageSize;
|
||||
const { data, loading, error, refetch } = useQuery(GET_VENUES, {
|
||||
variables: {
|
||||
pageNumber: currentPageNumber,
|
||||
pageSize: pageSize
|
||||
}
|
||||
});
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
fieldName: 'name',
|
||||
minWidth: 100,
|
||||
isResizable: true
|
||||
}
|
||||
];
|
||||
|
||||
const selection: Selection = new Selection({
|
||||
onSelectionChanged: () =>
|
||||
dispatch({ type: 'SET_SELECTED_ITEM', payload: selection.getSelection() })
|
||||
});
|
||||
|
||||
const onIsPanelOpen = () => {
|
||||
dispatch({ type: 'SET_IS_PANEL_OPEN', payload: !state.isPanelOpen });
|
||||
};
|
||||
|
||||
const onDismissMessageBar = () => {
|
||||
const notification: INotification = {
|
||||
...appContext.state.notification,
|
||||
isVisible: false
|
||||
};
|
||||
|
||||
appContext.dispatch({ type: 'SET_NOTIFICATION', payload: notification });
|
||||
};
|
||||
|
||||
const onPageChange = (newCurrentPage: number) => {
|
||||
console.log(newCurrentPage);
|
||||
dispatch({ type: 'SET_CURRENT_PAGE_NUMBER', payload: newCurrentPage });
|
||||
refetch({
|
||||
pageNumber: newCurrentPage,
|
||||
pageSize: pageSize
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: 'SET_COMMAND_BAR_ITEMS',
|
||||
payload: [
|
||||
{
|
||||
key: 'openItem',
|
||||
text: 'Open',
|
||||
iconProps: { iconName: 'OpenPane' },
|
||||
onClick: onIsPanelOpen,
|
||||
disabled: true
|
||||
}
|
||||
]
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(JSON.stringify(data));
|
||||
if (data) {
|
||||
dispatch({
|
||||
type: 'SET_ITEMS',
|
||||
payload: {
|
||||
items: data.venues.results,
|
||||
totalItemCount: data.venues.totalItemCount,
|
||||
totalPageCount: data.venues.totalPageCount
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
const notification: INotification = {
|
||||
messageBarType: MessageBarType.error,
|
||||
message: error?.message,
|
||||
isMultiline: false,
|
||||
isVisible: false
|
||||
};
|
||||
|
||||
appContext.dispatch({ type: 'SET_NOTIFICATION', payload: notification });
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.selectedItems.length > 0 && state.commandBarItems.length > 0) {
|
||||
const newCommandBarItems = state.commandBarItems.map(
|
||||
(commandBarItem: ICommandBarItemProps) => {
|
||||
return {
|
||||
...commandBarItem,
|
||||
disabled: false
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
dispatch({ type: 'SET_COMMAND_BAR_ITEMS', payload: newCommandBarItems });
|
||||
}
|
||||
}, [state.selectedItems]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <Spinner size={SpinnerSize.large} />}
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
{appContext.state.notification?.isVisible && (
|
||||
<MessageBar
|
||||
messageBarType={appContext.state.notification?.messageBarType}
|
||||
isMultiline={appContext.state.notification?.isMultiline}
|
||||
onDismiss={onDismissMessageBar}
|
||||
>
|
||||
{appContext.state.notification?.message}
|
||||
</MessageBar>
|
||||
)}
|
||||
<CommandBar items={state.commandBarItems} />
|
||||
<DetailsList
|
||||
columns={columns}
|
||||
items={state.items}
|
||||
selection={selection}
|
||||
selectionMode={SelectionMode.single}
|
||||
selectionPreservedOnEmptyClick={true}
|
||||
/>
|
||||
<Paginator
|
||||
totalPageCount={data.venues.totalPageCount}
|
||||
currentPage={state.currentPageNumber}
|
||||
pageSize={state.pageSize}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
<Panel
|
||||
headerText='Venue'
|
||||
isOpen={state.isPanelOpen}
|
||||
onDismiss={onIsPanelOpen}
|
||||
>
|
||||
{state.selectedItems.length > 0 &&
|
||||
state.selectedItems.map((item: IVenue) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p>{item.name}</p>
|
||||
<p>{item.id}</p>
|
||||
<p>{item.link}</p>
|
||||
<p>{item.active}</p>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Panel>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Venues;
|
||||
73
app/src/components/venues/reducer.ts
Normal file
73
app/src/components/venues/reducer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { IVenuesState } from './IVenuesState';
|
||||
import { IVenue } from '../../models/IVenue';
|
||||
import { ICommandBarItemProps } from '@fluentui/react/lib/CommandBar';
|
||||
import { IObjectWithKey } from '@fluentui/react';
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: 'SET_ITEMS';
|
||||
payload: {
|
||||
items: IVenue[];
|
||||
totalItemCount: number;
|
||||
totalPageCount: number;
|
||||
};
|
||||
}
|
||||
| { type: 'SET_CURRENT_PAGE_NUMBER'; payload: number }
|
||||
| { type: 'SET_PAGE_SIZE'; payload: number }
|
||||
| { type: 'SET_SELECTED_ITEM'; payload: IObjectWithKey[] }
|
||||
| { type: 'SET_COMMAND_BAR_ITEMS'; payload: ICommandBarItemProps[] }
|
||||
| { type: 'SET_IS_PANEL_OPEN'; payload: boolean };
|
||||
|
||||
export const initialState: IVenuesState = {
|
||||
items: [],
|
||||
totalItemCount: null,
|
||||
totalPageCount: null,
|
||||
currentPageNumber: 1,
|
||||
pageSize: 10,
|
||||
selectedItems: [],
|
||||
commandBarItems: [],
|
||||
isPanelOpen: false
|
||||
};
|
||||
|
||||
export const reducer = (state: IVenuesState, action: Action): IVenuesState => {
|
||||
switch (action.type) {
|
||||
case 'SET_ITEMS': {
|
||||
return {
|
||||
...state,
|
||||
items: action.payload.items,
|
||||
totalItemCount: action.payload.totalItemCount,
|
||||
totalPageCount: action.payload.totalPageCount
|
||||
};
|
||||
}
|
||||
case 'SET_CURRENT_PAGE_NUMBER': {
|
||||
return {
|
||||
...state,
|
||||
currentPageNumber: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_PAGE_SIZE': {
|
||||
return {
|
||||
...state,
|
||||
pageSize: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_SELECTED_ITEM': {
|
||||
return {
|
||||
...state,
|
||||
selectedItems: <IVenue[]>action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_COMMAND_BAR_ITEMS': {
|
||||
return {
|
||||
...state,
|
||||
commandBarItems: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_IS_PANEL_OPEN': {
|
||||
return {
|
||||
...state,
|
||||
isPanelOpen: action.payload
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
21
app/src/constants/graphQL.ts
Normal file
21
app/src/constants/graphQL.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_VENUES = gql`
|
||||
query GetVenues($pageNumber: Int, $pageSize: Int) {
|
||||
venues(pageNumber: $pageNumber, pageSize: $pageSize) {
|
||||
currentPage
|
||||
errors
|
||||
message
|
||||
pageSize
|
||||
succeeded
|
||||
totalItemCount
|
||||
totalPageCount
|
||||
results {
|
||||
active
|
||||
id
|
||||
link
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
5
app/src/context/AppContext.tsx
Normal file
5
app/src/context/AppContext.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Context, createContext } from 'react';
|
||||
import { IAppContextProps } from './IAppContextProps';
|
||||
|
||||
export const AppContext: Context<IAppContextProps> =
|
||||
createContext<IAppContextProps>({} as IAppContextProps);
|
||||
25
app/src/context/AppContextProvider.tsx
Normal file
25
app/src/context/AppContextProvider.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { useMemo, useReducer } from 'react';
|
||||
import { AppContext } from './AppContext';
|
||||
import { IAppContextProps } from './IAppContextProps';
|
||||
import { IAppContextProviderProps } from './IAppContextProviderProps';
|
||||
import { initialState, reducer } from './reducer';
|
||||
|
||||
const AppContextProvider: React.FC<IAppContextProviderProps> = (
|
||||
props: IAppContextProviderProps
|
||||
) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const contextValue: IAppContextProps = useMemo(() => {
|
||||
return {
|
||||
state,
|
||||
dispatch
|
||||
};
|
||||
}, [state, dispatch]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={contextValue}>
|
||||
{props.children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppContextProvider;
|
||||
8
app/src/context/IAppContextProps.ts
Normal file
8
app/src/context/IAppContextProps.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { IAppContextState } from './IAppContextState';
|
||||
import { Action } from './reducer';
|
||||
|
||||
export interface IAppContextProps {
|
||||
state: IAppContextState;
|
||||
dispatch: React.Dispatch<Action>;
|
||||
}
|
||||
5
app/src/context/IAppContextProviderProps.tsx
Normal file
5
app/src/context/IAppContextProviderProps.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface IAppContextProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
5
app/src/context/IAppContextState.ts
Normal file
5
app/src/context/IAppContextState.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { INotification } from '../models/INotification';
|
||||
|
||||
export interface IAppContextState {
|
||||
notification: INotification | undefined;
|
||||
}
|
||||
28
app/src/context/reducer.ts
Normal file
28
app/src/context/reducer.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { MessageBarType } from '@fluentui/react';
|
||||
import { INotification } from '../models/INotification';
|
||||
import { IAppContextState } from './IAppContextState';
|
||||
|
||||
export type Action = { type: 'SET_NOTIFICATION'; payload: INotification };
|
||||
|
||||
export const initialState: IAppContextState = {
|
||||
notification: {
|
||||
messageBarType: MessageBarType.info,
|
||||
message: 'Blah blah blah',
|
||||
isMultiline: false,
|
||||
isVisible: true
|
||||
}
|
||||
};
|
||||
|
||||
export const reducer = (
|
||||
state: IAppContextState,
|
||||
action: Action
|
||||
): IAppContextState => {
|
||||
switch (action.type) {
|
||||
case 'SET_NOTIFICATION': {
|
||||
return {
|
||||
...state,
|
||||
notification: action.payload
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
11
app/src/hooks/appContext/UseAppContext.tsx
Normal file
11
app/src/hooks/appContext/UseAppContext.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { AppContext } from '../../context/AppContext';
|
||||
|
||||
export const useAppContext = () => {
|
||||
const { state, dispatch } = useContext(AppContext);
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch
|
||||
};
|
||||
};
|
||||
6
app/src/hooks/usePaginiation/IUsePaginationProps.ts
Normal file
6
app/src/hooks/usePaginiation/IUsePaginationProps.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IUsePaginationProps {
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
siblingCount: number;
|
||||
currentPage: number;
|
||||
}
|
||||
71
app/src/hooks/usePaginiation/UsePagination.tsx
Normal file
71
app/src/hooks/usePaginiation/UsePagination.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IUsePaginationProps } from './IUsePaginationProps';
|
||||
|
||||
export const DOTS = '...';
|
||||
|
||||
const range = (start: number, end: number) => {
|
||||
const length = end - start + 1;
|
||||
return Array.from({ length }, (_, idx) => idx + start);
|
||||
};
|
||||
|
||||
export const usePagination = ({
|
||||
totalCount,
|
||||
pageSize,
|
||||
siblingCount = 1,
|
||||
currentPage
|
||||
}: IUsePaginationProps) => {
|
||||
const paginationRange = useMemo(() => {
|
||||
const totalPageCount = Math.ceil(totalCount / pageSize);
|
||||
|
||||
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
|
||||
const totalPageNumbers = siblingCount + 5;
|
||||
|
||||
/*
|
||||
If the number of pages is less than the page numbers we want to show in our
|
||||
paginationComponent, we return the range [1..totalPageCount]
|
||||
*/
|
||||
if (totalPageNumbers >= totalPageCount) {
|
||||
return range(1, totalPageCount);
|
||||
}
|
||||
|
||||
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
|
||||
const rightSiblingIndex = Math.min(
|
||||
currentPage + siblingCount,
|
||||
totalPageCount
|
||||
);
|
||||
|
||||
/*
|
||||
We do not want to show dots if there is only one position left
|
||||
after/before the left/right page count as that would lead to a change if our Pagination
|
||||
component size which we do not want
|
||||
*/
|
||||
const shouldShowLeftDots = leftSiblingIndex > 2;
|
||||
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
|
||||
|
||||
const firstPageIndex = 1;
|
||||
const lastPageIndex = totalPageCount;
|
||||
|
||||
if (!shouldShowLeftDots && shouldShowRightDots) {
|
||||
const leftItemCount = 3 + 2 * siblingCount;
|
||||
const leftRange = range(1, leftItemCount);
|
||||
|
||||
return [...leftRange, DOTS, totalPageCount];
|
||||
}
|
||||
|
||||
if (shouldShowLeftDots && !shouldShowRightDots) {
|
||||
const rightItemCount = 3 + 2 * siblingCount;
|
||||
const rightRange = range(
|
||||
totalPageCount - rightItemCount + 1,
|
||||
totalPageCount
|
||||
);
|
||||
return [firstPageIndex, DOTS, ...rightRange];
|
||||
}
|
||||
|
||||
if (shouldShowLeftDots && shouldShowRightDots) {
|
||||
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
|
||||
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
|
||||
}
|
||||
}, [totalCount, pageSize, siblingCount, currentPage]);
|
||||
|
||||
return paginationRange;
|
||||
};
|
||||
25
app/src/index.tsx
Normal file
25
app/src/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ApolloClient, InMemoryCache, ApolloProvider, NormalizedCacheObject } from '@apollo/client';
|
||||
import AppContextProvider from './context/AppContextProvider';
|
||||
import App from './components/app/App';
|
||||
|
||||
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
|
||||
uri: 'http://localhost:8080/v1/graphql',
|
||||
cache: new InMemoryCache()
|
||||
});
|
||||
|
||||
const container = document.getElementById('app-root') as HTMLElement;
|
||||
const root: Root = createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<AppContextProvider>
|
||||
<ApolloProvider client={client}>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ApolloProvider>
|
||||
</AppContextProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
8
app/src/models/INotification.ts
Normal file
8
app/src/models/INotification.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { MessageBarType } from '@fluentui/react/lib/MessageBar';
|
||||
|
||||
export interface INotification {
|
||||
messageBarType?: MessageBarType;
|
||||
message?: string;
|
||||
isMultiline?: boolean;
|
||||
isVisible: boolean;
|
||||
}
|
||||
6
app/src/models/IVenue.ts
Normal file
6
app/src/models/IVenue.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IVenue {
|
||||
active: boolean;
|
||||
id: number;
|
||||
link: string;
|
||||
name: string;
|
||||
}
|
||||
19
app/tsconfig.json
Normal file
19
app/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"dom",
|
||||
"esnext"
|
||||
],
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"target": "esnext",
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
39
app/webpack.config.js
Normal file
39
app/webpack.config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const prod = process.env.NODE_ENV === 'production';
|
||||
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: prod ? 'production' : 'development',
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
path: __dirname + '/dist/',
|
||||
publicPath: '/'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|js)x?$/,
|
||||
exclude: /node_modules/,
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
},
|
||||
use: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(s(a|c)ss)$/,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
||||
}
|
||||
]
|
||||
},
|
||||
devServer: {
|
||||
historyApiFallback: true
|
||||
},
|
||||
devtool: prod ? undefined : 'source-map',
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'index.html',
|
||||
}),
|
||||
new MiniCssExtractPlugin(),
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user