From 1aa65ef39b9bc6dc6499e82c460880418cec0310 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 3 Mar 2021 12:03:04 +0000
Subject: [PATCH 01/17] Feature/jest config (#1)
* add config files for jest and basic test
* reinstall husky
* install pretty-quick
* trying to trigger husky precommit hook
* install v4 husky
---
jest.config.js | 12 ++
package-lock.json | 253 +++++++++++++++++++++++++++++++-
package.json | 17 ++-
src/components/App/App.test.tsx | 14 ++
src/components/App/App.tsx | 6 +-
test/_mocks_/fileMock.js | 0
test/_mocks_/styleMock.js | 0
test/_mocks_/svgIconMock.js | 0
test/setupTests.ts | 5 +
9 files changed, 298 insertions(+), 9 deletions(-)
create mode 100644 jest.config.js
create mode 100644 src/components/App/App.test.tsx
create mode 100644 test/_mocks_/fileMock.js
create mode 100644 test/_mocks_/styleMock.js
create mode 100644 test/_mocks_/svgIconMock.js
create mode 100644 test/setupTests.ts
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..82ada15
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ preset: "ts-jest",
+ setupFilesAfterEnv: ["./test/setupTests.ts"],
+ moduleNameMapper: {
+ "\\.(css|less|sass|scss)$": "/test/__mocks__/styleMock.js",
+ "\\.(png|gif|ttf|eot)$": "/test/__mocks__/fileMock.js",
+ "\\.(svg)$": "/test/__mocks__/svgIconMock.js",
+ },
+ moduleDirectories: ["src", "node_modules"],
+ collectCoverageFrom: ["src/**/*.tsx", "src/**/*.ts"],
+ testEnvironment: "jsdom",
+};
diff --git a/package-lock.json b/package-lock.json
index 61b1026..aeb178e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1565,6 +1565,12 @@
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
"dev": true
},
+ "array-differ": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
+ "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==",
+ "dev": true
+ },
"array-flatten": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz",
@@ -1625,6 +1631,12 @@
"function-bind": "^1.1.1"
}
},
+ "arrify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+ "dev": true
+ },
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
@@ -2340,6 +2352,12 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
+ "compare-versions": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
+ "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==",
+ "dev": true
+ },
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -4228,6 +4246,15 @@
"path-exists": "^4.0.0"
}
},
+ "find-versions": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz",
+ "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==",
+ "dev": true,
+ "requires": {
+ "semver-regex": "^3.1.2"
+ }
+ },
"flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -4852,10 +4879,70 @@
"dev": true
},
"husky": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/husky/-/husky-5.1.1.tgz",
- "integrity": "sha512-80LZ736V0Nr4/st0c2COYaMbEQhHNmAbLMN8J/kLk7/mo0QdUkUGNDjv/7jVkhug377Wh8wfbWyaVXEJ/h2B/Q==",
- "dev": true
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
+ "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "ci-info": "^2.0.0",
+ "compare-versions": "^3.6.0",
+ "cosmiconfig": "^7.0.0",
+ "find-versions": "^4.0.0",
+ "opencollective-postinstall": "^2.0.2",
+ "pkg-dir": "^5.0.0",
+ "please-upgrade-node": "^3.2.0",
+ "slash": "^3.0.0",
+ "which-pm-runs": "^1.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "pkg-dir": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+ "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
+ "dev": true,
+ "requires": {
+ "find-up": "^5.0.0"
+ }
+ }
+ }
},
"iconv-lite": {
"version": "0.4.24",
@@ -6324,6 +6411,12 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
+ "mri": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
+ "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==",
+ "dev": true
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6346,6 +6439,19 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
+ "multimatch": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz",
+ "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==",
+ "dev": true,
+ "requires": {
+ "@types/minimatch": "^3.0.3",
+ "array-differ": "^3.0.0",
+ "array-union": "^2.1.0",
+ "arrify": "^2.0.1",
+ "minimatch": "^3.0.4"
+ }
+ },
"nanoid": {
"version": "3.1.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
@@ -6685,6 +6791,12 @@
"mimic-fn": "^2.1.0"
}
},
+ "opencollective-postinstall": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
+ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
+ "dev": true
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -6950,6 +7062,15 @@
"find-up": "^4.0.0"
}
},
+ "please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "requires": {
+ "semver-compare": "^1.0.0"
+ }
+ },
"portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -7143,6 +7264,112 @@
}
}
},
+ "pretty-quick": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.0.tgz",
+ "integrity": "sha512-DtxIxksaUWCgPFN7E1ZZk4+Aav3CCuRdhrDSFZENb404sYMtuo9Zka823F+Mgeyt8Zt3bUiCjFzzWYE9LYqkmQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^3.0.0",
+ "execa": "^4.0.0",
+ "find-up": "^4.1.0",
+ "ignore": "^5.1.4",
+ "mri": "^1.1.5",
+ "multimatch": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "execa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+ "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.0",
+ "get-stream": "^5.0.0",
+ "human-signals": "^1.1.1",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.0",
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -7981,6 +8208,18 @@
"lru-cache": "^6.0.0"
}
},
+ "semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "dev": true
+ },
+ "semver-regex": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz",
+ "integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==",
+ "dev": true
+ },
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
@@ -10151,6 +10390,12 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true
},
+ "which-pm-runs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
+ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
+ "dev": true
+ },
"wildcard": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
diff --git a/package.json b/package.json
index 02e88d6..1729a8e 100644
--- a/package.json
+++ b/package.json
@@ -5,12 +5,19 @@
"main": "src/index.tsx",
"scripts": {
"start": "webpack serve",
- "test": "echo \"Error: no test specified\" && exit 1",
+ "test": "jest --env=jsdom",
+ "test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx",
- "lint:fix": "eslint . --fix"
+ "lint:fix": "eslint . --fix",
+ "pretty": "pretty-quick --branch development",
+ "prettier-format": "prettier --config .prettierrc '**/*.{json,js,jsx,ts,tsx,css,scss,md}' --write"
},
"author": "Sean Jones",
"license": "ISC",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/cssmonkey/user-management.git"
+ },
"dependencies": {
"classnames": "^2.2.6",
"react": "^17.0.1",
@@ -44,12 +51,13 @@
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"html-webpack-plugin": "^5.2.0",
- "husky": "^5.1.1",
+ "husky": "^4.3.8",
"jest": "^26.6.3",
"postcss": "^8.2.6",
"postcss-loader": "^5.0.0",
"prettier": "^2.2.1",
"prettier-quick": "0.0.5",
+ "pretty-quick": "^3.1.0",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
@@ -62,7 +70,8 @@
},
"husky": {
"hooks": {
- "pre-commit": "npm run lint && npm run pretty -- --staged"
+ "pre-commit": "npm run lint && npm run pretty -- --staged",
+ "pre-push": "npm run test -- --bail"
}
}
}
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
new file mode 100644
index 0000000..dd895fc
--- /dev/null
+++ b/src/components/App/App.test.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { render } from "@testing-library/react";
+
+import App from "./App";
+
+const setup = () => render();
+
+describe("App", () => {
+ it("renders app", () => {
+ const { container } = setup();
+ const app = container.querySelector(".app");
+ expect(app).toBeInTheDocument();
+ });
+});
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 5a78983..4b644f8 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,5 +1,9 @@
import React, { FC } from "react";
-const App: FC = () => App
;
+const App: FC = () => (
+
+);
export default App;
diff --git a/test/_mocks_/fileMock.js b/test/_mocks_/fileMock.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/_mocks_/styleMock.js b/test/_mocks_/styleMock.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/_mocks_/svgIconMock.js b/test/_mocks_/svgIconMock.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/setupTests.ts b/test/setupTests.ts
new file mode 100644
index 0000000..1dd407a
--- /dev/null
+++ b/test/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import "@testing-library/jest-dom";
From ea9d07b0efff86409db30badadaabb54cea284b4 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 3 Mar 2021 12:24:09 +0000
Subject: [PATCH 02/17] Feature/tailwind css (#2)
* add tailwind for styling
* correct dir name typo
---
package-lock.json | 298 +++++++++++++++++++++
package.json | 1 +
postcss.config.js | 6 +
src/components/App/App.tsx | 4 +-
src/styles/app.scss | 3 +
tailwind.config.js | 11 +
test/{_mocks_ => __mocks__}/fileMock.js | 0
test/{_mocks_ => __mocks__}/styleMock.js | 0
test/{_mocks_ => __mocks__}/svgIconMock.js | 0
9 files changed, 322 insertions(+), 1 deletion(-)
create mode 100644 postcss.config.js
create mode 100644 src/styles/app.scss
create mode 100644 tailwind.config.js
rename test/{_mocks_ => __mocks__}/fileMock.js (100%)
rename test/{_mocks_ => __mocks__}/styleMock.js (100%)
rename test/{_mocks_ => __mocks__}/svgIconMock.js (100%)
diff --git a/package-lock.json b/package-lock.json
index aeb178e..ed27f8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -469,6 +469,15 @@
}
}
},
+ "@fullhuman/postcss-purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz",
+ "integrity": "sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA==",
+ "dev": true,
+ "requires": {
+ "purgecss": "^3.1.3"
+ }
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1444,6 +1453,17 @@
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
},
+ "acorn-node": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
+ "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.0.0",
+ "acorn-walk": "^7.0.0",
+ "xtend": "^4.0.2"
+ }
+ },
"acorn-walk": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
@@ -1697,6 +1717,12 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
+ "at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true
+ },
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -2120,6 +2146,12 @@
"integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==",
"dev": true
},
+ "camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true
+ },
"caniuse-lite": {
"version": "1.0.30001192",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz",
@@ -2316,6 +2348,16 @@
"object-visit": "^1.0.0"
}
},
+ "color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
+ "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.1",
+ "color-string": "^1.5.4"
+ }
+ },
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2331,6 +2373,16 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
+ "color-string": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
+ "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
@@ -2586,6 +2638,12 @@
}
}
},
+ "css-unit-converter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
+ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
+ "dev": true
+ },
"css-what": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
@@ -2772,6 +2830,12 @@
}
}
},
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+ "dev": true
+ },
"del": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
@@ -2858,6 +2922,23 @@
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
"dev": true
},
+ "detective": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
+ "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==",
+ "dev": true,
+ "requires": {
+ "acorn-node": "^1.6.1",
+ "defined": "^1.0.0",
+ "minimist": "^1.1.1"
+ }
+ },
+ "didyoumean": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz",
+ "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=",
+ "dev": true
+ },
"diff-sequences": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
@@ -4327,6 +4408,18 @@
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
"dev": true
},
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4666,6 +4759,12 @@
}
}
},
+ "html-tags": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
+ "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
+ "dev": true
+ },
"html-webpack-plugin": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.2.0.tgz",
@@ -6031,6 +6130,16 @@
"minimist": "^1.2.5"
}
},
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -6187,6 +6296,12 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
+ "lodash.toarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+ "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=",
+ "dev": true
+ },
"loglevel": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
@@ -6411,6 +6526,12 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
+ "modern-normalize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
+ "integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw==",
+ "dev": true
+ },
"mri": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz",
@@ -6519,6 +6640,15 @@
}
}
},
+ "node-emoji": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
+ "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
+ "dev": true,
+ "requires": {
+ "lodash.toarray": "^4.4.0"
+ }
+ },
"node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@@ -6664,6 +6794,12 @@
}
}
},
+ "object-hash": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
+ "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==",
+ "dev": true
+ },
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
@@ -7119,6 +7255,58 @@
"source-map": "^0.6.1"
}
},
+ "postcss-functions": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
+ "integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.2",
+ "object-assign": "^4.1.1",
+ "postcss": "^6.0.9",
+ "postcss-value-parser": "^3.3.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "postcss": {
+ "version": "6.0.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz",
+ "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.1",
+ "source-map": "^0.6.1",
+ "supports-color": "^5.4.0"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ }
+ }
+ },
+ "postcss-js": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz",
+ "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==",
+ "dev": true,
+ "requires": {
+ "camelcase-css": "^2.0.1",
+ "postcss": "^8.1.6"
+ }
+ },
"postcss-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-5.0.0.tgz",
@@ -7165,6 +7353,15 @@
"icss-utils": "^5.0.0"
}
},
+ "postcss-nested": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.4.tgz",
+ "integrity": "sha512-/dimXVqdUAVS2ZiIX0uvyk9UCI825y6LW4TnjG51JTKF89CcorHPAjTUGPF70k2wlQYts5OzfnhYMgfGfHCClQ==",
+ "dev": true,
+ "requires": {
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
"postcss-selector-parser": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz",
@@ -7264,6 +7461,12 @@
}
}
},
+ "pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+ "dev": true
+ },
"pretty-quick": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.0.tgz",
@@ -7440,6 +7643,26 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
+ "purgecss": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-3.1.3.tgz",
+ "integrity": "sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ==",
+ "dev": true,
+ "requires": {
+ "commander": "^6.0.0",
+ "glob": "^7.0.0",
+ "postcss": "^8.2.1",
+ "postcss-selector-parser": "^6.0.2"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true
+ }
+ }
+ },
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@@ -7609,6 +7832,24 @@
"strip-indent": "^3.0.0"
}
},
+ "reduce-css-calc": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
+ "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
+ "dev": true,
+ "requires": {
+ "css-unit-converter": "^1.1.1",
+ "postcss-value-parser": "^3.3.0"
+ },
+ "dependencies": {
+ "postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "dev": true
+ }
+ }
+ },
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
@@ -8426,6 +8667,23 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
+ "simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.3.1"
+ },
+ "dependencies": {
+ "is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "dev": true
+ }
+ }
+ },
"sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -9048,6 +9306,34 @@
}
}
},
+ "tailwindcss": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.0.3.tgz",
+ "integrity": "sha512-s8NEqdLBiVbbdL0a5XwTb8jKmIonOuI4RMENEcKLR61jw6SdKvBss7NWZzwCaD+ZIjlgmesv8tmrjXEp7C0eAQ==",
+ "dev": true,
+ "requires": {
+ "@fullhuman/postcss-purgecss": "^3.1.3",
+ "bytes": "^3.0.0",
+ "chalk": "^4.1.0",
+ "color": "^3.1.3",
+ "detective": "^5.2.0",
+ "didyoumean": "^1.2.1",
+ "fs-extra": "^9.1.0",
+ "html-tags": "^3.1.0",
+ "lodash": "^4.17.20",
+ "modern-normalize": "^1.0.0",
+ "node-emoji": "^1.8.1",
+ "object-hash": "^2.1.1",
+ "postcss-functions": "^3",
+ "postcss-js": "^3.0.3",
+ "postcss-nested": "^5.0.1",
+ "postcss-selector-parser": "^6.0.4",
+ "postcss-value-parser": "^4.1.0",
+ "pretty-hrtime": "^1.0.3",
+ "reduce-css-calc": "^2.1.8",
+ "resolve": "^1.19.0"
+ }
+ },
"tapable": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -9400,6 +9686,12 @@
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
"dev": true
},
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -10481,6 +10773,12 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
+ "xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true
+ },
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
diff --git a/package.json b/package.json
index 1729a8e..0ed3495 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
+ "tailwindcss": "^2.0.3",
"ts-jest": "^26.5.2",
"ts-loader": "^8.0.17",
"typescript": "^4.2.2",
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 4b644f8..229cd04 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,7 +1,9 @@
import React, { FC } from "react";
+import "../../styles/app.scss";
+
const App: FC = () => (
-
+
);
diff --git a/src/styles/app.scss b/src/styles/app.scss
new file mode 100644
index 0000000..76fcadc
--- /dev/null
+++ b/src/styles/app.scss
@@ -0,0 +1,3 @@
+@import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities";
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..eb924ba
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,11 @@
+module.exports = {
+ purge: [],
+ darkMode: false, // or 'media' or 'class'
+ theme: {
+ extend: {},
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/test/_mocks_/fileMock.js b/test/__mocks__/fileMock.js
similarity index 100%
rename from test/_mocks_/fileMock.js
rename to test/__mocks__/fileMock.js
diff --git a/test/_mocks_/styleMock.js b/test/__mocks__/styleMock.js
similarity index 100%
rename from test/_mocks_/styleMock.js
rename to test/__mocks__/styleMock.js
diff --git a/test/_mocks_/svgIconMock.js b/test/__mocks__/svgIconMock.js
similarity index 100%
rename from test/_mocks_/svgIconMock.js
rename to test/__mocks__/svgIconMock.js
From 3e979078b9821e268642840d77b264a9a9857e55 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 9 Mar 2021 18:44:18 +0000
Subject: [PATCH 03/17] Feature/redux state (#3)
* working on adding userProfile slice to state
* lint errors
* omit App tests until mockstore is added
---
package-lock.json | 37 ++++++++++++++++++++++++++++++--
package.json | 2 ++
src/api/apiConfig.ts | 7 ++++++
src/api/apiService.ts | 9 ++++++++
src/components/App/App.test.tsx | 2 +-
src/components/App/App.tsx | 21 ++++++++++++------
src/index.tsx | 9 +++++++-
src/state/rootReducer.ts | 8 +++++++
src/state/store.ts | 10 +++++++++
src/state/userProfile/actions.ts | 11 ++++++++++
src/state/userProfile/reducer.ts | 21 ++++++++++++++++++
11 files changed, 127 insertions(+), 10 deletions(-)
create mode 100644 src/api/apiConfig.ts
create mode 100644 src/api/apiService.ts
create mode 100644 src/state/rootReducer.ts
create mode 100644 src/state/store.ts
create mode 100644 src/state/userProfile/actions.ts
create mode 100644 src/state/userProfile/reducer.ts
diff --git a/package-lock.json b/package-lock.json
index ed27f8f..d32e422 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -776,6 +776,17 @@
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.1.0.tgz",
"integrity": "sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg=="
},
+ "@reduxjs/toolkit": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.0.tgz",
+ "integrity": "sha512-E/FUraRx+8guw9Hlg/Ja8jI/hwCrmIKed8Annt9YsZw3BQp+F24t5I5b2OWR6pkEHY4hn1BgP08FrTZFRKsdaQ==",
+ "requires": {
+ "immer": "^8.0.0",
+ "redux": "^4.0.0",
+ "redux-thunk": "^2.3.0",
+ "reselect": "^4.0.0"
+ }
+ },
"@sinonjs/commons": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
@@ -1761,6 +1772,14 @@
"integrity": "sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==",
"dev": true
},
+ "axios": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+ "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -4355,8 +4374,7 @@
"follow-redirects": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
- "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==",
- "dev": true
+ "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA=="
},
"for-in": {
"version": "1.0.2",
@@ -5064,6 +5082,11 @@
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
+ "immer": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
+ "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
+ },
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -7867,6 +7890,11 @@
"@redux-saga/core": "^1.1.3"
}
},
+ "redux-thunk": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
+ "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
+ },
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
@@ -8134,6 +8162,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
+ "reselect": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
+ "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
+ },
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
diff --git a/package.json b/package.json
index 0ed3495..4838c4d 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,8 @@
"url": "https://github.com/cssmonkey/user-management.git"
},
"dependencies": {
+ "@reduxjs/toolkit": "^1.5.0",
+ "axios": "^0.21.1",
"classnames": "^2.2.6",
"react": "^17.0.1",
"react-dom": "^17.0.1",
diff --git a/src/api/apiConfig.ts b/src/api/apiConfig.ts
new file mode 100644
index 0000000..8843386
--- /dev/null
+++ b/src/api/apiConfig.ts
@@ -0,0 +1,7 @@
+const API_BASE_URL = "https://jsonplaceholder.typicode.com";
+
+export default {
+ endpoints: {
+ userProfile: (userId: string): string => `${API_BASE_URL}/users/${userId}`,
+ },
+};
diff --git a/src/api/apiService.ts b/src/api/apiService.ts
new file mode 100644
index 0000000..ab47898
--- /dev/null
+++ b/src/api/apiService.ts
@@ -0,0 +1,9 @@
+import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios";
+
+class ApiService {
+ async callApi(config: AxiosRequestConfig): Promise> {
+ return axios.request(config);
+ }
+}
+
+export default new ApiService();
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index dd895fc..c9c224a 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -5,7 +5,7 @@ import App from "./App";
const setup = () => render();
-describe("App", () => {
+describe.skip("App", () => {
it("renders app", () => {
const { container } = setup();
const app = container.querySelector(".app");
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 229cd04..9b3a37c 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,11 +1,20 @@
-import React, { FC } from "react";
+import React, { FC, useEffect } from "react";
+import { useDispatch } from "react-redux";
+
+import { fetchUserById } from "../../state/userProfile/actions";
import "../../styles/app.scss";
-const App: FC = () => (
-
-);
+const App: FC = () => {
+ const dispatch = useDispatch();
+ useEffect(() => {
+ dispatch(fetchUserById());
+ }, [dispatch]);
+ return (
+
+ );
+};
export default App;
diff --git a/src/index.tsx b/src/index.tsx
index 316b54d..305dd19 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,13 @@
import React from "react";
import { render } from "react-dom";
+import { Provider } from "react-redux";
+import store from "./state/store";
import App from "./components/App/App";
-render(, document.getElementById("app"));
+render(
+
+
+ ,
+ document.getElementById("app")
+);
diff --git a/src/state/rootReducer.ts b/src/state/rootReducer.ts
new file mode 100644
index 0000000..4c4f36f
--- /dev/null
+++ b/src/state/rootReducer.ts
@@ -0,0 +1,8 @@
+import { combineReducers } from "@reduxjs/toolkit";
+
+import userProfile from "./userProfile/reducer";
+
+const rootReducer = combineReducers({ userProfile });
+
+export type RootState = ReturnType;
+export default rootReducer;
diff --git a/src/state/store.ts b/src/state/store.ts
new file mode 100644
index 0000000..1ae2c60
--- /dev/null
+++ b/src/state/store.ts
@@ -0,0 +1,10 @@
+import { configureStore } from "@reduxjs/toolkit";
+
+import rootReducer from "./rootReducer";
+
+const store = configureStore({
+ reducer: rootReducer,
+ devTools: process.env.NODE_ENV !== "production",
+});
+
+export default store;
diff --git a/src/state/userProfile/actions.ts b/src/state/userProfile/actions.ts
new file mode 100644
index 0000000..2d06d5c
--- /dev/null
+++ b/src/state/userProfile/actions.ts
@@ -0,0 +1,11 @@
+import { createAsyncThunk } from "@reduxjs/toolkit";
+import ApiService from "../../api/apiService";
+import apiConfig from "../../api/apiConfig";
+
+export const fetchUserById = createAsyncThunk("users/fetchById", async () => {
+ const response = await ApiService.callApi({
+ url: apiConfig.endpoints.userProfile("1"),
+ method: "GET",
+ });
+ return response.data;
+});
diff --git a/src/state/userProfile/reducer.ts b/src/state/userProfile/reducer.ts
new file mode 100644
index 0000000..725a468
--- /dev/null
+++ b/src/state/userProfile/reducer.ts
@@ -0,0 +1,21 @@
+import { createReducer } from "@reduxjs/toolkit";
+
+import { fetchUserById } from "./actions";
+
+interface UserState {
+ name?: string;
+ userName?: string;
+}
+
+const initialState = {} as UserState;
+
+const userProfileReducer = createReducer(initialState, (builder) => {
+ builder.addCase(fetchUserById.fulfilled, (state, action) => {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ });
+});
+
+export default userProfileReducer;
From 6c2ce5dc01d0667afbfa5574f83b51006722558a Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Fri, 19 Mar 2021 16:34:39 +0000
Subject: [PATCH 04/17] add store to test (#4)
---
.vscode/launch.json | 18 ++++++++++++++++++
package-lock.json | 24 ++++++++++++++++++++++++
src/components/App/App.test.tsx | 15 +++++++++++++--
3 files changed, 55 insertions(+), 2 deletions(-)
create mode 100644 .vscode/launch.json
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..d1360a6
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,18 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Jest Current File",
+ "program": "${workspaceFolder}/node_modules/.bin/jest",
+ "args": ["--runTestsByPath", "${relativeFile}"],
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "disableOptimisticBPs": true,
+ "windows": {
+ "program": "${workspaceFolder}/node_modules/jest/bin/jest"
+ }
+ }
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index d32e422..7cf02bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1134,6 +1134,15 @@
"redux": "^4.0.0"
}
},
+ "@types/redux-mock-store": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz",
+ "integrity": "sha512-6LBtAQBN34i7SI5X+Qs4zpTEZO1tTDZ6sZ9fzFjYwTl3nLQXaBtwYdoV44CzNnyKu438xJ1lSIYyw0YMvunESw==",
+ "dev": true,
+ "requires": {
+ "redux": "^4.0.5"
+ }
+ },
"@types/stack-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -6313,6 +6322,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
+ "lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=",
+ "dev": true
+ },
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -7882,6 +7897,15 @@
"symbol-observable": "^1.2.0"
}
},
+ "redux-mock-store": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz",
+ "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==",
+ "dev": true,
+ "requires": {
+ "lodash.isplainobject": "^4.0.6"
+ }
+ },
"redux-saga": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.3.tgz",
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index c9c224a..7bacd7f 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -1,13 +1,24 @@
import React from "react";
+import { Provider } from "react-redux";
import { render } from "@testing-library/react";
+import store from "../../state/store";
+
import App from "./App";
-const setup = () => render();
+const setup = () => {
+ const container = render(
+
+
+
+ );
+ return container;
+};
-describe.skip("App", () => {
+describe("App", () => {
it("renders app", () => {
const { container } = setup();
+
const app = container.querySelector(".app");
expect(app).toBeInTheDocument();
});
From 48d210b175ce85445ab96a1d86ab020510afb455 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 13 Apr 2021 09:11:37 +0100
Subject: [PATCH 05/17] Feature/user header component (#5)
* display user header once fetchUser fufilled
* renderConnected utility function, UserHeader component
---
package.json | 2 ++
src/components/App/App.test.tsx | 34 ++++++++++--------
src/components/App/App.tsx | 21 +++++++++--
src/components/Loader/Loader.tsx | 9 +++++
src/components/UserHeader/UserHeader.test.tsx | 23 ++++++++++++
src/components/UserHeader/UserHeader.tsx | 11 ++++++
src/state/userProfile/reducer.ts | 4 +--
src/state/userProfile/selectors.ts | 3 ++
src/utilities/test/renderConnected.tsx | 35 +++++++++++++++++++
9 files changed, 122 insertions(+), 20 deletions(-)
create mode 100644 src/components/Loader/Loader.tsx
create mode 100644 src/components/UserHeader/UserHeader.test.tsx
create mode 100644 src/components/UserHeader/UserHeader.tsx
create mode 100644 src/state/userProfile/selectors.ts
create mode 100644 src/utilities/test/renderConnected.tsx
diff --git a/package.json b/package.json
index 4838c4d..ec99e50 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"@types/node": "^14.14.31",
"@types/react-dom": "^17.0.1",
"@types/react-redux": "^7.1.16",
+ "@types/redux-mock-store": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"autoprefixer": "^10.2.4",
@@ -60,6 +61,7 @@
"prettier": "^2.2.1",
"prettier-quick": "0.0.5",
"pretty-quick": "^3.1.0",
+ "redux-mock-store": "^1.5.4",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index 7bacd7f..0d26919 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -1,24 +1,28 @@
import React from "react";
-import { Provider } from "react-redux";
-import { render } from "@testing-library/react";
-
-import store from "../../state/store";
+import renderConnected from "../../utilities/test/renderConnected";
import App from "./App";
-const setup = () => {
- const container = render(
-
-
-
- );
- return container;
-};
+const setup = (initialState = {}) =>
+ renderConnected({
+ ui: ,
+ initialState,
+ });
describe("App", () => {
- it("renders app", () => {
- const { container } = setup();
-
+ it("renders Loader when userProfile not loaded", () => {
+ const { container } = setup({
+ userProfile: {},
+ });
+ const app = container.querySelector(".loader");
+ expect(app).toBeInTheDocument();
+ });
+ it("renders App when userProfile loaded", () => {
+ const { container } = setup({
+ userProfile: {
+ username: "Steve",
+ },
+ });
const app = container.querySelector(".app");
expect(app).toBeInTheDocument();
});
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 9b3a37c..a7bca60 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,18 +1,33 @@
import React, { FC, useEffect } from "react";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { fetchUserById } from "../../state/userProfile/actions";
+import { getUsername } from "../../state/userProfile/selectors";
+
+import Loader from "../Loader/Loader";
+import UserHeader from "../UserHeader/UserHeader";
import "../../styles/app.scss";
const App: FC = () => {
const dispatch = useDispatch();
+ const username = useSelector(getUsername);
+
useEffect(() => {
- dispatch(fetchUserById());
+ if (!username) {
+ dispatch(fetchUserById());
+ }
}, [dispatch]);
+
+ const isPageReady = username;
+
+ if (!isPageReady) {
+ return ;
+ }
+
return (
);
};
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
new file mode 100644
index 0000000..478cb07
--- /dev/null
+++ b/src/components/Loader/Loader.tsx
@@ -0,0 +1,9 @@
+import React, { FC } from "react";
+
+const Loader: FC = () => (
+
+);
+
+export default Loader;
diff --git a/src/components/UserHeader/UserHeader.test.tsx b/src/components/UserHeader/UserHeader.test.tsx
new file mode 100644
index 0000000..90501f7
--- /dev/null
+++ b/src/components/UserHeader/UserHeader.test.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import renderConnected from "../../utilities/test/renderConnected";
+
+import UserHeader from "./UserHeader";
+
+const setup = (initialState = {}) =>
+ renderConnected({
+ ui: ,
+ initialState,
+ });
+
+describe("UserHeader", () => {
+ it("should display username", () => {
+ setup({
+ userProfile: {
+ username: "Steve",
+ },
+ });
+ const userHeaderText = screen.getByText("Logged in as Steve");
+ expect(userHeaderText).toBeInTheDocument();
+ });
+});
diff --git a/src/components/UserHeader/UserHeader.tsx b/src/components/UserHeader/UserHeader.tsx
new file mode 100644
index 0000000..8f6862b
--- /dev/null
+++ b/src/components/UserHeader/UserHeader.tsx
@@ -0,0 +1,11 @@
+import React, { FC } from "react";
+import { useSelector } from "react-redux";
+
+import { getUsername } from "../../state/userProfile/selectors";
+
+const UserHeader: FC = () => {
+ const userName = useSelector(getUsername);
+ return Logged in as {userName}
;
+};
+
+export default UserHeader;
diff --git a/src/state/userProfile/reducer.ts b/src/state/userProfile/reducer.ts
index 725a468..ec0751f 100644
--- a/src/state/userProfile/reducer.ts
+++ b/src/state/userProfile/reducer.ts
@@ -4,10 +4,10 @@ import { fetchUserById } from "./actions";
interface UserState {
name?: string;
- userName?: string;
+ username?: string;
}
-const initialState = {} as UserState;
+export const initialState = {} as UserState;
const userProfileReducer = createReducer(initialState, (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {
diff --git a/src/state/userProfile/selectors.ts b/src/state/userProfile/selectors.ts
new file mode 100644
index 0000000..d12acfc
--- /dev/null
+++ b/src/state/userProfile/selectors.ts
@@ -0,0 +1,3 @@
+import { RootState } from "../rootReducer";
+
+export const getUsername = (state: RootState): string => state.userProfile.username;
diff --git a/src/utilities/test/renderConnected.tsx b/src/utilities/test/renderConnected.tsx
new file mode 100644
index 0000000..b0f5d48
--- /dev/null
+++ b/src/utilities/test/renderConnected.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { render, RenderResult } from "@testing-library/react";
+import { Provider } from "react-redux";
+import configureStore, { MockStore } from "redux-mock-store";
+import thunk from "redux-thunk";
+
+interface RenderConnected {
+ ui: JSX.Element;
+ initialState: Record;
+ store?: MockStore;
+}
+
+interface RenderConnectedWithStore extends RenderResult {
+ store: MockStore;
+}
+
+const middleware = [thunk];
+const mockStore = configureStore(middleware);
+
+export const makeStore = (initialState: Record): MockStore =>
+ mockStore(initialState);
+
+const renderConnected = ({
+ ui,
+ initialState = {},
+ store = makeStore(initialState),
+}: RenderConnected): RenderConnectedWithStore => {
+ const rendered = render({ui});
+ return {
+ ...rendered,
+ store,
+ };
+};
+
+export default renderConnected;
From 9fb9709c623c4a3239fb5d437f2ae2ee690e3ebc Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 13 Apr 2021 09:23:13 +0100
Subject: [PATCH 06/17] add directory aliases (#6)
---
src/api/apiService.ts | 1 -
src/components/App/App.test.tsx | 2 +-
src/components/App/App.tsx | 6 +--
src/components/UserHeader/UserHeader.test.tsx | 2 +-
src/components/UserHeader/UserHeader.tsx | 2 +-
src/index.tsx | 4 +-
src/state/userProfile/actions.ts | 4 +-
tsconfig.json | 13 +++--
webpack.config.js | 47 +++++++++++--------
9 files changed, 47 insertions(+), 34 deletions(-)
diff --git a/src/api/apiService.ts b/src/api/apiService.ts
index ab47898..553d651 100644
--- a/src/api/apiService.ts
+++ b/src/api/apiService.ts
@@ -1,5 +1,4 @@
import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios";
-
class ApiService {
async callApi(config: AxiosRequestConfig): Promise> {
return axios.request(config);
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index 0d26919..215d71f 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import renderConnected from "../../utilities/test/renderConnected";
+import renderConnected from "utilities/test/renderConnected";
import App from "./App";
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index a7bca60..09fd880 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -1,13 +1,13 @@
import React, { FC, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
-import { fetchUserById } from "../../state/userProfile/actions";
-import { getUsername } from "../../state/userProfile/selectors";
+import { fetchUserById } from "state/userProfile/actions";
+import { getUsername } from "state/userProfile/selectors";
import Loader from "../Loader/Loader";
import UserHeader from "../UserHeader/UserHeader";
-import "../../styles/app.scss";
+import "styles/app.scss";
const App: FC = () => {
const dispatch = useDispatch();
diff --git a/src/components/UserHeader/UserHeader.test.tsx b/src/components/UserHeader/UserHeader.test.tsx
index 90501f7..3ebb33c 100644
--- a/src/components/UserHeader/UserHeader.test.tsx
+++ b/src/components/UserHeader/UserHeader.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { screen } from "@testing-library/react";
-import renderConnected from "../../utilities/test/renderConnected";
+import renderConnected from "utilities/test/renderConnected";
import UserHeader from "./UserHeader";
diff --git a/src/components/UserHeader/UserHeader.tsx b/src/components/UserHeader/UserHeader.tsx
index 8f6862b..960e2c7 100644
--- a/src/components/UserHeader/UserHeader.tsx
+++ b/src/components/UserHeader/UserHeader.tsx
@@ -1,7 +1,7 @@
import React, { FC } from "react";
import { useSelector } from "react-redux";
-import { getUsername } from "../../state/userProfile/selectors";
+import { getUsername } from "state/userProfile/selectors";
const UserHeader: FC = () => {
const userName = useSelector(getUsername);
diff --git a/src/index.tsx b/src/index.tsx
index 305dd19..a961e10 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,8 +2,8 @@ import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
-import store from "./state/store";
-import App from "./components/App/App";
+import store from "state/store";
+import App from "components/App/App";
render(
diff --git a/src/state/userProfile/actions.ts b/src/state/userProfile/actions.ts
index 2d06d5c..77c141c 100644
--- a/src/state/userProfile/actions.ts
+++ b/src/state/userProfile/actions.ts
@@ -1,6 +1,6 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
-import ApiService from "../../api/apiService";
-import apiConfig from "../../api/apiConfig";
+import ApiService from "api/apiService";
+import apiConfig from "api/apiConfig";
export const fetchUserById = createAsyncThunk("users/fetchById", async () => {
const response = await ApiService.callApi({
diff --git a/tsconfig.json b/tsconfig.json
index ec353f3..a6a8924 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,6 +6,13 @@
"outDir": "./dist",
"jsx": "react",
"esModuleInterop": true,
- "allowSyntheticDefaultImports": true
- },
-}
\ No newline at end of file
+ "allowSyntheticDefaultImports": true,
+ "paths": {
+ "api/*": ["./src/api/*"],
+ "components/*": ["./src/components/*"],
+ "state/*": ["./src/state/*"],
+ "styles/*": ["./src/styles/*"],
+ "utilities/*": ["./src/utilities/*"]
+ }
+ }
+}
diff --git a/webpack.config.js b/webpack.config.js
index 5ce4a9e..606639e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,9 +1,9 @@
-const path = require('path');
-const HtmlWebpackPlugin = require('html-webpack-plugin');
+const path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
-const buildPath = path.join(__dirname, './dist');
-const sourcePath = path.join(__dirname, './src');
-const publicPath = path.join(__dirname, './public');
+const buildPath = path.join(__dirname, "./dist");
+const sourcePath = path.join(__dirname, "./src");
+const publicPath = path.join(__dirname, "./public");
module.exports = () => ({
entry: {
@@ -11,13 +11,13 @@ module.exports = () => ({
},
output: {
path: buildPath,
- filename: '[name].[hash].js',
- chunkFilename: '[name].[hash].js',
+ filename: "[name].[hash].js",
+ chunkFilename: "[name].[hash].js",
},
- mode: 'development',
- devtool: 'source-map',
+ mode: "development",
+ devtool: "source-map",
devServer: {
- contentBase: './dist',
+ contentBase: "./dist",
historyApiFallback: true,
compress: true,
open: true,
@@ -29,8 +29,15 @@ module.exports = () => ({
minimize: false,
},
resolve: {
- modules: ['node_modules'],
- extensions: ['.ts', '.tsx', '.js', '.jsx'],
+ modules: ["node_modules"],
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
+ alias: {
+ api: path.resolve(sourcePath, "api/"),
+ components: path.resolve(sourcePath, "components/"),
+ state: path.resolve(sourcePath, "state/"),
+ styles: path.resolve(sourcePath, "styles/"),
+ utilities: path.resolve(sourcePath, "utilities/"),
+ },
},
module: {
rules: [
@@ -38,23 +45,23 @@ module.exports = () => ({
test: /\.(ts)x?$/,
exclude: /node_modules/,
use: {
- loader: 'ts-loader',
+ loader: "ts-loader",
},
},
{
test: /\.(scss|css)$/,
use: [
{
- loader: 'style-loader',
+ loader: "style-loader",
},
{
- loader: 'css-loader',
+ loader: "css-loader",
},
{
- loader: 'postcss-loader',
+ loader: "postcss-loader",
},
{
- loader: 'sass-loader',
+ loader: "sass-loader",
options: {
sourceMap: true,
},
@@ -67,9 +74,9 @@ module.exports = () => ({
plugins: [
new HtmlWebpackPlugin({
- chunks: ['vendor', 'app'],
- filename: 'index.html',
- template: path.join(publicPath, '/index.html'),
+ chunks: ["vendor", "app"],
+ filename: "index.html",
+ template: path.join(publicPath, "/index.html"),
}),
],
});
From f507e5010dd8ff7b7a49ad34b90e4eeb2dc812f3 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 13 Apr 2021 11:10:30 +0100
Subject: [PATCH 07/17] add placholder logo and site header component (#7)
---
.eslintrc | 5 ++++-
package-lock.json | 16 +++++++++++---
package.json | 3 ++-
src/assets/images/logo.svg | 22 +++++++++++++++++++
src/components/App/App.tsx | 4 ++--
src/components/Loader/Loader.tsx | 6 +++--
src/components/SiteHeader/SiteHeader.tsx | 13 +++++++++++
.../UserAvatar.test.tsx} | 10 ++++-----
src/components/UserAvatar/UserAvatar.tsx | 17 ++++++++++++++
src/components/UserHeader/UserHeader.tsx | 11 ----------
src/types/index.d.ts | 9 ++++++++
tsconfig.json | 1 +
webpack.config.js | 12 ++++++++++
13 files changed, 104 insertions(+), 25 deletions(-)
create mode 100644 src/assets/images/logo.svg
create mode 100644 src/components/SiteHeader/SiteHeader.tsx
rename src/components/{UserHeader/UserHeader.test.tsx => UserAvatar/UserAvatar.test.tsx} (63%)
create mode 100644 src/components/UserAvatar/UserAvatar.tsx
delete mode 100644 src/components/UserHeader/UserHeader.tsx
create mode 100644 src/types/index.d.ts
diff --git a/.eslintrc b/.eslintrc
index 0fc6dbf..3358b89 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -17,5 +17,8 @@
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
- ]
+ ],
+ "rules": {
+ "react/prop-types": "off"
+ }
}
diff --git a/package-lock.json b/package-lock.json
index 7cf02bf..3033604 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2319,9 +2319,9 @@
}
},
"classnames": {
- "version": "2.2.6",
- "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
- "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"clean-css": {
"version": "4.2.3",
@@ -4304,6 +4304,16 @@
"flat-cache": "^3.0.4"
}
},
+ "file-loader": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
+ "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^3.0.0"
+ }
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
diff --git a/package.json b/package.json
index ec99e50..cafd0e4 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"dependencies": {
"@reduxjs/toolkit": "^1.5.0",
"axios": "^0.21.1",
- "classnames": "^2.2.6",
+ "classnames": "^2.3.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
@@ -53,6 +53,7 @@
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
+ "file-loader": "^6.2.0",
"html-webpack-plugin": "^5.2.0",
"husky": "^4.3.8",
"jest": "^26.6.3",
diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg
new file mode 100644
index 0000000..10cf6ca
--- /dev/null
+++ b/src/assets/images/logo.svg
@@ -0,0 +1,22 @@
+
+
\ No newline at end of file
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 09fd880..1cfd8dd 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -5,7 +5,7 @@ import { fetchUserById } from "state/userProfile/actions";
import { getUsername } from "state/userProfile/selectors";
import Loader from "../Loader/Loader";
-import UserHeader from "../UserHeader/UserHeader";
+import SiteHeader from "../SiteHeader/SiteHeader";
import "styles/app.scss";
@@ -27,7 +27,7 @@ const App: FC = () => {
return (
-
+
);
};
diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx
index 478cb07..e25f612 100644
--- a/src/components/Loader/Loader.tsx
+++ b/src/components/Loader/Loader.tsx
@@ -1,8 +1,10 @@
import React, { FC } from "react";
const Loader: FC = () => (
-
-
Loading...
+
);
diff --git a/src/components/SiteHeader/SiteHeader.tsx b/src/components/SiteHeader/SiteHeader.tsx
new file mode 100644
index 0000000..afc29c2
--- /dev/null
+++ b/src/components/SiteHeader/SiteHeader.tsx
@@ -0,0 +1,13 @@
+import React, { FC } from "react";
+
+import Logo from "assets/images/logo.svg";
+import UserAvatar from "../UserAvatar/UserAvatar";
+
+const SiteHeader: FC = () => (
+
+

+
+
+);
+
+export default SiteHeader;
diff --git a/src/components/UserHeader/UserHeader.test.tsx b/src/components/UserAvatar/UserAvatar.test.tsx
similarity index 63%
rename from src/components/UserHeader/UserHeader.test.tsx
rename to src/components/UserAvatar/UserAvatar.test.tsx
index 3ebb33c..f6908ab 100644
--- a/src/components/UserHeader/UserHeader.test.tsx
+++ b/src/components/UserAvatar/UserAvatar.test.tsx
@@ -2,22 +2,22 @@ import React from "react";
import { screen } from "@testing-library/react";
import renderConnected from "utilities/test/renderConnected";
-import UserHeader from "./UserHeader";
+import UserAvatar from "./UserAvatar";
const setup = (initialState = {}) =>
renderConnected({
- ui:
,
+ ui:
,
initialState,
});
-describe("UserHeader", () => {
+describe("UserAvatar", () => {
it("should display username", () => {
setup({
userProfile: {
username: "Steve",
},
});
- const userHeaderText = screen.getByText("Logged in as Steve");
- expect(userHeaderText).toBeInTheDocument();
+ const userAvatarText = screen.getByText("Logged in as Steve");
+ expect(userAvatarText).toBeInTheDocument();
});
});
diff --git a/src/components/UserAvatar/UserAvatar.tsx b/src/components/UserAvatar/UserAvatar.tsx
new file mode 100644
index 0000000..a11617a
--- /dev/null
+++ b/src/components/UserAvatar/UserAvatar.tsx
@@ -0,0 +1,17 @@
+import React, { FC } from "react";
+import { useSelector } from "react-redux";
+import classNames from "classnames";
+
+import { getUsername } from "state/userProfile/selectors";
+
+interface UserHeaderProps {
+ className?: string;
+}
+
+const UserHeader: FC
= ({ className }) => {
+ const userName = useSelector(getUsername);
+ const classes = classNames("user-profile", className);
+ return Logged in as {userName}
;
+};
+
+export default UserHeader;
diff --git a/src/components/UserHeader/UserHeader.tsx b/src/components/UserHeader/UserHeader.tsx
deleted file mode 100644
index 960e2c7..0000000
--- a/src/components/UserHeader/UserHeader.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React, { FC } from "react";
-import { useSelector } from "react-redux";
-
-import { getUsername } from "state/userProfile/selectors";
-
-const UserHeader: FC = () => {
- const userName = useSelector(getUsername);
- return Logged in as {userName}
;
-};
-
-export default UserHeader;
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
new file mode 100644
index 0000000..e4d7e6d
--- /dev/null
+++ b/src/types/index.d.ts
@@ -0,0 +1,9 @@
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
diff --git a/tsconfig.json b/tsconfig.json
index a6a8924..87c7b53 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
"allowSyntheticDefaultImports": true,
"paths": {
"api/*": ["./src/api/*"],
+ "assets/*": ["./src/assets/*"],
"components/*": ["./src/components/*"],
"state/*": ["./src/state/*"],
"styles/*": ["./src/styles/*"],
diff --git a/webpack.config.js b/webpack.config.js
index 606639e..dfe584e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -33,6 +33,7 @@ module.exports = () => ({
extensions: [".ts", ".tsx", ".js", ".jsx"],
alias: {
api: path.resolve(sourcePath, "api/"),
+ assets: path.resolve(sourcePath, "assets/"),
components: path.resolve(sourcePath, "components/"),
state: path.resolve(sourcePath, "state/"),
styles: path.resolve(sourcePath, "styles/"),
@@ -69,6 +70,17 @@ module.exports = () => ({
],
exclude: /node_modules/,
},
+ {
+ test: /\.(jpe?g|png|gif|svg)$/i,
+ include: path.resolve(sourcePath, "assets/images"),
+ use: {
+ loader: "file-loader",
+ options: {
+ name: "[name].[ext]",
+ outputPath: "assets/images/",
+ },
+ },
+ },
],
},
From 8c4e6034144000c5690e3c800d0b0314c36e4062 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 13 Apr 2021 11:22:59 +0100
Subject: [PATCH 08/17] add more tests (#8)
---
src/components/Loader/Loader.test.tsx | 14 +++++++++++
src/components/SiteHeader/SiteHeader.test.tsx | 23 +++++++++++++++++++
src/state/userProfile/selectors.test.ts | 16 +++++++++++++
3 files changed, 53 insertions(+)
create mode 100644 src/components/Loader/Loader.test.tsx
create mode 100644 src/components/SiteHeader/SiteHeader.test.tsx
create mode 100644 src/state/userProfile/selectors.test.ts
diff --git a/src/components/Loader/Loader.test.tsx b/src/components/Loader/Loader.test.tsx
new file mode 100644
index 0000000..e5790af
--- /dev/null
+++ b/src/components/Loader/Loader.test.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { screen, render } from "@testing-library/react";
+
+import Loader from "./Loader";
+
+const setup = () => render();
+
+describe("Loader", () => {
+ it("should display loading message", () => {
+ setup();
+ const loadingText = screen.getByText("Loading...");
+ expect(loadingText).toBeInTheDocument();
+ });
+});
diff --git a/src/components/SiteHeader/SiteHeader.test.tsx b/src/components/SiteHeader/SiteHeader.test.tsx
new file mode 100644
index 0000000..4776995
--- /dev/null
+++ b/src/components/SiteHeader/SiteHeader.test.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import renderConnected from "utilities/test/renderConnected";
+
+import SiteHeader from "./SiteHeader";
+
+const setup = () =>
+ renderConnected({
+ ui: ,
+ initialState: {
+ userProfile: {
+ username: "Steve",
+ },
+ },
+ });
+
+describe("SiteHeader", () => {
+ it("should display logo", () => {
+ setup();
+ const logoImg = screen.getByAltText("Company name");
+ expect(logoImg).toBeInTheDocument();
+ });
+});
diff --git a/src/state/userProfile/selectors.test.ts b/src/state/userProfile/selectors.test.ts
new file mode 100644
index 0000000..603a424
--- /dev/null
+++ b/src/state/userProfile/selectors.test.ts
@@ -0,0 +1,16 @@
+import { getUsername } from "./selectors";
+
+import { RootState } from "../rootReducer";
+
+describe("UserProfile selectors", () => {
+ describe("getUsername", () => {
+ it("should return username", () => {
+ const state: RootState = {
+ userProfile: {
+ username: "Steve",
+ },
+ };
+ expect(getUsername(state)).toBe("Steve");
+ });
+ });
+});
From 6f5680d76c87ab976e8a5974032efc514d899598 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 13 Apr 2021 15:34:43 +0100
Subject: [PATCH 09/17] ui component for displaying list of users (#9)
---
src/components/App/App.tsx | 4 ++-
.../ContentCard/ContentCard.test.tsx | 17 ++++++++++++
src/components/ContentCard/ContentCard.tsx | 11 ++++++++
.../PageContainer/PageContainer.test.tsx | 17 ++++++++++++
.../PageContainer/PageContainer.tsx | 11 ++++++++
src/components/SiteHeader/SiteHeader.tsx | 2 +-
src/components/UsersTable/UsersTable.tsx | 26 +++++++++++++++++++
src/pages/LandingPage/LandingPage.test.tsx | 13 ++++++++++
src/pages/LandingPage/LandingPage.tsx | 19 ++++++++++++++
tsconfig.json | 1 +
webpack.config.js | 1 +
11 files changed, 120 insertions(+), 2 deletions(-)
create mode 100644 src/components/ContentCard/ContentCard.test.tsx
create mode 100644 src/components/ContentCard/ContentCard.tsx
create mode 100644 src/components/PageContainer/PageContainer.test.tsx
create mode 100644 src/components/PageContainer/PageContainer.tsx
create mode 100644 src/components/UsersTable/UsersTable.tsx
create mode 100644 src/pages/LandingPage/LandingPage.test.tsx
create mode 100644 src/pages/LandingPage/LandingPage.tsx
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 1cfd8dd..b1e8b42 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -6,6 +6,7 @@ import { getUsername } from "state/userProfile/selectors";
import Loader from "../Loader/Loader";
import SiteHeader from "../SiteHeader/SiteHeader";
+import LandingPage from "pages/LandingPage/LandingPage";
import "styles/app.scss";
@@ -15,7 +16,7 @@ const App: FC = () => {
useEffect(() => {
if (!username) {
- dispatch(fetchUserById());
+ setTimeout(() => dispatch(fetchUserById()), 2400);
}
}, [dispatch]);
@@ -28,6 +29,7 @@ const App: FC = () => {
return (
+
);
};
diff --git a/src/components/ContentCard/ContentCard.test.tsx b/src/components/ContentCard/ContentCard.test.tsx
new file mode 100644
index 0000000..ba8a560
--- /dev/null
+++ b/src/components/ContentCard/ContentCard.test.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { screen, render } from "@testing-library/react";
+
+import ContentCard from "./ContentCard";
+
+describe("ContentCard", () => {
+ it("renders", () => {
+ render(
+
+ Child element
+
+ );
+
+ const childElement = screen.getByText("Child element");
+ expect(childElement).toBeInTheDocument();
+ });
+});
diff --git a/src/components/ContentCard/ContentCard.tsx b/src/components/ContentCard/ContentCard.tsx
new file mode 100644
index 0000000..a175570
--- /dev/null
+++ b/src/components/ContentCard/ContentCard.tsx
@@ -0,0 +1,11 @@
+import React, { FC } from "react";
+
+interface ContentCardProps {
+ children: JSX.Element[] | JSX.Element;
+}
+
+const ContentCard: FC = ({ children }) => (
+ {children}
+);
+
+export default ContentCard;
diff --git a/src/components/PageContainer/PageContainer.test.tsx b/src/components/PageContainer/PageContainer.test.tsx
new file mode 100644
index 0000000..8334241
--- /dev/null
+++ b/src/components/PageContainer/PageContainer.test.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { screen, render } from "@testing-library/react";
+
+import PageContainer from "./PageContainer";
+
+describe("PageContainer", () => {
+ it("renders", () => {
+ render(
+
+ Child element
+
+ );
+
+ const childElement = screen.getByText("Child element");
+ expect(childElement).toBeInTheDocument();
+ });
+});
diff --git a/src/components/PageContainer/PageContainer.tsx b/src/components/PageContainer/PageContainer.tsx
new file mode 100644
index 0000000..f693cd5
--- /dev/null
+++ b/src/components/PageContainer/PageContainer.tsx
@@ -0,0 +1,11 @@
+import React, { FC } from "react";
+
+interface PageContainerProps {
+ children: JSX.Element[] | JSX.Element;
+}
+
+const PageContainer: FC = ({ children }) => (
+ {children}
+);
+
+export default PageContainer;
diff --git a/src/components/SiteHeader/SiteHeader.tsx b/src/components/SiteHeader/SiteHeader.tsx
index afc29c2..392c335 100644
--- a/src/components/SiteHeader/SiteHeader.tsx
+++ b/src/components/SiteHeader/SiteHeader.tsx
@@ -4,7 +4,7 @@ import Logo from "assets/images/logo.svg";
import UserAvatar from "../UserAvatar/UserAvatar";
const SiteHeader: FC = () => (
-
+
diff --git a/src/components/UsersTable/UsersTable.tsx b/src/components/UsersTable/UsersTable.tsx
new file mode 100644
index 0000000..7df4c36
--- /dev/null
+++ b/src/components/UsersTable/UsersTable.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+const UsersTable: FC = () => {
+ return (
+
+
+
+ | asdfasd |
+ asdf |
+ asdf |
+ asdf |
+
+
+
+
+ | a |
+ a |
+ a |
+ a |
+
+
+
+ );
+};
+
+export default UsersTable;
diff --git a/src/pages/LandingPage/LandingPage.test.tsx b/src/pages/LandingPage/LandingPage.test.tsx
new file mode 100644
index 0000000..ee05220
--- /dev/null
+++ b/src/pages/LandingPage/LandingPage.test.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import { screen, render } from "@testing-library/react";
+
+import LandingPage from "./LandingPage";
+
+describe("LandingPage", () => {
+ it("renders", () => {
+ render(
);
+
+ const heading = screen.getByText("Landing page");
+ expect(heading).toBeInTheDocument();
+ });
+});
diff --git a/src/pages/LandingPage/LandingPage.tsx b/src/pages/LandingPage/LandingPage.tsx
new file mode 100644
index 0000000..05adcae
--- /dev/null
+++ b/src/pages/LandingPage/LandingPage.tsx
@@ -0,0 +1,19 @@
+import React, { FC } from "react";
+
+import PageContainer from "components/PageContainer/PageContainer";
+import ContentCard from "components/ContentCard/ContentCard";
+import UsersTable from "components/UsersTable/UsersTable";
+
+const LandingPage: FC = () => {
+ return (
+
+ Landing page
+ Lorem ipsum dolor sit amet consectetur adipisicing elit.
+
+
+
+
+ );
+};
+
+export default LandingPage;
diff --git a/tsconfig.json b/tsconfig.json
index 87c7b53..380d190 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
"api/*": ["./src/api/*"],
"assets/*": ["./src/assets/*"],
"components/*": ["./src/components/*"],
+ "pages/*": ["./src/pages/*"],
"state/*": ["./src/state/*"],
"styles/*": ["./src/styles/*"],
"utilities/*": ["./src/utilities/*"]
diff --git a/webpack.config.js b/webpack.config.js
index dfe584e..55d4bf3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -35,6 +35,7 @@ module.exports = () => ({
api: path.resolve(sourcePath, "api/"),
assets: path.resolve(sourcePath, "assets/"),
components: path.resolve(sourcePath, "components/"),
+ pages: path.resolve(sourcePath, "pages/"),
state: path.resolve(sourcePath, "state/"),
styles: path.resolve(sourcePath, "styles/"),
utilities: path.resolve(sourcePath, "utilities/"),
From 412dcc5299e823356330c9a1fc981534391d308e Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 14 Apr 2021 11:49:14 +0100
Subject: [PATCH 10/17] add users list to state (#10)
---
src/api/apiConfig.ts | 1 +
src/api/apiService.ts | 4 +-
src/components/App/App.test.tsx | 2 +
.../PageContainer/PageContainer.tsx | 4 +-
src/components/UsersTable/UsersTable.tsx | 41 ++++++++++++++-----
src/pages/LandingPage/LandingPage.test.tsx | 14 ++++++-
src/state/rootReducer.ts | 3 +-
src/state/types.ts | 11 +++++
src/state/userProfile/actions.ts | 4 +-
src/state/userProfile/reducer.ts | 8 +---
src/state/userProfile/selectors.test.ts | 1 +
src/state/users/actions.ts | 13 ++++++
src/state/users/reducer.ts | 21 ++++++++++
src/state/users/selectors.ts | 4 ++
14 files changed, 107 insertions(+), 24 deletions(-)
create mode 100644 src/state/types.ts
create mode 100644 src/state/users/actions.ts
create mode 100644 src/state/users/reducer.ts
create mode 100644 src/state/users/selectors.ts
diff --git a/src/api/apiConfig.ts b/src/api/apiConfig.ts
index 8843386..624a9cf 100644
--- a/src/api/apiConfig.ts
+++ b/src/api/apiConfig.ts
@@ -3,5 +3,6 @@ const API_BASE_URL = "https://jsonplaceholder.typicode.com";
export default {
endpoints: {
userProfile: (userId: string): string => `${API_BASE_URL}/users/${userId}`,
+ users: `${API_BASE_URL}/users`,
},
};
diff --git a/src/api/apiService.ts b/src/api/apiService.ts
index 553d651..19fef0f 100644
--- a/src/api/apiService.ts
+++ b/src/api/apiService.ts
@@ -1,6 +1,6 @@
-import axios, { AxiosPromise, AxiosRequestConfig, AxiosResponse } from "axios";
+import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
class ApiService {
- async callApi(config: AxiosRequestConfig): Promise> {
+ async callApi(config: AxiosRequestConfig): Promise> {
return axios.request(config);
}
}
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index 215d71f..ea40ae6 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -13,6 +13,7 @@ describe("App", () => {
it("renders Loader when userProfile not loaded", () => {
const { container } = setup({
userProfile: {},
+ users: { list: null },
});
const app = container.querySelector(".loader");
expect(app).toBeInTheDocument();
@@ -22,6 +23,7 @@ describe("App", () => {
userProfile: {
username: "Steve",
},
+ users: { list: null },
});
const app = container.querySelector(".app");
expect(app).toBeInTheDocument();
diff --git a/src/components/PageContainer/PageContainer.tsx b/src/components/PageContainer/PageContainer.tsx
index f693cd5..6817aed 100644
--- a/src/components/PageContainer/PageContainer.tsx
+++ b/src/components/PageContainer/PageContainer.tsx
@@ -5,7 +5,9 @@ interface PageContainerProps {
}
const PageContainer: FC = ({ children }) => (
- {children}
+
);
export default PageContainer;
diff --git a/src/components/UsersTable/UsersTable.tsx b/src/components/UsersTable/UsersTable.tsx
index 7df4c36..251eedd 100644
--- a/src/components/UsersTable/UsersTable.tsx
+++ b/src/components/UsersTable/UsersTable.tsx
@@ -1,23 +1,42 @@
-import React, { FC } from "react";
+import React, { FC, useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import { getUsersList } from "state/users/selectors";
+import { fetchUsers } from "state/users/actions";
const UsersTable: FC = () => {
+ const dispatch = useDispatch();
+ const usersList = useSelector(getUsersList);
+
+ useEffect(() => {
+ if (!usersList) {
+ dispatch(fetchUsers());
+ }
+ }, [dispatch]);
+
+ if (!usersList || usersList.length === 0) {
+ return null;
+ }
+
return (
- | asdfasd |
- asdf |
- asdf |
- asdf |
+ Name |
+ Username |
+ Email |
+ Company |
-
- | a |
- a |
- a |
- a |
-
+ {usersList.map(({ name, email, company, username }, i) => (
+
+ | {name} |
+ {email} |
+ {company.name} |
+ {username} |
+
+ ))}
);
diff --git a/src/pages/LandingPage/LandingPage.test.tsx b/src/pages/LandingPage/LandingPage.test.tsx
index ee05220..773a72a 100644
--- a/src/pages/LandingPage/LandingPage.test.tsx
+++ b/src/pages/LandingPage/LandingPage.test.tsx
@@ -1,11 +1,21 @@
import React from "react";
-import { screen, render } from "@testing-library/react";
+import { screen } from "@testing-library/react";
+import renderConnected from "utilities/test/renderConnected";
import LandingPage from "./LandingPage";
+const setup = (initialState = {}) =>
+ renderConnected({
+ ui: ,
+ initialState,
+ });
+
describe("LandingPage", () => {
it("renders", () => {
- render();
+ setup({
+ userProfile: {},
+ users: { list: null },
+ });
const heading = screen.getByText("Landing page");
expect(heading).toBeInTheDocument();
diff --git a/src/state/rootReducer.ts b/src/state/rootReducer.ts
index 4c4f36f..4f04152 100644
--- a/src/state/rootReducer.ts
+++ b/src/state/rootReducer.ts
@@ -1,8 +1,9 @@
import { combineReducers } from "@reduxjs/toolkit";
import userProfile from "./userProfile/reducer";
+import users from "./users/reducer";
-const rootReducer = combineReducers({ userProfile });
+const rootReducer = combineReducers({ userProfile, users });
export type RootState = ReturnType;
export default rootReducer;
diff --git a/src/state/types.ts b/src/state/types.ts
new file mode 100644
index 0000000..1be73cf
--- /dev/null
+++ b/src/state/types.ts
@@ -0,0 +1,11 @@
+export interface User {
+ id?: string;
+ name?: string;
+ email?: string;
+ username?: string;
+ company?: {
+ name: string;
+ };
+}
+
+export type Users = User[];
diff --git a/src/state/userProfile/actions.ts b/src/state/userProfile/actions.ts
index 77c141c..573b0e3 100644
--- a/src/state/userProfile/actions.ts
+++ b/src/state/userProfile/actions.ts
@@ -2,8 +2,10 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
import ApiService from "api/apiService";
import apiConfig from "api/apiConfig";
+import { User } from "../types";
+
export const fetchUserById = createAsyncThunk("users/fetchById", async () => {
- const response = await ApiService.callApi({
+ const response = await ApiService.callApi({
url: apiConfig.endpoints.userProfile("1"),
method: "GET",
});
diff --git a/src/state/userProfile/reducer.ts b/src/state/userProfile/reducer.ts
index ec0751f..a086b31 100644
--- a/src/state/userProfile/reducer.ts
+++ b/src/state/userProfile/reducer.ts
@@ -1,13 +1,9 @@
import { createReducer } from "@reduxjs/toolkit";
+import { User } from "../types";
import { fetchUserById } from "./actions";
-interface UserState {
- name?: string;
- username?: string;
-}
-
-export const initialState = {} as UserState;
+export const initialState = {} as User;
const userProfileReducer = createReducer(initialState, (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {
diff --git a/src/state/userProfile/selectors.test.ts b/src/state/userProfile/selectors.test.ts
index 603a424..142bb36 100644
--- a/src/state/userProfile/selectors.test.ts
+++ b/src/state/userProfile/selectors.test.ts
@@ -9,6 +9,7 @@ describe("UserProfile selectors", () => {
userProfile: {
username: "Steve",
},
+ users: { list: null },
};
expect(getUsername(state)).toBe("Steve");
});
diff --git a/src/state/users/actions.ts b/src/state/users/actions.ts
new file mode 100644
index 0000000..03b7b7d
--- /dev/null
+++ b/src/state/users/actions.ts
@@ -0,0 +1,13 @@
+import { createAsyncThunk } from "@reduxjs/toolkit";
+import ApiService from "api/apiService";
+import apiConfig from "api/apiConfig";
+
+import { Users } from "../types";
+
+export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
+ const response = await ApiService.callApi({
+ url: apiConfig.endpoints.users,
+ method: "GET",
+ });
+ return response.data;
+});
diff --git a/src/state/users/reducer.ts b/src/state/users/reducer.ts
new file mode 100644
index 0000000..391625d
--- /dev/null
+++ b/src/state/users/reducer.ts
@@ -0,0 +1,21 @@
+import { createReducer } from "@reduxjs/toolkit";
+
+import { Users } from "../types";
+import { fetchUsers } from "./actions";
+
+interface UsersState {
+ list: Users | null;
+}
+
+export const initialState = {} as UsersState;
+
+const usersReducer = createReducer(initialState, (builder) => {
+ builder.addCase(fetchUsers.fulfilled, (state, action) => {
+ return {
+ ...state,
+ list: action.payload,
+ };
+ });
+});
+
+export default usersReducer;
diff --git a/src/state/users/selectors.ts b/src/state/users/selectors.ts
new file mode 100644
index 0000000..e2a86be
--- /dev/null
+++ b/src/state/users/selectors.ts
@@ -0,0 +1,4 @@
+import { RootState } from "../rootReducer";
+import { User } from "../types";
+
+export const getUsersList = (state: RootState): User[] => state.users.list;
From 2ebecb7dde3ef79174321e1b68b3e71a141f5267 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Mon, 26 Apr 2021 09:57:29 +0100
Subject: [PATCH 11/17] add mock state for unit tests (#11)
---
src/components/App/App.test.tsx | 2 --
src/pages/LandingPage/LandingPage.test.tsx | 5 +----
src/state/rootReducer.ts | 4 +++-
src/state/userProfile/selectors.test.ts | 4 ++--
src/utilities/test/mockState.ts | 9 +++++++++
src/utilities/test/renderConnected.tsx | 4 +++-
6 files changed, 18 insertions(+), 10 deletions(-)
create mode 100644 src/utilities/test/mockState.ts
diff --git a/src/components/App/App.test.tsx b/src/components/App/App.test.tsx
index ea40ae6..215d71f 100644
--- a/src/components/App/App.test.tsx
+++ b/src/components/App/App.test.tsx
@@ -13,7 +13,6 @@ describe("App", () => {
it("renders Loader when userProfile not loaded", () => {
const { container } = setup({
userProfile: {},
- users: { list: null },
});
const app = container.querySelector(".loader");
expect(app).toBeInTheDocument();
@@ -23,7 +22,6 @@ describe("App", () => {
userProfile: {
username: "Steve",
},
- users: { list: null },
});
const app = container.querySelector(".app");
expect(app).toBeInTheDocument();
diff --git a/src/pages/LandingPage/LandingPage.test.tsx b/src/pages/LandingPage/LandingPage.test.tsx
index 773a72a..b5faa68 100644
--- a/src/pages/LandingPage/LandingPage.test.tsx
+++ b/src/pages/LandingPage/LandingPage.test.tsx
@@ -12,10 +12,7 @@ const setup = (initialState = {}) =>
describe("LandingPage", () => {
it("renders", () => {
- setup({
- userProfile: {},
- users: { list: null },
- });
+ setup();
const heading = screen.getByText("Landing page");
expect(heading).toBeInTheDocument();
diff --git a/src/state/rootReducer.ts b/src/state/rootReducer.ts
index 4f04152..a6f2db4 100644
--- a/src/state/rootReducer.ts
+++ b/src/state/rootReducer.ts
@@ -3,7 +3,9 @@ import { combineReducers } from "@reduxjs/toolkit";
import userProfile from "./userProfile/reducer";
import users from "./users/reducer";
-const rootReducer = combineReducers({ userProfile, users });
+export const reducers = { userProfile, users } as const;
+
+const rootReducer = combineReducers(reducers);
export type RootState = ReturnType;
export default rootReducer;
diff --git a/src/state/userProfile/selectors.test.ts b/src/state/userProfile/selectors.test.ts
index 142bb36..99798bf 100644
--- a/src/state/userProfile/selectors.test.ts
+++ b/src/state/userProfile/selectors.test.ts
@@ -1,15 +1,15 @@
import { getUsername } from "./selectors";
-
+import mockState from "utilities/test/mockState";
import { RootState } from "../rootReducer";
describe("UserProfile selectors", () => {
describe("getUsername", () => {
it("should return username", () => {
const state: RootState = {
+ ...mockState,
userProfile: {
username: "Steve",
},
- users: { list: null },
};
expect(getUsername(state)).toBe("Steve");
});
diff --git a/src/utilities/test/mockState.ts b/src/utilities/test/mockState.ts
new file mode 100644
index 0000000..b129078
--- /dev/null
+++ b/src/utilities/test/mockState.ts
@@ -0,0 +1,9 @@
+import { initialState as userProfileInitialState } from "state/userProfile/reducer";
+import { initialState as usersInitialState } from "state/users/reducer";
+
+const state = {
+ userProfile: userProfileInitialState,
+ users: usersInitialState,
+};
+
+export default state;
diff --git a/src/utilities/test/renderConnected.tsx b/src/utilities/test/renderConnected.tsx
index b0f5d48..90c74f4 100644
--- a/src/utilities/test/renderConnected.tsx
+++ b/src/utilities/test/renderConnected.tsx
@@ -4,6 +4,8 @@ import { Provider } from "react-redux";
import configureStore, { MockStore } from "redux-mock-store";
import thunk from "redux-thunk";
+import { reducers } from "state/rootReducer";
+
interface RenderConnected {
ui: JSX.Element;
initialState: Record;
@@ -18,7 +20,7 @@ const middleware = [thunk];
const mockStore = configureStore(middleware);
export const makeStore = (initialState: Record): MockStore =>
- mockStore(initialState);
+ mockStore({ ...reducers, ...initialState });
const renderConnected = ({
ui,
From df480aa42c0f7d178c01c9d45323ff9545dd2e0d Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Mon, 26 Apr 2021 10:01:19 +0100
Subject: [PATCH 12/17] Create node.js.yml
---
.github/workflows/node.js.yml | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 .github/workflows/node.js.yml
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
new file mode 100644
index 0000000..218bff8
--- /dev/null
+++ b/.github/workflows/node.js.yml
@@ -0,0 +1,30 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: Node.js CI
+
+on:
+ push:
+ branches: [ development ]
+ pull_request:
+ branches: [ development ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [12.x, 14.x, 15.x]
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v2
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm ci
+ - run: npm run build --if-present
+ - run: npm test
From a96a046d646a025299113e16266b16439f829572 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Tue, 27 Apr 2021 10:57:51 +0100
Subject: [PATCH 13/17] optimise for production and add build task to scripts
(#12)
---
.gitignore | 1 +
package-lock.json | 357 +++++++++++++++++++++++++
package.json | 10 +-
tailwind.config.js | 2 +-
webpack.config.js => webpack.common.js | 21 --
webpack.dev.js | 25 ++
webpack.prod.js | 65 +++++
7 files changed, 457 insertions(+), 24 deletions(-)
rename webpack.config.js => webpack.common.js (81%)
create mode 100644 webpack.dev.js
create mode 100644 webpack.prod.js
diff --git a/.gitignore b/.gitignore
index 3c3629e..44d646d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
node_modules
+dist/
diff --git a/package-lock.json b/package-lock.json
index 3033604..dc06f45 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -729,6 +729,12 @@
"fastq": "^1.6.0"
}
},
+ "@polka/url": {
+ "version": "1.0.0-next.12",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz",
+ "integrity": "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==",
+ "dev": true
+ },
"@redux-saga/core": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.1.3.tgz",
@@ -1490,6 +1496,16 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
+ "adjust-sourcemap-loader": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz",
+ "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "regex-parser": "^2.2.11"
+ }
+ },
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -1587,6 +1603,12 @@
"@babel/runtime-corejs3": "^7.10.2"
}
},
+ "arity-n": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz",
+ "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=",
+ "dev": true
+ },
"arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -2444,6 +2466,15 @@
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true
},
+ "compose-function": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz",
+ "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=",
+ "dev": true,
+ "requires": {
+ "arity-n": "^1.0.4"
+ }
+ },
"compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -2719,6 +2750,16 @@
"integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==",
"dev": true
},
+ "d": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+ "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+ "dev": true,
+ "requires": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
"damerau-levenshtein": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
@@ -3125,6 +3166,12 @@
}
}
},
+ "duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "dev": true
+ },
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -3269,6 +3316,38 @@
"is-symbol": "^1.0.2"
}
},
+ "es5-ext": {
+ "version": "0.10.53",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+ "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+ "dev": true,
+ "requires": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.3",
+ "next-tick": "~1.0.0"
+ }
+ },
+ "es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+ "dev": true,
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "es6-symbol": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+ "dev": true,
+ "requires": {
+ "d": "^1.0.1",
+ "ext": "^1.1.2"
+ }
+ },
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -4126,6 +4205,23 @@
}
}
},
+ "ext": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+ "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+ "dev": true,
+ "requires": {
+ "type": "^2.0.0"
+ },
+ "dependencies": {
+ "type": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
+ "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==",
+ "dev": true
+ }
+ }
+ },
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4606,6 +4702,15 @@
"dev": true,
"optional": true
},
+ "gzip-size": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
+ "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
+ "dev": true,
+ "requires": {
+ "duplexer": "^0.1.2"
+ }
+ },
"handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@@ -6526,6 +6631,29 @@
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
+ "mini-css-extract-plugin": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.5.0.tgz",
+ "integrity": "sha512-SIbuLMv6jsk1FnLIU5OUG/+VMGUprEjM1+o2trOAx8i5KOKMrhyezb1dJ4Ugsykb8Jgq8/w5NEopy6escV9G7g==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^3.0.0",
+ "webpack-sources": "^1.1.0"
+ },
+ "dependencies": {
+ "webpack-sources": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
+ "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
+ "dev": true,
+ "requires": {
+ "source-list-map": "^2.0.0",
+ "source-map": "~0.6.1"
+ }
+ }
+ }
+ },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -6664,6 +6792,12 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=",
+ "dev": true
+ },
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -6981,6 +7115,12 @@
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"dev": true
},
+ "opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -7944,6 +8084,12 @@
"safe-regex": "^1.1.0"
}
},
+ "regex-parser": {
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz",
+ "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
+ "dev": true
+ },
"regexp.prototype.flags": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz",
@@ -8240,6 +8386,100 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
+ "resolve-url-loader": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz",
+ "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==",
+ "dev": true,
+ "requires": {
+ "adjust-sourcemap-loader": "3.0.0",
+ "camelcase": "5.3.1",
+ "compose-function": "3.0.3",
+ "convert-source-map": "1.7.0",
+ "es6-iterator": "2.0.3",
+ "loader-utils": "1.2.3",
+ "postcss": "7.0.21",
+ "rework": "1.0.1",
+ "rework-visit": "1.0.0",
+ "source-map": "0.6.1"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "emojis-list": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
+ "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
+ "dev": true
+ },
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^2.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "postcss": {
+ "version": "7.0.21",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz",
+ "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.2",
+ "source-map": "^0.6.1",
+ "supports-color": "^6.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
"ret": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
@@ -8258,6 +8498,55 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
+ "rework": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz",
+ "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=",
+ "dev": true,
+ "requires": {
+ "convert-source-map": "^0.3.3",
+ "css": "^2.0.0"
+ },
+ "dependencies": {
+ "convert-source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz",
+ "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=",
+ "dev": true
+ },
+ "css": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
+ "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.5.2",
+ "urix": "^0.1.0"
+ }
+ },
+ "source-map-resolve": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
+ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
+ "dev": true,
+ "requires": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0",
+ "resolve-url": "^0.2.1",
+ "source-map-url": "^0.4.0",
+ "urix": "^0.1.0"
+ }
+ }
+ }
+ },
+ "rework-visit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz",
+ "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=",
+ "dev": true
+ },
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -8751,6 +9040,25 @@
}
}
},
+ "sirv": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.11.tgz",
+ "integrity": "sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg==",
+ "dev": true,
+ "requires": {
+ "@polka/url": "^1.0.0-next.9",
+ "mime": "^2.3.1",
+ "totalist": "^1.0.0"
+ },
+ "dependencies": {
+ "mime": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
+ "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==",
+ "dev": true
+ }
+ }
+ },
"sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -9549,6 +9857,12 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
+ "totalist": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz",
+ "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
+ "dev": true
+ },
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
@@ -9668,6 +9982,12 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
+ "type": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
+ "dev": true
+ },
"type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -10044,6 +10364,43 @@
}
}
},
+ "webpack-bundle-analyzer": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz",
+ "integrity": "sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.0.4",
+ "acorn-walk": "^8.0.0",
+ "chalk": "^4.1.0",
+ "commander": "^6.2.0",
+ "gzip-size": "^6.0.0",
+ "lodash": "^4.17.20",
+ "opener": "^1.5.2",
+ "sirv": "^1.0.7",
+ "ws": "^7.3.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.2.1.tgz",
+ "integrity": "sha512-z716cpm5TX4uzOzILx8PavOE6C6DKshHDw1aQN52M/yNSqE9s5O8SMfyhCCfCJ3HmTL0NkVOi+8a/55T7YB3bg==",
+ "dev": true
+ },
+ "acorn-walk": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.0.tgz",
+ "integrity": "sha512-mjmzmv12YIG/G8JQdQuz2MUDShEJ6teYpT5bmWA4q7iwoGen8xtt3twF3OvzIUl+Q06aWIjvnwQUKvQ6TtMRjg==",
+ "dev": true
+ },
+ "commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "dev": true
+ }
+ }
+ },
"webpack-cli": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.5.0.tgz",
diff --git a/package.json b/package.json
index cafd0e4..2b666a4 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"description": "",
"main": "src/index.tsx",
"scripts": {
- "start": "webpack serve",
+ "build": "rimraf dist && NODE_ENV=production webpack --config webpack.prod.js",
+ "start": "webpack serve --config webpack.dev.js",
"test": "jest --env=jsdom",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx",
@@ -57,12 +58,15 @@
"html-webpack-plugin": "^5.2.0",
"husky": "^4.3.8",
"jest": "^26.6.3",
+ "mini-css-extract-plugin": "^1.5.0",
"postcss": "^8.2.6",
"postcss-loader": "^5.0.0",
"prettier": "^2.2.1",
"prettier-quick": "0.0.5",
"pretty-quick": "^3.1.0",
"redux-mock-store": "^1.5.4",
+ "resolve-url-loader": "^3.1.2",
+ "rimraf": "^3.0.2",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
@@ -71,8 +75,10 @@
"ts-loader": "^8.0.17",
"typescript": "^4.2.2",
"webpack": "^5.24.2",
+ "webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.5.0",
- "webpack-dev-server": "^3.11.2"
+ "webpack-dev-server": "^3.11.2",
+ "webpack-merge": "^5.7.3"
},
"husky": {
"hooks": {
diff --git a/tailwind.config.js b/tailwind.config.js
index eb924ba..8887a4f 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,5 +1,5 @@
module.exports = {
- purge: [],
+ purge: ["./src/**/*.{tsx,ts}"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
diff --git a/webpack.config.js b/webpack.common.js
similarity index 81%
rename from webpack.config.js
rename to webpack.common.js
index 55d4bf3..fd21e99 100644
--- a/webpack.config.js
+++ b/webpack.common.js
@@ -1,7 +1,6 @@
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
-const buildPath = path.join(__dirname, "./dist");
const sourcePath = path.join(__dirname, "./src");
const publicPath = path.join(__dirname, "./public");
@@ -9,25 +8,6 @@ module.exports = () => ({
entry: {
app: `${sourcePath}/index.tsx`,
},
- output: {
- path: buildPath,
- filename: "[name].[hash].js",
- chunkFilename: "[name].[hash].js",
- },
- mode: "development",
- devtool: "source-map",
- devServer: {
- contentBase: "./dist",
- historyApiFallback: true,
- compress: true,
- open: true,
- hot: true,
- overlay: true,
- port: 8082,
- },
- optimization: {
- minimize: false,
- },
resolve: {
modules: ["node_modules"],
extensions: [".ts", ".tsx", ".js", ".jsx"],
@@ -84,7 +64,6 @@ module.exports = () => ({
},
],
},
-
plugins: [
new HtmlWebpackPlugin({
chunks: ["vendor", "app"],
diff --git a/webpack.dev.js b/webpack.dev.js
new file mode 100644
index 0000000..d387b3b
--- /dev/null
+++ b/webpack.dev.js
@@ -0,0 +1,25 @@
+const { merge } = require("webpack-merge");
+const common = require("./webpack.common.js");
+
+module.exports = () => {
+ return merge(common(), {
+ output: {
+ filename: "[name].[hash].js",
+ chunkFilename: "[name].[hash].js",
+ },
+ mode: "development",
+ devtool: "source-map",
+ devServer: {
+ contentBase: "./dist",
+ historyApiFallback: true,
+ compress: true,
+ open: true,
+ hot: true,
+ overlay: true,
+ port: 8082,
+ },
+ optimization: {
+ minimize: false,
+ },
+ });
+};
diff --git a/webpack.prod.js b/webpack.prod.js
new file mode 100644
index 0000000..5c6732b
--- /dev/null
+++ b/webpack.prod.js
@@ -0,0 +1,65 @@
+const path = require("path");
+const { merge } = require("webpack-merge");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
+const common = require("./webpack.common.js");
+
+const buildPath = path.join(__dirname, "./dist");
+
+module.exports = () => {
+ return merge(common(), {
+ mode: "production",
+ output: {
+ path: buildPath,
+ filename: "[name].[chunkhash].js",
+ chunkFilename: "[name].[chunkhash].js",
+ },
+ plugins: [
+ new MiniCssExtractPlugin({
+ filename: "[name].css",
+ chunkFilename: "[id].css",
+ }),
+ new BundleAnalyzerPlugin({
+ defaultSizes: "gzip",
+ analyzerMode: "static",
+ openAnalyzer: true,
+ }),
+ ],
+ optimization: {
+ splitChunks: {
+ cacheGroups: {
+ vendor: {
+ test: /node_modules/,
+ chunks: "initial",
+ name: "vendor",
+ },
+ },
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /app\.(sa|sc|c)ss$/,
+ use: [
+ { loader: MiniCssExtractPlugin.loader },
+ {
+ loader: "css-loader",
+ },
+ {
+ loader: "postcss-loader",
+ },
+ {
+ loader: "resolve-url-loader",
+ },
+ {
+ loader: "sass-loader",
+ options: {
+ sourceMap: true,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ });
+};
From f134c5ffdaf3662deeb2d442ebe8da16d6c20bcc Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 28 Apr 2021 11:52:56 +0100
Subject: [PATCH 14/17] WIP express server for heroku
---
.eslintignore | 1 +
Procfile | 1 +
package-lock.json | 138 ++++++++++++++--------------------------------
package.json | 8 ++-
src/server.js | 6 ++
5 files changed, 55 insertions(+), 99 deletions(-)
create mode 100644 Procfile
create mode 100644 src/server.js
diff --git a/.eslintignore b/.eslintignore
index 3b0cce6..78aecbf 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
webpack.*.js
jest.config.js
+server.js
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..6401e49
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: npm run serve
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index dc06f45..a56f0e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1451,7 +1451,6 @@
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
- "dev": true,
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
@@ -2002,7 +2001,6 @@
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
- "dev": true,
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
@@ -2019,14 +2017,12 @@
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
- "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
- "dev": true
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -2034,14 +2030,12 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
- "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
- "dev": true
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@@ -2544,7 +2538,6 @@
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
- "dev": true,
"requires": {
"safe-buffer": "5.1.2"
}
@@ -2552,8 +2545,7 @@
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
- "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
- "dev": true
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"convert-source-map": {
"version": "1.7.0",
@@ -2567,14 +2559,12 @@
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
- "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
- "dev": true
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
- "dev": true
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"copy-descriptor": {
"version": "0.1.1",
@@ -2970,14 +2960,12 @@
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "dev": true
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
- "dev": true
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"detect-newline": {
"version": "3.1.0",
@@ -3185,8 +3173,7 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
- "dev": true
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-to-chromium": {
"version": "1.3.675",
@@ -3215,8 +3202,7 @@
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
- "dev": true
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"end-of-stream": {
"version": "1.4.4",
@@ -3357,8 +3343,7 @@
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
- "dev": true
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
@@ -3946,8 +3931,7 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
- "dev": true
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"eventemitter3": {
"version": "4.0.7",
@@ -4142,7 +4126,6 @@
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
- "dev": true,
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
@@ -4179,14 +4162,12 @@
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
- "dev": true
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -4194,14 +4175,12 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
- "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
- "dev": true
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@@ -4423,7 +4402,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
- "dev": true,
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
@@ -4438,7 +4416,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -4446,8 +4423,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
@@ -4517,8 +4493,7 @@
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
- "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
- "dev": true
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fraction.js": {
"version": "4.0.13",
@@ -4538,8 +4513,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
- "dev": true
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs-extra": {
"version": "9.1.0",
@@ -4951,7 +4925,6 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
- "dev": true,
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
@@ -4963,8 +4936,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
- "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
- "dev": true
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}
}
},
@@ -5189,7 +5161,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@@ -5307,8 +5278,7 @@
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "dev": true
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"is-absolute-url": {
"version": "3.0.3",
@@ -6551,8 +6521,7 @@
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
- "dev": true
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"memory-fs": {
"version": "0.5.0",
@@ -6567,8 +6536,7 @@
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
- "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
- "dev": true
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"merge-stream": {
"version": "2.0.0",
@@ -6585,8 +6553,7 @@
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
- "dev": true
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"micromatch": {
"version": "4.0.2",
@@ -6601,20 +6568,17 @@
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "dev": true
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz",
- "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==",
- "dev": true
+ "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ=="
},
"mime-types": {
"version": "2.1.29",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz",
"integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==",
- "dev": true,
"requires": {
"mime-db": "1.46.0"
}
@@ -6783,8 +6747,7 @@
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
- "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
- "dev": true
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"neo-async": {
"version": "2.6.2",
@@ -7080,7 +7043,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
- "dev": true,
"requires": {
"ee-first": "1.1.1"
}
@@ -7260,8 +7222,7 @@
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "dev": true
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"pascal-case": {
"version": "3.1.2",
@@ -7326,8 +7287,7 @@
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
- "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
- "dev": true
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"path-type": {
"version": "4.0.0",
@@ -7797,7 +7757,6 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
- "dev": true,
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
@@ -7887,14 +7846,12 @@
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "dev": true
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
- "dev": true,
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
@@ -7905,8 +7862,7 @@
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
- "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
- "dev": true
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
}
}
},
@@ -8574,8 +8530,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@@ -8589,8 +8544,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sane": {
"version": "4.1.0",
@@ -8821,7 +8775,6 @@
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
- "dev": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -8842,7 +8795,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
"requires": {
"ms": "2.0.0"
},
@@ -8850,16 +8802,14 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
@@ -8932,7 +8882,6 @@
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
- "dev": true,
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
@@ -8972,8 +8921,7 @@
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
- "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
- "dev": true
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"shallow-clone": {
"version": "3.0.1",
@@ -9476,8 +9424,7 @@
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
- "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
- "dev": true
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stealthy-require": {
"version": "1.1.1",
@@ -9854,8 +9801,7 @@
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
- "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
- "dev": true
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"totalist": {
"version": "1.1.0",
@@ -10013,7 +9959,6 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dev": true,
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -10082,8 +10027,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
- "dev": true
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unset-value": {
"version": "1.0.0",
@@ -10195,8 +10139,7 @@
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
- "dev": true
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "8.3.2",
@@ -10243,8 +10186,7 @@
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
- "dev": true
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"verror": {
"version": "1.10.0",
diff --git a/package.json b/package.json
index 2b666a4..a1c1d68 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,14 @@
"scripts": {
"build": "rimraf dist && NODE_ENV=production webpack --config webpack.prod.js",
"start": "webpack serve --config webpack.dev.js",
+ "serve": "node src/server.js",
"test": "jest --env=jsdom",
"test:coverage": "jest --coverage",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --fix",
"pretty": "pretty-quick --branch development",
- "prettier-format": "prettier --config .prettierrc '**/*.{json,js,jsx,ts,tsx,css,scss,md}' --write"
+ "prettier-format": "prettier --config .prettierrc '**/*.{json,js,jsx,ts,tsx,css,scss,md}' --write",
+ "postinstall": "npm run build"
},
"author": "Sean Jones",
"license": "ISC",
@@ -23,6 +25,7 @@
"@reduxjs/toolkit": "^1.5.0",
"axios": "^0.21.1",
"classnames": "^2.3.1",
+ "express": "^4.17.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
@@ -85,5 +88,8 @@
"pre-commit": "npm run lint && npm run pretty -- --staged",
"pre-push": "npm run test -- --bail"
}
+ },
+ "engines": {
+ "node": "12.14.1"
}
}
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..46a72f0
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,6 @@
+const express = require("express");
+const app = express();
+
+app.use(express.static(__dirname + "/dist"));
+
+app.listen(process.env.PORT || 8080);
From feb2c54e46ab469318863d22eacaddba7a83a454 Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 28 Apr 2021 13:05:26 +0100
Subject: [PATCH 15/17] add some tests to state
---
src/components/App/App.tsx | 2 +-
src/components/UsersTable/UsersTable.tsx | 42 ++++++++++++----------
src/components/UsersTable/users-table.scss | 19 ++++++++++
src/state/userProfile/actions.ts | 8 +++--
src/state/userProfile/reducer.test.ts | 18 ++++++++++
src/state/users/actions.ts | 6 +++-
src/state/users/reducer.test.ts | 19 ++++++++++
src/state/users/selectors.test.ts | 24 +++++++++++++
8 files changed, 115 insertions(+), 23 deletions(-)
create mode 100644 src/components/UsersTable/users-table.scss
create mode 100644 src/state/userProfile/reducer.test.ts
create mode 100644 src/state/users/reducer.test.ts
create mode 100644 src/state/users/selectors.test.ts
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index b1e8b42..1947fb2 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -16,7 +16,7 @@ const App: FC = () => {
useEffect(() => {
if (!username) {
- setTimeout(() => dispatch(fetchUserById()), 2400);
+ setTimeout(() => dispatch(fetchUserById("1")), 2400);
}
}, [dispatch]);
diff --git a/src/components/UsersTable/UsersTable.tsx b/src/components/UsersTable/UsersTable.tsx
index 251eedd..15ffb8f 100644
--- a/src/components/UsersTable/UsersTable.tsx
+++ b/src/components/UsersTable/UsersTable.tsx
@@ -4,6 +4,8 @@ import { useDispatch, useSelector } from "react-redux";
import { getUsersList } from "state/users/selectors";
import { fetchUsers } from "state/users/actions";
+import "./users-table.scss";
+
const UsersTable: FC = () => {
const dispatch = useDispatch();
const usersList = useSelector(getUsersList);
@@ -19,26 +21,28 @@ const UsersTable: FC = () => {
}
return (
-
-
-
- | Name |
- Username |
- Email |
- Company |
-
-
-
- {usersList.map(({ name, email, company, username }, i) => (
-
- | {name} |
- {email} |
- {company.name} |
- {username} |
+
+
+
+
+ | Name |
+ Username |
+ Email |
+ Company |
- ))}
-
-
+
+
+ {usersList.map(({ name, email, company, username }, i) => (
+
+ | {name} |
+ {email} |
+ {company.name} |
+ {username} |
+
+ ))}
+
+
+
);
};
diff --git a/src/components/UsersTable/users-table.scss b/src/components/UsersTable/users-table.scss
new file mode 100644
index 0000000..5b2734c
--- /dev/null
+++ b/src/components/UsersTable/users-table.scss
@@ -0,0 +1,19 @@
+.users-table {
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ }
+
+ thead th,
+ thead td {
+ font-size: 1rem;
+ padding: 1rem;
+ text-align: left;
+ }
+
+ tbody td {
+ font-size: 1.4rem;
+ padding: 0.25rem 1rem;
+ }
+}
diff --git a/src/state/userProfile/actions.ts b/src/state/userProfile/actions.ts
index 573b0e3..9f30dbc 100644
--- a/src/state/userProfile/actions.ts
+++ b/src/state/userProfile/actions.ts
@@ -4,9 +4,13 @@ import apiConfig from "api/apiConfig";
import { User } from "../types";
-export const fetchUserById = createAsyncThunk("users/fetchById", async () => {
+export const ACTION_TYPES = {
+ FETCH_USER_BY_ID: "users/fetchById",
+} as const;
+
+export const fetchUserById = createAsyncThunk("users/fetchById", async (userId: string) => {
const response = await ApiService.callApi({
- url: apiConfig.endpoints.userProfile("1"),
+ url: apiConfig.endpoints.userProfile(userId),
method: "GET",
});
return response.data;
diff --git a/src/state/userProfile/reducer.test.ts b/src/state/userProfile/reducer.test.ts
new file mode 100644
index 0000000..e1ec399
--- /dev/null
+++ b/src/state/userProfile/reducer.test.ts
@@ -0,0 +1,18 @@
+import reducer, { initialState } from "./reducer";
+import { ACTION_TYPES } from "./actions";
+import { User } from "../types";
+
+describe("UserProfile reducer", () => {
+ it("Fetch user by id fulfilled", () => {
+ const user: User = {
+ id: "1",
+ name: "Steve Smith",
+ };
+ const state = reducer(initialState, {
+ type: `${ACTION_TYPES.FETCH_USER_BY_ID}/fulfilled`,
+ payload: user,
+ });
+ expect(state.id).toBe("1");
+ expect(state.name).toBe("Steve Smith");
+ });
+});
diff --git a/src/state/users/actions.ts b/src/state/users/actions.ts
index 03b7b7d..e6259e3 100644
--- a/src/state/users/actions.ts
+++ b/src/state/users/actions.ts
@@ -4,7 +4,11 @@ import apiConfig from "api/apiConfig";
import { Users } from "../types";
-export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
+export const ACTION_TYPES = {
+ FETCH_USERS: "users/fetchUsers",
+} as const;
+
+export const fetchUsers = createAsyncThunk(ACTION_TYPES.FETCH_USERS, async () => {
const response = await ApiService.callApi({
url: apiConfig.endpoints.users,
method: "GET",
diff --git a/src/state/users/reducer.test.ts b/src/state/users/reducer.test.ts
new file mode 100644
index 0000000..3c850e7
--- /dev/null
+++ b/src/state/users/reducer.test.ts
@@ -0,0 +1,19 @@
+import reducer, { initialState } from "./reducer";
+import { ACTION_TYPES } from "./actions";
+import { Users } from "../types";
+
+describe("Users reducer", () => {
+ it("Fetch users fulfilled", () => {
+ const usersList: Users = [
+ {
+ id: "1",
+ name: "Steve Smith",
+ },
+ ];
+ const state = reducer(initialState, {
+ type: `${ACTION_TYPES.FETCH_USERS}/fulfilled`,
+ payload: usersList,
+ });
+ expect(state.list).toEqual(usersList);
+ });
+});
diff --git a/src/state/users/selectors.test.ts b/src/state/users/selectors.test.ts
new file mode 100644
index 0000000..2aab54d
--- /dev/null
+++ b/src/state/users/selectors.test.ts
@@ -0,0 +1,24 @@
+import { getUsersList } from "./selectors";
+import mockState from "utilities/test/mockState";
+import { RootState } from "../rootReducer";
+import { Users } from "../types";
+
+describe("Users selectors", () => {
+ describe("getUsersList", () => {
+ it("should return list of users", () => {
+ const usersList: Users = [
+ {
+ id: "1",
+ name: "Steve Smith",
+ },
+ ];
+ const state: RootState = {
+ ...mockState,
+ users: {
+ list: usersList,
+ },
+ };
+ expect(getUsersList(state)).toEqual(usersList);
+ });
+ });
+});
From 83191c81ad9f0fba43e2a11eb823ce372412d1de Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 28 Apr 2021 13:24:17 +0100
Subject: [PATCH 16/17] update readme
---
README.md | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 197f9e4..8416982 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,33 @@
-# user-management
\ No newline at end of file
+# user-management
+
+## Overview
+
+This is a demo app designed to demonstrate my React experience.
+
+## Setup
+
+Install all packages
+
+```sh
+npm i
+```
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm run start`
+
+Runs the app in the development mode.\
+Open [http://localhost:8082](http://localhost:8082) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `npm run test`
+
+Launches the test runner.
+
+### `npm run build`
+
+Generates a new build optimised for production. Files are output to the dist folder.
From 7fa194175b1a6192661dca026daafe856694cd5f Mon Sep 17 00:00:00 2001
From: Sean Jones
Date: Wed, 28 Apr 2021 13:31:37 +0100
Subject: [PATCH 17/17] update readme
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 8416982..65f3e6d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# user-management
+# user-management-app
## Overview
-This is a demo app designed to demonstrate my React experience.
+This is a demo app designed to demonstrate my React experience. The project UI consists of a single page which diplays a list of users from an Admin perspective.
## Setup