├── .DS_Store ├── .gitignore ├── README.md ├── Redux.gif ├── courses-app-redux ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── api │ │ ├── apiUtils.js │ │ ├── authorApi.js │ │ └── courseApi.js │ ├── components │ │ ├── App.js │ │ ├── PageNotFound.js │ │ ├── about │ │ │ └── AboutPage.js │ │ ├── common │ │ │ ├── Header.js │ │ │ ├── Header.test.js │ │ │ ├── SelectInput.js │ │ │ ├── Spinner.css │ │ │ ├── Spinner.js │ │ │ └── TextInput.js │ │ ├── courses │ │ │ ├── CourseForm.Enzyme.test.js │ │ │ ├── CourseForm.ReactTestingLibrary.test.js │ │ │ ├── CourseForm.Snapshots.test.js │ │ │ ├── CourseForm.js │ │ │ ├── CourseList.js │ │ │ ├── CoursesPage.js │ │ │ ├── ManageCoursePage.js │ │ │ ├── ManageCoursePage.test.js │ │ │ └── __snapshots__ │ │ │ │ └── CourseForm.Snapshots.test.js.snap │ │ └── home │ │ │ └── HomePage.js │ ├── favicon.ico │ ├── index.css │ ├── index.html │ ├── index.js │ ├── index.test.js │ └── redux │ │ ├── actions │ │ ├── actionTypes.js │ │ ├── apiStatusActions.js │ │ ├── authorActions.js │ │ ├── courseActions.js │ │ └── courseActions.test.js │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ ├── configureStore.prod.js │ │ ├── reducers │ │ ├── apiStatusReducer.js │ │ ├── authorReducer.js │ │ ├── courseReducer.js │ │ ├── courseReducer.test.js │ │ ├── index.js │ │ └── initialState.js │ │ └── store.test.js ├── tools │ ├── apiServer.js │ ├── createMockDb.js │ ├── db.json │ ├── fileMock.js │ ├── mockData.js │ ├── styleMock.js │ └── testSetup.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── dev-environment ├── .env ├── .github │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .storybook │ ├── addons.js │ └── config.js ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── api │ │ ├── apiUtils.js │ │ ├── authorApi.js │ │ └── courseApi.js │ ├── components │ │ ├── About.js │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── ErrorBoundary.js │ │ ├── Home.js │ │ ├── Nav.js │ │ ├── Spinner.css │ │ ├── Spinner.js │ │ └── reusable │ │ │ ├── TextInput │ │ │ ├── TextInput.js │ │ │ ├── TextInput.module.scss │ │ │ ├── TextInput.stories.js │ │ │ ├── TextInput.test.js │ │ │ ├── __snapshots__ │ │ │ │ └── TextInput.test.js.snap │ │ │ └── index.js │ │ │ └── Tooltip │ │ │ ├── Tooltip.js │ │ │ ├── Tooltip.module.scss │ │ │ ├── Tooltip.stories.js │ │ │ ├── Tooltip.tests.js │ │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── propTypes.js │ ├── serviceWorker.js │ ├── utils │ │ └── casing.js │ └── variables.scss └── tools │ ├── apiServer.js │ ├── bigMockData.js │ ├── createMockDb.js │ └── mockData.js ├── package-lock.json ├── package.json ├── performance-tuning ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.css │ ├── App.js │ ├── CheckboxList.css │ ├── CheckboxList.js │ ├── ErrorBoundary.js │ ├── Tooltip.css │ ├── Tooltip.js │ ├── Vehicles.css │ ├── Vehicles.js │ ├── api │ ├── driverApi.js │ ├── mockData.js │ ├── utils.js │ └── vehicleApi.js │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── utils │ ├── dates.js │ └── dates.test.js ├── refs ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── prism.css │ ├── serviceWorker.js │ └── snippets.js ├── responsive-web ├── .gitignore ├── .storybook │ ├── addons.js │ └── config.js ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt └── src │ ├── FlexboxPlayground │ ├── FlexboxPlayground.css │ ├── FlexboxPlayground.js │ ├── FlexboxPlayground.stories.js │ └── index.js │ ├── GridPlayground │ ├── GridPlayground.css │ ├── GridPlayground.js │ └── index.js │ ├── GridTemplateArea │ ├── GridTemplateArea.css │ ├── GridTemplateArea.js │ └── index.js │ ├── Nav │ ├── Nav.js │ ├── Nav.module.scss │ ├── Nav.stories.js │ └── Nav.test.js │ ├── SmartLink │ ├── SmartLink.js │ ├── SmartLink.stories.js │ └── index.js │ ├── Table │ ├── Table.js │ ├── Table.scss │ └── Table.stories.js │ ├── hooks │ └── useWindowSize.js │ ├── index.js │ ├── scss │ ├── _accessibility.scss │ ├── _colors.scss │ ├── _custom-grid.scss │ ├── _reach_components.scss │ ├── _utility.scss │ ├── _variables.scss │ └── global.scss │ └── utils │ └── debounce.js └── user-app-typescript ├── .eslintrc.yaml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── Users.spec.ts ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js └── tsconfig.json ├── db.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── routes.json ├── src ├── App.tsx ├── Home.tsx ├── Input.test.tsx ├── Input.tsx ├── InternationalizationContext.tsx ├── ManageUser.tsx ├── Nav.tsx ├── Users.tsx ├── api │ └── users.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── utils.test.ts └── utils.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/mock-api/node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | Please do these steps **before** the event. If you have any problems, email cory@reactjsconsulting.com. 4 | 5 | 1. Install [Node 10 or newer](http://nodejs.org) 6 | 1. Install [Git](https://git-scm.com/) 7 | 1. Install [VS Code](https://code.visualstudio.com/) 8 | 1. Install these VS Code extensions: [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode), [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint), and [React Code Snippets](https://marketplace.visualstudio.com/items?itemName=xabikos.ReactSnippets) 9 | 1. Click the green "Clone this template" button above. 10 | 1. [Enter a name and descripion for your repo. Click "Create Repository from Template"](https://www.dropbox.com/s/9vptw6tcac9snvx/Screenshot%202019-06-24%2019.42.10.png?dl=0) 11 | 1. Open a command line to the directory where you cloned this repo 12 | 1. Change directories into the `dev-environment` directory 13 | 1. Run this command to install dependencies: `npm install` 14 | 1. Run this command to start the app: `npm start` 15 | 1. Load http://localhost:3000. You should see the app start. 16 | 1. Load http://localhost:3001. You should see [json-server's landing page](https://www.dropbox.com/s/wooq97xyqze3fq2/Screenshot%202019-06-24%2019.40.22.png?dl=0). 17 | 18 | If you did all the above, you're all set! 19 | 20 | ## Keep in Touch! 21 | 22 | - [Cory's React Courses on Pluralsight](https://pluralsight.com/authors/cory-house) 23 | - Interested in more training or consulting? [reactjsconsulting.com](http://www.reactjsconsulting.com) 24 | - I tweet about software as [@housecor](http://www.twitter.com/housecor) on Twitter 25 | -------------------------------------------------------------------------------- /Redux.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/Redux.gif -------------------------------------------------------------------------------- /courses-app-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ps-redux", 3 | "description": "React and Redux Pluralsight course by Cory House", 4 | "scripts": { 5 | "start": "run-p start:dev start:api", 6 | "start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000", 7 | "prestart:api": "node tools/createMockDb.js", 8 | "start:api": "node tools/apiServer.js", 9 | "test": "jest --watch", 10 | "test:ci": "jest", 11 | "clean:build": "rimraf ./build && mkdir build", 12 | "prebuild": "run-p clean:build test:ci", 13 | "build": "webpack --config webpack.config.prod.js", 14 | "postbuild": "run-p start:api serve:build", 15 | "serve:build": "http-server ./build" 16 | }, 17 | "jest": { 18 | "setupFiles": [ 19 | "./tools/testSetup.js" 20 | ], 21 | "moduleNameMapper": { 22 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/tools/fileMock.js", 23 | "\\.(css|less)$": "/tools/styleMock.js" 24 | } 25 | }, 26 | "dependencies": { 27 | "bootstrap": "4.3.1", 28 | "immer": "3.1.2", 29 | "prop-types": "15.7.2", 30 | "react": "16.8.6", 31 | "react-dom": "16.8.6", 32 | "react-redux": "7.0.3", 33 | "react-router-dom": "5.0.0", 34 | "react-toastify": "5.2.1", 35 | "redux": "4.0.1", 36 | "redux-thunk": "2.3.0", 37 | "reselect": "4.0.0" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "7.4.5", 41 | "@testing-library/react": "8.0.1", 42 | "babel-eslint": "10.0.1", 43 | "babel-loader": "8.0.6", 44 | "babel-preset-react-app": "9.0.0", 45 | "css-loader": "2.1.1", 46 | "cssnano": "4.1.10", 47 | "enzyme": "3.9.0", 48 | "enzyme-adapter-react-16": "1.13.2", 49 | "eslint": "5.16.0", 50 | "eslint-loader": "2.1.2", 51 | "eslint-plugin-import": "2.17.3", 52 | "eslint-plugin-react": "7.13.0", 53 | "fetch-mock": "7.3.3", 54 | "html-webpack-plugin": "3.2.0", 55 | "http-server": "0.11.1", 56 | "jest": "24.8.0", 57 | "json-server": "0.14.2", 58 | "mini-css-extract-plugin": "0.7.0", 59 | "node-fetch": "^2.6.0", 60 | "npm-run-all": "4.1.5", 61 | "postcss-loader": "3.0.0", 62 | "react-test-renderer": "16.8.6", 63 | "redux-immutable-state-invariant": "2.1.0", 64 | "redux-mock-store": "1.5.3", 65 | "rimraf": "2.6.3", 66 | "style-loader": "0.23.1", 67 | "webpack": "4.32.2", 68 | "webpack-bundle-analyzer": "3.3.2", 69 | "webpack-cli": "3.3.2", 70 | "webpack-dev-server": "3.5.0" 71 | }, 72 | "engines": { 73 | "node": ">=8" 74 | }, 75 | "babel": { 76 | "presets": [ 77 | "babel-preset-react-app" 78 | ] 79 | }, 80 | "eslintConfig": { 81 | "extends": [ 82 | "eslint:recommended", 83 | "plugin:react/recommended", 84 | "plugin:import/errors", 85 | "plugin:import/warnings" 86 | ], 87 | "parser": "babel-eslint", 88 | "parserOptions": { 89 | "ecmaVersion": 2018, 90 | "sourceType": "module", 91 | "ecmaFeatures": { 92 | "jsx": true 93 | } 94 | }, 95 | "env": { 96 | "browser": true, 97 | "node": true, 98 | "es6": true, 99 | "jest": true 100 | }, 101 | "rules": { 102 | "no-debugger": "off", 103 | "no-console": "off", 104 | "no-unused-vars": "warn", 105 | "react/prop-types": "warn" 106 | }, 107 | "settings": { 108 | "react": { 109 | "version": "detect" 110 | } 111 | }, 112 | "root": true 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /courses-app-redux/src/api/apiUtils.js: -------------------------------------------------------------------------------- 1 | export async function handleResponse(response) { 2 | if (response.ok) return response.json(); 3 | if (response.status === 400) { 4 | // So, a server-side validation error occurred. 5 | // Server side validation returns a string error message, so parse as text instead of json. 6 | const error = await response.text(); 7 | throw new Error(error); 8 | } 9 | throw new Error("Network response was not ok."); 10 | } 11 | 12 | // In a real app, would likely call an error logging service. 13 | export function handleError(error) { 14 | // eslint-disable-next-line no-console 15 | console.error("API call failed. " + error); 16 | throw error; 17 | } 18 | -------------------------------------------------------------------------------- /courses-app-redux/src/api/authorApi.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleError } from "./apiUtils"; 2 | const baseUrl = process.env.API_URL + "/authors/"; 3 | 4 | export function getAuthors() { 5 | return fetch(baseUrl) 6 | .then(handleResponse) 7 | .catch(handleError); 8 | } 9 | -------------------------------------------------------------------------------- /courses-app-redux/src/api/courseApi.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleError } from "./apiUtils"; 2 | const baseUrl = process.env.API_URL + "/courses/"; 3 | 4 | export function getCourses() { 5 | return fetch(baseUrl) 6 | .then(handleResponse) 7 | .catch(handleError); 8 | } 9 | 10 | export function saveCourse(course) { 11 | return fetch(baseUrl + (course.id || ""), { 12 | method: course.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists. 13 | headers: { "content-type": "application/json" }, 14 | body: JSON.stringify(course) 15 | }) 16 | .then(handleResponse) 17 | .catch(handleError); 18 | } 19 | 20 | export function deleteCourse(courseId) { 21 | return fetch(baseUrl + courseId, { method: "DELETE" }) 22 | .then(handleResponse) 23 | .catch(handleError); 24 | } 25 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import HomePage from "./home/HomePage"; 4 | import AboutPage from "./about/AboutPage"; 5 | import Header from "./common/Header"; 6 | import PageNotFound from "./PageNotFound"; 7 | import CoursesPage from "./courses/CoursesPage"; 8 | import ManageCoursePage from "./courses/ManageCoursePage"; // eslint-disable-line import/no-named-as-default 9 | import { ToastContainer } from "react-toastify"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | 12 | function App() { 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PageNotFound = () =>

Oops! Page not found.

; 4 | 5 | export default PageNotFound; 6 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/about/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AboutPage = () => ( 4 |
5 |

About

6 |

7 | This app uses React, Redux, React Router, and many other helpful 8 | libraries. 9 |

10 |
11 | ); 12 | 13 | export default AboutPage; 14 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | const Header = () => { 5 | const activeStyle = { color: "#F15B2A" }; 6 | return ( 7 | 20 | ); 21 | }; 22 | 23 | export default Header; 24 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/Header.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | import { mount, shallow } from "enzyme"; 4 | import { MemoryRouter } from "react-router-dom"; 5 | 6 | // Note how with shallow render you search for the React component tag 7 | it("contains 3 NavLinks via shallow", () => { 8 | const numLinks = shallow(
).find("NavLink").length; 9 | expect(numLinks).toEqual(3); 10 | }); 11 | 12 | // Note how with mount you search for the final rendered HTML since it generates the final DOM. 13 | // We also need to pull in React Router's memoryRouter for testing since the Header expects to have React Router's props passed in. 14 | it("contains 3 anchors via mount", () => { 15 | const numAnchors = mount( 16 | 17 |
18 | 19 | ).find("a").length; 20 | 21 | expect(numAnchors).toEqual(3); 22 | }); 23 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const SelectInput = ({ 5 | name, 6 | label, 7 | onChange, 8 | defaultOption, 9 | value, 10 | error, 11 | options 12 | }) => { 13 | return ( 14 |
15 | 16 |
17 | {/* Note, value is set here rather than on the option - docs: https://facebook.github.io/react/docs/forms.html */} 18 | 33 | {error &&
{error}
} 34 |
35 |
36 | ); 37 | }; 38 | 39 | SelectInput.propTypes = { 40 | name: PropTypes.string.isRequired, 41 | label: PropTypes.string.isRequired, 42 | onChange: PropTypes.func.isRequired, 43 | defaultOption: PropTypes.string, 44 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 45 | error: PropTypes.string, 46 | options: PropTypes.arrayOf(PropTypes.object) 47 | }; 48 | 49 | export default SelectInput; 50 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/Spinner.css: -------------------------------------------------------------------------------- 1 | /* via https://projects.lukehaas.me/css-loaders/ */ 2 | .loader, 3 | .loader:after { 4 | border-radius: 50%; 5 | width: 10em; 6 | height: 10em; 7 | } 8 | .loader { 9 | margin: 60px auto; 10 | font-size: 10px; 11 | position: relative; 12 | text-indent: -9999em; 13 | border-top: 1.1em solid rgba(255, 151, 0, 0.2); 14 | border-right: 1.1em solid rgba(255, 151, 0, 0.2); 15 | border-bottom: 1.1em solid rgba(255, 151, 0, 0.2); 16 | border-left: 1.1em solid #ff9700; 17 | -webkit-transform: translateZ(0); 18 | -ms-transform: translateZ(0); 19 | transform: translateZ(0); 20 | -webkit-animation: load8 1.1s infinite linear; 21 | animation: load8 1.1s infinite linear; 22 | } 23 | @-webkit-keyframes load8 { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(360deg); 30 | transform: rotate(360deg); 31 | } 32 | } 33 | @keyframes load8 { 34 | 0% { 35 | -webkit-transform: rotate(0deg); 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | -webkit-transform: rotate(360deg); 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Spinner.css"; 3 | 4 | const Spinner = () => { 5 | return
Loading...
; 6 | }; 7 | 8 | export default Spinner; 9 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/common/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const TextInput = ({ name, label, onChange, placeholder, value, error }) => { 5 | let wrapperClass = "form-group"; 6 | if (error && error.length > 0) { 7 | wrapperClass += " " + "has-error"; 8 | } 9 | 10 | return ( 11 |
12 | 13 |
14 | 22 | {error &&
{error}
} 23 |
24 |
25 | ); 26 | }; 27 | 28 | TextInput.propTypes = { 29 | name: PropTypes.string.isRequired, 30 | label: PropTypes.string.isRequired, 31 | onChange: PropTypes.func.isRequired, 32 | placeholder: PropTypes.string, 33 | value: PropTypes.string, 34 | error: PropTypes.string 35 | }; 36 | 37 | export default TextInput; 38 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CourseForm.Enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CourseForm from "./CourseForm"; 3 | import { shallow } from "enzyme"; 4 | 5 | function renderCourseForm(args) { 6 | const defaultProps = { 7 | authors: [], 8 | course: {}, 9 | saving: false, 10 | errors: {}, 11 | onSave: () => {}, 12 | onChange: () => {} 13 | }; 14 | 15 | const props = { ...defaultProps, ...args }; 16 | return shallow(); 17 | } 18 | 19 | it("renders form and header", () => { 20 | const wrapper = renderCourseForm(); 21 | // console.log(wrapper.debug()); 22 | expect(wrapper.find("form").length).toBe(1); 23 | expect(wrapper.find("h2").text()).toEqual("Add Course"); 24 | }); 25 | 26 | it('labels save buttons as "Save" when not saving', () => { 27 | const wrapper = renderCourseForm(); 28 | expect(wrapper.find("button").text()).toBe("Save"); 29 | }); 30 | 31 | it('labels save button as "Saving..." when saving', () => { 32 | const wrapper = renderCourseForm({ saving: true }); 33 | expect(wrapper.find("button").text()).toBe("Saving..."); 34 | }); 35 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CourseForm.ReactTestingLibrary.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cleanup, render } from "react-testing-library"; 3 | import CourseForm from "./CourseForm"; 4 | 5 | afterEach(cleanup); 6 | 7 | function renderCourseForm(args) { 8 | let defaultProps = { 9 | authors: [], 10 | course: {}, 11 | saving: false, 12 | errors: {}, 13 | onSave: () => {}, 14 | onChange: () => {} 15 | }; 16 | 17 | const props = { ...defaultProps, ...args }; 18 | return render(); 19 | } 20 | 21 | it("should render Add Course header", () => { 22 | const { getByText } = renderCourseForm(); 23 | getByText("Add Course"); 24 | }); 25 | 26 | it('should label save button as "Save" when not saving', () => { 27 | const { getByText } = renderCourseForm(); 28 | getByText("Save"); 29 | }); 30 | 31 | it('should label save button as "Saving..." when saving', () => { 32 | const { getByText } = renderCourseForm({ saving: true }); 33 | getByText("Saving..."); 34 | }); 35 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CourseForm.Snapshots.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CourseForm from "./CourseForm"; 3 | import renderer from "react-test-renderer"; 4 | import { courses, authors } from "../../../tools/mockData"; 5 | 6 | it("sets submit button label 'Saving...' when saving is true", () => { 7 | const tree = renderer.create( 8 | 15 | ); 16 | 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | 20 | it("sets submit button label 'Save' when saving is false", () => { 21 | const tree = renderer.create( 22 | 29 | ); 30 | 31 | expect(tree).toMatchSnapshot(); 32 | }); 33 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CourseForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import TextInput from "../common/TextInput"; 4 | import SelectInput from "../common/SelectInput"; 5 | 6 | const CourseForm = ({ 7 | course, 8 | authors, 9 | onSave, 10 | onChange, 11 | saving = false, 12 | errors = {} 13 | }) => { 14 | return ( 15 |
16 |

{course.id ? "Edit" : "Add"} Course

17 | {errors.onSave && ( 18 |
19 | {errors.onSave} 20 |
21 | )} 22 | 29 | 30 | ({ 36 | value: author.id, 37 | text: author.name 38 | }))} 39 | onChange={onChange} 40 | error={errors.author} 41 | /> 42 | 43 | 50 | 51 | 54 | 55 | ); 56 | }; 57 | 58 | CourseForm.propTypes = { 59 | authors: PropTypes.array.isRequired, 60 | course: PropTypes.object.isRequired, 61 | errors: PropTypes.object, 62 | onSave: PropTypes.func.isRequired, 63 | onChange: PropTypes.func.isRequired, 64 | saving: PropTypes.bool 65 | }; 66 | 67 | export default CourseForm; 68 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CourseList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const CourseList = ({ courses, onDeleteClick }) => ( 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | {courses.map(course => { 18 | return ( 19 | 20 | 28 | 31 | 32 | 33 | 41 | 42 | ); 43 | })} 44 | 45 |
10 | TitleAuthorCategory 14 |
21 | 25 | Watch 26 | 27 | 29 | {course.title} 30 | {course.authorName}{course.category} 34 | 40 |
46 | ); 47 | 48 | CourseList.propTypes = { 49 | courses: PropTypes.array.isRequired, 50 | onDeleteClick: PropTypes.func.isRequired 51 | }; 52 | 53 | export default CourseList; 54 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/CoursesPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import * as courseActions from "../../redux/actions/courseActions"; 4 | import * as authorActions from "../../redux/actions/authorActions"; 5 | import PropTypes from "prop-types"; 6 | import { bindActionCreators } from "redux"; 7 | import CourseList from "./CourseList"; 8 | import { Redirect } from "react-router-dom"; 9 | import Spinner from "../common/Spinner"; 10 | import { toast } from "react-toastify"; 11 | 12 | class CoursesPage extends React.Component { 13 | state = { 14 | redirectToAddCoursePage: false 15 | }; 16 | 17 | componentDidMount() { 18 | const { courses, authors, actions } = this.props; 19 | 20 | if (courses.length === 0) { 21 | actions.loadCourses().catch(error => { 22 | alert("Loading courses failed" + error); 23 | }); 24 | } 25 | 26 | if (authors.length === 0) { 27 | actions.loadAuthors().catch(error => { 28 | alert("Loading authors failed" + error); 29 | }); 30 | } 31 | } 32 | 33 | handleDeleteCourse = async course => { 34 | toast.success("Course deleted"); 35 | try { 36 | await this.props.actions.deleteCourse(course); 37 | } catch (error) { 38 | toast.error("Delete failed. " + error.message, { autoClose: false }); 39 | } 40 | }; 41 | 42 | render() { 43 | return ( 44 | <> 45 | {this.state.redirectToAddCoursePage && } 46 |

Courses

47 | {this.props.loading ? ( 48 | 49 | ) : ( 50 | <> 51 | 58 | 59 | 63 | 64 | )} 65 | 66 | ); 67 | } 68 | } 69 | 70 | CoursesPage.propTypes = { 71 | authors: PropTypes.array.isRequired, 72 | courses: PropTypes.array.isRequired, 73 | actions: PropTypes.object.isRequired, 74 | loading: PropTypes.bool.isRequired 75 | }; 76 | 77 | function mapStateToProps(state) { 78 | return { 79 | courses: 80 | state.authors.length === 0 81 | ? [] 82 | : state.courses.map(course => { 83 | return { 84 | ...course, 85 | authorName: state.authors.find(a => a.id === course.authorId).name 86 | }; 87 | }), 88 | authors: state.authors, 89 | loading: state.apiCallsInProgress > 0 90 | }; 91 | } 92 | 93 | function mapDispatchToProps(dispatch) { 94 | return { 95 | actions: { 96 | loadCourses: bindActionCreators(courseActions.loadCourses, dispatch), 97 | loadAuthors: bindActionCreators(authorActions.loadAuthors, dispatch), 98 | deleteCourse: bindActionCreators(courseActions.deleteCourse, dispatch) 99 | } 100 | }; 101 | } 102 | 103 | export default connect( 104 | mapStateToProps, 105 | mapDispatchToProps 106 | )(CoursesPage); 107 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/ManageCoursePage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { loadCourses, saveCourse } from "../../redux/actions/courseActions"; 4 | import { loadAuthors } from "../../redux/actions/authorActions"; 5 | import PropTypes from "prop-types"; 6 | import CourseForm from "./CourseForm"; 7 | import { newCourse } from "../../../tools/mockData"; 8 | import Spinner from "../common/Spinner"; 9 | import { toast } from "react-toastify"; 10 | 11 | export function ManageCoursePage({ 12 | courses, 13 | authors, 14 | loadAuthors, 15 | loadCourses, 16 | saveCourse, 17 | history, 18 | ...props 19 | }) { 20 | const [course, setCourse] = useState({ ...props.course }); 21 | const [errors, setErrors] = useState({}); 22 | const [saving, setSaving] = useState(false); 23 | 24 | useEffect(() => { 25 | if (courses.length === 0) { 26 | loadCourses().catch(error => { 27 | alert("Loading courses failed" + error); 28 | }); 29 | } else { 30 | setCourse({ ...props.course }); 31 | } 32 | 33 | if (authors.length === 0) { 34 | loadAuthors().catch(error => { 35 | alert("Loading authors failed" + error); 36 | }); 37 | } 38 | }, [props.course]); 39 | 40 | function handleChange(event) { 41 | const { name, value } = event.target; 42 | setCourse(prevCourse => ({ 43 | ...prevCourse, 44 | [name]: name === "authorId" ? parseInt(value, 10) : value 45 | })); 46 | } 47 | 48 | function formIsValid() { 49 | const { title, authorId, category } = course; 50 | const errors = {}; 51 | 52 | if (!title) errors.title = "Title is required."; 53 | if (!authorId) errors.author = "Author is required"; 54 | if (!category) errors.category = "Category is required"; 55 | 56 | setErrors(errors); 57 | // Form is valid if the errors object still has no properties 58 | return Object.keys(errors).length === 0; 59 | } 60 | 61 | function handleSave(event) { 62 | event.preventDefault(); 63 | if (!formIsValid()) return; 64 | setSaving(true); 65 | saveCourse(course) 66 | .then(() => { 67 | toast.success("Course saved."); 68 | history.push("/courses"); 69 | }) 70 | .catch(error => { 71 | setSaving(false); 72 | setErrors({ onSave: error.message }); 73 | }); 74 | } 75 | 76 | return authors.length === 0 || courses.length === 0 ? ( 77 | 78 | ) : ( 79 | 87 | ); 88 | } 89 | 90 | ManageCoursePage.propTypes = { 91 | course: PropTypes.object.isRequired, 92 | authors: PropTypes.array.isRequired, 93 | courses: PropTypes.array.isRequired, 94 | loadCourses: PropTypes.func.isRequired, 95 | loadAuthors: PropTypes.func.isRequired, 96 | saveCourse: PropTypes.func.isRequired, 97 | history: PropTypes.object.isRequired 98 | }; 99 | 100 | export function getCourseBySlug(courses, slug) { 101 | return courses.find(course => course.slug === slug) || null; 102 | } 103 | 104 | function mapStateToProps(state, ownProps) { 105 | const slug = ownProps.match.params.slug; 106 | const course = 107 | slug && state.courses.length > 0 108 | ? getCourseBySlug(state.courses, slug) 109 | : newCourse; 110 | return { 111 | course, 112 | courses: state.courses, 113 | authors: state.authors 114 | }; 115 | } 116 | 117 | const mapDispatchToProps = { 118 | loadCourses, 119 | loadAuthors, 120 | saveCourse 121 | }; 122 | 123 | export default connect( 124 | mapStateToProps, 125 | mapDispatchToProps 126 | )(ManageCoursePage); 127 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/ManageCoursePage.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { authors, newCourse, courses } from "../../../tools/mockData"; 4 | import { ManageCoursePage } from "./ManageCoursePage"; 5 | 6 | function render(args) { 7 | const defaultProps = { 8 | authors, 9 | courses, 10 | // Passed from React Router in real app, so just stubbing in for test. 11 | // Could also choose to use MemoryRouter as shown in Header.test.js, 12 | // or even wrap with React Router, depending on whether I 13 | // need to test React Router related behavior. 14 | history: {}, 15 | saveCourse: jest.fn(), 16 | loadAuthors: jest.fn(), 17 | loadCourses: jest.fn(), 18 | course: newCourse, 19 | match: {} 20 | }; 21 | 22 | const props = { ...defaultProps, ...args }; 23 | 24 | return mount(); 25 | } 26 | 27 | it("sets error when attempting to save an empty title field", () => { 28 | const wrapper = render(); 29 | wrapper.find("form").simulate("submit"); 30 | const error = wrapper.find(".alert").first(); 31 | expect(error.text()).toBe("Title is required."); 32 | }); 33 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/courses/__snapshots__/CourseForm.Snapshots.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`sets submit button label 'Save' when saving is false 1`] = ` 4 |
7 |

8 | Edit 9 | Course 10 |

11 |
14 | 19 |
22 | 29 |
30 |
31 |
34 | 39 |
42 | 69 |
70 |
71 |
74 | 79 |
82 | 89 |
90 |
91 | 98 |
99 | `; 100 | 101 | exports[`sets submit button label 'Saving...' when saving is true 1`] = ` 102 |
105 |

106 | Edit 107 | Course 108 |

109 |
112 | 117 |
120 | 127 |
128 |
129 |
132 | 137 |
140 | 167 |
168 |
169 |
172 | 177 |
180 | 187 |
188 |
189 | 196 |
197 | `; 198 | -------------------------------------------------------------------------------- /courses-app-redux/src/components/home/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const HomePage = () => ( 5 |
6 |

Pluralsight Administration

7 |

React, Redux and React Router for ultra-responsive web apps.

8 | 9 | Learn more 10 | 11 |
12 | ); 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /courses-app-redux/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/courses-app-redux/src/favicon.ico -------------------------------------------------------------------------------- /courses-app-redux/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | max-width: 850px; 3 | margin: 0 auto; 4 | } 5 | 6 | nav { 7 | padding: 20px 0 20px 0; 8 | } 9 | -------------------------------------------------------------------------------- /courses-app-redux/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pluralsight Redux 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /courses-app-redux/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import "bootstrap/dist/css/bootstrap.min.css"; 5 | import App from "./components/App"; 6 | import "./index.css"; 7 | import configureStore from "./redux/configureStore"; 8 | import { Provider as ReduxProvider } from "react-redux"; 9 | 10 | const store = configureStore(); 11 | 12 | render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("app") 19 | ); 20 | -------------------------------------------------------------------------------- /courses-app-redux/src/index.test.js: -------------------------------------------------------------------------------- 1 | it("should pass", () => { 2 | expect(true).toEqual(true); 3 | }); 4 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CREATE_COURSE = "CREATE_COURSE"; 2 | export const LOAD_COURSES_SUCCESS = "LOAD_COURSES_SUCCESS"; 3 | export const LOAD_AUTHORS_SUCCESS = "LOAD_AUTHORS_SUCCESS"; 4 | export const CREATE_COURSE_SUCCESS = "CREATE_COURSE_SUCCESS"; 5 | export const UPDATE_COURSE_SUCCESS = "UPDATE_COURSE_SUCCESS"; 6 | export const BEGIN_API_CALL = "BEGIN_API_CALL"; 7 | export const API_CALL_ERROR = "API_CALL_ERROR"; 8 | 9 | // By convention, actions that end in "_SUCCESS" are assumed to have been the result of a completed 10 | // API call. But since we're doing an optimistic delete, we're hiding loading state. 11 | // So this action name deliberately omits the "_SUCCESS" suffix. 12 | // If it had one, our apiCallsInProgress counter would be decremented below zero 13 | // because we're not incrementing the number of apiCallInProgress when the delete request begins. 14 | export const DELETE_COURSE_OPTIMISTIC = "DELETE_COURSE_OPTIMISTIC"; 15 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/actions/apiStatusActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes"; 2 | 3 | export function beginApiCall() { 4 | return { type: types.BEGIN_API_CALL }; 5 | } 6 | 7 | export function apiCallError() { 8 | return { type: types.API_CALL_ERROR }; 9 | } 10 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/actions/authorActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes"; 2 | import * as authorApi from "../../api/authorApi"; 3 | import { beginApiCall, apiCallError } from "./apiStatusActions"; 4 | 5 | export function loadAuthorsSuccess(authors) { 6 | return { type: types.LOAD_AUTHORS_SUCCESS, authors }; 7 | } 8 | 9 | export function loadAuthors() { 10 | return function(dispatch) { 11 | dispatch(beginApiCall()); 12 | return authorApi 13 | .getAuthors() 14 | .then(authors => { 15 | dispatch(loadAuthorsSuccess(authors)); 16 | }) 17 | .catch(error => { 18 | dispatch(apiCallError(error)); 19 | throw error; 20 | }); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/actions/courseActions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes"; 2 | import * as courseApi from "../../api/courseApi"; 3 | import { beginApiCall, apiCallError } from "./apiStatusActions"; 4 | 5 | export function loadCourseSuccess(courses) { 6 | return { type: types.LOAD_COURSES_SUCCESS, courses }; 7 | } 8 | 9 | export function createCourseSuccess(course) { 10 | return { type: types.CREATE_COURSE_SUCCESS, course }; 11 | } 12 | 13 | export function updateCourseSuccess(course) { 14 | return { type: types.UPDATE_COURSE_SUCCESS, course }; 15 | } 16 | 17 | export function deleteCourseOptimistic(course) { 18 | return { type: types.DELETE_COURSE_OPTIMISTIC, course }; 19 | } 20 | 21 | export function loadCourses() { 22 | return function(dispatch) { 23 | dispatch(beginApiCall()); 24 | return courseApi 25 | .getCourses() 26 | .then(courses => { 27 | dispatch(loadCourseSuccess(courses)); 28 | }) 29 | .catch(error => { 30 | dispatch(apiCallError(error)); 31 | throw error; 32 | }); 33 | }; 34 | } 35 | 36 | export function saveCourse(course) { 37 | //eslint-disable-next-line no-unused-vars 38 | return function(dispatch, getState) { 39 | dispatch(beginApiCall()); 40 | return courseApi 41 | .saveCourse(course) 42 | .then(savedCourse => { 43 | course.id 44 | ? dispatch(updateCourseSuccess(savedCourse)) 45 | : dispatch(createCourseSuccess(savedCourse)); 46 | }) 47 | .catch(error => { 48 | dispatch(apiCallError(error)); 49 | throw error; 50 | }); 51 | }; 52 | } 53 | 54 | export function deleteCourse(course) { 55 | return function(dispatch) { 56 | // Doing optimistic delete, so not dispatching begin/end api call 57 | // actions, or apiCallError action since we're not showing the loading status for this. 58 | dispatch(deleteCourseOptimistic(course)); 59 | return courseApi.deleteCourse(course.id); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/actions/courseActions.test.js: -------------------------------------------------------------------------------- 1 | import * as courseActions from "./courseActions"; 2 | import * as types from "./actionTypes"; 3 | import { courses } from "../../../tools/mockData"; 4 | import thunk from "redux-thunk"; 5 | import fetchMock from "fetch-mock"; 6 | import configureMockStore from "redux-mock-store"; 7 | 8 | // Test an async action 9 | const middleware = [thunk]; 10 | const mockStore = configureMockStore(middleware); 11 | 12 | describe("Async Actions", () => { 13 | afterEach(() => { 14 | fetchMock.restore(); 15 | }); 16 | 17 | describe("Load Courses Thunk", () => { 18 | it("should create BEGIN_API_CALL and LOAD_COURSES_SUCCESS when loading courses", () => { 19 | fetchMock.mock("*", { 20 | body: courses, 21 | headers: { "content-type": "application/json" } 22 | }); 23 | 24 | const expectedActions = [ 25 | { type: types.BEGIN_API_CALL }, 26 | { type: types.LOAD_COURSES_SUCCESS, courses } 27 | ]; 28 | 29 | const store = mockStore({ courses: [] }); 30 | return store.dispatch(courseActions.loadCourses()).then(() => { 31 | expect(store.getActions()).toEqual(expectedActions); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | describe("createCourseSuccess", () => { 38 | it("should create a CREATE_COURSE_SUCCESS action", () => { 39 | //arrange 40 | const course = courses[0]; 41 | const expectedAction = { 42 | type: types.CREATE_COURSE_SUCCESS, 43 | course 44 | }; 45 | 46 | //act 47 | const action = courseActions.createCourseSuccess(course); 48 | 49 | //assert 50 | expect(action).toEqual(expectedAction); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import rootReducer from "./reducers"; 3 | import reduxImmutableStateInvariant from "redux-immutable-state-invariant"; 4 | import thunk from "redux-thunk"; 5 | 6 | export default function configureStore(initialState) { 7 | const composeEnhancers = 8 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools 9 | 10 | return createStore( 11 | rootReducer, 12 | initialState, 13 | composeEnhancers(applyMiddleware(thunk, reduxImmutableStateInvariant())) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | // Use CommonJS require below so we can dynamically import during build-time. 2 | if (process.env.NODE_ENV === "production") { 3 | module.exports = require("./configureStore.prod"); 4 | } else { 5 | module.exports = require("./configureStore.dev"); 6 | } 7 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import rootReducer from "./reducers"; 3 | import thunk from "redux-thunk"; 4 | 5 | export default function configureStore(initialState) { 6 | return createStore(rootReducer, initialState, applyMiddleware(thunk)); 7 | } 8 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/apiStatusReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes"; 2 | import initialState from "./initialState"; 3 | 4 | function actionTypeEndsInSuccess(type) { 5 | return type.substring(type.length - 8) === "_SUCCESS"; 6 | } 7 | 8 | export default function apiCallStatusReducer( 9 | state = initialState.apiCallsInProgress, 10 | action 11 | ) { 12 | if (action.type == types.BEGIN_API_CALL) { 13 | return state + 1; 14 | } else if ( 15 | action.type === types.API_CALL_ERROR || 16 | actionTypeEndsInSuccess(action.type) 17 | ) { 18 | return state - 1; 19 | } 20 | 21 | return state; 22 | } 23 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/authorReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes"; 2 | import initialState from "./initialState"; 3 | 4 | export default function authorReducer(state = initialState.authors, action) { 5 | switch (action.type) { 6 | case types.LOAD_AUTHORS_SUCCESS: 7 | return action.authors; 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/courseReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes"; 2 | import initialState from "./initialState"; 3 | 4 | export default function courseReducer(state = initialState.courses, action) { 5 | switch (action.type) { 6 | case types.CREATE_COURSE_SUCCESS: 7 | return [...state, { ...action.course }]; 8 | case types.UPDATE_COURSE_SUCCESS: 9 | return state.map(course => 10 | course.id === action.course.id ? action.course : course 11 | ); 12 | case types.LOAD_COURSES_SUCCESS: 13 | return action.courses; 14 | case types.DELETE_COURSE_OPTIMISTIC: 15 | return state.filter(course => course.id !== action.course.id); 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/courseReducer.test.js: -------------------------------------------------------------------------------- 1 | import courseReducer from "./courseReducer"; 2 | import * as actions from "../actions/courseActions"; 3 | 4 | it("should add course when passed CREATE_COURSE_SUCCESS", () => { 5 | // arrange 6 | const initialState = [ 7 | { 8 | title: "A" 9 | }, 10 | { 11 | title: "B" 12 | } 13 | ]; 14 | 15 | const newCourse = { 16 | title: "C" 17 | }; 18 | 19 | const action = actions.createCourseSuccess(newCourse); 20 | 21 | // act 22 | const newState = courseReducer(initialState, action); 23 | 24 | // assert 25 | expect(newState.length).toEqual(3); 26 | expect(newState[0].title).toEqual("A"); 27 | expect(newState[1].title).toEqual("B"); 28 | expect(newState[2].title).toEqual("C"); 29 | }); 30 | 31 | it("should update course when passed UPDATE_COURSE_SUCCESS", () => { 32 | // arrange 33 | const initialState = [ 34 | { id: 1, title: "A" }, 35 | { id: 2, title: "B" }, 36 | { id: 3, title: "C" } 37 | ]; 38 | 39 | const course = { id: 2, title: "New Title" }; 40 | const action = actions.updateCourseSuccess(course); 41 | 42 | // act 43 | const newState = courseReducer(initialState, action); 44 | const updatedCourse = newState.find(a => a.id == course.id); 45 | const untouchedCourse = newState.find(a => a.id == 1); 46 | 47 | // assert 48 | expect(updatedCourse.title).toEqual("New Title"); 49 | expect(untouchedCourse.title).toEqual("A"); 50 | expect(newState.length).toEqual(3); 51 | }); 52 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import courses from "./courseReducer"; 3 | import authors from "./authorReducer"; 4 | import apiCallsInProgress from "./apiStatusReducer"; 5 | 6 | const rootReducer = combineReducers({ 7 | courses, 8 | authors, 9 | apiCallsInProgress 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/reducers/initialState.js: -------------------------------------------------------------------------------- 1 | export default { 2 | courses: [], 3 | authors: [], 4 | apiCallsInProgress: 0 5 | }; 6 | -------------------------------------------------------------------------------- /courses-app-redux/src/redux/store.test.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import rootReducer from "./reducers"; 3 | import initialState from "./reducers/initialState"; 4 | import * as courseActions from "./actions/courseActions"; 5 | 6 | it("Should handle creating courses", function() { 7 | // arrange 8 | const store = createStore(rootReducer, initialState); 9 | const course = { 10 | title: "Clean Code" 11 | }; 12 | 13 | // act 14 | const action = courseActions.createCourseSuccess(course); 15 | store.dispatch(action); 16 | 17 | // assert 18 | const createdCourse = store.getState().courses[0]; 19 | expect(createdCourse).toEqual(course); 20 | }); 21 | -------------------------------------------------------------------------------- /courses-app-redux/tools/apiServer.js: -------------------------------------------------------------------------------- 1 | /* 2 | This uses json-server, but with the module approach: https://github.com/typicode/json-server#module 3 | Downside: You can't pass the json-server command line options. 4 | Instead, can override some defaults by passing a config object to jsonServer.defaults(); 5 | You have to check the source code to set some items. 6 | Examples: 7 | Validation/Customization: https://github.com/typicode/json-server/issues/266 8 | Delay: https://github.com/typicode/json-server/issues/534 9 | ID: https://github.com/typicode/json-server/issues/613#issuecomment-325393041 10 | Relevant source code: https://github.com/typicode/json-server/blob/master/src/cli/run.js 11 | */ 12 | 13 | /* eslint-disable no-console */ 14 | const jsonServer = require("json-server"); 15 | const server = jsonServer.create(); 16 | const path = require("path"); 17 | const router = jsonServer.router(path.join(__dirname, "db.json")); 18 | 19 | // Can pass a limited number of options to this to override (some) defaults. See https://github.com/typicode/json-server#api 20 | const middlewares = jsonServer.defaults(); 21 | 22 | // Set default middlewares (logger, static, cors and no-cache) 23 | server.use(middlewares); 24 | 25 | // To handle POST, PUT and PATCH you need to use a body-parser. Using JSON Server's bodyParser 26 | server.use(jsonServer.bodyParser); 27 | 28 | // Simulate delay on all requests 29 | server.use(function(req, res, next) { 30 | setTimeout(next, 2000); 31 | }); 32 | 33 | // Declaring custom routes below. Add custom routes before JSON Server router 34 | 35 | // Add createdAt to all POSTS 36 | server.use((req, res, next) => { 37 | if (req.method === "POST") { 38 | req.body.createdAt = Date.now(); 39 | } 40 | // Continue to JSON Server router 41 | next(); 42 | }); 43 | 44 | server.post("/courses/", function(req, res, next) { 45 | const error = validateCourse(req.body); 46 | if (error) { 47 | res.status(400).send(error); 48 | } else { 49 | req.body.slug = createSlug(req.body.title); // Generate a slug for new courses. 50 | next(); 51 | } 52 | }); 53 | 54 | // Use default router 55 | server.use(router); 56 | 57 | // Start server 58 | const port = 3001; 59 | server.listen(port, () => { 60 | console.log(`JSON Server is running on port ${port}`); 61 | }); 62 | 63 | // Centralized logic 64 | 65 | // Returns a URL friendly slug 66 | function createSlug(value) { 67 | return value 68 | .replace(/[^a-z0-9_]+/gi, "-") 69 | .replace(/^-|-$/g, "") 70 | .toLowerCase(); 71 | } 72 | 73 | function validateCourse(course) { 74 | if (!course.title) return "Title is required."; 75 | if (!course.authorId) return "Author is required."; 76 | if (!course.category) return "Category is required."; 77 | return ""; 78 | } 79 | -------------------------------------------------------------------------------- /courses-app-redux/tools/createMockDb.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const mockData = require("./mockData"); 5 | 6 | const { courses, authors } = mockData; 7 | const data = JSON.stringify({ courses, authors }); 8 | const filepath = path.join(__dirname, "db.json"); 9 | 10 | fs.writeFile(filepath, data, function(err) { 11 | err ? console.log(err) : console.log("Mock DB created."); 12 | }); 13 | -------------------------------------------------------------------------------- /courses-app-redux/tools/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "courses": [ 3 | { 4 | "id": 1, 5 | "title": "3Securing React Apps with Auth0", 6 | "slug": "react-auth0-authentication-security", 7 | "authorId": 1, 8 | "category": "JavaScript" 9 | }, 10 | { 11 | "id": 3, 12 | "title": "Creating Reusable React Components", 13 | "slug": "react-creating-reusable-components", 14 | "authorId": 1, 15 | "category": "JavaScript" 16 | }, 17 | { 18 | "id": 4, 19 | "title": "Building a JavaScript Development Environment", 20 | "slug": "javascript-development-environment", 21 | "authorId": 1, 22 | "category": "JavaScript" 23 | }, 24 | { 25 | "id": 5, 26 | "title": "Building Applications with React and Redux", 27 | "slug": "react-redux-react-router-es6", 28 | "authorId": 1, 29 | "category": "JavaScript" 30 | }, 31 | { 32 | "id": 6, 33 | "title": "Building Applications in React and Flux", 34 | "slug": "react-flux-building-applications", 35 | "authorId": 1, 36 | "category": "JavaScript" 37 | }, 38 | { 39 | "id": 7, 40 | "title": "Clean Code: Writing Code for Humans", 41 | "slug": "writing-clean-code-humans", 42 | "authorId": 1, 43 | "category": "Software Practices" 44 | }, 45 | { 46 | "id": 8, 47 | "title": "Architecting Applications for the Real World", 48 | "slug": "architecting-applications-dotnet", 49 | "authorId": 1, 50 | "category": "Software Architecture" 51 | }, 52 | { 53 | "id": 9, 54 | "title": "Becoming an Outlier: Reprogramming the Developer Mind", 55 | "slug": "career-reboot-for-developer-mind", 56 | "authorId": 1, 57 | "category": "Career" 58 | }, 59 | { 60 | "id": 10, 61 | "title": "Web Component Fundamentals", 62 | "slug": "web-components-shadow-dom", 63 | "authorId": 1, 64 | "category": "HTML5" 65 | }, 66 | { 67 | "id": 11, 68 | "title": "sdgsdg", 69 | "authorId": 1, 70 | "category": "cat", 71 | "createdAt": 1559342094630, 72 | "slug": "sdgsdg" 73 | } 74 | ], 75 | "authors": [ 76 | { 77 | "id": 1, 78 | "name": "Cory House" 79 | }, 80 | { 81 | "id": 2, 82 | "name": "Scott Allen" 83 | }, 84 | { 85 | "id": 3, 86 | "name": "Dan Wahlin" 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /courses-app-redux/tools/fileMock.js: -------------------------------------------------------------------------------- 1 | // Mocks file imports for Jest. As suggested by https://jestjs.io/docs/en/webpack 2 | module.exports = "test-file-stub"; 3 | -------------------------------------------------------------------------------- /courses-app-redux/tools/mockData.js: -------------------------------------------------------------------------------- 1 | const courses = [ 2 | { 3 | id: 1, 4 | title: "Securing React Apps with Auth0", 5 | slug: "react-auth0-authentication-security", 6 | authorId: 1, 7 | category: "JavaScript" 8 | }, 9 | { 10 | id: 2, 11 | title: "React: The Big Picture", 12 | slug: "react-big-picture", 13 | authorId: 1, 14 | category: "JavaScript" 15 | }, 16 | { 17 | id: 3, 18 | title: "Creating Reusable React Components", 19 | slug: "react-creating-reusable-components", 20 | authorId: 1, 21 | category: "JavaScript" 22 | }, 23 | { 24 | id: 4, 25 | title: "Building a JavaScript Development Environment", 26 | slug: "javascript-development-environment", 27 | authorId: 1, 28 | category: "JavaScript" 29 | }, 30 | { 31 | id: 5, 32 | title: "Building Applications with React and Redux", 33 | slug: "react-redux-react-router-es6", 34 | authorId: 1, 35 | category: "JavaScript" 36 | }, 37 | { 38 | id: 6, 39 | title: "Building Applications in React and Flux", 40 | slug: "react-flux-building-applications", 41 | authorId: 1, 42 | category: "JavaScript" 43 | }, 44 | { 45 | id: 7, 46 | title: "Clean Code: Writing Code for Humans", 47 | slug: "writing-clean-code-humans", 48 | authorId: 1, 49 | category: "Software Practices" 50 | }, 51 | { 52 | id: 8, 53 | title: "Architecting Applications for the Real World", 54 | slug: "architecting-applications-dotnet", 55 | authorId: 1, 56 | category: "Software Architecture" 57 | }, 58 | { 59 | id: 9, 60 | title: "Becoming an Outlier: Reprogramming the Developer Mind", 61 | slug: "career-reboot-for-developer-mind", 62 | authorId: 1, 63 | category: "Career" 64 | }, 65 | { 66 | id: 10, 67 | title: "Web Component Fundamentals", 68 | slug: "web-components-shadow-dom", 69 | authorId: 1, 70 | category: "HTML5" 71 | } 72 | ]; 73 | 74 | const authors = [ 75 | { id: 1, name: "Cory House" }, 76 | { id: 2, name: "Scott Allen" }, 77 | { id: 3, name: "Dan Wahlin" } 78 | ]; 79 | 80 | const newCourse = { 81 | id: null, 82 | title: "", 83 | authorId: null, 84 | category: "" 85 | }; 86 | 87 | // Using CommonJS style export so we can consume via Node (without using Babel-node) 88 | module.exports = { 89 | newCourse, 90 | courses, 91 | authors 92 | }; 93 | -------------------------------------------------------------------------------- /courses-app-redux/tools/styleMock.js: -------------------------------------------------------------------------------- 1 | // Mocks CSS imports for Jest. As suggested by https://jestjs.io/docs/en/webpack 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /courses-app-redux/tools/testSetup.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | configure({ adapter: new Adapter() }); 4 | -------------------------------------------------------------------------------- /courses-app-redux/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | process.env.NODE_ENV = "development"; 6 | 7 | module.exports = { 8 | mode: "development", 9 | target: "web", 10 | devtool: "cheap-module-source-map", 11 | entry: "./src/index", 12 | output: { 13 | path: path.resolve(__dirname, "build"), 14 | publicPath: "/", 15 | filename: "bundle.js" 16 | }, 17 | devServer: { 18 | stats: "minimal", 19 | overlay: true, 20 | historyApiFallback: true, 21 | disableHostCheck: true, 22 | headers: { "Access-Control-Allow-Origin": "*" }, 23 | https: false 24 | }, 25 | plugins: [ 26 | new webpack.DefinePlugin({ 27 | "process.env.API_URL": JSON.stringify("http://localhost:3001") 28 | }), 29 | new HtmlWebpackPlugin({ 30 | template: "src/index.html", 31 | favicon: "src/favicon.ico" 32 | }) 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.(js|jsx)$/, 38 | exclude: /node_modules/, 39 | use: ["babel-loader", "eslint-loader"] 40 | }, 41 | { 42 | test: /(\.css)$/, 43 | use: ["style-loader", "css-loader"] 44 | } 45 | ] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /courses-app-redux/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const webpackBundleAnalyzer = require("webpack-bundle-analyzer"); 6 | 7 | process.env.NODE_ENV = "production"; 8 | 9 | module.exports = { 10 | mode: "production", 11 | target: "web", 12 | devtool: "source-map", 13 | entry: "./src/index", 14 | output: { 15 | path: path.resolve(__dirname, "build"), 16 | publicPath: "/", 17 | filename: "bundle.js" 18 | }, 19 | plugins: [ 20 | // Display bundle stats 21 | new webpackBundleAnalyzer.BundleAnalyzerPlugin({ analyzerMode: "static" }), 22 | 23 | new MiniCssExtractPlugin({ 24 | filename: "[name].[contenthash].css" 25 | }), 26 | 27 | new webpack.DefinePlugin({ 28 | // This global makes sure React is built in prod mode. 29 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), 30 | "process.env.API_URL": JSON.stringify("http://localhost:3001") 31 | }), 32 | new HtmlWebpackPlugin({ 33 | template: "src/index.html", 34 | favicon: "src/favicon.ico", 35 | minify: { 36 | // see https://github.com/kangax/html-minifier#options-quick-reference 37 | removeComments: true, 38 | collapseWhitespace: true, 39 | removeRedundantAttributes: true, 40 | useShortDoctype: true, 41 | removeEmptyAttributes: true, 42 | removeStyleLinkTypeAttributes: true, 43 | keepClosingSlash: true, 44 | minifyJS: true, 45 | minifyCSS: true, 46 | minifyURLs: true 47 | } 48 | }) 49 | ], 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.(js|jsx)$/, 54 | exclude: /node_modules/, 55 | use: ["babel-loader", "eslint-loader"] 56 | }, 57 | { 58 | test: /(\.css)$/, 59 | use: [ 60 | MiniCssExtractPlugin.loader, 61 | { 62 | loader: "css-loader", 63 | options: { 64 | sourceMap: true 65 | } 66 | }, 67 | { 68 | loader: "postcss-loader", 69 | options: { 70 | plugins: () => [require("cssnano")], 71 | sourceMap: true 72 | } 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /dev-environment/.env: -------------------------------------------------------------------------------- 1 | # Env vars that being with REACT_APP_ are automatically replaced in code by create-react-app. 2 | # TODO: This should be .gitignored. 3 | # Alternatively, can specify env vars in npm start/npm build scripts. 4 | REACT_APP_API_URL=http://localhost:3001 -------------------------------------------------------------------------------- /dev-environment/.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Code Review Checklist 2 | 3 | This checklist is stored in `.github/PULL_REQUEST_TEMPLATE.md`. This will automatically populate in all pull requests. Edit as desired. 4 | 5 | - [ ] Ticket is referenced in PR description 6 | - [ ] The ticket is "In Progress" on the team board 7 | - [ ] No linting issues 8 | - [ ] Automated tests exist and pass 9 | -------------------------------------------------------------------------------- /dev-environment/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # json-server db 26 | db.json -------------------------------------------------------------------------------- /dev-environment/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | import "@storybook/addon-a11y/register"; 4 | import "@storybook/addon-knobs/register"; 5 | -------------------------------------------------------------------------------- /dev-environment/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator } from "@storybook/react"; 2 | import { withA11y } from "@storybook/addon-a11y"; 3 | import { withInfo } from "@storybook/addon-info"; 4 | import { withKnobs } from "@storybook/addon-knobs"; 5 | import "storybook-chromatic"; 6 | 7 | const req = require.context("../src/components", true, /\.stories\.js$/); 8 | 9 | function loadStories() { 10 | req.keys().forEach(filename => req(filename)); 11 | } 12 | 13 | // Globally enable addons 14 | addDecorator(withA11y); 15 | addDecorator(withInfo); 16 | addDecorator(withKnobs); 17 | 18 | configure(loadStories, module); 19 | -------------------------------------------------------------------------------- /dev-environment/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "xabikos.reactsnippets", 5 | "christian-kohler.path-intellisense", 6 | "dbaeumer.vscode-eslint", 7 | "asvetliakov.snapshot-tools", 8 | "mikestead.dotenv", 9 | "tombonnike.vscode-status-bar-format-toggle", 10 | "wix.glean", 11 | "msjsdiag.debugger-for-chrome", 12 | "pflannery.vscode-versionlens" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /dev-environment/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Chrome", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}/src", 10 | "sourceMapPathOverrides": { 11 | "webpack:///src/*": "${webRoot}/*" 12 | } 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /dev-environment/README.md: -------------------------------------------------------------------------------- 1 | # ReactJSConsulting Dev Environment 2 | 3 | ## Quick Start 4 | 5 | ``` 6 | git clone https://github.com/coryhouse/reactjsconsulting reactjsconsulting 7 | cd reactjsconsulting/dev-environment 8 | npm i 9 | npm start 10 | ``` 11 | 12 | This installs dependencies, and starts the app and mock API. 13 | 14 | ## Recommended Extensions 15 | 16 | It's recommended to install the extensions listed in the .vscode directory. VS Code will automatically prompt you to install the extensions the first time you open the project. 17 | 18 | This project uses [Prettier](https://prettier.io) to autoformat code via a pre-commit hook. It's recommended to also run the Prettier extension in VS Code and enable format on save: 19 | 20 | 1. Install the Prettier extension 21 | 1. Open VS Code settings 22 | 1. Search for "Format on save" and enable 23 | 24 | ## npm Scripts 25 | 26 | | Script | Description | 27 | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | 28 | | start | Start the app and [json-server](https://github.com/typicode/json-server) mock API | 29 | | test | Run automated unit tests via [Jest](https://jestjs.io) and [React Testing Library](https://github.com/kentcdodds/react-testing-library) | 30 | | cypress | Run automated integration tests via [Cypress](https://www.cypress.io/) | 31 | | build | Generate the production build | 32 | | storybook | Run [Storybook](https://storybook.js.org/) to build components in isolation and view existing components | 33 | | build-storybook | Build Storybook for production deployment (not currently hosted anywhere) | 34 | | chromatic | Run image tests on Storybook using [Chromatic](https://www.chromaticqa.com/) | 35 | 36 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 37 | 38 | ## Mock API 39 | 40 | This project uses [json-server](https://github.com/typicode/json-server) as a mock API. It utilizes mock data in `/tools/mockData.js` to populate `db.json` when you run `npm start`. The `db.json` file simulates a database, so the mock API supports CRUD. 41 | -------------------------------------------------------------------------------- /dev-environment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rjc-dev-environment", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "run-p start:dev start:api", 7 | "start:dev": "react-scripts start", 8 | "prestart:api": "node tools/createMockDb.js", 9 | "start:api": "nodemon --ignore tools/db.json tools/apiServer.js", 10 | "build": "react-scripts build --stats && webpack-bundle-analyzer build/bundle-stats.json -m server", 11 | "test": "react-scripts test", 12 | "test:ci": "cross-env NODE_ENV=CI react-scripts test", 13 | "eject": "react-scripts eject", 14 | "storybook": "start-storybook -p 9009 -s public", 15 | "build-storybook": "build-storybook -s public", 16 | "cypress": "cypress", 17 | "chromatic": "chromatic test --app-code=\"gkbd875bekh\"", 18 | "test:all": "run-p test:ci cypress chromatic", 19 | "lint": "eslint src cypress/integration" 20 | }, 21 | "dependencies": { 22 | "axios": "^0.19.0", 23 | "bootstrap": "^4.3.1", 24 | "react": "16.11.0", 25 | "react-dom": "16.11.0", 26 | "react-router-dom": "^5.1.2", 27 | "react-scripts": "3.2.0", 28 | "reactstrap": "^7.1.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.4.5", 32 | "@storybook/addon-a11y": "^5.1.9", 33 | "@storybook/addon-actions": "^5.1.9", 34 | "@storybook/addon-info": "^5.1.9", 35 | "@storybook/addon-knobs": "^5.1.9", 36 | "@storybook/addon-links": "^5.1.9", 37 | "@storybook/addons": "^5.1.9", 38 | "@storybook/react": "^5.1.9", 39 | "@testing-library/react": "^8.0.4", 40 | "cross-env": "^5.2.0", 41 | "cypress": "^3.3.2", 42 | "eslint-plugin-cypress": "^2.7.0", 43 | "eslint-plugin-lean-imports": "^0.3.3", 44 | "eslint-plugin-no-loops": "^0.3.0", 45 | "eslint-plugin-no-only-tests": "^2.3.1", 46 | "eslint-plugin-testing-library": "^1.3.1", 47 | "eslint-plugin-you-dont-need-lodash-underscore": "^6.5.0", 48 | "husky": "^2.7.0", 49 | "json-server": "^0.15.0", 50 | "lint-staged": "^8.2.1", 51 | "node-sass": "^4.12.0", 52 | "nodemon": "^1.19.4", 53 | "npm-run-all": "^4.1.5", 54 | "prettier": "^1.18.2", 55 | "react-test-renderer": "^16.8.6", 56 | "storybook-chromatic": "^1.4.0", 57 | "storybook-host": "^5.1.0", 58 | "webpack-bundle-analyzer": "^3.3.2" 59 | }, 60 | "eslintConfig": { 61 | "parser": "babel-eslint", 62 | "plugins": [ 63 | "cypress", 64 | "testing-library", 65 | "no-loops", 66 | "no-only-tests" 67 | ], 68 | "env": { 69 | "es6": true, 70 | "jest": true, 71 | "cypress/globals": true 72 | }, 73 | "extends": [ 74 | "react-app", 75 | "plugin:jsx-a11y/recommended", 76 | "plugin:testing-library/recommended", 77 | "plugin:cypress/recommended" 78 | ], 79 | "rules": { 80 | "no-loops/no-loops": "warn", 81 | "no-only-tests/no-only-tests": "warn" 82 | } 83 | }, 84 | "browserslist": [ 85 | ">0.2%", 86 | "not dead", 87 | "not ie <= 11", 88 | "not op_mini all" 89 | ], 90 | "husky": { 91 | "hooks": { 92 | "pre-commit": "lint-staged" 93 | } 94 | }, 95 | "lint-staged": { 96 | "*.{js,json,css,md}": [ 97 | "prettier --write", 98 | "git add" 99 | ] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /dev-environment/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/dev-environment/public/favicon.ico -------------------------------------------------------------------------------- /dev-environment/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dev-environment/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /dev-environment/src/api/apiUtils.js: -------------------------------------------------------------------------------- 1 | export async function handleResponse(response) { 2 | if (response.ok) return response.json(); 3 | if (response.status === 400) { 4 | // So, a server-side validation error occurred. 5 | // Server side validation returns a string error message, so parse as text instead of json. 6 | const error = await response.text(); 7 | throw new Error(error); 8 | } 9 | throw new Error("Network response was not ok."); 10 | } 11 | 12 | // In a real app, would likely call an error logging service. 13 | export function handleError(error) { 14 | // eslint-disable-next-line no-console 15 | console.error("API call failed. " + error); 16 | throw error; 17 | } 18 | -------------------------------------------------------------------------------- /dev-environment/src/api/authorApi.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleError } from "./apiUtils"; 2 | const baseUrl = process.env.REACT_APP_API_URL + "/authors/"; 3 | 4 | export function getAuthors() { 5 | return fetch(baseUrl) 6 | .then(handleResponse) 7 | .catch(handleError); 8 | } 9 | -------------------------------------------------------------------------------- /dev-environment/src/api/courseApi.js: -------------------------------------------------------------------------------- 1 | import { handleResponse, handleError } from "./apiUtils"; 2 | const baseUrl = process.env.REACT_APP_API_URL + "/courses/"; 3 | 4 | export function getCourses() { 5 | return fetch(baseUrl) 6 | .then(handleResponse) 7 | .catch(handleError); 8 | } 9 | 10 | export function saveCourse(course) { 11 | return fetch(baseUrl + (course.id || ""), { 12 | method: course.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists. 13 | headers: { "content-type": "application/json" }, 14 | body: JSON.stringify(course) 15 | }) 16 | .then(handleResponse) 17 | .catch(handleError); 18 | } 19 | 20 | export function deleteCourse(courseId) { 21 | return fetch(baseUrl + courseId, { method: "DELETE" }) 22 | .then(handleResponse) 23 | .catch(handleError); 24 | } 25 | -------------------------------------------------------------------------------- /dev-environment/src/components/About.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const About = () => ( 4 | <> 5 |

About

6 |
This is an about page. It's lazy loaded.
7 | 8 | ); 9 | 10 | export default About; 11 | -------------------------------------------------------------------------------- /dev-environment/src/components/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | max-width: 450px; 3 | margin: auto; 4 | } 5 | 6 | .App-logo { 7 | animation: App-logo-spin infinite 20s linear; 8 | height: 40vmin; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dev-environment/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Router } from "@reach/router"; 3 | import Spinner from "./Spinner"; 4 | import ErrorBoundary from "./ErrorBoundary"; 5 | import Home from "./Home"; 6 | import Nav from "./Nav"; 7 | import "./App.css"; 8 | 9 | // Lazy load about page 10 | const About = React.lazy(() => import("./About")); 11 | 12 | function App() { 13 | return ( 14 |
15 | 16 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /dev-environment/src/components/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /dev-environment/src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class ErrorBoundary extends React.Component { 4 | state = { hasError: false }; 5 | 6 | static getDerivedStateFromError(error) { 7 | // Update state so the next render will show the fallback UI. 8 | return { hasError: true }; 9 | } 10 | 11 | componentDidCatch(error, info) { 12 | // You can also log the error to an error reporting service 13 | } 14 | 15 | render() { 16 | if (this.state.hasError) { 17 | // You can render any custom fallback UI 18 | return ( 19 | <> 20 |

Oops!

21 |

Sorry, an error occurred. Please reload the page and try again.

22 | 23 | ); 24 | } 25 | 26 | return this.props.children; 27 | } 28 | } 29 | 30 | export default ErrorBoundary; 31 | -------------------------------------------------------------------------------- /dev-environment/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Home() { 4 | return ( 5 | <> 6 |

Home

7 |

This is the home page.

8 | 9 | ); 10 | } 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /dev-environment/src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "@reach/router"; 3 | 4 | const Nav = () => ( 5 | 8 | ); 9 | 10 | export default Nav; 11 | -------------------------------------------------------------------------------- /dev-environment/src/components/Spinner.css: -------------------------------------------------------------------------------- 1 | /* via https://projects.lukehaas.me/css-loaders/ */ 2 | .loader, 3 | .loader:after { 4 | border-radius: 50%; 5 | width: 10em; 6 | height: 10em; 7 | } 8 | .loader { 9 | margin: 60px auto; 10 | font-size: 10px; 11 | position: relative; 12 | text-indent: -9999em; 13 | border-top: 1.1em solid rgba(255, 151, 0, 0.2); 14 | border-right: 1.1em solid rgba(255, 151, 0, 0.2); 15 | border-bottom: 1.1em solid rgba(255, 151, 0, 0.2); 16 | border-left: 1.1em solid #ff9700; 17 | -webkit-transform: translateZ(0); 18 | -ms-transform: translateZ(0); 19 | transform: translateZ(0); 20 | -webkit-animation: load8 1.1s infinite linear; 21 | animation: load8 1.1s infinite linear; 22 | } 23 | @-webkit-keyframes load8 { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(360deg); 30 | transform: rotate(360deg); 31 | } 32 | } 33 | @keyframes load8 { 34 | 0% { 35 | -webkit-transform: rotate(0deg); 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | -webkit-transform: rotate(360deg); 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev-environment/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Spinner.css"; 3 | 4 | const Spinner = () => { 5 | return
Loading...
; 6 | }; 7 | 8 | export default Spinner; 9 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/TextInput.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-for */ 2 | import React, { useState } from "react"; 3 | import PropTypes from "prop-types"; 4 | import Tooltip from "../Tooltip"; 5 | import styles from "./TextInput.module.scss"; 6 | 7 | /** Reusable Text Input with integrated label, error, and required field validation */ 8 | const TextInput = ({ 9 | id, 10 | label, 11 | type, 12 | value, 13 | error, 14 | maxLength, 15 | required, 16 | onBlur, 17 | onChange 18 | }) => { 19 | const [localError, setLocalError] = useState(""); 20 | 21 | function handleBlur(event) { 22 | setLocalError(required && !value ? "Required." : ""); 23 | if (onBlur) onBlur(event); 24 | } 25 | 26 | const hasError = error || localError; 27 | 28 | return ( 29 | <> 30 | 33 | 43 | {localError && } 44 | 45 | ); 46 | }; 47 | 48 | TextInput.propTypes = { 49 | /** Input ID */ 50 | id: PropTypes.string.isRequired, 51 | 52 | /** Input label */ 53 | label: PropTypes.string.isRequired, 54 | 55 | /** Input type */ 56 | type: PropTypes.oneOf(["text", "email", "number"]).isRequired, 57 | 58 | /** Input value */ 59 | value: PropTypes.string.isRequired, 60 | 61 | /** Function called onBlur */ 62 | onBlur: PropTypes.func, 63 | 64 | /** Function called onChange */ 65 | onChange: PropTypes.func.isRequired, 66 | 67 | /** Input max length */ 68 | maxLength: PropTypes.number, 69 | 70 | /** Set to true to enable required field validation on blur */ 71 | required: PropTypes.bool, 72 | 73 | /** Set error state and display below the input */ 74 | error: PropTypes.string 75 | }; 76 | 77 | TextInput.defaultProps = { 78 | required: false, 79 | error: "", 80 | type: "text" 81 | }; 82 | 83 | export default TextInput; 84 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/TextInput.module.scss: -------------------------------------------------------------------------------- 1 | /* Consider https://github.com/gajus/babel-plugin-react-css-modules */ 2 | @import "../../../variables"; 3 | 4 | .label { 5 | display: block; 6 | padding-bottom: 5px; 7 | font-size: $fontSizeMedium; 8 | color: $darkGray; 9 | font-family: $fontFamily; 10 | font-weight: bold; 11 | } 12 | 13 | .input { 14 | border: 1px solid $lightgray; 15 | padding: 0 10px; 16 | height: 44px; 17 | font-size: $fontSizeMedium; 18 | font-weight: 400; 19 | } 20 | 21 | .error { 22 | border-color: $red; 23 | } 24 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/TextInput.stories.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import TextInput from "./TextInput"; 4 | import { text, boolean } from "@storybook/addon-knobs"; 5 | import { host } from "storybook-host"; 6 | 7 | // Consider https://github.com/Sambego/storybook-state instead 8 | const TextInputExample = ({ value = "", required = false }) => { 9 | const [firstName, setFirstName] = useState(value); 10 | 11 | function handleChange(event) { 12 | setFirstName(event.target.value); 13 | } 14 | 15 | return ( 16 | 23 | ); 24 | }; 25 | 26 | storiesOf("TextInput", module) 27 | .addDecorator( 28 | host({ 29 | title: "TextInput", 30 | align: "center bottom", 31 | height: "80%", 32 | width: 400 33 | }) 34 | ) 35 | .add("Docs (no change handler)", () => ( 36 | {}} 40 | required={boolean("required", false)} 41 | id="first-name" 42 | /> 43 | )) 44 | .add("Optional example", () => ) 45 | .add("Required example", () => ); 46 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/TextInput.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TextInput from "./TextInput"; 3 | import renderer from "react-test-renderer"; 4 | 5 | it("renders correctly", () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/__snapshots__/TextInput.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | Array [ 5 | , 10 | , 16 | "", 17 | ] 18 | `; 19 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/TextInput/index.js: -------------------------------------------------------------------------------- 1 | // Barrel for shortening imports 2 | export { default } from "./TextInput"; 3 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/Tooltip/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styles from "./Tooltip.module.scss"; 4 | 5 | /** Reusable Tooltip for displaying validation errors */ 6 | const Tooltip = ({ tip }) => { 7 | return
{tip}
; 8 | }; 9 | 10 | Tooltip.propTypes = { 11 | /** Message to display */ 12 | tip: PropTypes.string.isRequired 13 | }; 14 | 15 | export default Tooltip; 16 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/Tooltip/Tooltip.module.scss: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | position: absolute; 3 | margin-top: 12px; 4 | margin-left: 40px; 5 | padding: 18px 20px; 6 | border: 1px solid #dbdbdb; 7 | background: #fff; 8 | -moz-box-shadow: 0px 8px 0px -5px #dbdbdb; 9 | -webkit-box-shadow: 0px 8px 0px -5px #dbdbdb; 10 | box-shadow: 0px 8px 0px -5px #dbdbdb; 11 | z-index: 10; 12 | font-family: Arial, Helvetica, sans-serif; 13 | font-size: 15px; 14 | color: #293033; 15 | } 16 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/Tooltip/Tooltip.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import Tooltip from "./Tooltip"; 4 | import { text } from "@storybook/addon-knobs"; 5 | 6 | storiesOf("Tooltip", module).add("Default", () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/Tooltip/Tooltip.tests.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/dev-environment/src/components/reusable/Tooltip/Tooltip.tests.js -------------------------------------------------------------------------------- /dev-environment/src/components/reusable/Tooltip/index.js: -------------------------------------------------------------------------------- 1 | // Barrel for shortening imports 2 | export { default } from "./Tooltip"; 3 | -------------------------------------------------------------------------------- /dev-environment/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /dev-environment/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./components/App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import "bootstrap/dist/css/bootstrap.min.css"; 7 | 8 | ReactDOM.render(, document.getElementById("root")); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: http://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /dev-environment/src/propTypes.js: -------------------------------------------------------------------------------- 1 | import { string, number, shape } from "prop-types"; 2 | 3 | export const course = shape({ 4 | id: number, 5 | title: string, 6 | category: string 7 | }); 8 | -------------------------------------------------------------------------------- /dev-environment/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /dev-environment/src/utils/casing.js: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /dev-environment/src/variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | $darkGray: #293033; 3 | $lightgray: #dbdbdb; 4 | $red: #ff1616; 5 | 6 | // Fonts 7 | $fontFamily: Arial, Helvetica, sans-serif; 8 | $fontSizeMedium: 15px; 9 | -------------------------------------------------------------------------------- /dev-environment/tools/apiServer.js: -------------------------------------------------------------------------------- 1 | /* 2 | This uses json-server, but with the module approach: https://github.com/typicode/json-server#module 3 | Downside: You can't pass the json-server command line options. 4 | Instead, can override some defaults by passing a config object to jsonServer.defaults(); 5 | You have to check the source code to set some items. 6 | Examples: 7 | Validation/Customization: https://github.com/typicode/json-server/issues/266 8 | Delay: https://github.com/typicode/json-server/issues/534 9 | ID: https://github.com/typicode/json-server/issues/613#issuecomment-325393041 10 | Relevant source code: https://github.com/typicode/json-server/blob/master/src/cli/run.js 11 | */ 12 | 13 | /* eslint-disable no-console */ 14 | const jsonServer = require("json-server"); 15 | const server = jsonServer.create(); 16 | const path = require("path"); 17 | const router = jsonServer.router(path.join(__dirname, "db.json")); 18 | const db = router.db; 19 | 20 | // Can pass a limited number of options to this to override (some) defaults. See https://github.com/typicode/json-server#api 21 | const middlewares = jsonServer.defaults({ 22 | // Display json-server's built in homepage when json-server starts. 23 | static: "node_modules/json-server/dist" 24 | }); 25 | 26 | // Set default middlewares (logger, static, cors and no-cache) 27 | server.use(middlewares); 28 | 29 | // To handle POST, PUT and PATCH you need to use a body-parser. Using JSON Server's bodyParser 30 | server.use(jsonServer.bodyParser); 31 | 32 | // Simulate delay on all requests 33 | server.use(function(req, res, next) { 34 | setTimeout(next, 0); 35 | }); 36 | 37 | // Declaring custom routes below. Add custom routes before JSON Server router 38 | 39 | // Add createdAt to all POSTS 40 | server.use((req, res, next) => { 41 | if (req.method === "POST") { 42 | req.body.createdAt = Date.now(); 43 | } 44 | // Continue to JSON Server router 45 | next(); 46 | }); 47 | 48 | server.post("/courses/", function(req, res, next) { 49 | const error = validateCourse(req.body); 50 | if (error) { 51 | res.status(400).send(error); 52 | } else { 53 | req.body.slug = createSlug(req.body.title); // Generate a slug for new courses. 54 | next(); 55 | } 56 | }); 57 | 58 | // Use default router 59 | server.use(router); 60 | 61 | // Start server 62 | const port = 3001; 63 | server.listen(port, () => { 64 | console.log(`JSON Server is running on port ${port}`); 65 | }); 66 | 67 | // Centralized logic 68 | 69 | // json-server uses lowdb behind the scenes. So showing how to call it directly here to get the relevant record. lowdb docs: https://github.com/typicode/lowdb 70 | // this is just an unused example here for reference 71 | server.get("/users/:id", function(req, res, next) { 72 | const state = db.getState(); // returns entire db 73 | const users = state.payees.find(p => p.id === parseInt(req.params.id)); // find the requested record 74 | return res.status(200).json(users); 75 | }); 76 | 77 | // Returns a URL friendly slug 78 | function createSlug(value) { 79 | return value 80 | .replace(/[^a-z0-9_]+/gi, "-") 81 | .replace(/^-|-$/g, "") 82 | .toLowerCase(); 83 | } 84 | 85 | function validateCourse(course) { 86 | if (!course.title) return "Title is required."; 87 | if (!course.authorId) return "Author is required."; 88 | if (!course.category) return "Category is required."; 89 | return ""; 90 | } 91 | -------------------------------------------------------------------------------- /dev-environment/tools/createMockDb.js: -------------------------------------------------------------------------------- 1 | // Create a db.json file using mockData.js as the source. 2 | // This way json-server has consistent data to serve upon app start. 3 | /* eslint-disable no-console */ 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | const mockData = require("./mockData"); 7 | 8 | const data = JSON.stringify(mockData); 9 | const filepath = path.join(__dirname, "db.json"); 10 | 11 | fs.writeFile(filepath, data, function(err) { 12 | err ? console.log(err) : console.log("Mock DB created."); 13 | }); 14 | -------------------------------------------------------------------------------- /dev-environment/tools/mockData.js: -------------------------------------------------------------------------------- 1 | const courses = [ 2 | { 3 | id: 1, 4 | title: "Securing React Apps with Auth0", 5 | slug: "react-auth0-authentication-security", 6 | authorId: 1, 7 | category: "JavaScript" 8 | }, 9 | { 10 | id: 2, 11 | title: "React: The Big Picture", 12 | slug: "react-big-picture", 13 | authorId: 1, 14 | category: "JavaScript" 15 | }, 16 | { 17 | id: 3, 18 | title: "Creating Reusable React Components", 19 | slug: "react-creating-reusable-components", 20 | authorId: 1, 21 | category: "JavaScript" 22 | }, 23 | { 24 | id: 4, 25 | title: "Building a JavaScript Development Environment", 26 | slug: "javascript-development-environment", 27 | authorId: 1, 28 | category: "JavaScript" 29 | }, 30 | { 31 | id: 5, 32 | title: "Building Applications with React and Redux", 33 | slug: "react-redux-react-router-es6", 34 | authorId: 1, 35 | category: "JavaScript" 36 | }, 37 | { 38 | id: 6, 39 | title: "Building Applications in React and Flux", 40 | slug: "react-flux-building-applications", 41 | authorId: 1, 42 | category: "JavaScript" 43 | }, 44 | { 45 | id: 7, 46 | title: "Clean Code: Writing Code for Humans", 47 | slug: "writing-clean-code-humans", 48 | authorId: 1, 49 | category: "Software Practices" 50 | }, 51 | { 52 | id: 8, 53 | title: "Architecting Applications for the Real World", 54 | slug: "architecting-applications-dotnet", 55 | authorId: 1, 56 | category: "Software Architecture" 57 | }, 58 | { 59 | id: 9, 60 | title: "Becoming an Outlier: Reprogramming the Developer Mind", 61 | slug: "career-reboot-for-developer-mind", 62 | authorId: 1, 63 | category: "Career" 64 | }, 65 | { 66 | id: 10, 67 | title: "Web Component Fundamentals", 68 | slug: "web-components-shadow-dom", 69 | authorId: 1, 70 | category: "HTML5" 71 | } 72 | ]; 73 | 74 | const authors = [ 75 | { id: 1, name: "Cory House" }, 76 | { id: 2, name: "Scott Allen" }, 77 | { id: 3, name: "Dan Wahlin" } 78 | ]; 79 | 80 | const users = [ 81 | { id: 1, email: "test@example.com", password: "123" }, 82 | { id: 2, email: "bob@example.com", password: "345" } 83 | ]; 84 | 85 | const newCourse = { 86 | id: null, 87 | title: "", 88 | authorId: null, 89 | category: "" 90 | }; 91 | 92 | // Using CommonJS style export so we can consume via Node (without using Babel-node) 93 | module.exports = { 94 | newCourse, 95 | courses, 96 | authors, 97 | users 98 | }; 99 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjsconsulting", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactjsconsulting", 3 | "version": "1.0.0", 4 | "description": "ReactJS Consulting Resources", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/coryhouse/rjc.git" 12 | }, 13 | "keywords": [ 14 | "React" 15 | ], 16 | "author": "Cory House", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/coryhouse/rjc/issues" 20 | }, 21 | "homepage": "https://github.com/coryhouse/rjc#readme" 22 | } 23 | -------------------------------------------------------------------------------- /performance-tuning/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /performance-tuning/README.md: -------------------------------------------------------------------------------- 1 | # Performance Tuning Demo 2 | 3 | This project is useful for toying with React performance tuning. 4 | 5 | # Quick Start 6 | 7 | ``` 8 | npm i 9 | npm start 10 | ``` 11 | 12 | To specify the number of records, set `numVehicles` in querystring. 13 | 14 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 15 | 16 | ## Available Scripts 17 | 18 | In the project directory, you can run: 19 | 20 | ### `npm start` 21 | 22 | Runs the app in the development mode.
23 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 24 | 25 | The page will reload if you make edits.
26 | You will also see any lint errors in the console. 27 | 28 | ### `npm test` 29 | 30 | Launches the test runner in the interactive watch mode.
31 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 32 | 33 | ### `npm run build` 34 | 35 | Builds the app for production to the `build` folder.
36 | It correctly bundles React in production mode and optimizes the build for the best performance. 37 | 38 | The build is minified and the filenames include the hashes.
39 | Your app is ready to be deployed! 40 | 41 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 42 | 43 | ### `npm run eject` 44 | 45 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 46 | 47 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 48 | 49 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 50 | 51 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 52 | 53 | ## Learn More 54 | 55 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 56 | 57 | To learn React, check out the [React documentation](https://reactjs.org/). 58 | -------------------------------------------------------------------------------- /performance-tuning/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "performance-tuning-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "16.8.2", 7 | "react-dom": "16.8.2", 8 | "react-scripts": "2.1.5" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": [ 18 | "react-app", 19 | "plugin:you-dont-need-lodash-underscore/compatible", 20 | "plugin:promise/recommended" 21 | ], 22 | "plugins": [ 23 | "react-hooks", 24 | "promise" 25 | ], 26 | "rules": { 27 | "react-hooks/rules-of-hooks": "error" 28 | } 29 | }, 30 | "browserslist": [ 31 | ">0.2%", 32 | "not dead", 33 | "not ie <= 11", 34 | "not op_mini all" 35 | ], 36 | "devDependencies": { 37 | "cross-env": "^5.2.0", 38 | "eslint-plugin-react-hooks": "1.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /performance-tuning/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/performance-tuning/public/favicon.ico -------------------------------------------------------------------------------- /performance-tuning/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /performance-tuning/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /performance-tuning/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .filters { 6 | text-align: left; 7 | } 8 | 9 | .filter-label { 10 | font-weight: bold; 11 | } 12 | 13 | .filter { 14 | float: left; 15 | margin: 20px; 16 | } 17 | 18 | .save-button { 19 | background-color: #0082c0; 20 | margin-top: 37px; 21 | color: white; 22 | font-size: 16px; 23 | border: none; 24 | } 25 | 26 | .link-button { 27 | align-items: normal; 28 | background-color: rgba(0, 0, 0, 0); 29 | border-color: rgb(0, 0, 238); 30 | border-style: none; 31 | box-sizing: content-box; 32 | color: rgb(0, 0, 238); 33 | cursor: pointer; 34 | display: inline; 35 | font: inherit; 36 | height: auto; 37 | padding: 0; 38 | perspective-origin: 0 0; 39 | text-align: start; 40 | text-decoration: underline; 41 | transform-origin: 0 0; 42 | width: auto; 43 | -moz-appearance: none; 44 | -webkit-logical-height: 1em; /* Chrome ignores auto, so we have to use this hack to set the correct height */ 45 | -webkit-logical-width: auto; /* Chrome ignores auto, but here for completeness */ 46 | } 47 | 48 | /* Mozilla uses a pseudo-element to show focus on buttons, */ 49 | /* but anchors are highlighted via the focus pseudo-class. */ 50 | 51 | @supports (-moz-appearance: none) { 52 | /* Mozilla-only */ 53 | .link-button::-moz-focus-inner { 54 | /* reset any predefined properties */ 55 | border: none; 56 | padding: 0; 57 | } 58 | .link-button:focus { 59 | /* add outline to focus pseudo-class */ 60 | outline-style: dotted; 61 | outline-width: 1px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /performance-tuning/src/CheckboxList.css: -------------------------------------------------------------------------------- 1 | .checkboxlist-wrapper { 2 | border: solid 1px lightgray; 3 | padding: 5px; 4 | } 5 | 6 | .checkboxlist-options-wrapper { 7 | height: 100px; 8 | overflow-y: scroll; 9 | } 10 | 11 | .checkboxlist-count-wrapper { 12 | margin-left: 20px; 13 | float: right; 14 | } 15 | -------------------------------------------------------------------------------- /performance-tuning/src/CheckboxList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./CheckboxList.css"; 4 | 5 | const CheckboxList = ({ options, label, onChange, selectAll, selectNone }) => { 6 | return ( 7 | <> 8 | 9 |
10 |
11 | {" "} 14 | 17 |
18 |
19 | {label === "Dispatch Class" && ( 20 | <> 21 | Show 22 |
Count
23 | 24 | )} 25 | {options.map(option => ( 26 |
27 | onChange(option)} 33 | checked={option.checked} 34 | /> 35 | 36 | {option.count && ( 37 |
{option.count}
38 | )} 39 |
40 | ))} 41 |
42 |
43 | 44 | ); 45 | }; 46 | 47 | CheckboxList.propTypes = { 48 | options: PropTypes.array.isRequired, 49 | onChange: PropTypes.func.isRequired, 50 | label: PropTypes.string.isRequired 51 | }; 52 | 53 | export default CheckboxList; 54 | -------------------------------------------------------------------------------- /performance-tuning/src/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | componentDidCatch(error, info) { 10 | this.setState({ hasError: true }); 11 | } 12 | 13 | render() { 14 | if (this.state.hasError) { 15 | // You can render any custom fallback UI 16 | return ( 17 | <> 18 |

Something went wrong.

{" "} 19 |

Sorry, an error occurred. Please try again later.

20 | 21 | ); 22 | } 23 | return this.props.children; 24 | } 25 | } 26 | 27 | export default ErrorBoundary; 28 | -------------------------------------------------------------------------------- /performance-tuning/src/Tooltip.css: -------------------------------------------------------------------------------- 1 | div.tip { 2 | text-decoration: none; 3 | border: #c0c0c0 1px dotted; 4 | text-align: left; 5 | padding: 5px 20px 5px 5px; 6 | display: block; 7 | z-index: 10000; 8 | background-color: #f0f0f0; 9 | left: 0px; 10 | margin: 10px; 11 | position: absolute; 12 | top: 10px; 13 | text-decoration: none; 14 | } 15 | -------------------------------------------------------------------------------- /performance-tuning/src/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { convertToDateTimeString } from "./utils/dates"; 3 | import "./Tooltip.css"; 4 | 5 | function Tooltip({ vehicle, booking, left, top }) { 6 | if (!vehicle || !booking) return null; 7 | return ( 8 |
9 | {booking.type} 10 |
11 | Trip ID: {booking.id}
12 | Trip Name: {booking.name} 13 |
14 | Vehicle: {vehicle.id}, {vehicle.type} 15 |
16 | Driver: {booking.driver} 17 |
18 | Time: {convertToDateTimeString(booking.start)} to{" "} 19 | {convertToDateTimeString(booking.end)} 20 |
21 |
22 | ); 23 | } 24 | 25 | export default Tooltip; 26 | -------------------------------------------------------------------------------- /performance-tuning/src/Vehicles.css: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | display: grid; /* This is a (hacky) way to make the .grid element size to fit its content */ 3 | overflow: auto; 4 | padding-top: 15px; 5 | } 6 | 7 | th.sticky-header { 8 | position: sticky; 9 | top: 0; 10 | z-index: 10; 11 | background-color: white; 12 | } 13 | 14 | td, 15 | th { 16 | padding: 0px; 17 | margin: 0px; 18 | } 19 | 20 | table.vehicles { 21 | table-layout: fixed; 22 | width: 100%; 23 | border-collapse: separate; 24 | border-spacing: 0px 7px; 25 | } 26 | 27 | th.timeslot { 28 | color: gray; 29 | font-weight: 100; 30 | width: 30px; 31 | } 32 | 33 | td.timeslot { 34 | /* border-right: solid 1px lightgray; */ 35 | } 36 | 37 | .vehicle-type { 38 | text-align: right; 39 | white-space: nowrap; 40 | overflow: hidden; 41 | width: 150px; 42 | position: sticky; 43 | left: 0; 44 | background-color: white; 45 | padding-right: 10px; 46 | } 47 | 48 | .new-day { 49 | border-left: solid 2px orange; 50 | } 51 | 52 | /* Must position date relative since we can only have one sticky header */ 53 | /* And the first row determines the size of each cell */ 54 | .date { 55 | background-color: white; 56 | font-weight: bold; 57 | position: relative; 58 | width: 975px; 59 | height: 17px; 60 | top: -16px; 61 | white-space: nowrap; 62 | z-index: 90000; 63 | overflow: visible; 64 | text-align: center; 65 | left: -150px; 66 | } 67 | 68 | /* necessary to offset the position of the relative positioned date label above. */ 69 | .first-hour { 70 | position: relative; 71 | top: -9px; 72 | } 73 | -------------------------------------------------------------------------------- /performance-tuning/src/Vehicles.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { getBooking } from "./utils/dates"; 3 | import "./Vehicles.css"; 4 | 5 | const HOURS_PER_DAY = 24; 6 | 7 | function formatTime(hour) { 8 | if (hour === 0) return "12a"; 9 | if (hour === 12) return "12p"; 10 | return hour < 12 ? hour + "a" : hour - 12 + "p"; 11 | } 12 | 13 | const Vehicles = ({ 14 | vehicles, 15 | selectedDates, 16 | bookingTypeColors, 17 | showTooltip, 18 | hideTooltip 19 | }) => { 20 | const vehicleTableRef = useRef(null); 21 | const [ 22 | vehicleTableRefDistanceFromTop, 23 | setvehicleTableRefDistanceFromTop 24 | ] = useState(0); 25 | const [windowInnerHeight, setWindowInnerHeight] = useState( 26 | window.innerHeight 27 | ); 28 | 29 | const [gridContainerHeight, setGridContainerHeight] = useState( 30 | windowInnerHeight - vehicleTableRefDistanceFromTop 31 | ); 32 | 33 | const [gridContainerWidth, setGridContainerWidth] = useState( 34 | window.innerWidth 35 | ); 36 | 37 | function updateWindowDimensions() { 38 | setWindowInnerHeight(window.innerHeight); 39 | setGridContainerWidth(window.innerWidth); 40 | } 41 | 42 | // Runs once, after initial render, so we can watch for resize and get the vehicleTableRef's offset. 43 | useEffect(() => { 44 | window.addEventListener("resize", updateWindowDimensions); 45 | const offset = vehicleTableRef.current.getBoundingClientRect(); 46 | setvehicleTableRefDistanceFromTop(offset.top); 47 | 48 | // componentWillUnmount 49 | return () => { 50 | window.removeEventListener("resize", updateWindowDimensions); 51 | }; 52 | }, []); 53 | 54 | useEffect(() => { 55 | setGridContainerHeight(windowInnerHeight - vehicleTableRefDistanceFromTop); 56 | }, [windowInnerHeight]); 57 | 58 | return ( 59 | <> 60 |
64 | 65 | 66 | {/* 67 | The table-layout is "fixed" for rendering performance. 68 | This means the widths of this first row are applied to all rows below */} 69 | 70 | 85 | ); 86 | } 87 | return cells; 88 | })} 89 | 90 | 91 | 92 | {vehicles.map((vehicle, vehicleRowNumber) => { 93 | return ( 94 | 95 | 96 | {selectedDates.map((selectedDate, dateIndex) => { 97 | const cells = []; 98 | for (let i = 0; i < HOURS_PER_DAY; i++) { 99 | const dateTime = new Date( 100 | selectedDate.getFullYear(), 101 | selectedDate.getMonth(), 102 | selectedDate.getDate(), 103 | i 104 | ); 105 | 106 | const booking = getBooking(dateTime, vehicle.bookings); 107 | 108 | // Don't apply new-day style for first td on each row 109 | const className = 110 | dateIndex !== 0 && i === 0 111 | ? "timeslot new-day" 112 | : "timeslot"; 113 | 114 | // Color bookings based on booking type 115 | const style = booking 116 | ? { backgroundColor: bookingTypeColors[booking.type] } 117 | : null; 118 | 119 | cells.push( 120 | 133 | ); 134 | })} 135 | 136 |
71 | {selectedDates.map(date => { 72 | const cells = []; 73 | for (let i = 0; i < HOURS_PER_DAY; i++) { 74 | cells.push( 75 | 76 | {i === 0 ? ( 77 | <> 78 |
{date.toDateString()}
79 |
{formatTime(i)}
80 | 81 | ) : ( 82 | formatTime(i) 83 | )} 84 |
{vehicle.type} 128 | ); 129 | } 130 | return cells; 131 | })} 132 |
137 |
138 | 139 | ); 140 | }; 141 | 142 | // For perf, only re-render this table when a new search is performed. 143 | // function areEqual(prevProps, nextProps) { 144 | // return prevProps.searchTimestamp === nextProps.searchTimestamp; 145 | // } 146 | 147 | // To avoid needless renders, wrap in React.memo 148 | // export default React.memo(Vehicles, areEqual); 149 | 150 | export default Vehicles; 151 | -------------------------------------------------------------------------------- /performance-tuning/src/api/driverApi.js: -------------------------------------------------------------------------------- 1 | import { getBaseUrl, get } from "./utils"; 2 | 3 | const baseUrl = getBaseUrl("driver"); 4 | 5 | export async function getDrivers() { 6 | const response = await get(baseUrl); 7 | if (response.ok) { 8 | const drivers = await response.json(); 9 | return drivers; 10 | } 11 | const error = await response.json(); 12 | return error; 13 | } 14 | -------------------------------------------------------------------------------- /performance-tuning/src/api/mockData.js: -------------------------------------------------------------------------------- 1 | function randomDate(start, end) { 2 | return new Date( 3 | start.getTime() + Math.random() * (end.getTime() - start.getTime()) 4 | ); 5 | } 6 | 7 | export function getMockCscs() { 8 | return [ 9 | { 10 | id: 1, 11 | name: "csc 1", 12 | checked: true 13 | }, 14 | { id: 2, name: "csc 2", checked: true } 15 | ]; 16 | } 17 | 18 | const bookingTypes = ["Conflict", "Trip", "Route", "Out of Service"]; 19 | 20 | function randomBookingType() { 21 | return bookingTypes[Math.floor(Math.random() * bookingTypes.length)]; 22 | } 23 | 24 | function randomString() { 25 | return Math.random() 26 | .toString(36) 27 | .substring(7); 28 | } 29 | 30 | export function getMockVehicles() { 31 | const urlParams = new URLSearchParams(window.location.search); 32 | const numResults = urlParams.get("numVehicles") || 10; 33 | const mockVehicles = []; 34 | let i = 1; 35 | while (i < numResults) { 36 | mockVehicles.push({ 37 | id: i, 38 | csc: 1, 39 | type: randomString(), 40 | bookings: [ 41 | { 42 | id: i, 43 | name: randomString(), 44 | start: randomDate( 45 | new Date(2018, 11, 2, 4), 46 | new Date(2018, 11, 2, 16) 47 | ), 48 | end: randomDate(new Date(2018, 11, 3, 4), new Date(2018, 11, 3, 16)), 49 | driver: randomString(), 50 | type: randomBookingType() 51 | }, 52 | { 53 | id: i + 1, 54 | name: randomString(), 55 | start: randomDate( 56 | new Date(2018, 11, 4, 4), 57 | new Date(2018, 11, 4, 16) 58 | ), 59 | end: randomDate(new Date(2018, 11, 5, 4), new Date(2018, 11, 5, 16)), 60 | driver: randomString(), 61 | type: randomBookingType() 62 | } 63 | ] 64 | }); 65 | i++; 66 | } 67 | return mockVehicles; 68 | } 69 | -------------------------------------------------------------------------------- /performance-tuning/src/api/utils.js: -------------------------------------------------------------------------------- 1 | export function getBaseUrl(api) { 2 | return process.env.REACT_APP_API_URL + "/" + api; 3 | } 4 | 5 | function getHeaders(user) { 6 | const headers = { 7 | "content-type": "application/json" 8 | }; 9 | 10 | if (user) headers.Authorization = "Bearer " + user.accessToken; 11 | return headers; 12 | } 13 | 14 | export async function get(url, user) { 15 | return await fetch(url, { 16 | headers: getHeaders(user) 17 | }) 18 | .then(response => { 19 | // redirect to login if the user's session has timed out 20 | if (response.status === 401) global.window.location.replace("/login"); 21 | return response; 22 | }) 23 | .catch(error => { 24 | throw new Error(error); 25 | }); 26 | } 27 | 28 | export async function post(url, data, user) { 29 | return await fetch(url, { 30 | method: "POST", 31 | body: JSON.stringify(data), 32 | headers: getHeaders(user) 33 | }) 34 | .then(response => { 35 | // redirect to login if the user isn't authorized 36 | // but don't redirect if the user is actually trying to login, since that's just a failed login attempt. 37 | const tryingToLogin = window.location.href.indexOf("/login") > -1; 38 | if (response.status === 401 && !tryingToLogin) 39 | global.window.location.replace("/login"); 40 | return response; 41 | }) 42 | .catch(error => { 43 | throw new Error(error); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /performance-tuning/src/api/vehicleApi.js: -------------------------------------------------------------------------------- 1 | import { getBaseUrl, get } from "./utils"; 2 | 3 | const baseUrl = getBaseUrl("vehicle/schedule"); 4 | 5 | export async function getVehicles() { 6 | const response = await get(baseUrl); 7 | if (response.ok) { 8 | const vehicles = await response.json(); 9 | return vehicles; 10 | } 11 | const error = await response.json(); 12 | return error; 13 | } 14 | -------------------------------------------------------------------------------- /performance-tuning/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | -------------------------------------------------------------------------------- /performance-tuning/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | import ErrorBoundary from "./ErrorBoundary"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: http://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /performance-tuning/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /performance-tuning/src/utils/dates.js: -------------------------------------------------------------------------------- 1 | // Returns the corresponding booking for a given dateTime, if one exists. 2 | export function getBooking(dateTime, bookings) { 3 | if (bookings.length === 0) return false; 4 | let booking = null; 5 | bookings.forEach(b => { 6 | if (dateTime >= Date.parse(b.start) && dateTime <= Date.parse(b.end)) { 7 | booking = b; 8 | } 9 | }); 10 | return booking; 11 | } 12 | 13 | // Convert date to YYYY-MM-DD format 14 | export function convertDateToLeadingYearFormat(date) { 15 | var yyyy = date.getFullYear().toString(); 16 | var mm = (date.getMonth() + 1).toString(); 17 | var dd = date.getDate().toString(); 18 | 19 | var mmChars = mm.split(""); 20 | var ddChars = dd.split(""); 21 | 22 | return ( 23 | yyyy + 24 | "-" + 25 | (mmChars[1] ? mm : "0" + mmChars[0]) + 26 | "-" + 27 | (ddChars[1] ? dd : "0" + ddChars[0]) 28 | ); 29 | } 30 | 31 | // Convert JS date to MM/DD/YYYY format 32 | export function convertDateToLeadingMonthFormat(date) { 33 | return date.getMonth() + 1 + "/" + date.getDate() + "/" + date.getFullYear(); 34 | } 35 | 36 | // Convert JS date object to a US time. 37 | export function convertDateToTime(date) { 38 | return date.toLocaleString("en-US", { 39 | hour: "numeric", 40 | minute: "numeric", 41 | hour12: true 42 | }); 43 | } 44 | 45 | export function convertToDateTimeString(date) { 46 | return convertDateToLeadingMonthFormat(date) + " " + convertDateToTime(date); 47 | } 48 | -------------------------------------------------------------------------------- /performance-tuning/src/utils/dates.test.js: -------------------------------------------------------------------------------- 1 | import { vehicleIsScheduled } from "./dates"; 2 | describe("vehicleIsScheduled", () => { 3 | it("should return false when passed an empty array of scheduled dates", () => { 4 | const date = new Date(2019, 3, 2, 9); 5 | const result = vehicleIsScheduled(date, []); 6 | expect(result).toBe(false); 7 | }); 8 | 9 | it("should return false when passed an array of scheduled dates that doesn't include the date passed", () => { 10 | // 4/2/2019 at 9am 11 | const date = new Date(2019, 3, 2, 7); 12 | const scheduledDates = [ 13 | { 14 | // 4/2/2019 at 8am 15 | start: new Date(2019, 3, 2, 8), 16 | 17 | // 4/2/2019 at 12:30pm 18 | end: new Date(2019, 3, 2, 12, 30) 19 | } 20 | ]; 21 | const result = vehicleIsScheduled(date, scheduledDates); 22 | expect(result).toBe(false); 23 | }); 24 | 25 | it("should return true when passed an array of scheduled dates that includes the date passed", () => { 26 | // 4/2/2019 at 9am 27 | const date = new Date(2019, 3, 2, 9); 28 | const scheduledDates = [ 29 | { 30 | // 4/2/2019 at 8am 31 | start: new Date(2019, 3, 2, 8), 32 | 33 | // 4/2/2019 at 12:30pm 34 | end: new Date(2019, 3, 2, 12, 30) 35 | } 36 | ]; 37 | const result = vehicleIsScheduled(date, scheduledDates); 38 | expect(result).toBe(true); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /refs/README.md: -------------------------------------------------------------------------------- 1 | # Refs 2 | 3 | This project shows varies examples of working with Refs. 4 | 5 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 6 | 7 | ## Quick Start 8 | 9 | ``` 10 | npm install 11 | npm start 12 | ``` 13 | 14 | ## Credit 15 | 16 | Forked from [Rafael Quintanilha's Refs cheat sheet](https://github.com/rafaelquintanilha/refs). Blog post [here](https://rafaelquintanilha.com/the-complete-guide-to-react-refs). 17 | -------------------------------------------------------------------------------- /refs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "prismjs": "^1.15.0", 7 | "randomcolor": "^0.5.3", 8 | "react": "^16.8.3", 9 | "react-dom": "^16.8.3", 10 | "react-scripts": "2.1.3" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /refs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/refs/public/favicon.ico -------------------------------------------------------------------------------- /refs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React Refs Cheat Sheet 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /refs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /refs/src/App.css: -------------------------------------------------------------------------------- 1 | header { 2 | text-align: center; 3 | text-transform: uppercase; 4 | margin-bottom: 50px; 5 | } 6 | 7 | main { 8 | display: flex; 9 | flex-direction: column; 10 | margin: auto; 11 | max-width: 600px; 12 | } 13 | 14 | main > h2 { 15 | padding-bottom: 10px; 16 | border-bottom: 1px solid #ccc; 17 | text-transform: uppercase; 18 | } 19 | 20 | main p { 21 | background-color: #f8ffff; 22 | color: #276f86; 23 | padding: 20px 10px; 24 | box-shadow: 0 0 0 1px #a9d5de inset, 0 0 0 0 transparent; 25 | border-radius: 5px; 26 | } 27 | 28 | @media only screen and (max-width: 600px) { 29 | main { 30 | max-width: auto; 31 | padding-left: 20px; 32 | padding-right: 20px; 33 | } 34 | } 35 | 36 | footer { 37 | display: flex; 38 | justify-content: center; 39 | padding: 15px; 40 | background-color: #eee; 41 | } 42 | 43 | footer a:visited, 44 | footer a:visited { 45 | color: blue; 46 | } -------------------------------------------------------------------------------- /refs/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /refs/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | code:not(.language-javascript) { 7 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 8 | monospace; 9 | background: rgba(255,229,100,.2); 10 | } 11 | -------------------------------------------------------------------------------- /refs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /refs/src/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.15.0 2 | https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript */ 3 | /* 4 | Solarized Color Schemes originally by Ethan Schoonover 5 | http://ethanschoonover.com/solarized 6 | 7 | Ported for PrismJS by Hector Matos 8 | Website: https://krakendev.io 9 | Twitter Handle: https://twitter.com/allonsykraken) 10 | */ 11 | 12 | /* 13 | SOLARIZED HEX 14 | --------- ------- 15 | base03 #002b36 16 | base02 #073642 17 | base01 #586e75 18 | base00 #657b83 19 | base0 #839496 20 | base1 #93a1a1 21 | base2 #eee8d5 22 | base3 #fdf6e3 23 | yellow #b58900 24 | orange #cb4b16 25 | red #dc322f 26 | magenta #d33682 27 | violet #6c71c4 28 | blue #268bd2 29 | cyan #2aa198 30 | green #859900 31 | */ 32 | 33 | code[class*="language-"], 34 | pre[class*="language-"] { 35 | color: #657b83; /* base00 */ 36 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 37 | text-align: left; 38 | white-space: pre; 39 | word-spacing: normal; 40 | word-break: normal; 41 | word-wrap: normal; 42 | 43 | line-height: 1.5; 44 | 45 | -moz-tab-size: 4; 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | 49 | -webkit-hyphens: none; 50 | -moz-hyphens: none; 51 | -ms-hyphens: none; 52 | hyphens: none; 53 | } 54 | 55 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 56 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 57 | background: #073642; /* base02 */ 58 | } 59 | 60 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 61 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 62 | background: #073642; /* base02 */ 63 | } 64 | 65 | /* Code blocks */ 66 | pre[class*="language-"] { 67 | padding: 1em; 68 | margin: .5em 0; 69 | overflow: auto; 70 | border-radius: 0.3em; 71 | } 72 | 73 | :not(pre) > code[class*="language-"], 74 | pre[class*="language-"] { 75 | background-color: #fdf6e3; /* base3 */ 76 | } 77 | 78 | /* Inline code */ 79 | :not(pre) > code[class*="language-"] { 80 | padding: .1em; 81 | border-radius: .3em; 82 | } 83 | 84 | .token.comment, 85 | .token.prolog, 86 | .token.doctype, 87 | .token.cdata { 88 | color: #93a1a1; /* base1 */ 89 | } 90 | 91 | .token.punctuation { 92 | color: #586e75; /* base01 */ 93 | } 94 | 95 | .namespace { 96 | opacity: .7; 97 | } 98 | 99 | .token.property, 100 | .token.tag, 101 | .token.boolean, 102 | .token.number, 103 | .token.constant, 104 | .token.symbol, 105 | .token.deleted { 106 | color: #268bd2; /* blue */ 107 | } 108 | 109 | .token.selector, 110 | .token.attr-name, 111 | .token.string, 112 | .token.char, 113 | .token.builtin, 114 | .token.url, 115 | .token.inserted { 116 | color: #2aa198; /* cyan */ 117 | } 118 | 119 | .token.entity { 120 | color: #657b83; /* base00 */ 121 | background: #eee8d5; /* base2 */ 122 | } 123 | 124 | .token.atrule, 125 | .token.attr-value, 126 | .token.keyword { 127 | color: #859900; /* green */ 128 | } 129 | 130 | .token.function, 131 | .token.class-name { 132 | color: #b58900; /* yellow */ 133 | } 134 | 135 | .token.regex, 136 | .token.important, 137 | .token.variable { 138 | color: #cb4b16; /* orange */ 139 | } 140 | 141 | .token.important, 142 | .token.bold { 143 | font-weight: bold; 144 | } 145 | .token.italic { 146 | font-style: italic; 147 | } 148 | 149 | .token.entity { 150 | cursor: help; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /refs/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /responsive-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /responsive-web/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | import "@storybook/addon-viewport/register"; 4 | import "@storybook/addon-knobs/register"; 5 | -------------------------------------------------------------------------------- /responsive-web/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | import "../src/scss/global.scss"; 3 | 4 | // automatically import all files ending in *.stories.js 5 | const req = require.context("../src", true, /\.stories\.js$/); 6 | function loadStories() { 7 | req.keys().forEach(filename => req(filename)); 8 | } 9 | 10 | configure(loadStories, module); 11 | -------------------------------------------------------------------------------- /responsive-web/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Modifications to create-react-app 4 | 5 | 1. Add Storybook: `npx -p @storybook/cli sb init --type react` 6 | 1. Add addons to Storybook: `npm i -D @storybook/addon-viewport @storybook/addon-knobs`, then add these lines to .storybook/addons.js: `import '@storybook/addon-viewport/register';` 7 | 1. Configure storybook to find stories in src by changing .storybook/config.js to this: `const req = require.context('../src', true, /\.stories\.js$/);` 8 | ) 9 | 1. Install @testing-library/react: `npm i -D @testing-library/react` 10 | 1. Install Cypress: `npm i -D cypress` 11 | 1. Install @testing-library/cypress : `npm i -D @testing-library/cypress` 12 | 1. Install react-router-dom: `npm i -D react-router-dom` 13 | 14 | Exercises: 15 | 16 | 1. [Menu using grid, state machine, and Emotion](https://codesandbox.io/s/state-machines-in-react-2llje) 17 | 1. [Responsive table](https://stackblitz.com/edit/mobile-table?file=style.css) - [Mobile table approaches](https://medium.com/appnroll-publication/5-practical-solutions-to-make-responsive-data-tables-ff031c48b122) 18 | 1. [Calendar using Flexbox](https://codepen.io/ljm/pen/JjfAv) [Blog](https://thoughtbot.com/blog/flexboxes-media-queries-awesome-layouts) 19 | 1. [Toy with Flexplorer](https://bennettfeely.com/flexplorer/) (move to tools) 20 | -------------------------------------------------------------------------------- /responsive-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsive-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^4.3.1", 7 | "is-url-external": "^1.0.3", 8 | "react": "^16.9.0", 9 | "react-dom": "^16.9.0", 10 | "react-router-dom": "^5.0.1", 11 | "react-scripts": "3.1.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject", 18 | "storybook": "start-storybook -p 6006", 19 | "build-storybook": "build-storybook" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.5.5", 38 | "@storybook/addon-actions": "^5.1.11", 39 | "@storybook/addon-knobs": "^5.1.11", 40 | "@storybook/addon-links": "^5.1.11", 41 | "@storybook/addon-viewport": "^5.1.11", 42 | "@storybook/addons": "^5.1.11", 43 | "@storybook/react": "^5.1.11", 44 | "@testing-library/cypress": "^4.2.0", 45 | "@testing-library/react": "^9.1.3", 46 | "babel-loader": "^8.0.6", 47 | "cypress": "^3.4.1", 48 | "node-sass": "^4.12.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /responsive-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/responsive-web/public/favicon.ico -------------------------------------------------------------------------------- /responsive-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /responsive-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /responsive-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /responsive-web/src/FlexboxPlayground/FlexboxPlayground.css: -------------------------------------------------------------------------------- 1 | /* This is styled mobile first 2 | 1. Style for mobile 3 | 2. Add styles when it looks like shit 4 | */ 5 | .container { 6 | display: flex; 7 | flex-flow: row wrap; 8 | height: 100vh; 9 | justify-content: last baseline; 10 | /* Aligns lines, so only applies when wrapping */ 11 | align-content: space-around; 12 | align-items: center; 13 | background-color: lightgray; 14 | } 15 | 16 | .box { 17 | background-color: orange; 18 | border: solid 1px gray; 19 | text-align: center; 20 | flex: 1 1 400px; 21 | } 22 | 23 | @media (width: 500px) { 24 | .container { 25 | flex-flow: row wrap; 26 | } 27 | 28 | .box { 29 | flex: 1 1 200px; 30 | } 31 | } 32 | 33 | .container :nth-child(odd) { 34 | /* flex-grow: 2; */ 35 | flex-shrink: 4; 36 | min-width: 40px; 37 | background-color: gainsboro; 38 | } 39 | 40 | @media only screen and (orientation: landscape) { 41 | body { 42 | background-color: lightblue; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /responsive-web/src/FlexboxPlayground/FlexboxPlayground.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./FlexboxPlayground.css"; 3 | 4 | function FlexboxPlayground() { 5 | return ( 6 |
7 |
8 |
1
9 |
2
10 |
3
11 |
4
12 |
13 |
14 | ); 15 | } 16 | 17 | export default FlexboxPlayground; 18 | -------------------------------------------------------------------------------- /responsive-web/src/FlexboxPlayground/FlexboxPlayground.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import { storiesOf } from "@storybook/react"; 4 | import "../scss/global.scss"; 5 | import "./FlexboxPlayground.css"; 6 | import FlexboxPlayground from "./FlexboxPlayground"; 7 | 8 | storiesOf("FlexboxPlayground", module).add("Boxes", () => { 9 | return ; 10 | }); 11 | -------------------------------------------------------------------------------- /responsive-web/src/FlexboxPlayground/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./FlexboxPlayground"; 2 | -------------------------------------------------------------------------------- /responsive-web/src/GridPlayground/GridPlayground.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 40px; 3 | } 4 | 5 | .grid { 6 | display: grid; 7 | /* 8 | Each value represents a column. 9 | Note the optional names before each column def. 10 | Can have multiple, space delimited. 11 | Using repeat to declare multiple columns with the same settings in a row. */ 12 | grid-template-columns: [first-line1] 1fr [line2] repeat(2, 20px [col-end]); 13 | 14 | /* each value represents a row */ 15 | grid-template-rows: [row-one] 1fr [row-two] 4fr [last-row] 300px; 16 | background: lightgrey; 17 | grid-gap: 20px; 18 | } 19 | 20 | /* style all immediate children */ 21 | .grid > * { 22 | background-color: orange; 23 | text-align: center; 24 | } 25 | 26 | /* put 2nd div in last column and row, referenced via name */ 27 | #div2 { 28 | grid-column-start: col-end; 29 | grid-row-start: last-row; 30 | } 31 | -------------------------------------------------------------------------------- /responsive-web/src/GridPlayground/GridPlayground.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./GridPlayground.css"; 3 | 4 | function GridPlayground() { 5 | return ( 6 |
7 |
div 1
8 | div 2 9 |
div 3
10 |
div 4
11 |
div 5
12 |
div 6
13 |
div 7
14 |
div 8
15 |
div 9
16 |
17 | ); 18 | } 19 | 20 | export default GridPlayground; 21 | -------------------------------------------------------------------------------- /responsive-web/src/GridPlayground/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./GridPlayground"; 2 | -------------------------------------------------------------------------------- /responsive-web/src/GridTemplateArea/GridTemplateArea.css: -------------------------------------------------------------------------------- 1 | .header { 2 | grid-area: header; 3 | } 4 | .main { 5 | grid-area: main; 6 | } 7 | .sidebar { 8 | grid-area: sidebar; 9 | } 10 | .footer { 11 | grid-area: footer; 12 | } 13 | 14 | /* Note mobile first CSS. Desktop grid is applied via media query below */ 15 | .grid { 16 | display: grid; 17 | grid-template-columns: 1fr; 18 | grid-template-areas: 19 | "header" 20 | "main" 21 | "sidebar" 22 | "footer"; 23 | } 24 | 25 | @media only screen and (min-width: 600px) { 26 | .grid { 27 | grid-template-columns: 50px 1fr 50px 200px; 28 | grid-template-rows: auto; 29 | grid-template-areas: 30 | "header header header header" 31 | "main main . sidebar" 32 | "footer footer footer footer"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /responsive-web/src/GridTemplateArea/GridTemplateArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./GridTemplateArea.css"; 3 | 4 | function GridTemplateArea() { 5 | return ( 6 |
7 |
Header
8 |
Main
9 |
Sidebar
10 |
Footer
11 |
12 | ); 13 | } 14 | 15 | export default GridTemplateArea; 16 | -------------------------------------------------------------------------------- /responsive-web/src/GridTemplateArea/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./GridTemplateArea"; 2 | -------------------------------------------------------------------------------- /responsive-web/src/Nav/Nav.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/interactive-supports-focus */ 3 | import React, { useRef, useEffect, useState } from "react"; 4 | import SmartLink from "../SmartLink"; 5 | import PropTypes from "prop-types"; 6 | import cx from "classnames"; 7 | import styles from "./Nav.module.scss"; 8 | 9 | export const NavItem = ({ isFlyout, to, isActive, children }) => { 10 | return ( 11 |
  • 12 | 13 | {children} 14 | 15 |
  • 16 | ); 17 | }; 18 | 19 | const Nav = ({ flyoutId, activeIndex, ...props }) => { 20 | const [flyouts, setFlyouts] = useState([]); 21 | const [menuIsOpen, setMenuIsOpen] = useState(false); 22 | 23 | const isMobile = props.screenWidth < 768; 24 | const hasFlyout = flyouts.length > 0; 25 | const ref = useRef(null); 26 | 27 | const handleMenu = () => { 28 | if (isMobile) { 29 | setMenuIsOpen(!menuIsOpen); 30 | } else if (hasFlyout) { 31 | setMenuIsOpen(!menuIsOpen); 32 | } else { 33 | return false; 34 | } 35 | }; 36 | 37 | useEffect(() => { 38 | // Reset the flyouts when window size changes. Prefixing this var with underscore to avoid naming conflict with flyouts held in state. 39 | const _flyouts = []; 40 | 41 | if (!isMobile) { 42 | let itemsWidth = 50; //accounting for the trigger also 43 | for (let [i, item] of ref.current.querySelectorAll("li").entries()) { 44 | itemsWidth = itemsWidth + item.getBoundingClientRect().width; 45 | if (itemsWidth > ref.current.getBoundingClientRect().width) { 46 | _flyouts.push(i); 47 | } 48 | } 49 | setFlyouts(_flyouts); 50 | } 51 | }, [flyouts.length, isMobile, props.children, props.screenWidth]); 52 | 53 | const renderChildren = () => { 54 | return props.children.map((item, ind) => ( 55 | 61 | )); 62 | }; 63 | 64 | const renderFlyout = () => { 65 | return props.children 66 | .filter((item, ind) => flyouts.includes(ind)) 67 | .map(item => ); 68 | }; 69 | 70 | return ( 71 | 110 | ); 111 | }; 112 | 113 | Nav.propTypes = { 114 | /** Active Menu item index */ 115 | activeIndex: PropTypes.number, 116 | 117 | /** Menu Items */ 118 | children: PropTypes.node.isRequired, 119 | 120 | /** Class to apply */ 121 | className: PropTypes.string, 122 | 123 | /** Unique identifier to be used for button/content pairing with aria-controls/id */ 124 | flyoutId: PropTypes.string, 125 | 126 | /** Screen width in pixels */ 127 | screenWidth: PropTypes.number.isRequired 128 | }; 129 | 130 | NavItem.propTypes = { 131 | /** Menu Item text */ 132 | children: PropTypes.string.isRequired, 133 | 134 | /** Is Menu Item active */ 135 | isActive: PropTypes.bool, 136 | 137 | /** Does Menu Item appear in flyout for tablet and desktop, calculated on window resize */ 138 | isFlyout: PropTypes.bool, 139 | 140 | /** Link URL */ 141 | to: PropTypes.string 142 | }; 143 | 144 | export default Nav; 145 | -------------------------------------------------------------------------------- /responsive-web/src/Nav/Nav.module.scss: -------------------------------------------------------------------------------- 1 | @import "src/scss/variables"; 2 | 3 | .root { 4 | background: $graybg; 5 | font-family: "Meta Web Normal"; 6 | font-size: 15px; 7 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); 8 | position: relative; 9 | 10 | ul { 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | position: relative; 15 | list-style-type: none; 16 | width: 100%; 17 | margin: 0 auto; 18 | padding: 0; 19 | overflow: hidden; 20 | } 21 | li { 22 | flex: 0 1 auto; 23 | padding: 0 15px; 24 | } 25 | a { 26 | display: inline-block; 27 | padding: 20px 10px 12px 10px; 28 | border-bottom: 6px solid $graybg; 29 | text-align: center; 30 | color: $links; 31 | white-space: nowrap; 32 | outline-offset: -4px; 33 | } 34 | } 35 | 36 | a.active { 37 | color: $copy; 38 | font-family: "Meta Web Bold"; 39 | border-bottom: 6px solid $orange; 40 | } 41 | 42 | .trigger { 43 | display: none; 44 | position: absolute; 45 | right: 0; 46 | top: 0; 47 | z-index: 1; 48 | } 49 | 50 | .mobile { 51 | box-shadow: none; 52 | margin-left: -10px; 53 | margin-right: -10px; 54 | ul { 55 | display: block; 56 | position: absolute; 57 | width: 100%; 58 | overflow: hidden; 59 | background: $graybg; 60 | transition: all 0.2s; 61 | max-height: 500px; 62 | z-index: 1; 63 | &.mobileHide { 64 | @media #{$mobile_only} { 65 | max-height: 0; 66 | } 67 | } 68 | li { 69 | display: block; 70 | padding: 0; 71 | border-bottom: 1px solid $lightgray; 72 | a { 73 | width: 100%; 74 | display: block; 75 | padding: 14px 10px 0 10px; 76 | border-bottom: none; 77 | min-height: 48px; 78 | text-align: left; 79 | &.active { 80 | border-left: 6px solid $orange; 81 | padding-left: 14px; 82 | } 83 | } 84 | } 85 | } 86 | .trigger { 87 | display: block; 88 | width: 50px; 89 | border: 1px solid $lightgray; 90 | background: $graybg; 91 | min-height: 48px; 92 | } 93 | } 94 | 95 | .triggerDot { 96 | display: block; 97 | width: 5px; 98 | height: 5px; 99 | background: #b2b3b5; 100 | border-radius: 5px; 101 | position: relative; 102 | margin: 0 auto; 103 | &:before { 104 | content: ""; 105 | display: block; 106 | width: 5px; 107 | height: 5px; 108 | background: #b2b3b5; 109 | border-radius: 5px; 110 | position: absolute; 111 | top: -9px; 112 | left: 0px; 113 | } 114 | &:after { 115 | content: ""; 116 | display: block; 117 | width: 5px; 118 | height: 5px; 119 | background: #b2b3b5; 120 | border-radius: 5px; 121 | position: absolute; 122 | top: 9px; 123 | left: 0px; 124 | } 125 | } 126 | 127 | .mobileHeader { 128 | @media #{$mobile_only} { 129 | min-height: 48px; 130 | padding: 14px 0 0 10px; 131 | font-family: "Meta Web Bold"; 132 | border-top: 1px solid $lightgray; 133 | border-bottom: 1px solid $lightgray; 134 | background: $graybg; 135 | &:hover { 136 | text-decoration: underline; 137 | cursor: pointer; 138 | } 139 | } 140 | } 141 | 142 | .hasFlyout { 143 | ul { 144 | justify-content: left; 145 | align-items: flex-start; 146 | } 147 | .flyout { 148 | @media #{$tablet} { 149 | visibility: hidden; 150 | } 151 | } 152 | .trigger { 153 | display: block; 154 | width: 50px; 155 | @media #{$tablet} { 156 | height: 60px; 157 | } 158 | border: 1px solid $lightgray; 159 | background: $graybg; 160 | } 161 | .flyoutMenu { 162 | background: $white; 163 | position: absolute; 164 | display: block; 165 | width: auto; 166 | right: 0; 167 | transition: all 0.3s; 168 | max-height: 500px; 169 | 170 | li { 171 | border: 1px solid $lightgray; 172 | border-bottom: none; 173 | background: $graybg; 174 | text-align: right; 175 | padding-left: 0; 176 | padding-right: 0; 177 | &:last-child { 178 | border-bottom: 1px solid $lightgray; 179 | } 180 | a { 181 | padding: 20px 25px 12px 25px; 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /responsive-web/src/Nav/Nav.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import Nav, { NavItem } from "./Nav"; 5 | import { storiesOf } from "@storybook/react"; 6 | import "../scss/global.scss"; 7 | import useWindowSize from "../hooks/useWindowSize"; 8 | import { number } from "@storybook/addon-knobs"; 9 | 10 | function NavWrapper() { 11 | const { w } = useWindowSize(100); 12 | 13 | return ( 14 | 15 | 26 | 27 | ); 28 | } 29 | 30 | storiesOf("Nav", module) 31 | .addDecorator(storyFn =>
    {storyFn()}
    ) 32 | .add("Responsive", () => ) 33 | .addParameters({ viewport: { defaultViewport: "iphone6" } }) 34 | .add("Mobile", () => { 35 | return ( 36 | 37 | 48 | 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /responsive-web/src/Nav/Nav.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import Nav from "./Nav"; 4 | import NavItem from "./Nav"; 5 | import { render, cleanup } from "@testing-library/react"; 6 | 7 | afterEach(cleanup); 8 | 9 | const renderNav = (screenWidth = 975) => 10 | render( 11 | 12 | 30 | 31 | ); 32 | 33 | it("should display nav items as list elements", () => { 34 | renderNav(); 35 | expect(document.querySelectorAll("li").length).toBe(5); 36 | }); 37 | 38 | it("should display a url for each nav item", () => { 39 | renderNav(); 40 | expect(document.querySelectorAll("li a").length).toBe(5); 41 | }); 42 | 43 | it("should display text passed as child", () => { 44 | const { getByText } = renderNav(); 45 | getByText("Make a Bill Payment"); 46 | }); 47 | -------------------------------------------------------------------------------- /responsive-web/src/SmartLink/SmartLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import isExternal from "is-url-external"; 5 | 6 | /** Render a plain anchor for absolute links and anchor tags, and a React Router otherwise. React Router's Link doesn't support external links: https://github.com/ReactTraining/react-router/issues/1147 */ 7 | const SmartLink = ({ to, className, children }) => { 8 | return isExternal(to) ? ( 9 | 10 | {children} 11 | 12 | ) : ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | SmartLink.propTypes = { 20 | /** Link text */ 21 | children: PropTypes.any.isRequired, 22 | 23 | /** CSS classname applied to link */ 24 | className: PropTypes.string, 25 | 26 | /** Link URL */ 27 | to: PropTypes.string.isRequired 28 | }; 29 | 30 | export default SmartLink; 31 | -------------------------------------------------------------------------------- /responsive-web/src/SmartLink/SmartLink.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import SmartLink from "./SmartLink"; 4 | import { storiesOf } from "@storybook/react"; 5 | import "../scss/global.scss"; 6 | 7 | storiesOf("Links/SmartLink", module) 8 | .add("Absolute URL", () => ( 9 | http://microsoft.com 10 | )) 11 | .add("Relative URL", () => ( 12 | 13 |

    14 | /accounts 15 |
    16 | This relative URL won't load in Storybook since there is no 17 | matching route configured. In a real app, this will render as a 18 | <Link> so React Router will handle it. 19 |

    20 |
    21 | )) 22 | .add("Anchor URL", () => ( 23 | 24 | #faq 25 | 26 | )); 27 | -------------------------------------------------------------------------------- /responsive-web/src/SmartLink/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SmartLink"; 2 | -------------------------------------------------------------------------------- /responsive-web/src/Table/Table.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Table.scss"; 3 | 4 | const Table = props => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
    AccountDue DateAmountPeriod
    Visa - 341204/01/2016$1,19003/01/2016 - 03/31/2016
    Visa - 607603/01/2016$2,44302/01/2016 - 02/29/2016
    Corporate AMEX03/01/2016$1,18102/01/2016 - 02/29/2016
    Visa - 341202/01/2016$84201/01/2016 - 01/31/2016
    42 | ); 43 | }; 44 | 45 | export default Table; 46 | -------------------------------------------------------------------------------- /responsive-web/src/Table/Table.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", sans-serif; 3 | } 4 | th, 5 | td { 6 | background: #eee; 7 | padding: 8px; 8 | } 9 | 10 | @media screen and (max-width: 600px) { 11 | table { 12 | width: 100%; 13 | } 14 | 15 | table thead { 16 | display: none; 17 | } 18 | /* 19 | table tr, table td { 20 | border-bottom: 1px solid #ddd; 21 | } */ 22 | 23 | tr { 24 | border-bottom: 1px solid #ddd; 25 | } 26 | 27 | table td { 28 | display: flex; 29 | } 30 | 31 | table td::before { 32 | content: attr(label); 33 | font-weight: bold; 34 | width: 120px; 35 | min-width: 120px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /responsive-web/src/Table/Table.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import Table from "./Table"; 4 | import { storiesOf } from "@storybook/react"; 5 | 6 | storiesOf("Table", module) 7 | .addDecorator(storyFn =>
    {storyFn()}
    ) 8 | .add("Web", () => ) 9 | .addParameters({ viewport: { defaultViewport: "iphone6" } }) 10 | .add("Mobile", () => { 11 | return
    ; 12 | }); 13 | -------------------------------------------------------------------------------- /responsive-web/src/hooks/useWindowSize.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { debounce } from "../utils/debounce"; 3 | 4 | const useWindowSize = lag => { 5 | const [windowSize, getWindowSize] = useState({ 6 | w: window.innerWidth, 7 | h: window.innerHeight 8 | }); 9 | useEffect(() => { 10 | const handleResize = debounce((lag = 100) => { 11 | getWindowSize({ 12 | w: window.innerWidth, 13 | h: window.innerHeight 14 | }); 15 | }, lag); 16 | window.addEventListener("resize", handleResize); 17 | return () => { 18 | window.removeEventListener("resize", handleResize); 19 | }; 20 | }, [lag]); 21 | return windowSize; 22 | }; 23 | 24 | export default useWindowSize; 25 | -------------------------------------------------------------------------------- /responsive-web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import FlexboxPlayground from "./FlexboxPlayground"; 4 | import GridPlayground from "./GridPlayground"; 5 | import GridTemplateArea from "./GridTemplateArea"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /responsive-web/src/scss/_accessibility.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/utilities/screenreaders"; 2 | @import "~bootstrap/scss/utilities/visibility"; 3 | 4 | .sr-only { 5 | margin: -1px; 6 | } 7 | 8 | .sr-only--focusable:active, 9 | .sr-only--focusable:focus { 10 | position: static; 11 | width: auto; 12 | height: auto; 13 | margin: 0; 14 | overflow: visible; 15 | clip: auto; 16 | } -------------------------------------------------------------------------------- /responsive-web/src/scss/_colors.scss: -------------------------------------------------------------------------------- 1 | // Primary Colors 2 | $black: #000000; 3 | $white: #ffffff; 4 | $orange: #ff6000; 5 | 6 | //grays 7 | $copy: #293033; //charcoal 8 | $darkgray: #63686b; // graphite 9 | $medgray: #57505F; //plum 10 | $lightgray: #dbdbdb; //slate gray 11 | $graybg: #f2f2f2; //background gray 12 | 13 | //accent colors 14 | $accent: #00548A; //corporate blue 15 | $accentlight: #f6fcfc; //ice blue 16 | $links: #1E71AC; //link blue 17 | $error: #EB0000; //error red 18 | $success: #008a25; //success green 19 | $warning: #EBD22F; //warning yellow 20 | $plum: #57505F; 21 | $turqoise-blue: #06ABC0; 22 | 23 | //type 24 | $copy: #293033; //charcoal 25 | 26 | :export { 27 | black: $black; 28 | white: $white; 29 | orange: $orange; 30 | charcoal-basetext: $copy; 31 | dark-gray: $darkgray; 32 | accent: $accent; 33 | ice-blue: $accentlight; 34 | link-blue: $links; 35 | bg-gray: $graybg; 36 | light-gray: $lightgray; 37 | plum: $plum; 38 | success: $success; 39 | turqoise-blue: $turqoise-blue; 40 | error: $error; 41 | warning: $warning; 42 | } -------------------------------------------------------------------------------- /responsive-web/src/scss/_custom-grid.scss: -------------------------------------------------------------------------------- 1 | /* Custom theming and bootstrap grid overrides 2 | // $grid-breakpoints: (xs: 0, 3 | // sm: 576px, 4 | // md: 768px, 5 | // lg: 992px, 6 | // xl: 1200px); */ 7 | 8 | /* Import Bootstrap and its default variables */ 9 | @import "~bootstrap/scss/bootstrap-grid.scss"; 10 | -------------------------------------------------------------------------------- /responsive-web/src/scss/_reach_components.scss: -------------------------------------------------------------------------------- 1 | /* This code is subject to LICENSE in root of this repository https://github.com/reach/reach-ui* and is modified here as/ 2 | 3 | /* Used to detect in JavaScript if apps have loaded styles or not. */ 4 | :root { 5 | --reach-dialog: 1; 6 | } 7 | 8 | [data-reach-dialog-overlay] { 9 | background: hsla(0, 0%, 0%, 0.4); 10 | position: fixed; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | overflow: auto; 16 | height: 100%; 17 | width: 100%; 18 | display: flex; 19 | align-items: center; 20 | flex-direction: column; 21 | 22 | &.tooltip { 23 | position: absolute; 24 | display: inline-block; 25 | width: auto; 26 | height: auto; 27 | left: auto; 28 | right: auto; 29 | top: auto; 30 | bottom: auto; 31 | border: 1px solid $lightgray; 32 | min-width: 200px; 33 | box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); 34 | overflow: visible; 35 | } 36 | } 37 | 38 | [data-reach-dialog-content] { 39 | background: white; 40 | padding: 20px; 41 | align-self: center; 42 | position: relative; 43 | margin: auto; 44 | } 45 | 46 | .tooltip__pointer { 47 | display: block; 48 | width: 20px; 49 | height: 20px; 50 | transform: rotate(45deg); 51 | border-left: 3px solid $lightgray; 52 | border-top: 3px solid $lightgray; 53 | background: $white; 54 | position: absolute; 55 | z-index: 2; 56 | } -------------------------------------------------------------------------------- /responsive-web/src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | $mobile_only: "only screen and (max-width: 767px)"; 4 | $tablet_only: "only screen and (min-width: 768px) and (max-width: 991px)"; 5 | $tablet: "only screen and (min-width: 768px)"; 6 | $desktop: "only screen and (min-width: 992px)"; 7 | 8 | //Fonts 9 | $fontFamily: Arial, Helvetica, sans-serif; 10 | $fontSizeMedium: 15px; 11 | -------------------------------------------------------------------------------- /responsive-web/src/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap-reboot.scss"; 2 | @import "accessibility"; 3 | @import "variables"; 4 | @import "utility"; 5 | @import "custom-grid"; 6 | @import "reach_components"; 7 | 8 | .body { 9 | font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif; 10 | } 11 | -------------------------------------------------------------------------------- /responsive-web/src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | //Source: https://github.com/jashkenas/underscore 2 | 3 | // Returns a function, that, as long as it continues to be invoked, will not 4 | // be triggered. The function will be called after it stops being called for 5 | // N milliseconds. If `immediate` is passed, trigger the function on the 6 | // leading edge, instead of the trailing. 7 | export function debounce(func, wait, immediate) { 8 | var timeout; 9 | return function() { 10 | var context = this, 11 | args = arguments; 12 | var later = function() { 13 | timeout = null; 14 | if (!immediate) func.apply(context, args); 15 | }; 16 | var callNow = immediate && !timeout; 17 | clearTimeout(timeout); 18 | timeout = setTimeout(later, wait); 19 | if (callNow) func.apply(context, args); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /user-app-typescript/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - react-app 4 | - 'plugin:@typescript-eslint/recommended' 5 | - 'plugin:prettier/recommended' 6 | - 'prettier/@typescript-eslint' 7 | parser: '@typescript-eslint/parser' 8 | plugins: ['@typescript-eslint', 'cypress'] 9 | env: 10 | cypress/globals: true 11 | rules: 12 | prefer-template: error 13 | prefer-const: error 14 | '@typescript-eslint/explicit-member-accessibility': 'off' 15 | '@typescript-eslint/explicit-function-return-type': [error, {allowExpressions: true}] 16 | '@typescript-eslint/no-explicit-any': 'off' 17 | '@typescript-eslint/no-empty-interface': 'warn' 18 | '@typescript-eslint/no-use-before-define': [error, nofunc] -------------------------------------------------------------------------------- /user-app-typescript/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /user-app-typescript/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /user-app-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Athena Watertown React Training 2 | 3 | Fork of Cory's React training seminar he did in Watertown on 11/6/19 - 11/8/19. In this fork, I have done our React training exercises in Typescript. 4 | -------------------------------------------------------------------------------- /user-app-typescript/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /user-app-typescript/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /user-app-typescript/cypress/integration/Users.spec.ts: -------------------------------------------------------------------------------- 1 | context('Users', () => { 2 | beforeEach(() => { 3 | cy.visit('http://localhost:3000/users'); 4 | }); 5 | 6 | it('should add a user when delete is clicked', () => { 7 | cy.findByText('Add User').click(); 8 | cy.findByLabelText('Name').type('Bob'); 9 | cy.findByLabelText('Email').type('Bob@compulink.com{enter}'); 10 | cy.findByLabelText('Delete user Bob').click(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /user-app-typescript/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /user-app-typescript/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | import '@testing-library/cypress/add-commands'; 28 | -------------------------------------------------------------------------------- /user-app-typescript/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /user-app-typescript/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "lib": [ 7 | "es5", 8 | "dom" 9 | ], 10 | "types": ["cypress", "@testing-library/cypress"] 11 | }, 12 | "include": [ 13 | "**/*.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /user-app-typescript/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { "id": 1, "name": "Cory", "email": "a@h.com" }, 4 | { "id": 2, "name": "Megan", "email": "b@h.com" }, 5 | { "id": 3, "name": "Tami", "email": "c@h.com" } 6 | ] 7 | } -------------------------------------------------------------------------------- /user-app-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "athena-watertown", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:3001", 6 | "dependencies": { 7 | "@types/jest": "24.0.22", 8 | "@types/node": "12.12.6", 9 | "@types/react": "16.9.11", 10 | "@types/react-dom": "16.9.3", 11 | "@types/react-router-dom": "^5.1.2", 12 | "bootswatch": "^4.3.1", 13 | "json-server": "^0.15.1", 14 | "npm-run-all": "^4.1.5", 15 | "react": "^16.11.0", 16 | "react-dom": "^16.11.0", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.2.0", 19 | "typescript": "3.7.2" 20 | }, 21 | "scripts": { 22 | "start": "run-p start:app start:api", 23 | "start:app": "react-scripts start", 24 | "start:api": "json-server --port 3001 --watch db.json --routes routes.json", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject", 28 | "cy": "start-server-and-test start http://localhost:3000 cy:open", 29 | "cy:open": "cypress open" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "prettier": { 47 | "singleQuote": true 48 | }, 49 | "devDependencies": { 50 | "@testing-library/cypress": "^5.0.2", 51 | "@testing-library/react": "^9.3.2", 52 | "@types/testing-library__cypress": "^5.0.1", 53 | "cypress": "^3.6.0", 54 | "eslint-config-prettier": "^6.5.0", 55 | "eslint-plugin-cypress": "^2.7.0", 56 | "eslint-plugin-prettier": "^3.1.1", 57 | "prettier": "^1.18.2", 58 | "start-server-and-test": "^1.10.6" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /user-app-typescript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/user-app-typescript/public/favicon.ico -------------------------------------------------------------------------------- /user-app-typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /user-app-typescript/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/user-app-typescript/public/logo192.png -------------------------------------------------------------------------------- /user-app-typescript/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coryhouse/reactjsconsulting/2b0ebdb22aea127e6965f7516df6f95ba086e7df/user-app-typescript/public/logo512.png -------------------------------------------------------------------------------- /user-app-typescript/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /user-app-typescript/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /user-app-typescript/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1" 3 | } -------------------------------------------------------------------------------- /user-app-typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Home from './Home'; 3 | import Nav from './Nav'; 4 | import Users from './Users'; 5 | import ManageUser from './ManageUser'; 6 | import { Route } from 'react-router-dom'; 7 | import { User, getUsers, deleteUser, addUser, editUser } from './api/users'; 8 | import { 9 | InternationalizationContext, 10 | Language 11 | } from './InternationalizationContext'; 12 | 13 | function App(): JSX.Element { 14 | const [users, setUsers] = useState([]); 15 | const [language, setLanguage] = useState('English'); 16 | 17 | useEffect(() => { 18 | getUsers().then(_users => setUsers(_users)); 19 | }, []); 20 | 21 | async function handleDelete(id: number): Promise { 22 | await deleteUser(id); 23 | const newUsers = users.filter(user => user.id !== id); 24 | setUsers(newUsers); 25 | } 26 | 27 | async function handleAddUser(user: Omit): Promise { 28 | const newUser = await addUser(user); 29 | setUsers([...users, newUser]); 30 | } 31 | 32 | async function handleEditUser(changedUser: User): Promise { 33 | await editUser(changedUser); 34 | const newUsers = users.map(user => 35 | user.id === changedUser.id ? changedUser : user 36 | ); 37 | setUsers(newUsers); 38 | } 39 | 40 | return ( 41 | 42 |