diff --git a/proof-of-concepts/features/.env b/proof-of-concepts/features/.env index 7d910f1..2569802 100644 --- a/proof-of-concepts/features/.env +++ b/proof-of-concepts/features/.env @@ -1 +1,2 @@ -SKIP_PREFLIGHT_CHECK=true \ No newline at end of file +SKIP_PREFLIGHT_CHECK=true +PROTOTYPE=true \ No newline at end of file diff --git a/proof-of-concepts/features/db.json b/proof-of-concepts/features/db.json new file mode 100644 index 0000000..84982b7 --- /dev/null +++ b/proof-of-concepts/features/db.json @@ -0,0 +1,10 @@ +{ + "tags": [ + { "id": 1, "name": "material-analysis", "count": 5, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, + { "id": 2, "name": "class-analysis", "count": 33, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, + { "id": 3, "name": "materialism", "count": 12, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, + { "id": 4, "name": "dialectical-materialism", "count": 9, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, + { "id": 5, "name": "historical-materialism", "count": 5, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, + { "id": 6, "name": "materialist-theory", "count": 2, "excerpt":"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." } + ] +} \ No newline at end of file diff --git a/proof-of-concepts/features/jest-setup.ts b/proof-of-concepts/features/jest-setup.ts new file mode 100644 index 0000000..53ccede --- /dev/null +++ b/proof-of-concepts/features/jest-setup.ts @@ -0,0 +1,9 @@ +import '@testing-library/jest-dom'; +import { server } from './stories/test/server'; + +beforeAll(() => server.listen()); +// if you need to add a handler after calling setupServer for some specific test +// this will remove that handler for the rest of them +// (which is important for test isolation): +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/proof-of-concepts/features/jest.config.js b/proof-of-concepts/features/jest.config.js index 08fe0f3..ede3be4 100644 --- a/proof-of-concepts/features/jest.config.js +++ b/proof-of-concepts/features/jest.config.js @@ -18,5 +18,6 @@ module.exports = { '/stories/link/', 'utils.ts', 'testingUtils.ts' - ] + ], + setupFilesAfterEnv: ['/jest-setup.ts'] }; diff --git a/proof-of-concepts/features/package-lock.json b/proof-of-concepts/features/package-lock.json index 9938381..8fe57b0 100644 --- a/proof-of-concepts/features/package-lock.json +++ b/proof-of-concepts/features/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@chakra-ui/react": "^1.7.2", + "@emotion/react": "^11.7.0", + "@emotion/styled": "^11.6.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -17,15 +20,24 @@ "@types/react": "^17.0.30", "@types/react-dom": "^17.0.9", "create-react-app": "^4.0.3", + "draft-js": "^0.11.7", + "draft-js-export-markdown": "^1.4.0", + "draft-js-import-markdown": "^1.4.0", + "final-form": "^4.20.4", + "final-form-focus": "^1.1.2", + "framer-motion": "^4.1.17", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-final-form": "^6.5.7", "react-icons": "^4.3.1", "react-scripts": "4.0.3", + "react-transition-group": "^4.4.2", "typescript": "^4.4.4" }, "devDependencies": { "@babel/core": "^7.15.8", "@babel/preset-typescript": "^7.15.0", + "@jackfranklin/test-data-bot": "^1.3.0", "@storybook/addon-a11y": "^6.3.12", "@storybook/addon-actions": "^6.3.12", "@storybook/addon-essentials": "^6.3.12", @@ -35,6 +47,9 @@ "@storybook/react": "^6.3.12", "@testing-library/react-hooks": "^7.0.2", "@types/classnames": "^2.3.1", + "@types/draft-js": "^0.11.7", + "@types/final-form-focus": "^1.1.2", + "@types/react-transition-group": "^4.4.4", "babel-loader": "^8.2.2", "classnames": "^2.3.1", "css-loader": "^3.6.0", @@ -44,9 +59,13 @@ "eslint-plugin-react-hooks": "^4.2.0", "fork-ts-checker-webpack-plugin": "^4.1.6", "jest": "^27.3.1", + "jest-transform-css": "^3.0.0", + "json-server": "^0.17.0", "msw": "^0.35.0", + "node-fetch": "^3.1.0", "react-docgen-typescript": "^2.1.1", "react-docgen-typescript-plugin": "^1.0.0", + "react-error-boundary": "^3.1.4", "sass": "^1.43.2", "sass-loader": "^10.2.0", "storybook": "^6.3.12", @@ -1974,6 +1993,830 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "node_modules/@chakra-ui/accordion": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-1.4.1.tgz", + "integrity": "sha512-/E0FW5YHNVD6WwMGiuQuXpA70P2CKAV+MzcMITnSGPWsh9XD0mcXvMkIALVojfFk9tcCFdIGnxX/HWr41LzgIg==", + "dependencies": { + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/alert": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-1.3.1.tgz", + "integrity": "sha512-BeR6l/1CLZarA3uAe+5Q3hioYf7SixYfy9rOte/29ck1lx9PLjjuPYYmuDPtZNbGibhUCh48z4U/uK2x8mbpKQ==", + "dependencies": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/anatomy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-1.2.1.tgz", + "integrity": "sha512-kNS+FiEDTSnwpQUW4dEjZ5745xhkvB0XtmqjY1wpclUSpFfptLZM9QIHPTnBt2bzM9R+idmRRP+WkTt6kyTrLw==", + "dependencies": { + "@chakra-ui/theme-tools": "^1.3.1" + } + }, + "node_modules/@chakra-ui/avatar": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-1.3.1.tgz", + "integrity": "sha512-WI0/kcpTJViOH093V0bz8EB+e/rc+gjF+T5DkOuh1YWFxRRG5v+4Yd3PdEJtQgzWtBVhlbGWmE7WvBizyKwFCA==", + "dependencies": { + "@chakra-ui/image": "1.1.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/breadcrumb": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-1.3.1.tgz", + "integrity": "sha512-b1IoBmtr5FcP2fn5NRbdOdQo2c866OQ/WhcTcZ6UKae1jjik+36/qWE+X+RKzxC6FLfqo5qayV5zSgsnZym7Pg==", + "dependencies": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/button": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-1.5.1.tgz", + "integrity": "sha512-BvP29quEhP6OTgDiRsugD6adgkeOTEQpoDsZUVEmHnNVrbFfdsICEKKQTtDJ2iPf+hmpFrtnpN50vCLdAANKcw==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/spinner": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/checkbox": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-1.6.1.tgz", + "integrity": "sha512-Z5ZMeUYIRjRbi/knhYhSQshZH7OnROA7ezl9a9oVSKRF7iLMNMibQSlQLXmqUWaTKSgrS37cpKAzfgEuemyiUQ==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/clickable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-1.2.1.tgz", + "integrity": "sha512-B0CIbKzDMwzG1APeTpW9H2Jl8dkarI1Qstb3hDOy23O+N5TU6lpDdVnXQ7fpFJS6mu5JjFqtkwzGAVZnkkv1rw==", + "dependencies": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/close-button": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-1.2.1.tgz", + "integrity": "sha512-A/cuFtJPF8rp5p6tCIGlQdHB89gLCSOzxWssoTXAGJnmlwY2YunFHxgkYZXwPbDqFrM8ndya7Ys+AuL1JZsa3g==", + "dependencies": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/color-mode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-1.3.2.tgz", + "integrity": "sha512-/rWcbrzbaWCyyUnT07Qjz0xf/ltHS31CHOKtVCWr2uTgfn2gOQpdxsKRbjrLYPOYZGTMdINUHNiAsqQjLoAoTQ==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/control-box": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-1.1.1.tgz", + "integrity": "sha512-ZFbh85pzzZoiSjGnvLUzMB5BoA8Xm6TBMWvMtzLY5xiFGb9/mBeRDH2KFjr1GJzoqleWKkQwvFD6JM0kXcekpg==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/counter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-1.2.1.tgz", + "integrity": "sha512-Gm4njMzEsDyAzdQtExn40TvmupzkPBrT5DiCu0DlxYqpLqCfqV49HgJHEG5oW3WV+WaC9mzg7VV+idKYh/d+Gg==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/css-reset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", + "peerDependencies": { + "@emotion/react": ">=10.0.35", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/descendant": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-2.1.1.tgz", + "integrity": "sha512-JasdVaN4MjL7QFo1vMnADy6EtFAlPKT1kTJ1LwMtl9AaF9VFLBsfGxm0L+WQK+3NJMuCSDBXWJB8mV4AQ11Edg==", + "dependencies": { + "@chakra-ui/react-utils": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/editable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-1.3.1.tgz", + "integrity": "sha512-MwyTtsnHNqmKmHv9SH3KIHWa06D4gBwcuTawTiSnYBUJL6My8ry/Wdca1to9So2tD6hcjz3TPTzOJOlyv0eiZg==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/focus-lock": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-1.2.1.tgz", + "integrity": "sha512-HYu39nvfaXUrBx+dIDJkFgebNCGEi9oZTfLUKzIJC+zPkmReTDSXV0dzSb/8vCAOq5fph1gFKsdbGy2U98P8GQ==", + "dependencies": { + "@chakra-ui/utils": "1.9.1", + "react-focus-lock": "2.5.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/form-control": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-1.5.1.tgz", + "integrity": "sha512-ASZYQFOs5mAoaNXAN/ZaesMy3XV07F0/Eba5PQ7Dejdn91aep6lqF889hmr8yqcR646xCOY7ISyYsskfh9QHrQ==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/hooks": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-1.7.1.tgz", + "integrity": "sha512-hgN19X6GUKQYAHczmFY+GAT8vl9h+X+nGWrIAnmvZ6BgUXxDajnTNhZeWhj0ZkR+7A7dCE6Y/3X44GafUgChMw==", + "dependencies": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "compute-scroll-into-view": "1.0.14", + "copy-to-clipboard": "3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/hooks/node_modules/compute-scroll-into-view": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz", + "integrity": "sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ==" + }, + "node_modules/@chakra-ui/icon": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-1.2.1.tgz", + "integrity": "sha512-uZxFsiY4Tld+LvGIX7cky0H6oMRac8udPMQRzIk/UQeNZcsWisGetatbQsew3y1lWV/iH/8+TlDuW13GWGyGGQ==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/image": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-1.1.1.tgz", + "integrity": "sha512-bz1pn08XlXcO3r1KnpdjQgN3R2soiTx10sG2d5Pw9BdGdySf7Y73wiLh+Tan1xJHp6p2KH1hz4f7uKXXDn7Qmw==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-1.3.1.tgz", + "integrity": "sha512-Z+LqkwVPMeUBuvB9dLDPKkBnWV52Q1PVl3KW9ouDIFg7SoemeYkBt3p4ttEKE+eIPsPlrcH1u2A/RGcCTZOe1g==", + "dependencies": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/layout": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-1.5.1.tgz", + "integrity": "sha512-nKiyZ5adjNTbBV3oFIUGIPijwutO1NGdev1jHtnZc3xo2urCIkBvKU8+mVjlX04IwZ7oLKoP3EiDDv0g7+o41Q==", + "dependencies": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/live-region": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-1.1.1.tgz", + "integrity": "sha512-BSdI5gLIffNRETEp6W18kBNg9tL0ZLLzfWGRnuO9tEbox7NrcgqIeLF8mNKwhDOZz88NKHtUOPVzjAUKW1SryQ==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/media-query": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-1.2.1.tgz", + "integrity": "sha512-Ho/qiPGTjNukFTE9WBdYV9FIXU7KFTJPqdRQPWANkz+j275n6sqSE1j5LRJllP+ett21KeuWLN4zL33pP0Ox+g==", + "dependencies": { + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "@chakra-ui/theme": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/menu": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-1.8.1.tgz", + "integrity": "sha512-fgzzFukBj4sQzTRf4q/+nHiVTKhrMtJdofnluqce/SCRJ1G+bbovUySblTzfI8iFlTSZt/eWc/Nju4JB1S+3Yg==", + "dependencies": { + "@chakra-ui/clickable": "1.2.1", + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/modal": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-1.10.1.tgz", + "integrity": "sha512-cboC2ITm+5FjhrBc6yJ5cW4VXnfwlLhFa1EkPqF1k4kvYGyUHArvPN1q8AiPYOIrupHYu2Iu6YmQPg7TJwNImg==", + "dependencies": { + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/focus-lock": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.4.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/number-input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-1.3.1.tgz", + "integrity": "sha512-4vBRSShT5pedElgP9YGVC+9RHzQGmUVZqu3p0gZW0fLGVVQ9C1EGrO7djL+k3tgklyu8RvSwkRDJqEPvbQKDgQ==", + "dependencies": { + "@chakra-ui/counter": "1.2.1", + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/pin-input": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-1.7.1.tgz", + "integrity": "sha512-eFFc5sofiyion+NxELWfCzD23XHIBDrJcfKKbNxt8jdXg9Ek4mFpmvnxBVrK0DIz6cVYgKY8c364OmxNUf4IyA==", + "dependencies": { + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/popover": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-1.10.1.tgz", + "integrity": "sha512-/dMUQfd+h9j3GBtkA/nYaQ5xeu4vk0psUClFvLOAJRwXGN3aMrzn/mhrvHWQ/cJuwQrO1WzxH2+g6pwsFOm9ng==", + "dependencies": { + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/popper": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-2.4.1.tgz", + "integrity": "sha512-cuwnwXx6RUXZGGynVOGG8fEIiMNBXUCy3UqWQD1eEd8200eWQobgNk4Z0YwzKuSzJwp0Auy+j5iKefi5FSkyog==", + "dependencies": { + "@chakra-ui/react-utils": "1.2.1", + "@popperjs/core": "^2.9.3" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/portal": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-1.3.1.tgz", + "integrity": "sha512-6UOGZCfujgdijcPs/JTEY5IB5WtKvUbfrSQYsG5CDa+guIwvnoP5qZ+rH6BR6DSSM8Wr/1n+WrtanhfFZShHKA==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/progress": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-1.2.1.tgz", + "integrity": "sha512-213nN8nbODvD/A23vAtg+r3bRKKatWQHafgmLzeznUcxa/+ac0eVurIS8XSYLRkY4EXQ505re3ZkLhDd98a7QA==", + "dependencies": { + "@chakra-ui/theme-tools": "1.3.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/provider": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-1.7.2.tgz", + "integrity": "sha512-5Tk7K6aY2gWQQn62MOmeOtRdDJjmtHau2klxhKDlEIPs/smrdBF/ymK7eI5pJiU/BeKHAbM6DbvelR2khUZP+w==", + "dependencies": { + "@chakra-ui/css-reset": "1.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/radio": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-1.4.2.tgz", + "integrity": "sha512-QWIZAy/PLHNnlI6Z6o3kAdUzkSMpaJ9kOlMIFcsen6tAfPob3R+cyMQFiaX6RBw7yHWYIrfD9gaAqwd+FihQWQ==", + "dependencies": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/react": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-1.7.2.tgz", + "integrity": "sha512-2RxJHqYLSSB/dQQ9M4APxMopPfsU89A++63v8U8jiR7J8sUaa6Ded9ypTiIUY09J8v7mJAus4/ZSJxG7Zfd7aA==", + "dependencies": { + "@chakra-ui/accordion": "1.4.1", + "@chakra-ui/alert": "1.3.1", + "@chakra-ui/avatar": "1.3.1", + "@chakra-ui/breadcrumb": "1.3.1", + "@chakra-ui/button": "1.5.1", + "@chakra-ui/checkbox": "1.6.1", + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/control-box": "1.1.1", + "@chakra-ui/counter": "1.2.1", + "@chakra-ui/css-reset": "1.1.1", + "@chakra-ui/editable": "1.3.1", + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/image": "1.1.1", + "@chakra-ui/input": "1.3.1", + "@chakra-ui/layout": "1.5.1", + "@chakra-ui/live-region": "1.1.1", + "@chakra-ui/media-query": "1.2.1", + "@chakra-ui/menu": "1.8.1", + "@chakra-ui/modal": "1.10.1", + "@chakra-ui/number-input": "1.3.1", + "@chakra-ui/pin-input": "1.7.1", + "@chakra-ui/popover": "1.10.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/progress": "1.2.1", + "@chakra-ui/provider": "1.7.2", + "@chakra-ui/radio": "1.4.2", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/select": "1.2.1", + "@chakra-ui/skeleton": "1.2.2", + "@chakra-ui/slider": "1.5.2", + "@chakra-ui/spinner": "1.2.1", + "@chakra-ui/stat": "1.2.1", + "@chakra-ui/switch": "1.3.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/table": "1.3.1", + "@chakra-ui/tabs": "1.6.1", + "@chakra-ui/tag": "1.2.1", + "@chakra-ui/textarea": "1.2.1", + "@chakra-ui/theme": "1.12.1", + "@chakra-ui/toast": "1.4.1", + "@chakra-ui/tooltip": "1.4.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/react-env": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-1.1.1.tgz", + "integrity": "sha512-Lgmb0y4kv0ffsGMelAOaYOd4tYZAv4FYWgV86ckGMjmYQWA8drv4v/lHTNltixxWMmBEpjcHALpJuS6yAZYHug==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/react-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-1.2.1.tgz", + "integrity": "sha512-bV8FRaXiOgGxOg03iTNin/B02I+tHH9PQtqUTl3U7cJaoI+5AUYhrqXvl1Ya2/R7zxSFrb/gBVDTgbZiVkJ+Dg==", + "dependencies": { + "@chakra-ui/utils": "^1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/select": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-1.2.1.tgz", + "integrity": "sha512-GqRmYGjVnw/Z/2RQiW7Ywuu9O5E0spmMUBjeE/v0rqjixBqrmdApjg5pmJ4YmUMvUI/WkGtR3FR5W9Y5PpvfKw==", + "dependencies": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/skeleton": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-1.2.2.tgz", + "integrity": "sha512-kA3DmhaazVX31iIOY87Jskqj+TFd+FKhOCWjPgZiFeEtJDYriU9BfzYSo2j6ePLtMRMc6Z99vXIHmjoOIEy5xQ==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/media-query": "1.2.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/slider": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-1.5.2.tgz", + "integrity": "sha512-zP07TMew61GkJe47Nu7zEg/SUEwPHpN4alW6VUM6Y8UaVpQaDx7InarbWTc/bXdTP03SfE+hQ6WD9Oy7noe4hQ==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/spinner": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-1.2.1.tgz", + "integrity": "sha512-CQsUJNJWWSot1ku5Se41Nz1dXIDhk+/7FIhTbfRHSjtYZnAab3CPMHBkTGqwbJxQ9oHYgk9Rso3cfG+/ra6aTQ==", + "dependencies": { + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/stat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-1.2.1.tgz", + "integrity": "sha512-BTZFeh/8VdgUX080taCQj1g/rS4wGc+y3GQnklqlZ9N/bEv0gyLqQga7TFC/NkVl3cvjRiMnCCPj6vRih9x+Og==", + "dependencies": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/styled-system": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-1.14.1.tgz", + "integrity": "sha512-dgXFYZdJicsddUnPV1X7lQksgMD0z5EvwGaIh2JHJERqNRIvth/CBAnVLQQvy/xSJK5YaSEmeuVVU0veUOQcXg==", + "dependencies": { + "@chakra-ui/utils": "1.9.1", + "csstype": "^3.0.9" + } + }, + "node_modules/@chakra-ui/switch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-1.3.1.tgz", + "integrity": "sha512-92hXJ2/ozj7B3cJNT259mFNoad7Ck892uHTuEQ/GIdXb25doE6F1wCp0TreOnGiEgU5YSaxpdrcZjA0QODP//w==", + "dependencies": { + "@chakra-ui/checkbox": "1.6.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/system": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-1.8.2.tgz", + "integrity": "sha512-k/43bv5exPGc6hBVcq5dUdGC+0pmQthJP/VqcTUt0oIc3oyoVSAnJhpmBkFHXYNqj6yYd2K7qdD2PjjbZ5KCLA==", + "dependencies": { + "@chakra-ui/color-mode": "1.3.2", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/styled-system": "1.14.1", + "@chakra-ui/utils": "1.9.1", + "react-fast-compare": "3.2.0" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/table": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-1.3.1.tgz", + "integrity": "sha512-+ia/7zs7AGj01lon301EEx+mK4918yGc0K6e68Kxomex8tnxkwbskFWs6hX+6Kzbj56ZBm99eLlKpo2iGYX0HA==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/tabs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-1.6.1.tgz", + "integrity": "sha512-p7HdHcleJWNwteWYVPt2KF52YbS5pIIfs/IpgtnYZRsJbqvRVxSwgg5Wsn+vuxFXBKW0cA2rDGbyzsZ+ChtEXQ==", + "dependencies": { + "@chakra-ui/clickable": "1.2.1", + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/tag": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-1.2.1.tgz", + "integrity": "sha512-O068n+qBc+CSyvpRBJ6Lwep6SydQ9UysRqw1ETF+4fJSp9dMrBp8vOcl2SVacKaCu13qdv8UdRMBxUiTz3lh7A==", + "dependencies": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/textarea": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-1.2.1.tgz", + "integrity": "sha512-3xDsL1qQ+eY5r4GcRL4bg90vtV/xxVlw0Z3PFehFP5JW7VwXNZIRjauR/+HlOA8eYq0cF6ch2boR1GPso6rQtw==", + "dependencies": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/theme": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-1.12.1.tgz", + "integrity": "sha512-8yDril3rSzv42eKR0x7KdnrpN1ubY0m6q37CVUADgtboJqoJwWWX2/hqkv8CX6WJf8ZwPwFL5QIwS2FPSGgi+g==", + "dependencies": { + "@chakra-ui/anatomy": "1.2.1", + "@chakra-ui/theme-tools": "1.3.1", + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0" + } + }, + "node_modules/@chakra-ui/theme-tools": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-1.3.1.tgz", + "integrity": "sha512-D8arJ5uFGuYZrrFGpXqgov8FhsJYWRyar5oBZY5TJR9gsVYBlJ8Ai91pwM/NflCFqzerTOgyt7bNSGQMdZ8ghA==", + "dependencies": { + "@chakra-ui/utils": "1.9.1", + "@ctrl/tinycolor": "^3.4.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0" + } + }, + "node_modules/@chakra-ui/toast": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-1.4.1.tgz", + "integrity": "sha512-vzQkYwnGq2nx0bOKIQ6XpJaGzUwnWKmUjcVrz9NzGwVI4g93PS7+13515R0m1NrDp30132OeDXQ+tmQwCRRe6w==", + "dependencies": { + "@chakra-ui/alert": "1.3.1", + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/theme": "1.12.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "@reach/alert": "0.13.2" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/tooltip": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-1.4.1.tgz", + "integrity": "sha512-KvTuqSqIpIgE+YNUwN7ONDRkSGR6SK9+dgSx2PfKy0Sel7UgDPVtxByuZ6tfJ9O1VTRYEdF9k+s6Gf8eRFQbNA==", + "dependencies": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6", + "react-dom": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/transition": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-1.4.1.tgz", + "integrity": "sha512-s/VFucc6grNdP1bxw0oQLzy167gjAgyl/GiGH9nt54nioDEiSsvn70qKg7sjajNTvpoot+urQUdr4Qh+fIUFZQ==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "framer-motion": "3.x || 4.x || 5.x", + "react": ">=16.8.6" + } + }, + "node_modules/@chakra-ui/utils": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-1.9.1.tgz", + "integrity": "sha512-Tue8JfpzOqeHd8vSqAnX1l/Y3Gg456+BXFP/TH6mCIeqMAMbrvv25vDskds0wlXRjMYdmpqHxCEzkalFrscGHA==", + "dependencies": { + "@types/lodash.mergewith": "4.6.6", + "css-box-model": "1.2.1", + "framesync": "5.3.0", + "lodash.mergewith": "4.6.2" + } + }, + "node_modules/@chakra-ui/visually-hidden": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-1.1.1.tgz", + "integrity": "sha512-AGK9YBQS2FW/1e5tfivS8VVXn8y2uTyJ9ACOnGiLm9FNdth9pR0fGil9axlcmhZpEYcSRlnCuma3nkqaCjJnAA==", + "dependencies": { + "@chakra-ui/utils": "1.9.1" + }, + "peerDependencies": { + "@chakra-ui/system": ">=1.0.0", + "react": ">=16.8.6" + } + }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -2002,6 +2845,14 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "node_modules/@ctrl/tinycolor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", + "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", @@ -2011,6 +2862,83 @@ "node": ">=10.0.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz", + "integrity": "sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==", + "dependencies": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/runtime": "^7.13.10", + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.5", + "@emotion/serialize": "^1.0.2", + "babel-plugin-macros": "^2.6.1", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "^4.0.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@babel/plugin-syntax-jsx": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz", + "integrity": "sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", + "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "dependencies": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -2054,14 +2982,13 @@ "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "dev": true + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "dev": true, + "devOptional": true, "dependencies": { "@emotion/memoize": "0.7.4" } @@ -2069,8 +2996,67 @@ "node_modules/@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "dev": true + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "node_modules/@emotion/react": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.7.0.tgz", + "integrity": "sha512-WL93hf9+/2s3cA1JVJlz8+Uy6p6QWukqQFOm2OZO5ki51hfucHMOmbSjiyC3t2Y4RI8XUmBoepoc/24ny/VBbA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@emotion/cache": "^11.6.0", + "@emotion/serialize": "^1.0.2", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/react/node_modules/@emotion/cache": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.6.0.tgz", + "integrity": "sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==", + "dependencies": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.10" + } + }, + "node_modules/@emotion/react/node_modules/@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "dependencies": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/react/node_modules/@emotion/sheet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", + "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + }, + "node_modules/@emotion/react/node_modules/@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" }, "node_modules/@emotion/serialize": { "version": "0.11.16", @@ -2098,23 +3084,34 @@ "dev": true }, "node_modules/@emotion/styled": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", - "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", - "dev": true, + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.6.0.tgz", + "integrity": "sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==", "dependencies": { - "@emotion/styled-base": "^10.0.27", - "babel-plugin-emotion": "^10.0.27" + "@babel/runtime": "^7.13.10", + "@emotion/babel-plugin": "^11.3.0", + "@emotion/is-prop-valid": "^1.1.1", + "@emotion/serialize": "^1.0.2", + "@emotion/utils": "^1.0.0" }, "peerDependencies": { - "@emotion/core": "^10.0.27", - "react": ">=16.3.0" + "@babel/core": "^7.0.0", + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/react": { + "optional": true + } } }, "node_modules/@emotion/styled-base": { - "version": "10.0.31", - "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.0.31.tgz", - "integrity": "sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.3.0.tgz", + "integrity": "sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==", "dev": true, "dependencies": { "@babel/runtime": "^7.5.5", @@ -2127,6 +3124,31 @@ "react": ">=16.3.0" } }, + "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz", + "integrity": "sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==", + "dependencies": { + "@emotion/memoize": "^0.7.4" + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "dependencies": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/styled/node_modules/@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, "node_modules/@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -2136,8 +3158,7 @@ "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "dev": true + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "node_modules/@emotion/utils": { "version": "0.11.3", @@ -2148,8 +3169,7 @@ "node_modules/@emotion/weak-memoize": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", - "dev": true + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", @@ -2288,6 +3308,20 @@ "node": ">=8" } }, + "node_modules/@jackfranklin/test-data-bot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jackfranklin/test-data-bot/-/test-data-bot-1.3.0.tgz", + "integrity": "sha512-68OfPjgHT58ftfd0XKVWOp0p/1K+bbxSPu3wdH9N+/Ox9a3aM/d8sS0AFSJ48Vuoww+MkbHDH1cEE5tTkpTPlw==", + "dev": true, + "dependencies": { + "@types/faker": "^4.1.9", + "faker": "4.1.0", + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=10.13" + } + }, "node_modules/@jest/console": { "version": "27.3.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.3.1.tgz", @@ -2814,12 +3848,26 @@ "version": "2.10.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, + "node_modules/@reach/alert": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/alert/-/alert-0.13.2.tgz", + "integrity": "sha512-LDz83AXCrClyq/MWe+0vaZfHp1Ytqn+kgL5VxG7rirUvmluWaj/snxzfNPWn0Ma4K2YENmXXRC/iHt5X95SqIg==", + "dependencies": { + "@reach/utils": "0.13.2", + "@reach/visually-hidden": "0.13.2", + "prop-types": "^15.7.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, "node_modules/@reach/router": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz", @@ -2836,6 +3884,33 @@ "react-dom": "15.x || 16.x || 16.4.0-alpha.0911da3" } }, + "node_modules/@reach/utils": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.13.2.tgz", + "integrity": "sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==", + "dependencies": { + "@types/warning": "^3.0.0", + "tslib": "^2.1.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, + "node_modules/@reach/visually-hidden": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.13.2.tgz", + "integrity": "sha512-sPZwNS0/duOuG0mYwE5DmgEAzW9VhgU3aIt1+mrfT/xiT9Cdncqke+kRBQgU708q/Ttm9tWsoHni03nn/SuPTQ==", + "dependencies": { + "prop-types": "^15.7.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "^16.8.0 || 17.x", + "react-dom": "^16.8.0 || 17.x" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -4718,6 +5793,40 @@ "integrity": "sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw==", "dev": true }, + "node_modules/@storybook/core-server/node_modules/node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@storybook/core-server/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "node_modules/@storybook/core-server/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "node_modules/@storybook/core-server/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@storybook/csf": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", @@ -4848,6 +5957,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@storybook/manager-webpack4/node_modules/node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/@storybook/manager-webpack4/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4878,6 +5999,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@storybook/manager-webpack4/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "node_modules/@storybook/manager-webpack4/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "node_modules/@storybook/manager-webpack4/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@storybook/node-logger": { "version": "6.3.12", "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", @@ -5158,6 +6301,20 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, + "node_modules/@storybook/theming/node_modules/@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "dev": true, + "dependencies": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + }, + "peerDependencies": { + "@emotion/core": "^10.0.27", + "react": ">=16.3.0" + } + }, "node_modules/@storybook/ui": { "version": "6.3.12", "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.12.tgz", @@ -5766,6 +6923,16 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, + "node_modules/@types/draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-NeCRIPcfrlUItA46boNG2kxuVhohPdE4Plp3GcxEv11FXZ0kZFa6X8fNPpKrp8AEUqeAr/B0TsX/wp2sYZaK3w==", + "dev": true, + "dependencies": { + "@types/react": "*", + "immutable": "~3.7.4" + } + }, "node_modules/@types/eslint": { "version": "7.28.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz", @@ -5780,6 +6947,21 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==" }, + "node_modules/@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==", + "dev": true + }, + "node_modules/@types/final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-2DcNjeyiv+MJo2SjYt8o0yrvnZ15sxkLBzoIeMq9Rt1tN+prMHixHrRLAqcbSbx7+RapJVszqH8p/RLnm4U2Tw==", + "dev": true, + "dependencies": { + "final-form": "4.x.x" + } + }, "node_modules/@types/glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", @@ -5879,6 +7061,19 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "node_modules/@types/lodash": { + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" + }, + "node_modules/@types/lodash.mergewith": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz", + "integrity": "sha512-RY/8IaVENjG19rxTZu9Nukqh0W2UrYgmBj5sdns4hWRZaV8PqR7wIKHFKzvOTjo4zVRV7sVI+yFhAJql12Kfqg==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/markdown-to-jsx": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz", @@ -6038,6 +7233,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", + "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -6106,6 +7310,11 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "node_modules/@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + }, "node_modules/@types/webpack": { "version": "4.41.31", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.31.tgz", @@ -6830,6 +8039,22 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz", + "integrity": "sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==", + "dependencies": { + "tslib": "^1.0.0" + }, + "engines": { + "node": ">=8.5.0" + } + }, + "node_modules/aria-hidden/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -7894,6 +9119,18 @@ } ] }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -9309,9 +10546,9 @@ } }, "node_modules/common-tags": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", - "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "engines": { "node": ">=4.0.0" } @@ -9438,6 +10675,15 @@ "node": ">=0.8" } }, + "node_modules/connect-pause": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", + "integrity": "sha1-smmyu4Ldsaw9tQmcD7WCq6mfs3o=", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -9530,7 +10776,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", - "dev": true, "dependencies": { "toggle-selection": "^1.0.6" } @@ -9581,6 +10826,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -10056,6 +11314,22 @@ "react": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dependencies": { + "node-fetch": "2.6.1" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -10123,6 +11397,14 @@ "node": ">=6.0.0" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -10239,6 +11521,167 @@ "node": ">=4.0.0" } }, + "node_modules/css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "dependencies": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/chalk/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "dependencies": { + "postcss": "^6.0.1" + } + }, + "node_modules/css-modules-loader-core/node_modules/postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "node_modules/css-modules-loader-core/node_modules/postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "node_modules/css-modules-loader-core/node_modules/postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "dependencies": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "node_modules/css-modules-loader-core/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-modules-loader-core/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/css-prefers-color-scheme": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", @@ -10273,6 +11716,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -10534,6 +11987,15 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -10957,6 +12419,11 @@ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/detect-port": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", @@ -11098,6 +12565,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -11254,6 +12730,67 @@ "react": ">=16.12.0" } }, + "node_modules/draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==", + "dependencies": { + "fbjs": "^2.0.0", + "immutable": "~3.7.4", + "object-assign": "^4.1.1" + }, + "peerDependencies": { + "react": ">=0.14.0", + "react-dom": ">=0.14.0" + } + }, + "node_modules/draft-js-export-markdown": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-export-markdown/-/draft-js-export-markdown-1.4.0.tgz", + "integrity": "sha512-blfAvlhGhjVlHNaZ5WJKlrXhcftnwwC5VC+Eu3ztOGpGLaOom4hxhBjbKEWjvbQZJ9zL+xo57ukm39prYZMG5Q==", + "dependencies": { + "draft-js-utils": "^1.4.0" + }, + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draft-js-import-element": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz", + "integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==", + "dependencies": { + "draft-js-utils": "^1.4.0", + "synthetic-dom": "^1.4.0" + }, + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draft-js-import-markdown": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-markdown/-/draft-js-import-markdown-1.4.0.tgz", + "integrity": "sha512-mpKUxzDM+x7W+eCZegCAxl3QJzNGA3Y+DbBMMekzCdPHONLJAZ1QYYjegbXa6+pZGq8FAIhgWaVbfKWMb8M8dQ==", + "dependencies": { + "draft-js-import-element": "^1.4.0", + "synthetic-dom": "^1.4.0" + }, + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, + "node_modules/draft-js-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.0.tgz", + "integrity": "sha512-8s9FFuKC+lOWGwJ0b3om2PF+uXrqQPaEQlPJI7UxdzxTYGMeKouMPA9+YlPn52zcAVElIZtd2tXj6eQmvlKelw==", + "peerDependencies": { + "draft-js": ">=0.10.0", + "immutable": "3.x.x" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -11509,6 +13046,19 @@ "stackframe": "^1.1.1" } }, + "node_modules/errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "dev": true, + "dependencies": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -12877,6 +14427,31 @@ "node": ">= 0.10.0" } }, + "node_modules/express-urlrewrite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz", + "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==", + "dev": true, + "dependencies": { + "debug": "*", + "path-to-regexp": "^1.0.3" + } + }, + "node_modules/express-urlrewrite/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/express-urlrewrite/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -13025,6 +14600,12 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13073,6 +14654,12 @@ "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -13113,6 +14700,34 @@ "bser": "2.1.1" } }, + "node_modules/fbjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz", + "integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==", + "dependencies": { + "core-js": "^3.6.4", + "cross-fetch": "^3.0.4", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, + "node_modules/fbjs/node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -13122,6 +14737,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", + "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -13270,6 +14907,26 @@ "node": ">=8" } }, + "node_modules/final-form": { + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.4.tgz", + "integrity": "sha512-hyoOVVilPLpkTvgi+FSJkFZrh0Yhy4BhE6lk/NiBwrF4aRV8/ykKEyXYvQH/pfUbRkOosvpESYouFb+FscsLrw==", + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, + "node_modules/final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-Gd+Bd2Ll7ijo3/sd6kJ/bwLkhc2bUJPxTON6fIqee/008EJpACWhT+zoWCm9q6NcfMcWRS+Sp5ikRX8iqdXeGQ==", + "peerDependencies": { + "final-form": ">=1.3.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -13389,8 +15046,7 @@ "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { "version": "4.1.0", @@ -13445,6 +15101,17 @@ "readable-stream": "^2.3.6" } }, + "node_modules/focus-lock": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz", + "integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/follow-redirects": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", @@ -13723,6 +15390,18 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13742,6 +15421,33 @@ "node": ">=0.10.0" } }, + "node_modules/framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0", + "react-dom": ">=16.8 || ^17.0.0" + } + }, + "node_modules/framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -13971,6 +15677,54 @@ "node": ">=0.10.0" } }, + "node_modules/generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha1-LXhqEhruUIh2eWk56OO/+DbCCRc=", + "dev": true, + "dependencies": { + "loader-utils": "^0.2.16" + } + }, + "node_modules/generic-names/node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/generic-names/node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/generic-names/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/generic-names/node_modules/loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14000,6 +15754,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -14339,9 +16101,9 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/graphql": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.0.tgz", - "integrity": "sha512-1jvUsS5mSzcgXLTQNQyrP7eKkBZW+HUnmx2LYSnfvkyseVpij8wwO/sFBGgxbkZ+zzFwYQxrHsOana5oMXmMxg==", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.2.tgz", + "integrity": "sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==", "dev": true, "engines": { "node": ">= 10.x" @@ -14401,6 +16163,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", @@ -14720,6 +16503,11 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -14743,7 +16531,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "dependencies": { "react-is": "^16.7.0" } @@ -14751,8 +16538,7 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/hoopy": { "version": "0.1.4", @@ -15176,6 +16962,12 @@ "node": ">=0.10.0" } }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, "node_modules/icss-utils": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", @@ -15239,6 +17031,14 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks=", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -15449,7 +17249,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -15963,6 +17762,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -16748,6 +18553,80 @@ "node": ">=10" } }, + "node_modules/jest-transform-css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jest-transform-css/-/jest-transform-css-3.0.0.tgz", + "integrity": "sha512-MR/mHg5GdVPjyWT2r+TWZoM7ryh+apo2cB0AcmLEw4yH37/Htjm2afMGOiUtXG2guLfsGabg8uYw1NTjVINj5Q==", + "dev": true, + "dependencies": { + "common-tags": "1.8.2", + "cosmiconfig": "7.0.1", + "cross-spawn": "7.0.3", + "postcss-load-config": "2.0.0", + "postcss-modules": "1.3.2", + "style-inject": "0.3.0" + }, + "peerDependencies": { + "postcss": "^7.0.2" + } + }, + "node_modules/jest-transform-css/node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-transform-css/node_modules/postcss-load-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/jest-transform-css/node_modules/postcss-load-config/node_modules/cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "dev": true, + "dependencies": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-transform-css/node_modules/postcss-load-config/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/jest-util": { "version": "27.3.1", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.3.1.tgz", @@ -16982,6 +18861,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -17296,11 +19181,72 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "dev": true, + "dependencies": { + "jju": "^1.1.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-server": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.0.tgz", + "integrity": "sha512-+e/nW0mf666j1yTK+5dRx7hgxq5wJTkc5QhTYa/cBfD6vLlQWHfB4l8XKPgzeO55A8Hqm38g44OtZ5SooXi6MQ==", + "dev": true, + "dependencies": { + "body-parser": "^1.19.0", + "chalk": "^4.1.2", + "compression": "^1.7.4", + "connect-pause": "^0.1.1", + "cors": "^2.8.5", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-urlrewrite": "^1.4.0", + "json-parse-helpfulerror": "^1.0.3", + "lodash": "^4.17.21", + "lodash-id": "^0.14.1", + "lowdb": "^1.0.0", + "method-override": "^3.0.0", + "morgan": "^1.10.0", + "nanoid": "^3.1.23", + "please-upgrade-node": "^3.2.0", + "pluralize": "^8.0.0", + "server-destroy": "^1.0.1", + "update-notifier": "^5.1.0", + "yargs": "^17.0.1" + }, + "bin": { + "json-server": "lib/cli/bin.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/json-server/node_modules/yargs": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -17541,11 +19487,26 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-id": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", + "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -17566,6 +19527,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "node_modules/lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -17642,6 +19608,31 @@ "loose-envify": "cli.js" } }, + "node_modules/lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lowdb/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -18065,6 +20056,36 @@ "node": ">= 8" } }, + "node_modules/method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "dev": true, + "dependencies": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/method-override/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/method-override/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -18437,6 +20458,46 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -18511,6 +20572,18 @@ "node": ">= 0.6" } }, + "node_modules/msw/node_modules/node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/msw/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -18520,6 +20593,12 @@ "node": ">= 0.8" } }, + "node_modules/msw/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, "node_modules/msw/node_modules/type-fest": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", @@ -18532,24 +20611,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/msw/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "node_modules/msw/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/msw/node_modules/yargs": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", - "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.0.tgz", + "integrity": "sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew==", "dev": true, "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" }, "engines": { "node": ">=12" } }, + "node_modules/msw/node_modules/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -18675,37 +20779,21 @@ } }, "node_modules/node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz", + "integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==", "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.2", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "node_modules/node-forge": { @@ -19802,6 +21890,24 @@ "node": ">=4" } }, + "node_modules/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, + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -19825,6 +21931,17 @@ "node": ">=10" } }, + "node_modules/popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "node_modules/portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -20657,6 +22774,19 @@ "node": ">=8" } }, + "node_modules/postcss-modules": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-1.3.2.tgz", + "integrity": "sha512-QujH5ZpPtr1fBWTKDa43Hx45gm7p19aEtHaAtkMCBZZiB/D5za2wXSMtAf94tDUZHF3F5KZcTXISUNqgEQRiDw==", + "dev": true, + "dependencies": { + "css-modules-loader-core": "^1.1.0", + "generic-names": "^1.0.3", + "lodash.camelcase": "^4.3.0", + "postcss": "^7.0.1", + "string-hash": "^1.1.1" + } + }, "node_modules/postcss-modules-extract-imports": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", @@ -21840,6 +23970,17 @@ "node": ">=10" } }, + "node_modules/react-clientside-effect": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz", + "integrity": "sha512-2bL8qFW1TGBHozGGbVeyvnggRpMjibeZM2536AKNENLECutp2yfs44IL8Hmpn8qjFQ2K7A9PnYf3vc7aQq/cPA==", + "dependencies": { + "@babel/runtime": "^7.12.13" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/react-colorful": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.5.0.tgz", @@ -22185,9 +24326,9 @@ } }, "node_modules/react-error-boundary": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz", - "integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5" @@ -22208,8 +24349,39 @@ "node_modules/react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", - "dev": true + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "node_modules/react-final-form": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz", + "integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + }, + "peerDependencies": { + "final-form": "4.20.4", + "react": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/react-focus-lock": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.2.tgz", + "integrity": "sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^0.9.1", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.5", + "use-callback-ref": "^1.2.5", + "use-sidecar": "^1.0.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } }, "node_modules/react-helmet-async": { "version": "1.1.2", @@ -22298,6 +24470,61 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.1.tgz", + "integrity": "sha512-K7XZySEzOHMTq7dDwcHsZA6Y7/1uX5RsWhRXVYv8rdh+y9Qz2nMwl9RX/Mwnj/j7JstCGmxyfyC0zbVGXYh3mA==", + "dependencies": { + "react-remove-scroll-bar": "^2.1.0", + "react-style-singleton": "^2.1.0", + "tslib": "^1.0.0", + "use-callback-ref": "^1.2.3", + "use-sidecar": "^1.0.1" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.2.0.tgz", + "integrity": "sha512-UU9ZBP1wdMR8qoUs7owiVcpaPwsQxUDC2lypP6mmixaGlARZa7ZIBx1jcuObLdhMOvCsnZcvetOho0wzPa9PYg==", + "dependencies": { + "react-style-singleton": "^2.1.0", + "tslib": "^1.0.0" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/react-remove-scroll/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/react-scripts": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz", @@ -24163,6 +26390,33 @@ "throttle-debounce": "^3.0.1" } }, + "node_modules/react-style-singleton": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", + "integrity": "sha512-jNRp07Jza6CBqdRKNgGhT3u9umWvils1xsuMOjZlghBDH2MU0PL2WZor4PGYjXpnRCa9DQSlHMs/xnABWOwYbA==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^1.0.0" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/react-syntax-highlighter": { "version": "13.5.3", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz", @@ -24196,6 +26450,21 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -25690,6 +27959,12 @@ "semver": "bin/semver.js" } }, + "node_modules/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 + }, "node_modules/semver-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", @@ -25858,6 +28133,12 @@ "node": ">= 0.8.0" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=", + "dev": true + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -26588,6 +28869,15 @@ "node": ">= 0.6" } }, + "node_modules/steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.3" + } + }, "node_modules/store2": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/store2/-/store2-2.12.0.tgz", @@ -26688,6 +28978,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -26884,6 +29180,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, "node_modules/style-loader": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", @@ -26918,6 +29220,15 @@ "inline-style-parser": "0.1.1" } }, + "node_modules/style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/stylehacks": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", @@ -27145,6 +29456,11 @@ "node": ">=6.0.0" } }, + "node_modules/stylis": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz", + "integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==" + }, "node_modules/sugarss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", @@ -27355,6 +29671,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synthetic-dom": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz", + "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==" + }, "node_modules/table": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", @@ -27798,6 +30119,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "node_modules/tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -27891,8 +30217,7 @@ "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", - "dev": true + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, "node_modules/toidentifier": { "version": "1.0.0", @@ -28180,6 +30505,24 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/uid-number": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", @@ -28729,6 +31072,23 @@ "node": ">=0.10.0" } }, + "node_modules/use-callback-ref": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-composed-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz", @@ -28772,6 +31132,26 @@ } } }, + "node_modules/use-sidecar": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz", + "integrity": "sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/use-sidecar/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -28963,7 +31343,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -29273,6 +31652,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -32121,6 +34509,618 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, + "@chakra-ui/accordion": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-1.4.1.tgz", + "integrity": "sha512-/E0FW5YHNVD6WwMGiuQuXpA70P2CKAV+MzcMITnSGPWsh9XD0mcXvMkIALVojfFk9tcCFdIGnxX/HWr41LzgIg==", + "requires": { + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/alert": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-1.3.1.tgz", + "integrity": "sha512-BeR6l/1CLZarA3uAe+5Q3hioYf7SixYfy9rOte/29ck1lx9PLjjuPYYmuDPtZNbGibhUCh48z4U/uK2x8mbpKQ==", + "requires": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/anatomy": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-1.2.1.tgz", + "integrity": "sha512-kNS+FiEDTSnwpQUW4dEjZ5745xhkvB0XtmqjY1wpclUSpFfptLZM9QIHPTnBt2bzM9R+idmRRP+WkTt6kyTrLw==", + "requires": { + "@chakra-ui/theme-tools": "^1.3.1" + } + }, + "@chakra-ui/avatar": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-1.3.1.tgz", + "integrity": "sha512-WI0/kcpTJViOH093V0bz8EB+e/rc+gjF+T5DkOuh1YWFxRRG5v+4Yd3PdEJtQgzWtBVhlbGWmE7WvBizyKwFCA==", + "requires": { + "@chakra-ui/image": "1.1.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/breadcrumb": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-1.3.1.tgz", + "integrity": "sha512-b1IoBmtr5FcP2fn5NRbdOdQo2c866OQ/WhcTcZ6UKae1jjik+36/qWE+X+RKzxC6FLfqo5qayV5zSgsnZym7Pg==", + "requires": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/button": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-1.5.1.tgz", + "integrity": "sha512-BvP29quEhP6OTgDiRsugD6adgkeOTEQpoDsZUVEmHnNVrbFfdsICEKKQTtDJ2iPf+hmpFrtnpN50vCLdAANKcw==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/spinner": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/checkbox": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-1.6.1.tgz", + "integrity": "sha512-Z5ZMeUYIRjRbi/knhYhSQshZH7OnROA7ezl9a9oVSKRF7iLMNMibQSlQLXmqUWaTKSgrS37cpKAzfgEuemyiUQ==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/clickable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-1.2.1.tgz", + "integrity": "sha512-B0CIbKzDMwzG1APeTpW9H2Jl8dkarI1Qstb3hDOy23O+N5TU6lpDdVnXQ7fpFJS6mu5JjFqtkwzGAVZnkkv1rw==", + "requires": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/close-button": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-1.2.1.tgz", + "integrity": "sha512-A/cuFtJPF8rp5p6tCIGlQdHB89gLCSOzxWssoTXAGJnmlwY2YunFHxgkYZXwPbDqFrM8ndya7Ys+AuL1JZsa3g==", + "requires": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/color-mode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-1.3.2.tgz", + "integrity": "sha512-/rWcbrzbaWCyyUnT07Qjz0xf/ltHS31CHOKtVCWr2uTgfn2gOQpdxsKRbjrLYPOYZGTMdINUHNiAsqQjLoAoTQ==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/control-box": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-1.1.1.tgz", + "integrity": "sha512-ZFbh85pzzZoiSjGnvLUzMB5BoA8Xm6TBMWvMtzLY5xiFGb9/mBeRDH2KFjr1GJzoqleWKkQwvFD6JM0kXcekpg==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/counter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-1.2.1.tgz", + "integrity": "sha512-Gm4njMzEsDyAzdQtExn40TvmupzkPBrT5DiCu0DlxYqpLqCfqV49HgJHEG5oW3WV+WaC9mzg7VV+idKYh/d+Gg==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/css-reset": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", + "requires": {} + }, + "@chakra-ui/descendant": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-2.1.1.tgz", + "integrity": "sha512-JasdVaN4MjL7QFo1vMnADy6EtFAlPKT1kTJ1LwMtl9AaF9VFLBsfGxm0L+WQK+3NJMuCSDBXWJB8mV4AQ11Edg==", + "requires": { + "@chakra-ui/react-utils": "^1.2.1" + } + }, + "@chakra-ui/editable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-1.3.1.tgz", + "integrity": "sha512-MwyTtsnHNqmKmHv9SH3KIHWa06D4gBwcuTawTiSnYBUJL6My8ry/Wdca1to9So2tD6hcjz3TPTzOJOlyv0eiZg==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/focus-lock": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-1.2.1.tgz", + "integrity": "sha512-HYu39nvfaXUrBx+dIDJkFgebNCGEi9oZTfLUKzIJC+zPkmReTDSXV0dzSb/8vCAOq5fph1gFKsdbGy2U98P8GQ==", + "requires": { + "@chakra-ui/utils": "1.9.1", + "react-focus-lock": "2.5.2" + } + }, + "@chakra-ui/form-control": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-1.5.1.tgz", + "integrity": "sha512-ASZYQFOs5mAoaNXAN/ZaesMy3XV07F0/Eba5PQ7Dejdn91aep6lqF889hmr8yqcR646xCOY7ISyYsskfh9QHrQ==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/hooks": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-1.7.1.tgz", + "integrity": "sha512-hgN19X6GUKQYAHczmFY+GAT8vl9h+X+nGWrIAnmvZ6BgUXxDajnTNhZeWhj0ZkR+7A7dCE6Y/3X44GafUgChMw==", + "requires": { + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "compute-scroll-into-view": "1.0.14", + "copy-to-clipboard": "3.3.1" + }, + "dependencies": { + "compute-scroll-into-view": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz", + "integrity": "sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ==" + } + } + }, + "@chakra-ui/icon": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-1.2.1.tgz", + "integrity": "sha512-uZxFsiY4Tld+LvGIX7cky0H6oMRac8udPMQRzIk/UQeNZcsWisGetatbQsew3y1lWV/iH/8+TlDuW13GWGyGGQ==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/image": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-1.1.1.tgz", + "integrity": "sha512-bz1pn08XlXcO3r1KnpdjQgN3R2soiTx10sG2d5Pw9BdGdySf7Y73wiLh+Tan1xJHp6p2KH1hz4f7uKXXDn7Qmw==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-1.3.1.tgz", + "integrity": "sha512-Z+LqkwVPMeUBuvB9dLDPKkBnWV52Q1PVl3KW9ouDIFg7SoemeYkBt3p4ttEKE+eIPsPlrcH1u2A/RGcCTZOe1g==", + "requires": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/layout": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-1.5.1.tgz", + "integrity": "sha512-nKiyZ5adjNTbBV3oFIUGIPijwutO1NGdev1jHtnZc3xo2urCIkBvKU8+mVjlX04IwZ7oLKoP3EiDDv0g7+o41Q==", + "requires": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/live-region": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-1.1.1.tgz", + "integrity": "sha512-BSdI5gLIffNRETEp6W18kBNg9tL0ZLLzfWGRnuO9tEbox7NrcgqIeLF8mNKwhDOZz88NKHtUOPVzjAUKW1SryQ==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/media-query": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-1.2.1.tgz", + "integrity": "sha512-Ho/qiPGTjNukFTE9WBdYV9FIXU7KFTJPqdRQPWANkz+j275n6sqSE1j5LRJllP+ett21KeuWLN4zL33pP0Ox+g==", + "requires": { + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/menu": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-1.8.1.tgz", + "integrity": "sha512-fgzzFukBj4sQzTRf4q/+nHiVTKhrMtJdofnluqce/SCRJ1G+bbovUySblTzfI8iFlTSZt/eWc/Nju4JB1S+3Yg==", + "requires": { + "@chakra-ui/clickable": "1.2.1", + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/modal": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-1.10.1.tgz", + "integrity": "sha512-cboC2ITm+5FjhrBc6yJ5cW4VXnfwlLhFa1EkPqF1k4kvYGyUHArvPN1q8AiPYOIrupHYu2Iu6YmQPg7TJwNImg==", + "requires": { + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/focus-lock": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.4.1" + } + }, + "@chakra-ui/number-input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-1.3.1.tgz", + "integrity": "sha512-4vBRSShT5pedElgP9YGVC+9RHzQGmUVZqu3p0gZW0fLGVVQ9C1EGrO7djL+k3tgklyu8RvSwkRDJqEPvbQKDgQ==", + "requires": { + "@chakra-ui/counter": "1.2.1", + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/pin-input": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-1.7.1.tgz", + "integrity": "sha512-eFFc5sofiyion+NxELWfCzD23XHIBDrJcfKKbNxt8jdXg9Ek4mFpmvnxBVrK0DIz6cVYgKY8c364OmxNUf4IyA==", + "requires": { + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/popover": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-1.10.1.tgz", + "integrity": "sha512-/dMUQfd+h9j3GBtkA/nYaQ5xeu4vk0psUClFvLOAJRwXGN3aMrzn/mhrvHWQ/cJuwQrO1WzxH2+g6pwsFOm9ng==", + "requires": { + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/popper": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-2.4.1.tgz", + "integrity": "sha512-cuwnwXx6RUXZGGynVOGG8fEIiMNBXUCy3UqWQD1eEd8200eWQobgNk4Z0YwzKuSzJwp0Auy+j5iKefi5FSkyog==", + "requires": { + "@chakra-ui/react-utils": "1.2.1", + "@popperjs/core": "^2.9.3" + } + }, + "@chakra-ui/portal": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-1.3.1.tgz", + "integrity": "sha512-6UOGZCfujgdijcPs/JTEY5IB5WtKvUbfrSQYsG5CDa+guIwvnoP5qZ+rH6BR6DSSM8Wr/1n+WrtanhfFZShHKA==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/progress": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-1.2.1.tgz", + "integrity": "sha512-213nN8nbODvD/A23vAtg+r3bRKKatWQHafgmLzeznUcxa/+ac0eVurIS8XSYLRkY4EXQ505re3ZkLhDd98a7QA==", + "requires": { + "@chakra-ui/theme-tools": "1.3.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/provider": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-1.7.2.tgz", + "integrity": "sha512-5Tk7K6aY2gWQQn62MOmeOtRdDJjmtHau2klxhKDlEIPs/smrdBF/ymK7eI5pJiU/BeKHAbM6DbvelR2khUZP+w==", + "requires": { + "@chakra-ui/css-reset": "1.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/radio": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-1.4.2.tgz", + "integrity": "sha512-QWIZAy/PLHNnlI6Z6o3kAdUzkSMpaJ9kOlMIFcsen6tAfPob3R+cyMQFiaX6RBw7yHWYIrfD9gaAqwd+FihQWQ==", + "requires": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/react": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-1.7.2.tgz", + "integrity": "sha512-2RxJHqYLSSB/dQQ9M4APxMopPfsU89A++63v8U8jiR7J8sUaa6Ded9ypTiIUY09J8v7mJAus4/ZSJxG7Zfd7aA==", + "requires": { + "@chakra-ui/accordion": "1.4.1", + "@chakra-ui/alert": "1.3.1", + "@chakra-ui/avatar": "1.3.1", + "@chakra-ui/breadcrumb": "1.3.1", + "@chakra-ui/button": "1.5.1", + "@chakra-ui/checkbox": "1.6.1", + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/control-box": "1.1.1", + "@chakra-ui/counter": "1.2.1", + "@chakra-ui/css-reset": "1.1.1", + "@chakra-ui/editable": "1.3.1", + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/image": "1.1.1", + "@chakra-ui/input": "1.3.1", + "@chakra-ui/layout": "1.5.1", + "@chakra-ui/live-region": "1.1.1", + "@chakra-ui/media-query": "1.2.1", + "@chakra-ui/menu": "1.8.1", + "@chakra-ui/modal": "1.10.1", + "@chakra-ui/number-input": "1.3.1", + "@chakra-ui/pin-input": "1.7.1", + "@chakra-ui/popover": "1.10.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/progress": "1.2.1", + "@chakra-ui/provider": "1.7.2", + "@chakra-ui/radio": "1.4.2", + "@chakra-ui/react-env": "1.1.1", + "@chakra-ui/select": "1.2.1", + "@chakra-ui/skeleton": "1.2.2", + "@chakra-ui/slider": "1.5.2", + "@chakra-ui/spinner": "1.2.1", + "@chakra-ui/stat": "1.2.1", + "@chakra-ui/switch": "1.3.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/table": "1.3.1", + "@chakra-ui/tabs": "1.6.1", + "@chakra-ui/tag": "1.2.1", + "@chakra-ui/textarea": "1.2.1", + "@chakra-ui/theme": "1.12.1", + "@chakra-ui/toast": "1.4.1", + "@chakra-ui/tooltip": "1.4.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/react-env": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-1.1.1.tgz", + "integrity": "sha512-Lgmb0y4kv0ffsGMelAOaYOd4tYZAv4FYWgV86ckGMjmYQWA8drv4v/lHTNltixxWMmBEpjcHALpJuS6yAZYHug==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/react-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-1.2.1.tgz", + "integrity": "sha512-bV8FRaXiOgGxOg03iTNin/B02I+tHH9PQtqUTl3U7cJaoI+5AUYhrqXvl1Ya2/R7zxSFrb/gBVDTgbZiVkJ+Dg==", + "requires": { + "@chakra-ui/utils": "^1.9.1" + } + }, + "@chakra-ui/select": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-1.2.1.tgz", + "integrity": "sha512-GqRmYGjVnw/Z/2RQiW7Ywuu9O5E0spmMUBjeE/v0rqjixBqrmdApjg5pmJ4YmUMvUI/WkGtR3FR5W9Y5PpvfKw==", + "requires": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/skeleton": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-1.2.2.tgz", + "integrity": "sha512-kA3DmhaazVX31iIOY87Jskqj+TFd+FKhOCWjPgZiFeEtJDYriU9BfzYSo2j6ePLtMRMc6Z99vXIHmjoOIEy5xQ==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/media-query": "1.2.1", + "@chakra-ui/system": "1.8.2", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/slider": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-1.5.2.tgz", + "integrity": "sha512-zP07TMew61GkJe47Nu7zEg/SUEwPHpN4alW6VUM6Y8UaVpQaDx7InarbWTc/bXdTP03SfE+hQ6WD9Oy7noe4hQ==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/spinner": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-1.2.1.tgz", + "integrity": "sha512-CQsUJNJWWSot1ku5Se41Nz1dXIDhk+/7FIhTbfRHSjtYZnAab3CPMHBkTGqwbJxQ9oHYgk9Rso3cfG+/ra6aTQ==", + "requires": { + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/stat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-1.2.1.tgz", + "integrity": "sha512-BTZFeh/8VdgUX080taCQj1g/rS4wGc+y3GQnklqlZ9N/bEv0gyLqQga7TFC/NkVl3cvjRiMnCCPj6vRih9x+Og==", + "requires": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/styled-system": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-1.14.1.tgz", + "integrity": "sha512-dgXFYZdJicsddUnPV1X7lQksgMD0z5EvwGaIh2JHJERqNRIvth/CBAnVLQQvy/xSJK5YaSEmeuVVU0veUOQcXg==", + "requires": { + "@chakra-ui/utils": "1.9.1", + "csstype": "^3.0.9" + } + }, + "@chakra-ui/switch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-1.3.1.tgz", + "integrity": "sha512-92hXJ2/ozj7B3cJNT259mFNoad7Ck892uHTuEQ/GIdXb25doE6F1wCp0TreOnGiEgU5YSaxpdrcZjA0QODP//w==", + "requires": { + "@chakra-ui/checkbox": "1.6.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/system": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-1.8.2.tgz", + "integrity": "sha512-k/43bv5exPGc6hBVcq5dUdGC+0pmQthJP/VqcTUt0oIc3oyoVSAnJhpmBkFHXYNqj6yYd2K7qdD2PjjbZ5KCLA==", + "requires": { + "@chakra-ui/color-mode": "1.3.2", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/styled-system": "1.14.1", + "@chakra-ui/utils": "1.9.1", + "react-fast-compare": "3.2.0" + } + }, + "@chakra-ui/table": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-1.3.1.tgz", + "integrity": "sha512-+ia/7zs7AGj01lon301EEx+mK4918yGc0K6e68Kxomex8tnxkwbskFWs6hX+6Kzbj56ZBm99eLlKpo2iGYX0HA==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/tabs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-1.6.1.tgz", + "integrity": "sha512-p7HdHcleJWNwteWYVPt2KF52YbS5pIIfs/IpgtnYZRsJbqvRVxSwgg5Wsn+vuxFXBKW0cA2rDGbyzsZ+ChtEXQ==", + "requires": { + "@chakra-ui/clickable": "1.2.1", + "@chakra-ui/descendant": "2.1.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/tag": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-1.2.1.tgz", + "integrity": "sha512-O068n+qBc+CSyvpRBJ6Lwep6SydQ9UysRqw1ETF+4fJSp9dMrBp8vOcl2SVacKaCu13qdv8UdRMBxUiTz3lh7A==", + "requires": { + "@chakra-ui/icon": "1.2.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/textarea": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-1.2.1.tgz", + "integrity": "sha512-3xDsL1qQ+eY5r4GcRL4bg90vtV/xxVlw0Z3PFehFP5JW7VwXNZIRjauR/+HlOA8eYq0cF6ch2boR1GPso6rQtw==", + "requires": { + "@chakra-ui/form-control": "1.5.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/theme": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-1.12.1.tgz", + "integrity": "sha512-8yDril3rSzv42eKR0x7KdnrpN1ubY0m6q37CVUADgtboJqoJwWWX2/hqkv8CX6WJf8ZwPwFL5QIwS2FPSGgi+g==", + "requires": { + "@chakra-ui/anatomy": "1.2.1", + "@chakra-ui/theme-tools": "1.3.1", + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/theme-tools": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-1.3.1.tgz", + "integrity": "sha512-D8arJ5uFGuYZrrFGpXqgov8FhsJYWRyar5oBZY5TJR9gsVYBlJ8Ai91pwM/NflCFqzerTOgyt7bNSGQMdZ8ghA==", + "requires": { + "@chakra-ui/utils": "1.9.1", + "@ctrl/tinycolor": "^3.4.0" + } + }, + "@chakra-ui/toast": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-1.4.1.tgz", + "integrity": "sha512-vzQkYwnGq2nx0bOKIQ6XpJaGzUwnWKmUjcVrz9NzGwVI4g93PS7+13515R0m1NrDp30132OeDXQ+tmQwCRRe6w==", + "requires": { + "@chakra-ui/alert": "1.3.1", + "@chakra-ui/close-button": "1.2.1", + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/theme": "1.12.1", + "@chakra-ui/transition": "1.4.1", + "@chakra-ui/utils": "1.9.1", + "@reach/alert": "0.13.2" + } + }, + "@chakra-ui/tooltip": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-1.4.1.tgz", + "integrity": "sha512-KvTuqSqIpIgE+YNUwN7ONDRkSGR6SK9+dgSx2PfKy0Sel7UgDPVtxByuZ6tfJ9O1VTRYEdF9k+s6Gf8eRFQbNA==", + "requires": { + "@chakra-ui/hooks": "1.7.1", + "@chakra-ui/popper": "2.4.1", + "@chakra-ui/portal": "1.3.1", + "@chakra-ui/react-utils": "1.2.1", + "@chakra-ui/utils": "1.9.1", + "@chakra-ui/visually-hidden": "1.1.1" + } + }, + "@chakra-ui/transition": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-1.4.1.tgz", + "integrity": "sha512-s/VFucc6grNdP1bxw0oQLzy167gjAgyl/GiGH9nt54nioDEiSsvn70qKg7sjajNTvpoot+urQUdr4Qh+fIUFZQ==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, + "@chakra-ui/utils": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-1.9.1.tgz", + "integrity": "sha512-Tue8JfpzOqeHd8vSqAnX1l/Y3Gg456+BXFP/TH6mCIeqMAMbrvv25vDskds0wlXRjMYdmpqHxCEzkalFrscGHA==", + "requires": { + "@types/lodash.mergewith": "4.6.6", + "css-box-model": "1.2.1", + "framesync": "5.3.0", + "lodash.mergewith": "4.6.2" + } + }, + "@chakra-ui/visually-hidden": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-1.1.1.tgz", + "integrity": "sha512-AGK9YBQS2FW/1e5tfivS8VVXn8y2uTyJ9ACOnGiLm9FNdth9pR0fGil9axlcmhZpEYcSRlnCuma3nkqaCjJnAA==", + "requires": { + "@chakra-ui/utils": "1.9.1" + } + }, "@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -32140,12 +35140,78 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@ctrl/tinycolor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", + "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" + }, "@discoveryjs/json-ext": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", "dev": true }, + "@emotion/babel-plugin": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz", + "integrity": "sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==", + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/runtime": "^7.13.10", + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.5", + "@emotion/serialize": "^1.0.2", + "babel-plugin-macros": "^2.6.1", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "^4.0.3" + }, + "dependencies": { + "@babel/plugin-syntax-jsx": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz", + "integrity": "sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@emotion/memoize": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz", + "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" + }, + "@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -32186,14 +35252,13 @@ "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "dev": true + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, "@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "dev": true, + "devOptional": true, "requires": { "@emotion/memoize": "0.7.4" } @@ -32201,8 +35266,57 @@ "@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "dev": true + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/react": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.7.0.tgz", + "integrity": "sha512-WL93hf9+/2s3cA1JVJlz8+Uy6p6QWukqQFOm2OZO5ki51hfucHMOmbSjiyC3t2Y4RI8XUmBoepoc/24ny/VBbA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@emotion/cache": "^11.6.0", + "@emotion/serialize": "^1.0.2", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + }, + "dependencies": { + "@emotion/cache": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.6.0.tgz", + "integrity": "sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.1.0", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.10" + } + }, + "@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", + "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + } + } }, "@emotion/serialize": { "version": "0.11.16", @@ -32232,19 +35346,48 @@ "dev": true }, "@emotion/styled": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", - "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", - "dev": true, + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.6.0.tgz", + "integrity": "sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw==", "requires": { - "@emotion/styled-base": "^10.0.27", - "babel-plugin-emotion": "^10.0.27" + "@babel/runtime": "^7.13.10", + "@emotion/babel-plugin": "^11.3.0", + "@emotion/is-prop-valid": "^1.1.1", + "@emotion/serialize": "^1.0.2", + "@emotion/utils": "^1.0.0" + }, + "dependencies": { + "@emotion/is-prop-valid": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz", + "integrity": "sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw==", + "requires": { + "@emotion/memoize": "^0.7.4" + } + }, + "@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + } + }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + } } }, "@emotion/styled-base": { - "version": "10.0.31", - "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.0.31.tgz", - "integrity": "sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.3.0.tgz", + "integrity": "sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==", "dev": true, "requires": { "@babel/runtime": "^7.5.5", @@ -32262,8 +35405,7 @@ "@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "dev": true + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "@emotion/utils": { "version": "0.11.3", @@ -32274,8 +35416,7 @@ "@emotion/weak-memoize": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==", - "dev": true + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "@eslint/eslintrc": { "version": "0.4.3", @@ -32384,6 +35525,17 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" }, + "@jackfranklin/test-data-bot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jackfranklin/test-data-bot/-/test-data-bot-1.3.0.tgz", + "integrity": "sha512-68OfPjgHT58ftfd0XKVWOp0p/1K+bbxSPu3wdH9N+/Ox9a3aM/d8sS0AFSJ48Vuoww+MkbHDH1cEE5tTkpTPlw==", + "dev": true, + "requires": { + "@types/faker": "^4.1.9", + "faker": "4.1.0", + "lodash": "^4.17.15" + } + }, "@jest/console": { "version": "27.3.1", "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.3.1.tgz", @@ -32774,8 +35926,18 @@ "@popperjs/core": { "version": "2.10.2", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", - "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", - "dev": true + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" + }, + "@reach/alert": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/alert/-/alert-0.13.2.tgz", + "integrity": "sha512-LDz83AXCrClyq/MWe+0vaZfHp1Ytqn+kgL5VxG7rirUvmluWaj/snxzfNPWn0Ma4K2YENmXXRC/iHt5X95SqIg==", + "requires": { + "@reach/utils": "0.13.2", + "@reach/visually-hidden": "0.13.2", + "prop-types": "^15.7.2", + "tslib": "^2.1.0" + } }, "@reach/router": { "version": "1.3.4", @@ -32789,6 +35951,25 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@reach/utils": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.13.2.tgz", + "integrity": "sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==", + "requires": { + "@types/warning": "^3.0.0", + "tslib": "^2.1.0", + "warning": "^4.0.3" + } + }, + "@reach/visually-hidden": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.13.2.tgz", + "integrity": "sha512-sPZwNS0/duOuG0mYwE5DmgEAzW9VhgU3aIt1+mrfT/xiT9Cdncqke+kRBQgU708q/Ttm9tWsoHni03nn/SuPTQ==", + "requires": { + "prop-types": "^15.7.2", + "tslib": "^2.1.0" + } + }, "@rollup/plugin-node-resolve": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz", @@ -34101,6 +37282,37 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.27.tgz", "integrity": "sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw==", "dev": true + }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, @@ -34205,6 +37417,15 @@ "p-locate": "^5.0.0" } }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -34222,6 +37443,28 @@ "requires": { "p-limit": "^3.0.2" } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, @@ -34414,6 +37657,18 @@ "polished": "^4.0.5", "resolve-from": "^5.0.0", "ts-dedent": "^2.0.0" + }, + "dependencies": { + "@emotion/styled": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.3.0.tgz", + "integrity": "sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==", + "dev": true, + "requires": { + "@emotion/styled-base": "^10.3.0", + "babel-plugin-emotion": "^10.0.27" + } + } } }, "@storybook/ui": { @@ -34841,6 +38096,16 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", "dev": true }, + "@types/draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-NeCRIPcfrlUItA46boNG2kxuVhohPdE4Plp3GcxEv11FXZ0kZFa6X8fNPpKrp8AEUqeAr/B0TsX/wp2sYZaK3w==", + "dev": true, + "requires": { + "@types/react": "*", + "immutable": "~3.7.4" + } + }, "@types/eslint": { "version": "7.28.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz", @@ -34855,6 +38120,21 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==" }, + "@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==", + "dev": true + }, + "@types/final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-2DcNjeyiv+MJo2SjYt8o0yrvnZ15sxkLBzoIeMq9Rt1tN+prMHixHrRLAqcbSbx7+RapJVszqH8p/RLnm4U2Tw==", + "dev": true, + "requires": { + "final-form": "4.x.x" + } + }, "@types/glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", @@ -34954,6 +38234,19 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" + }, + "@types/lodash.mergewith": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz", + "integrity": "sha512-RY/8IaVENjG19rxTZu9Nukqh0W2UrYgmBj5sdns4hWRZaV8PqR7wIKHFKzvOTjo4zVRV7sVI+yFhAJql12Kfqg==", + "requires": { + "@types/lodash": "*" + } + }, "@types/markdown-to-jsx": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz", @@ -35113,6 +38406,15 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", + "integrity": "sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -35181,6 +38483,11 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + }, "@types/webpack": { "version": "4.41.31", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.31.tgz", @@ -35738,6 +39045,21 @@ "sprintf-js": "~1.0.2" } }, + "aria-hidden": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.1.3.tgz", + "integrity": "sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==", + "requires": { + "tslib": "^1.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "aria-query": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", @@ -36591,6 +39913,15 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -37692,9 +41023,9 @@ "dev": true }, "common-tags": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", - "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" }, "commondir": { "version": "1.0.1", @@ -37802,6 +41133,12 @@ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" }, + "connect-pause": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz", + "integrity": "sha1-smmyu4Ldsaw9tQmcD7WCq6mfs3o=", + "dev": true + }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -37881,7 +41218,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", - "dev": true, "requires": { "toggle-selection": "^1.0.6" } @@ -37917,6 +41253,16 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", @@ -38299,6 +41645,21 @@ "warning": "^4.0.3" } }, + "cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "requires": { + "node-fetch": "2.6.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -38362,6 +41723,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -38445,6 +41814,141 @@ } } }, + "css-modules-loader-core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", + "integrity": "sha1-WQhmgpShvs0mGuCkziGwtVHyHRY=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.1", + "postcss-modules-extract-imports": "1.1.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "postcss": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.1.tgz", + "integrity": "sha1-AA29H47vIXqjaLmiEsX8QLKo8/I=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + } + }, + "postcss-modules-extract-imports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", + "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, "css-prefers-color-scheme": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", @@ -38470,6 +41974,16 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -38674,6 +42188,12 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==" }, + "data-uri-to-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", + "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==", + "dev": true + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -39000,6 +42520,11 @@ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "detect-port": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz", @@ -39124,6 +42649,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -39242,6 +42776,48 @@ "tslib": "^2.3.0" } }, + "draft-js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/draft-js/-/draft-js-0.11.7.tgz", + "integrity": "sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg==", + "requires": { + "fbjs": "^2.0.0", + "immutable": "~3.7.4", + "object-assign": "^4.1.1" + } + }, + "draft-js-export-markdown": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-export-markdown/-/draft-js-export-markdown-1.4.0.tgz", + "integrity": "sha512-blfAvlhGhjVlHNaZ5WJKlrXhcftnwwC5VC+Eu3ztOGpGLaOom4hxhBjbKEWjvbQZJ9zL+xo57ukm39prYZMG5Q==", + "requires": { + "draft-js-utils": "^1.4.0" + } + }, + "draft-js-import-element": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-element/-/draft-js-import-element-1.4.0.tgz", + "integrity": "sha512-WmYT5PrCm47lGL5FkH6sRO3TTAcn7qNHsD3igiPqLG/RXrqyKrqN4+wBgbcT2lhna/yfWTRtgzAbQsSJoS1Meg==", + "requires": { + "draft-js-utils": "^1.4.0", + "synthetic-dom": "^1.4.0" + } + }, + "draft-js-import-markdown": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-import-markdown/-/draft-js-import-markdown-1.4.0.tgz", + "integrity": "sha512-mpKUxzDM+x7W+eCZegCAxl3QJzNGA3Y+DbBMMekzCdPHONLJAZ1QYYjegbXa6+pZGq8FAIhgWaVbfKWMb8M8dQ==", + "requires": { + "draft-js-import-element": "^1.4.0", + "synthetic-dom": "^1.4.0" + } + }, + "draft-js-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/draft-js-utils/-/draft-js-utils-1.4.0.tgz", + "integrity": "sha512-8s9FFuKC+lOWGwJ0b3om2PF+uXrqQPaEQlPJI7UxdzxTYGMeKouMPA9+YlPn52zcAVElIZtd2tXj6eQmvlKelw==", + "requires": {} + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -39461,6 +43037,16 @@ "stackframe": "^1.1.1" } }, + "errorhandler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz", + "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + } + }, "es-abstract": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", @@ -40462,6 +44048,33 @@ } } }, + "express-urlrewrite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz", + "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==", + "dev": true, + "requires": { + "debug": "*", + "path-to-regexp": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "ext": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", @@ -40581,6 +44194,12 @@ } } }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -40626,6 +44245,12 @@ "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -40659,6 +44284,36 @@ "bser": "2.1.1" } }, + "fbjs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-2.0.0.tgz", + "integrity": "sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ==", + "requires": { + "core-js": "^3.6.4", + "cross-fetch": "^3.0.4", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + } + } + }, + "fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==" + }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -40668,6 +44323,15 @@ "pend": "~1.2.0" } }, + "fetch-blob": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", + "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", + "dev": true, + "requires": { + "web-streams-polyfill": "^3.0.3" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -40784,6 +44448,20 @@ "to-regex-range": "^5.0.1" } }, + "final-form": { + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.4.tgz", + "integrity": "sha512-hyoOVVilPLpkTvgi+FSJkFZrh0Yhy4BhE6lk/NiBwrF4aRV8/ykKEyXYvQH/pfUbRkOosvpESYouFb+FscsLrw==", + "requires": { + "@babel/runtime": "^7.10.0" + } + }, + "final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-Gd+Bd2Ll7ijo3/sd6kJ/bwLkhc2bUJPxTON6fIqee/008EJpACWhT+zoWCm9q6NcfMcWRS+Sp5ikRX8iqdXeGQ==", + "requires": {} + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -40880,8 +44558,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "4.1.0", @@ -40926,6 +44603,14 @@ "readable-stream": "^2.3.6" } }, + "focus-lock": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-0.9.2.tgz", + "integrity": "sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==", + "requires": { + "tslib": "^2.0.3" + } + }, "follow-redirects": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", @@ -41137,6 +44822,15 @@ "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", "dev": true }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "requires": { + "fetch-blob": "^3.1.2" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -41150,6 +44844,27 @@ "map-cache": "^0.2.2" } }, + "framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, + "framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "requires": { + "tslib": "^2.1.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -41336,6 +45051,47 @@ } } }, + "generic-names": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-1.0.3.tgz", + "integrity": "sha1-LXhqEhruUIh2eWk56OO/+DbCCRc=", + "dev": true, + "requires": { + "loader-utils": "^0.2.16" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "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": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -41356,6 +45112,11 @@ "has-symbols": "^1.0.1" } }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, "get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -41605,9 +45366,9 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "graphql": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.0.tgz", - "integrity": "sha512-1jvUsS5mSzcgXLTQNQyrP7eKkBZW+HUnmx2LYSnfvkyseVpij8wwO/sFBGgxbkZ+zzFwYQxrHsOana5oMXmMxg==", + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.7.2.tgz", + "integrity": "sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A==", "dev": true }, "growly": { @@ -41655,6 +45416,23 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, "has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", @@ -41893,6 +45671,11 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -41913,7 +45696,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "requires": { "react-is": "^16.7.0" }, @@ -41921,8 +45703,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -42275,6 +46056,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, "icss-utils": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", @@ -42311,6 +46098,11 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" }, + "immutable": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", + "integrity": "sha1-E7TTyxK++hVIKib+Gy665kAHHks=" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -42479,7 +46271,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -42826,6 +46617,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -43410,6 +47207,69 @@ } } }, + "jest-transform-css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jest-transform-css/-/jest-transform-css-3.0.0.tgz", + "integrity": "sha512-MR/mHg5GdVPjyWT2r+TWZoM7ryh+apo2cB0AcmLEw4yH37/Htjm2afMGOiUtXG2guLfsGabg8uYw1NTjVINj5Q==", + "dev": true, + "requires": { + "common-tags": "1.8.2", + "cosmiconfig": "7.0.1", + "cross-spawn": "7.0.3", + "postcss-load-config": "2.0.0", + "postcss-modules": "1.3.2", + "style-inject": "0.3.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "postcss-load-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.0.0.tgz", + "integrity": "sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==", + "dev": true, + "requires": { + "cosmiconfig": "^4.0.0", + "import-cwd": "^2.0.0" + }, + "dependencies": { + "cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "dev": true, + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0", + "require-from-string": "^2.0.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + } + } + }, "jest-util": { "version": "27.3.1", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.3.1.tgz", @@ -43596,6 +47456,12 @@ } } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, "js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", @@ -43853,11 +47719,65 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, + "json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha1-E/FM4C7tTpgSl7ZOueO5MuLdE9w=", + "dev": true, + "requires": { + "jju": "^1.1.0" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-server": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.0.tgz", + "integrity": "sha512-+e/nW0mf666j1yTK+5dRx7hgxq5wJTkc5QhTYa/cBfD6vLlQWHfB4l8XKPgzeO55A8Hqm38g44OtZ5SooXi6MQ==", + "dev": true, + "requires": { + "body-parser": "^1.19.0", + "chalk": "^4.1.2", + "compression": "^1.7.4", + "connect-pause": "^0.1.1", + "cors": "^2.8.5", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-urlrewrite": "^1.4.0", + "json-parse-helpfulerror": "^1.0.3", + "lodash": "^4.17.21", + "lodash-id": "^0.14.1", + "lowdb": "^1.0.0", + "method-override": "^3.0.0", + "morgan": "^1.10.0", + "nanoid": "^3.1.23", + "please-upgrade-node": "^3.2.0", + "pluralize": "^8.0.0", + "server-destroy": "^1.0.1", + "update-notifier": "^5.1.0", + "yargs": "^17.0.1" + }, + "dependencies": { + "yargs": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", + "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + } + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -44050,11 +47970,23 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-id": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz", + "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==", + "dev": true + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -44075,6 +48007,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -44131,6 +48068,27 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -44451,6 +48409,35 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, + "method-override": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", + "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", + "dev": true, + "requires": { + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -44730,6 +48717,42 @@ "minimist": "^1.2.5" } }, + "morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "requires": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "dependencies": { + "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" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -44792,32 +48815,69 @@ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", "dev": true }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, "type-fest": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "yargs": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.2.1.tgz", - "integrity": "sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.0.tgz", + "integrity": "sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew==", "dev": true, "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" } + }, + "yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", + "dev": true } } }, @@ -44928,36 +48988,14 @@ } }, "node-fetch": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz", - "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.1.0.tgz", + "integrity": "sha512-QU0WbIfMUjd5+MUzQOYhenAazakV7Irh1SGkWCsRzBwvm4fAhzEUaHMJ6QLP7gWT6WO9/oH2zhKMMGMuIrDyKw==", "dev": true, "requires": { - "whatwg-url": "^5.0.0" - }, - "dependencies": { - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.2", + "formdata-polyfill": "^4.0.10" } }, "node-forge": { @@ -45801,6 +49839,21 @@ } } }, + "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" + } + }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, "pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -45818,6 +49871,17 @@ "@babel/runtime": "^7.14.0" } }, + "popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "requires": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -46489,6 +50553,19 @@ } } }, + "postcss-modules": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-1.3.2.tgz", + "integrity": "sha512-QujH5ZpPtr1fBWTKDa43Hx45gm7p19aEtHaAtkMCBZZiB/D5za2wXSMtAf94tDUZHF3F5KZcTXISUNqgEQRiDw==", + "dev": true, + "requires": { + "css-modules-loader-core": "^1.1.0", + "generic-names": "^1.0.3", + "lodash.camelcase": "^4.3.0", + "postcss": "^7.0.1", + "string-hash": "^1.1.1" + } + }, "postcss-modules-extract-imports": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", @@ -47447,6 +51524,14 @@ "whatwg-fetch": "^3.4.1" } }, + "react-clientside-effect": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz", + "integrity": "sha512-2bL8qFW1TGBHozGGbVeyvnggRpMjibeZM2536AKNENLECutp2yfs44IL8Hmpn8qjFQ2K7A9PnYf3vc7aQq/cPA==", + "requires": { + "@babel/runtime": "^7.12.13" + } + }, "react-colorful": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.5.0.tgz", @@ -47719,9 +51804,9 @@ } }, "react-error-boundary": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.3.tgz", - "integrity": "sha512-A+F9HHy9fvt9t8SNDlonq01prnU8AmkjvGKV4kk8seB9kU3xMEO8J/PQlLVmoOIDODl5U2kufSBs4vrWIqhsAA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", "dev": true, "requires": { "@babel/runtime": "^7.12.5" @@ -47735,8 +51820,28 @@ "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", - "dev": true + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "react-final-form": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.7.tgz", + "integrity": "sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==", + "requires": { + "@babel/runtime": "^7.15.4" + } + }, + "react-focus-lock": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.5.2.tgz", + "integrity": "sha512-WzpdOnEqjf+/A3EH9opMZWauag7gV0BxFl+EY4ElA4qFqYsUsBLnmo2sELbN5OC30S16GAWMy16B9DLPpdJKAQ==", + "requires": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^0.9.1", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.5", + "use-callback-ref": "^1.2.5", + "use-sidecar": "^1.0.5" + } }, "react-helmet-async": { "version": "1.1.2", @@ -47805,6 +51910,41 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-remove-scroll": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.4.1.tgz", + "integrity": "sha512-K7XZySEzOHMTq7dDwcHsZA6Y7/1uX5RsWhRXVYv8rdh+y9Qz2nMwl9RX/Mwnj/j7JstCGmxyfyC0zbVGXYh3mA==", + "requires": { + "react-remove-scroll-bar": "^2.1.0", + "react-style-singleton": "^2.1.0", + "tslib": "^1.0.0", + "use-callback-ref": "^1.2.3", + "use-sidecar": "^1.0.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "react-remove-scroll-bar": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.2.0.tgz", + "integrity": "sha512-UU9ZBP1wdMR8qoUs7owiVcpaPwsQxUDC2lypP6mmixaGlARZa7ZIBx1jcuObLdhMOvCsnZcvetOho0wzPa9PYg==", + "requires": { + "react-style-singleton": "^2.1.0", + "tslib": "^1.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "react-scripts": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz", @@ -49291,6 +53431,23 @@ "throttle-debounce": "^3.0.1" } }, + "react-style-singleton": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.1.1.tgz", + "integrity": "sha512-jNRp07Jza6CBqdRKNgGhT3u9umWvils1xsuMOjZlghBDH2MU0PL2WZor4PGYjXpnRCa9DQSlHMs/xnABWOwYbA==", + "requires": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^1.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "react-syntax-highlighter": { "version": "13.5.3", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz", @@ -49315,6 +53472,17 @@ "use-latest": "^1.0.0" } }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -50464,6 +54632,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, + "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-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", @@ -50616,6 +54790,12 @@ "send": "0.17.1" } }, + "server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=", + "dev": true + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -51223,6 +55403,15 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.3" + } + }, "store2": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/store2/-/store2-2.12.0.tgz", @@ -51308,6 +55497,12 @@ "safe-buffer": "~5.1.0" } }, + "string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -51449,6 +55644,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, "style-loader": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", @@ -51473,6 +55674,15 @@ "inline-style-parser": "0.1.1" } }, + "style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "requires": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "stylehacks": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", @@ -51653,6 +55863,11 @@ "postcss-value-parser": "^4.1.0" } }, + "stylis": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz", + "integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==" + }, "sugarss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", @@ -51827,6 +56042,11 @@ "object.getownpropertydescriptors": "^2.1.2" } }, + "synthetic-dom": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/synthetic-dom/-/synthetic-dom-1.4.0.tgz", + "integrity": "sha512-mHv51ZsmZ+ShT/4s5kg+MGUIhY7Ltq4v03xpN1c8T1Krb5pScsh/lzEjyhrVD0soVDbThbd2e+4dD9vnDG4rhg==" + }, "table": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", @@ -52182,6 +56402,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -52256,8 +56481,7 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", - "dev": true + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, "toidentifier": { "version": "1.0.0", @@ -52456,6 +56680,11 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==" }, + "ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==" + }, "uid-number": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", @@ -52856,6 +57085,12 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-callback-ref": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", + "requires": {} + }, "use-composed-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.1.0.tgz", @@ -52881,6 +57116,22 @@ "use-isomorphic-layout-effect": "^1.0.0" } }, + "use-sidecar": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.0.5.tgz", + "integrity": "sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==", + "requires": { + "detect-node-es": "^1.1.0", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -53041,7 +57292,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -53304,6 +57554,12 @@ "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", "dev": true }, + "web-streams-polyfill": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz", + "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==", + "dev": true + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", diff --git a/proof-of-concepts/features/package.json b/proof-of-concepts/features/package.json index 273f7b2..c883704 100644 --- a/proof-of-concepts/features/package.json +++ b/proof-of-concepts/features/package.json @@ -8,6 +8,7 @@ "test:watch": "jest --watch --maxWorkers=50%", "start": "react-scripts start", "storybook": "start-storybook -p 6006", + "mockServer": "json-server --watch db.json", "build-storybook": "build-storybook" }, "author": "", @@ -27,6 +28,7 @@ "devDependencies": { "@babel/core": "^7.15.8", "@babel/preset-typescript": "^7.15.0", + "@jackfranklin/test-data-bot": "^1.3.0", "@storybook/addon-a11y": "^6.3.12", "@storybook/addon-actions": "^6.3.12", "@storybook/addon-essentials": "^6.3.12", @@ -36,6 +38,9 @@ "@storybook/react": "^6.3.12", "@testing-library/react-hooks": "^7.0.2", "@types/classnames": "^2.3.1", + "@types/draft-js": "^0.11.7", + "@types/final-form-focus": "^1.1.2", + "@types/react-transition-group": "^4.4.4", "babel-loader": "^8.2.2", "classnames": "^2.3.1", "css-loader": "^3.6.0", @@ -45,9 +50,13 @@ "eslint-plugin-react-hooks": "^4.2.0", "fork-ts-checker-webpack-plugin": "^4.1.6", "jest": "^27.3.1", + "jest-transform-css": "^3.0.0", + "json-server": "^0.17.0", "msw": "^0.35.0", + "node-fetch": "^3.1.0", "react-docgen-typescript": "^2.1.1", "react-docgen-typescript-plugin": "^1.0.0", + "react-error-boundary": "^3.1.4", "sass": "^1.43.2", "sass-loader": "^10.2.0", "storybook": "^6.3.12", @@ -58,6 +67,9 @@ "ts-jest": "^27.0.7" }, "dependencies": { + "@chakra-ui/react": "^1.7.2", + "@emotion/react": "^11.7.0", + "@emotion/styled": "^11.6.0", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -66,10 +78,18 @@ "@types/react": "^17.0.30", "@types/react-dom": "^17.0.9", "create-react-app": "^4.0.3", + "draft-js": "^0.11.7", + "draft-js-export-markdown": "^1.4.0", + "draft-js-import-markdown": "^1.4.0", + "final-form": "^4.20.4", + "final-form-focus": "^1.1.2", + "framer-motion": "^4.1.17", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-final-form": "^6.5.7", "react-icons": "^4.3.1", "react-scripts": "4.0.3", + "react-transition-group": "^4.4.2", "typescript": "^4.4.4" } } diff --git a/proof-of-concepts/features/stories/EditorTags/TagEditor.scss b/proof-of-concepts/features/stories/EditorTags/TagEditor.scss deleted file mode 100644 index 2aed024..0000000 --- a/proof-of-concepts/features/stories/EditorTags/TagEditor.scss +++ /dev/null @@ -1,120 +0,0 @@ -@use '../styles/colors.scss'; -@use '../styles/fonts.scss'; -.tag-editor-section { - padding: 0; -} - -.tag-editor-container { - border: 1px solid colors.$Gray-8; - border-radius: 4px; -} - -.tag-editor-focused { - border: 1px solid #0978d9; - box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3); - outline: none; -} - -.tag-editor-input { - outline: none; - border: none; - min-width: 1rem; -} - -.tag-suggestions { - box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); - background-color: color; - width: 100%; -} - -.tag-suggestions__list { - display: flex; - // allow items to fill rows if it exceeds the width of the parent - flex-wrap: wrap; - flex-direction: row; - // just enough spacing to avoid overflow onto tag input container - margin-top: 3px; - list-style-type: none; - padding: 0; -} - -.tag-suggestions__list-item { - // we want three items per row - flex-basis: 29%; - // when there is enough space, the item should fill the remaining space, rather than the percentage we specify - flex-grow: 1; - max-height: 150px; - font-size: fonts.$Size-0; - // allows us to set flexbox props on item information - display: flex; - flex-direction: column; - - padding: 1em; - cursor: pointer; - margin-bottom: 0px; - border-radius: 4px; -} -.tag-suggestions__list-item:hover { - background-color: rgba(236, 238, 241, 0.7); -} -.tag-suggestions__list-item:focus { - outline: none; - box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3); - background-color: colors.$Gray-1; -} - -.tag-suggestions__header { - display: inline-grid; - grid-template-rows: 1fr; - // tag should be allowed to have 100% of its content visible, the rest of the space is distributed equally - grid-template-columns: fit-content(100%) 1fr 1fr; - padding-bottom: 0.5rem; - align-items: center; -} - -.tag-suggestions__count { - // if the count is high, abbreviate the overflow - // that's a sign that we're doing things right in this app - overflow: hidden; - max-width: 100%; - text-overflow: ellipsis; - white-space: nowrap; - margin-left: 8px; -} - -.tag-suggestions__info { - // flex combined with margin-auto is translated as the following: - // margin-x: auto, push the element away from that direction - // source: https://css-tricks.com/the-peculiar-magic-of-flexbox-and-auto-margins/ - display: inline-flex; - margin-left: auto; -} - -.tag-suggestions__body { - // put ellipsis on text past n-lines long - display: -webkit-box; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; - overflow: hidden; - line-height: fonts.$Size-4; -} - -// on small screens, we should not display all the tag suggestion info -@media screen and (max-width: 25em) { - .tag-suggestions__header { - // just want to show the tags, no other info so we only need 1 column - grid-template-columns: 1fr; - } - .tag-suggestions__list-item { - align-items: center; - } - .tag-suggestions__count { - display: none; - } - .tag-suggestions__info { - display: none; - } - .tag-suggestions__body { - display: none; - } -} diff --git a/proof-of-concepts/features/stories/EditorTags/TagEditor.tsx b/proof-of-concepts/features/stories/EditorTags/TagEditor.tsx deleted file mode 100644 index d5d2266..0000000 --- a/proof-of-concepts/features/stories/EditorTags/TagEditor.tsx +++ /dev/null @@ -1,755 +0,0 @@ -import React from 'react'; -import { Tag, TagIcon, TagCloseButton, TagProps } from './Tag'; -import { GoQuestion } from 'react-icons/go'; -import classNames from 'classnames'; -import './TagUseCase.scss'; -import './TagEditor.scss'; -import { TagSuggestions } from './__tests__/testingUtils'; -/** - * ******************* - * - * Tag Use Case - * - * ******************* - * The Tag Editor is composed of four (4) properties - * Tag Selection: an existing tag being edited, otherwise undefined - * Dictionary of Tags: a Map of tags that contain all the valid user input tags - * Left-Hand Side (lhs) Container: a list of tags that belong on the lhs of the tag-input - * Right-Hand Side (rhs) Container: a list of tags that belong on the rhs of the tag-input - * - * In the interest of simplifying the implementation of the editor's UI/UX - * we put the data structures for rendering in state. As a result, we use a little more space, - * but we expect less than 5 tags per submission, so the tradeoff is negligible - * - */ -type TagEditor = { - inputState: inputState; - tags: Map; - lhs: EditorTag[]; - rhs: EditorTag[]; - inputValue: string; -}; - -type inputState = { - text: string; - index: number; -}; - -interface EditorTag { - name: string; -} - -const initialState = { - inputState: { - index: 0, - text: undefined - }, - tags: new Map(), - lhs: [], - rhs: [], - inputValue: '' -} as TagEditor; - -const props = { - size: 'small', - type: 'no-outline', - text: '', - orientation: 'right' -} as TagProps; - -const ARROW_LEFT = 'ArrowLeft'; -const ARROW_RIGHT = 'ArrowRight'; -const BACKSPACE = 'Backspace'; -const SPACEBAR = 32; -const initTagEditorValue = ''; - -interface TagUseCaseProps { - maxLen?: number; - maxNumTags: number; - convertTextOnSubmission: boolean; - onTagCreation?: () => void; - onChange?: () => void; - defaultIsHighlighted?: boolean; -} -const sugg = [ - { name: 'material-analysis', count: 5 }, - { name: 'class-analysis', count: 33 }, - { name: 'materialism', count: 12 }, - { name: 'dialectical-materialism', count: 9 }, - { name: 'historical-materialism', count: 5 }, - { name: 'materialist-theory', count: 2 } -]; - -/** - * ******************* - * - * Tag Editor - * - * ******************* - * - * user inputs text in input - * user confirms text via an event (click, keyboard) - * tag is created and inserted in the box of tags - * user can remove tags - * user has option to use the editor as an autocomplete - * - */ - -interface ItemsListRef { - [key: string]: Item; -} -export const TagEditor = () => { - const itemsListRef = React.useRef({}); - // we want to store the items and only update the list reference when the items change - // in the getItemProps, we want to assign each item an id that we can reference - React.useCallback(() => { - sugg.forEach((item: Item, index: number) => { - console.log('num times ran'); - const id = `item-${index}`; - itemsListRef.current[id] = item; - }); - }, [itemsListRef]); - console.log(itemsListRef); - const [state, setState] = React.useState(initialState); - const [tagSuggestions, setTagSuggestions] = React.useState({ data: null }); - /********************************************************* - * * - * Screen Reader Dialogue and Form Errors * - * * - *********************************************************/ - const [duplicateTagAlert, setDuplicateTagAlert] = React.useState(''); - - /** - * ******************* - * - * Focusing - * - * ******************* - * - * If leaving focus from text, convert the current text into a tag, apply TagCreation - * - */ - const focusRef = React.useRef(); - const [editorHasFocus, setEditorHasFocus] = React.useState(false); - const focusTagEditor = () => { - if (focusRef.current) { - focusRef.current.focus(); - setEditorHasFocus(true); - } - }; - const tagEditorClass = classNames({ - 'tag-editor-container': true, - 'tag-editor-focused': editorHasFocus - }); - const handleBlur = () => { - // console.log('[HANDLE_BLUR]'); - setEditorHasFocus(false); - }; - const handleFocus = () => { - // console.log('[HANDLE_FOCUS]'); - setEditorHasFocus(true); - }; - - /** - * ******************* - * - * Update - * - * ******************* - * - * Update an existing tag for a click event - * If a user click's on a tag, convert the tag into input an input - * - */ - const editTag = (e: React.MouseEvent) => { - console.log('[EDIT_TAG]'); - const { name, index, tagContainer } = getTagAttributes(e.target); - console.log(name, index, tagContainer); - console.log(state.inputValue); - }; - - /** - * ******************* - * - * Deletion - * - * ******************* - * There are two ways to delete a tag - * - remove the tag via its close button - * - edit and delete existing tags text - * - */ - const removeTag = (e: React.MouseEvent) => { - // prevents other click handlers on the Tag component from firing - e.stopPropagation(); - console.log('[REMOVE_TAG]'); - const { name, index, tagContainer } = getTagAttributes(e.target); - const newState = reconstructContainer(state, { - name, - index, - tagContainer - }); - newState.inputState.index = index; - newState.inputValue = initTagEditorValue; - newState.tags.delete(name); - console.log('[REMOVE_TAG]:', newState); - setState(newState); - focusRef.current.focus(); - }; - - /** - * ******************* - * - * Rendering - * - * ******************* - * - * Given the tag-input-rendering-state - * if data-index is defined - * iterate over the Tags State and a list of Tags* - * must consider data-index over where to append the tags* - * Use the Tag Component - * - */ - const LHSTags = () => - state.lhs.map((tag, index) => ( - - - - )); - - /** - * ******************* - * - * Rendering - * - * ******************* - * - * Given the tag-input-rendering-state - * if data-index is defined - * iterate over the Tags State and a list of Tags* - * must consider data-index over where to append the tags* - * Use the Tag Component - */ - const RHSTags = () => - state.rhs.map((tag, index) => ( - - - - )); - - /** - * ******************* - * - * Create - * - * ******************* - * - * Pre-condition: - * the tag container must be focused - * Condition: - * 1. pre is satisfied - * 2. a spacebar or click event uses the existing input text - * apply 3 and evaluate the returned string - * apply 4 on the returned string and evaluate the result - * given the returned result - * if true - * do not create a tag - * delete the current text in the input box - * keep focus on the current input - * apply 5 - * if false - * create the tag - - * 3. the text input is stripped of non-A-Z-0-9 characters - * underscores are transformed into hyphenations - * text input is converted into lowercase characters - * return the converted string - - * 4. the resulting string is then compared to other existing tags - * if there is a match, then apply 5. and return true - * else return false - - * 5. a screen-reader: you've already created a tag with the label: [text] - * - */ - const createTag = (userInput: string) => { - // strip text from characters not allowed - let text = cleanText(userInput); - - // if the text contained bad characters, then return - if (text === '') return; - - // check if we have a duplicate tag entry - if (isDuplicate(text, state)) { - setDuplicateTagAlert(getDuplicateTagAlert(text)); - - // create the tag - } else { - // create a fresh piece of state to modify - let newState = { ...state }; - - let newTag = { - name: text - } as EditorTag; - - // move our input to the next index - newState.inputState.index++; - - // insert the tag into our dictionary of tags - newState.tags.set(text, newTag); - - // update our left-hand side container - newState.lhs.push(newTag); - - // refresh the value of the input - newState.inputValue = ''; - - // update state - setState(newState); - } - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.charCode === SPACEBAR && isEmpty(state.inputValue) == false) { - console.log('submit: ', { ...state, ...{ inputValue: initTagEditorValue } }); - setState({ ...state, ...{ inputValue: initTagEditorValue } }); - createTag(e.target.value); - } - }; - - /** - * ******************* - * - * Navigation - * - * ******************* - * - * We allow the following key commands to navigate between tags in the editor - * - Arrow Left: move cursor left and edit the previous tag - * - Arrow Right: move cursor right and edit the next tag - * - Backspace: move cursor left and merge the current input text with the previous tag's text - */ - const handKeyDown = (e: React.KeyboardEvent) => { - // while we have more than one tag and we're not currently editing the first tag of the LHS container - if (editPrevIsValid(e, state)) { - if (e.key === ARROW_LEFT) { - // store value of the input's current value, used later to create a tag - let inputTextValue = state.inputValue; - - // store the index of the left-hand container's last tag, used later to be the input value - let index = state.lhs.length - 1; - - if (state.lhs.length > 0) { - // store the last tag to operate on - let leftMostTag = state.lhs[index]; - - // delete the left hand-side tag - let newState = reconstructContainer(state, { - name: leftMostTag.name, - tagContainer: 'lhs', - index: state.lhs.length - 1 - }); - newState.tags.delete(leftMostTag.name); - - if (isEmpty(inputTextValue) === false) { - // create and prepend the tag to the RHS container - let newTag = { name: inputTextValue } as EditorTag; - newState.rhs.unshift(newTag); - newState.tags.set(newTag.name, newTag); - } - // update the current index of the input, relative to the items in the LH and RH containers - newState.inputState.index = index; - - // update the controlled component's input value - newState.inputValue = leftMostTag.name; - - // update the editor state - console.log('[EDIT_PREV]:', newState); - setState(newState); - - return; - } - } else if (e.key === BACKSPACE) { - // prevent on change handle from firing, it will overrwrite our merged input text that we create below - e.preventDefault(); - - // store value of the input's current value, used later to create a tag - let inputTextValue = state.inputValue; - - // store the index of the left-hand container's last tag, used later to be the input value - let index = state.lhs.length - 1; - - if (state.lhs.length > 0) { - // store the last tag to operate on - let leftMostTag = state.lhs[index]; - let newInputText = leftMostTag.name.concat('', inputTextValue); - - // delete the left hand-side tag - let newState = reconstructContainer(state, { - name: leftMostTag.name, - tagContainer: 'lhs', - index: state.lhs.length - 1 - }); - newState.tags.delete(leftMostTag.name); - - // update the current index of the input, relative to the items in the LH and RH containers - newState.inputState.index = index; - - // update the controlled component's input value - newState.inputValue = newInputText; - - // update the editor state - setState(newState); - - return; - } - } - } - - if (editForwardIsValid(e, state)) { - if (e.key === ARROW_RIGHT) { - // store value of the input's current value, used later to create a tag - let inputTextValue = state.inputValue; - - // the index of the new input, relateive to the lh and rh containers - let newInputIndex = state.lhs.length + 1; - - // store the head of the rh container, the tag that we will edit - let head = state.rhs[0]; - - let newState = { ...state }; - - // create a tag from the current input value - if (isEmpty(inputTextValue) === false) { - let newTag = { name: inputTextValue } as EditorTag; - newState.lhs.push(newTag); - newState.tags.set(newTag.name, newTag); - } - - // delete the head from the rh container - newState = reconstructContainer(newState, { - name: head.name, - index: 0, // the head of the RHS container - tagContainer: 'rhs' - }); - - newState.tags.delete(head.name); - - // update the input state to be the new index - newState.inputState.index = newInputIndex; - - // update the controlled component's input value - newState.inputValue = head.name; - - // update the editor state - console.log('[ARROW_RIGHT]:', newState); - setState(newState); - - return; - } - } - }; - - const handleOnChange = (e: React.ChangeEvent) => { - console.log('[HANDLE_ONCHANGE]: ', e.target.value); - // console.log('[HANDLE_ONCHANGE]: ', e.charCode); - - const text = e.target.value; - - if (isWhitespace(text) === false) { - // update the text value - console.log({ ...state, ...{ inputValue: text } }); - setState({ ...state, ...{ inputValue: text } }); - - // if the editor has a duplicate tag, we remove the duplicate flag in the onChange handler - // because the user is signaling that they've decided to correct the duplicate - if (duplicateTagAlert) { - setDuplicateTagAlert(''); - } - } - }; - - const handleOnPaste = (e: React.ClipboardEvent) => { - console.log('[ON_PASTE]'); - const text = event.clipboardData.getData('text'); - - if (text.length > 1) { - // create tags from the user's pasted text - text.split(' ').forEach((pieceOfText) => createTag(pieceOfText)); - } - }; - - return ( -
- - Add up to 5 (five) tags to describe what your question is about - -
-
    - {LHSTags().map((tag, index) => ( -
  • - {tag} -
  • - ))} -
- -
    - {RHSTags().map((tag, index) => ( -
  • - {tag} -
  • - ))} -
- {duplicateTagAlert ?

{duplicateTagAlert}

: null} -
- {tagSuggestions.data ? ( -
-
    - {tagSuggestions.suggestions.map((suggestion) => { - const url = `thebottomlineapp.com/tags/${suggestion.name}/info`; - return ( -
  • -
    - - {suggestion.count} - - - -
    - {suggestion.excerpt} -
  • - ); - })} -
-
- ) : null} -
- ); -}; - -function reconstructContainer(state, { name, index, tagContainer }) { - const newState = { ...state }; - switch (tagContainer) { - case 'lhs': - // - // LHS Tag Removal - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 4 5 6 7 k RHS Container: k+1 k+2 ... k+n - // tag tag tag tag tag tag tag tag tag tag ... tag - // ^ - // | - // remove - // - // ------------------------------------------------------------------------------------------- - // - // - Get the total length of all tags, len(lhs) + len(rhs) - // - Beginning from index + 1 of the removal tag - // (i.e. don't include the tag to be removed) to the index of the input - // Prepend tags to the RHS container - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 RHS Container: 5 6 7 k k+1 k+2 ... k+n - // tag tag tag tag tag tag tag tag tag ... tag - // - // ------------------------------------------------------------------------------------------- - // - // - Index of the input tag = index of the tag removed - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 4 (new k) RHS Container: 5 6 7 k+1 k+2 ... k+n - // tag tag tag tag tag tag tag tag tag ... tag - // - // ------------------------------------------------------------------------------------------- - // - - if (index === 0) { - // delete the head - let tagsToPrepend = newState.lhs.splice(index + 1); - - newState.rhs = tagsToPrepend.concat(newState.rhs); - - newState.lhs = []; - } else if (index === newState.lhs.length - 1) { - // delete the tail - newState.lhs.pop(); - } else { - // otherwise, delete in-place - let numItems = newState.inputState.index - index + 1; - - let tagsToPrepend = newState.lhs.splice(index + 1, numItems); - - newState.rhs = tagsToPrepend.concat(newState.rhs); - - newState.lhs.pop(); - } - break; - case 'rhs': - // - // RHS Tag Removal - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 4 5 6 7 k RHS Container: k+1 k+2 ... k+n - // tag tag tag tag tag tag tag tag tag tag tag - // ^ - // | - // remove - // - // ------------------------------------------------------------------------------------------- - // - // - Get the total length of all tags, lhs + rhs - // - Beginning from the index of the input+1 to the index of the removal tag - // Append tags to the LHS container - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 4 5 6 7 k k+1 RHS Container: ... k+n - // tag tag tag tag tag tag tag tag tag tag tag - // - // ------------------------------------------------------------------------------------------- - // - // - Index of the input tag = index of the tag removed - // - // ------------------------------------------------------------------------------------------- - // - // LHS Container: 0 1 2 3 4 5 6 7 8 k RHS Container: ... k+n - // tag tag tag tag tag tag tag tag tag tag tag - // - // ------------------------------------------------------------------------------------------- - // - - if (index === 0) { - // delete the head - newState.rhs.shift(); - } else if (index === newState.rhs.length - 1) { - // delete the tail - newState.rhs.pop(); - - newState.lhs = newState.lhs.concat(newState.rhs); - - newState.rhs = []; - } else { - // otherwise, delete in-place - // delete up to the index, splice is right-boundary non-inclusive - let appendRHS = newState.rhs.splice(0, index); - - newState.lhs = newState.lhs.concat(appendRHS); - - newState.rhs.shift(); - } - break; - default: - throw new TypeError('Unhandled exception'); - } - - return newState; -} - -function editForwardIsValid( - e: React.KeyboardEvent, - state: TagEditor -): boolean { - return ( - e.target.selectionEnd === e.target.value.length && - state.rhs.length > 0 && - state.inputState.index !== state.lhs.length + state.rhs.length - ); -} -function editPrevIsValid( - e: React.KeyboardEvent, - state: TagEditor -): boolean { - return ( - e.target.selectionStart === 0 && - state.lhs.length > 0 && - state.inputState.index !== 0 - ); -} - -function getTagAttributes(target: HTMLElement) { - return { - name: target.getAttribute('data-name'), - index: parseInt(target.getAttribute('data-index')), - tagContainer: target.getAttribute('data-container') - }; -} - -function getDuplicateTagAlert(text: string): string { - return `Unable to create the tag "${text}", duplicate of a tag that you've already created. Text cleared, please continue typing as you were`; -} - -function isWhitespace(str: string): boolean { - const whitespaceTest = new RegExp(/\s/); - return whitespaceTest.test(str); -} - -function getTags(state: TagEditor): string[] { - let tags = state.tags; - if (tags.size === 0) return []; - let tagsList = []; - tags.forEach((_, tagName) => tagsList.push(tagName)); - return tagsList; -} - -function cleanText(text) { - let regex = /[a-z]|[A-Z]|[0-9]|[\-]/g; - let cleaned = text.match(regex); - if (!cleaned) return ''; - const res = cleaned.join('').toLowerCase(); - return res; -} - -function isDuplicate(tagText: string, state: TagEditor): boolean { - let tags: string[] = getTags(state); - if (tags.length === 0) return false; - return tags.includes(tagText); -} - -function isEmpty(text: string): boolean { - return text === ''; -} diff --git a/proof-of-concepts/features/stories/EditorTags/TagUseCase.scss b/proof-of-concepts/features/stories/EditorTags/TagUseCase.scss deleted file mode 100644 index dfb9831..0000000 --- a/proof-of-concepts/features/stories/EditorTags/TagUseCase.scss +++ /dev/null @@ -1,12 +0,0 @@ -@use '../styles/colors.scss'; -@use '../styles/fonts.scss'; - -.b-tag-list { - display: inline; - padding: none; - list-style-type: none; -} -.b-tag { - display: inline; - margin-right: 4px; -} diff --git a/proof-of-concepts/features/stories/combobox/hooks/__tests__/useCombobox.test.tsx b/proof-of-concepts/features/stories/combobox/__tests__/useCombobox.test.tsx similarity index 92% rename from proof-of-concepts/features/stories/combobox/hooks/__tests__/useCombobox.test.tsx rename to proof-of-concepts/features/stories/combobox/__tests__/useCombobox.test.tsx index be877b7..091a783 100644 --- a/proof-of-concepts/features/stories/combobox/hooks/__tests__/useCombobox.test.tsx +++ b/proof-of-concepts/features/stories/combobox/__tests__/useCombobox.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; -import { renderCombobox, renderUseCombobox } from './utils'; +import { renderCombobox, SampleItems } from './utils'; import { screen, render, @@ -9,7 +9,6 @@ import { getByText } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { SampleItems } from './testingUtils'; import '@testing-library/jest-dom/extend-expect'; describe('useCombobox hook', () => { @@ -21,8 +20,12 @@ describe('useCombobox hook', () => { * **************** */ test('the popup is open when initial isOpen is true', () => { - const { input } = renderCombobox({ initialIsOpen: true, items: SampleItems }); - expect(input).toHaveFocus(); + renderCombobox({ + initialIsOpen: true, + items: SampleItems + }); + const comboboxItems = screen.getAllByRole('gridcell'); + expect(comboboxItems).toBeDefined(); }); /** @@ -32,11 +35,12 @@ describe('useCombobox hook', () => { * * **************** */ - test('when the popup is open, the escape keydown event closes the popup', () => { + test('when the popup is open, the escape keydown event closes the popup if open, and clears the textbox', () => { const { input } = renderCombobox({ initialIsOpen: true, items: SampleItems }); + userEvent.click(input); fireEvent.keyDown(input, { key: 'Escape', code: 'Escape', charCode: 27 }); expect(input).toHaveFocus(); }); @@ -82,16 +86,16 @@ describe('useCombobox hook', () => { * * **************** */ - test('backspace on an open popup with highlight does nothing', () => { - const { combobox, input, popup } = renderCombobox({ - initialIsOpen: false, + test('backspace on an open popup that has a cell highlighted doesnt close the popup, i.e. the input maintains focus', () => { + const { combobox, input, outside } = renderCombobox({ + initialIsOpen: true, items: SampleItems }); userEvent.click(input); userEvent.type(input, 'any-random-string'); fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown', charCode: 40 }); fireEvent.keyDown(input, { key: 'Backspace', code: 'Backspace', charCode: 8 }); - expect(screen.getByDisplayValue('any-random-string')).toBeDefined(); + expect(input).toHaveFocus(); }); test('only when the highlightedIndex is -1, can we change the input value popup', () => { @@ -120,15 +124,14 @@ describe('useCombobox hook', () => { initialIsOpen: true, items: SampleItems }); + userEvent.click(input); expect(input).toHaveFocus(); expect(popup).toBeDefined(); userEvent.click(outside); - const items = screen.queryByRole('gridcell'); - expect(input).not.toHaveFocus(); - expect(items).not.toBeInTheDocument(); - expect(outside).toHaveFocus(); + // ok fine there is a little leak of abstraction, who cares, the rent is increasing + expect(screen.queryByRole('gridcell')).not.toBeInTheDocument(); }); /** @@ -146,6 +149,7 @@ describe('useCombobox hook', () => { const items = getAllByRole(popup, 'gridcell'); const firstItem = items[0]; expect(firstItem.classList).not.toContain('current-item-highlight'); + userEvent.click(input); fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown', charCode: 40 }); expect(input).toHaveFocus(); expect(firstItem.classList).toContain('current-item-highlight'); @@ -156,6 +160,7 @@ describe('useCombobox hook', () => { initialIsOpen: true, items: SampleItems }); + userEvent.click(input); const items = getAllByRole(popup, 'gridcell'); fireEvent.keyDown(input, { key: 'ArrowLeft', code: 'ArrowLeft', charCode: 37 }); fireEvent.keyDown(input, { key: 'ArrowUp', code: 'ArrowUp', charCode: 38 }); @@ -173,6 +178,7 @@ describe('useCombobox hook', () => { initialIsOpen: true, items: SampleItems }); + userEvent.click(input); fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown', charCode: 40 }); fireEvent.keyDown(input, { key: 'ArrowUp', code: 'ArrowUp', charCode: 38 }); const items = getAllByRole(popup, 'gridcell'); @@ -346,28 +352,36 @@ describe('combobox accessibility', () => { }); test('when the popup is not visible, the combobox has aria-expanded == false', () => { - const { combobox } = renderCombobox({ initialIsOpen: false, items: SampleItems }); + const { combobox } = renderCombobox({ + initialIsOpen: false, + items: SampleItems + }); expect(combobox.getAttribute('aria-expanded')).toBeDefined(); expect(combobox.getAttribute('aria-expanded')).toBe('false'); }); test('when the popup is visible, the combobox has aria-expanded == true', () => { - const { combobox } = renderCombobox({ initialIsOpen: true, items: SampleItems }); + const { combobox } = renderCombobox({ + initialIsOpen: true, + items: SampleItems + }); expect(combobox.getAttribute('aria-expanded')).toBeDefined(); expect(combobox.getAttribute('aria-expanded')).toBe('true'); }); test('when the combobox recieves focus, the input is the default element with focus', () => { - const { combobox, input } = renderCombobox({ - initialIsOpen: false, + const { input } = renderCombobox({ + initialIsOpen: true, items: SampleItems }); - fireEvent.click(combobox); + userEvent.click(input); + fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown', charCode: 40 }); expect(input).toHaveFocus(); }); test('when a descendant (gridcell in our case) is highlighted, the input continues to have focus ', () => { const { input } = renderCombobox({ initialIsOpen: true, items: SampleItems }); + userEvent.click(input); fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown', charCode: 40 }); const item = screen.getByRole('gridcell', { selected: true }); expect(item).toBeDefined(); diff --git a/proof-of-concepts/features/stories/combobox/hooks/__tests__/utils.tsx b/proof-of-concepts/features/stories/combobox/__tests__/utils.tsx similarity index 71% rename from proof-of-concepts/features/stories/combobox/hooks/__tests__/utils.tsx rename to proof-of-concepts/features/stories/combobox/__tests__/utils.tsx index 1f895d8..b6c165e 100644 --- a/proof-of-concepts/features/stories/combobox/hooks/__tests__/utils.tsx +++ b/proof-of-concepts/features/stories/combobox/__tests__/utils.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { useCombobox } from '../useCombobox'; -import { BL } from '../combobox/types'; +import { ComboboxProps } from '../types'; import { render, screen, getAllByRole } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; -import { SampleItems } from './testingUtils'; const dataTestIds = { input: 'input-testid', @@ -12,14 +11,14 @@ const dataTestIds = { outside: 'outside-testid' }; -export function renderCombobox(props?: BL.ComboboxProps) { +export function renderCombobox(props?: ComboboxProps) { const container = render(); const input = screen.getByTestId(dataTestIds.input); const popup = screen.getByTestId(dataTestIds.popup); const label = screen.getByTestId(dataTestIds.label); const outside = screen.getByTestId(dataTestIds.outside); + const combobox = screen.queryByRole('combobox'); - const combobox = screen.getByRole('combobox'); return { input, popup, @@ -29,7 +28,7 @@ export function renderCombobox(props?: BL.ComboboxProps) { }; } -function ComboboxGrid(props: BL.ComboboxProps) { +function ComboboxGrid(props: ComboboxProps) { const { isOpen, getLabelProps, @@ -37,13 +36,21 @@ function ComboboxGrid(props: BL.ComboboxProps) { getInputProps, getPopupProps, getItemProps, - highlightedIndex - } = useCombobox(props); + highlightedIndex, + inputValue + } = useCombobox({ + itemToString: (item) => item.name, + ...props + }); return (
- +
    @@ -79,3 +86,12 @@ function ComboboxGrid(props: BL.ComboboxProps) { export function renderUseCombobox() { return renderHook(() => useCombobox({ items: SampleItems })); } + +export const SampleItems = [ + { name: 'material-analysis', count: 5, contents: '' }, + { name: 'class-analysis', count: 33, contents: '' }, + { name: 'materialism', count: 12, contents: '' }, + { name: 'dialectical-materialism', count: 9, contents: '' }, + { name: 'historical-materialism', count: 5, contents: '' }, + { name: 'materialist-theory', count: 2, contents: '' } +]; diff --git a/proof-of-concepts/features/stories/combobox/hooks/__tests__/testingUtils.ts b/proof-of-concepts/features/stories/combobox/hooks/__tests__/testingUtils.ts deleted file mode 100644 index e53f729..0000000 --- a/proof-of-concepts/features/stories/combobox/hooks/__tests__/testingUtils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const SampleItems = [ - { name: 'material-analysis', count: 5, contents: '' }, - { name: 'class-analysis', count: 33, contents: '' }, - { name: 'materialism', count: 12, contents: '' }, - { name: 'dialectical-materialism', count: 9, contents: '' }, - { name: 'historical-materialism', count: 5, contents: '' }, - { name: 'materialist-theory', count: 2, contents: '' } -]; diff --git a/proof-of-concepts/features/stories/combobox/hooks/combobox/types.ts b/proof-of-concepts/features/stories/combobox/hooks/combobox/types.ts deleted file mode 100644 index 4e98d51..0000000 --- a/proof-of-concepts/features/stories/combobox/hooks/combobox/types.ts +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; - -export interface ComponentProps { - stateReducer?: (state: State, actionAndChanges: ActionAndChanges) => State; -} -export namespace BL { - // Combobox - export type ComboboxProps = { - id?: string; - labelId?: string; - inputId?: string; - menuId?: string; - initialIsOpen?: boolean; - initialInputValue?: string; - initialHighlightedIndex?: number; - selectedItem?: Item; - items: Item[]; - itemToString?: (item: Item) => string; - onSelectedItemChange?: (changes: ComboboxState) => void; - onInputValueChange?: (changes: ComboboxState) => void; - onHighightedIndexChange?: (changes: ComboboxState) => void; - onIsOpenChange?: (changes: ComboboxState) => void; - } & ComponentProps; - - export type ComboboxState = { - isOpen: boolean; - inputValue: string; - highlightedIndex: number; - selectedItem: Item | null; - }; - - // Combobox Items - export interface Item { - name: string; - contents: any; - } - export type ItemsList = Record; - - // Prop Getters - export interface ComboboxGetterProps { - ariaPopup?: ComboboxAriaPopup; - } - export interface ComboboxInputProps { - ariaAutoComplete?: ComboboxAriaAutoComplete; - } - export type ComboboxAriaAutoComplete = 'none' | 'list' | 'both'; - export type ComboboxAriaPopup = - | boolean - | 'dialog' - | 'menu' - | 'grid' - | 'false' - | 'true' - | 'listbox' - | 'tree'; - export interface ComboboxPopupProps { - ariaLabel?: string; - role?: string; - } - - // Combobox reducer - export enum ComboboxActions { - INPUT_KEYDOWN_ARROW_UP = '[input-keydown-arrow-up]', - INPUT_KEYDOWN_ARROW_DOWN = '[input-keydown-arrow-down]', - INPUT_KEYDOWN_ARROW_LEFT = '[input-keydown-arrow-left]', - INPUT_KEYDOWN_ARROW_RIGHT = '[input-keydown-arrow-right]', - INPUT_KEYDOWN_ESCAPE = '[input-keydown-escape]', - INPUT_KEYDOWN_DELETE = '[input-keydown-delete]', - INPUT_KEYDOWN_ENTER = '[input-keydown-enter]', - INPUT_KEYDOWN_HOME = '[input-keydown-home]', - INPUT_KEYDOWN_END = '[input-keydown-end]', - INPUT_ITEM_CLICK = '[input-item-click]', - INPUT_BLUR = '[input-blur]', - INPUT_VALUE_CHANGE = '[input-value-change]', - FUNCTION_OPEN_POPUP = '[function-open-popup]', - FUNCTION_CLOSE_POPUP = '[function-close-popup]', - FUNCTION_SET_HIGHLIGHTED_INDEX = '[function-set-highlighted-index]', - FUNCTION_SELECT_ITEM = '[function-select-item]' - } - - export type ComboBoxStateChangeTypes = ComboboxActions; - - export type ComboboxActionAndChanges = ComboboxState & ComboBoxStateChangeTypes; - - export interface ComboboxAction { - type: ComboBoxStateChangeTypes; - getItemFromIndex?: (index: number) => Item; - index?: number; - text?: string; - props?: ComboboxProps; - } -} diff --git a/proof-of-concepts/features/stories/combobox/hooks/combobox/utils.ts b/proof-of-concepts/features/stories/combobox/hooks/combobox/utils.ts deleted file mode 100644 index 57b1afa..0000000 --- a/proof-of-concepts/features/stories/combobox/hooks/combobox/utils.ts +++ /dev/null @@ -1,217 +0,0 @@ -import React from 'react'; -import { BL, ComponentProps } from './types'; - -export const initialState: BL.ComboboxState = { - selectedItem: null, - isOpen: false, - highlightedIndex: -1, - inputValue: '' -}; - -export function computeInitialState(props: BL.ComboboxProps): BL.ComboboxState { - const isOpen = getInitialValue(props, 'isOpen'); - const inputValue = getInitialValue(props, 'inputValue'); - const selectedItem = getInitialValue(props, 'selectedItem'); - let highlightedIndex = getInitialValue(props, 'highlightedIndex'); - // console.log('[COMPUTE_INITIAL_STATE]:highlightedIndex', highlightedIndex); - // console.log('items length:', props.items.length - 1); - if (highlightedIndex > props.items.length - 1) { - highlightedIndex = -1 as Partial; - } - return { - isOpen, - inputValue, - highlightedIndex, - selectedItem - } as BL.ComboboxState; -} - -/** - * Use the keys of state to get the initial values of properties defined in props - * Props and State share the same keys. - */ -export function getInitialValue( - props: BL.ComboboxProps, - propKey: keyof BL.ComboboxState -): Partial { - if (propKey in props) { - return props[propKey as keyof BL.ComboboxProps] as Partial; - } - - // get the user-provided initial prop state, it is a piece of state - const initialPropKey = `initial${capitalizeString( - propKey - )}` as keyof BL.ComboboxProps; - if (initialPropKey in props) { - // console.log('initialPropKey', initialPropKey); - // console.log('initialPropKey', props[initialPropKey]); - return props[initialPropKey] as Partial; - } - - // return values from statically defined initial state object - return initialState[propKey] as Partial; -} - -/** - * Generates unique ids for the combobox component to avoid naming conflicts w/ other ids - */ -export function useElementId({ - id = `bottomline-${generateId()}`, - labelId, - inputId, - menuId -}: Partial) { - const elementIds = React.useRef({ - id, - labelId: labelId || `${id}-label`, - inputId: inputId || `${id}-input`, - menuId: menuId || `${id}-menu`, - // computes id relative to other indices of items - getItemId: (index: number) => `${id}-item-${index}` - }); - return elementIds.current; -} - -export function useControlledReducer< - ComponentReducer extends React.Reducer, - ComponentState extends React.ReducerState, - Props extends ComponentProps, - StateChangeType, - ActionAndChanges ->( - reducer: ComponentReducer, - initialState: ComponentState, - props: Props -): [ - React.ReducerState, - React.Dispatch> -] { - // console.log('[CONTROLLED_REDUCER]:initialState', initialState); - // console.log('[CONTROLLED_REDUCER]:props', props); - // store and track dispatched actions - const actionRef = React.useRef(); - - // store and track the "previous" state, as a ref. updating as a side-effect - // allows us to choose when to update this ref - const prevStateRef = React.useRef(); - - // const propsRef = React.useRef(props); - - // return either the internal changes based on our state reducer, - // or the internal changes based on the user's recommendations - const controlledReducer = React.useCallback( - (state: ComponentState, action) => { - actionRef.current = action; - const internalChanges = reducer(state, action); - if (props && props.stateReducer) { - const userRecommendedChanges = props.stateReducer(state, ({ - action, - changes: internalChanges - } as unknown) as ActionAndChanges); - return userRecommendedChanges; - } - return internalChanges; - }, - [props, reducer] - ); - - const [state, dispatch] = React.useReducer(controlledReducer, initialState); - - // our component calls this dispatch function with props added as a convenience - // this function saves us from having to declare props on every dispatch, - // if we declared useReducer within the component itself - const dispatchWithProps = React.useCallback( - ({ type, ...rest }: { type: StateChangeType }) => { - // console.log('dispatch with props:', propsRef); - // dispatch({ type, props: propsRef.current }); - dispatch({ type, props, ...rest }); - }, - [props] - ); - - // recall: useEffect runs after the render phase, therefore - // the reference of state is the most up-to-date state - // and prevStateRef references the state from the previous render - React.useEffect(() => { - if (actionRef.current && prevStateRef.current && prevStateRef.current !== state) { - onStateChange(props, prevStateRef.current, state); - } - prevStateRef.current = state; - }, [props, state]); - - return [state, dispatchWithProps]; -} - -// Calls any callback the users' of our component have registered when a piece of state -// has changed -export function onStateChange( - props: ComponentProps, - state: ComponentState, - newState: ComponentState -) { - for (let pieceOfState in state) { - const stateValue = state[pieceOfState]; - const newStateValue = newState[pieceOfState]; - if (stateValue !== newStateValue) { - invokeOnStateChange(pieceOfState, props, state, newState); - } - } -} - -export function invokeOnStateChange< - ComponentProps extends Record, - ComponentState ->( - pieceOfState: keyof ComponentState, - props: ComponentProps, - state: ComponentState, - newState: ComponentState -) { - const prop = capitalizeString(pieceOfState as string); - const stateChangeCallback = `on${prop}Change`; - if (stateChangeCallback in props) { - props[stateChangeCallback]({ changes: newState }); - } -} - -export function capitalizeString(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function generateId() { - return Math.floor(Math.random() * 1000); -} - -export function normalizeKey(e: React.KeyboardEvent) { - return { - name: e.key, - code: e.charCode - }; -} - -export function getNewIndex( - currentIndex: number, - length: number, - action: BL.ComboboxActions -): number { - switch (action) { - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN: { - if (currentIndex === length - 1) return currentIndex; - return currentIndex + 1; - } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_UP: { - if (currentIndex === -1) return currentIndex; - return currentIndex - 1; - } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT: { - if (currentIndex === -1 || currentIndex === 0) return currentIndex; - return currentIndex - 1; - } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT: { - if (currentIndex === length - 1 || currentIndex === -1) return currentIndex; - return currentIndex + 1; - } - default: - return 0; - } -} diff --git a/proof-of-concepts/features/stories/combobox/hooks/useCombobox.ts b/proof-of-concepts/features/stories/combobox/hooks/useCombobox.ts deleted file mode 100644 index a23a20c..0000000 --- a/proof-of-concepts/features/stories/combobox/hooks/useCombobox.ts +++ /dev/null @@ -1,298 +0,0 @@ -import * as React from 'react'; -import bottomlineComboboxReducer from './combobox/reducer'; -import { - useElementId, - normalizeKey, - useControlledReducer, - computeInitialState -} from './combobox/utils'; -import { BL } from './combobox/types'; - -export function useCombobox(props: BL.ComboboxProps = {}) { - /** - * **************** - * - * Combobox State - * - * **************** - * - */ - const [state, dispatch] = useControlledReducer< - (state: BL.ComboboxState, action: BL.ComboboxAction) => BL.ComboboxState, - BL.ComboboxState, - BL.ComboboxProps, - BL.ComboBoxStateChangeTypes, - BL.ComboboxActionAndChanges - >(bottomlineComboboxReducer, computeInitialState(props), props); - const { isOpen, highlightedIndex, inputValue } = state; - - /** - * ****** - * - * Refs - * - * ****** - * // stores the DOM reference to the label for the combobox - * // const labelRef = React.useRef(null); - * // stores the DOM reference to the popup box - * // const popupRef = React.useRef(null); - * // stores the DOM reference to the current item - * // const latestItem = React.useRef(null); - * - */ - // stores the DOM reference to the input element - const inputRef = React.useRef(null); - // stores the DOM reference to the list of items - const itemsListRef = React.useRef({}); - - /** - * ************************* - * - * Accessibility Identifiers - * - * ************************* - * - */ - // Returns accessibility identifiers and identifiers for items - const elementIds = useElementId(props); - - /** - * ************************* - * - * Items List (Combobox Items) - * - * ************************* - * - */ - props.items.forEach((item: BL.Item, index: number) => { - itemsListRef.current[elementIds.getItemId(index)] = item; - }); - // fetches the item based on the index as argument - const getItemFromIndex = React.useCallback( - (index: number) => itemsListRef.current[elementIds.getItemId(index)], - [elementIds] - ); - - /** - * ****************** - * - * Focus Management - * - * ****************** - * - * - */ - React.useEffect(() => { - if (inputRef.current && isOpen) { - inputRef.current.focus(); - } - }, [isOpen]); - - // Area of concern: when a combobox receives focus, DOM focus is placed on the textbox element - const setComboboxFocus = () => { - if (document.activeElement !== inputRef.current && inputRef.current) { - inputRef.current.focus(); - } - }; - // implement: when an item of the popup is box focused, DOM focus remains on thextbox - - /** - * ************** - * - * Event Handlers - * - * ************** - */ - const inputKeyDownHandlers: { [eventHandler: string]: () => void } = { - Enter: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ENTER, - getItemFromIndex - }); - }, - Escape: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ESCAPE, - getItemFromIndex - }); - }, - Delete: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_DELETE, - getItemFromIndex - }); - }, - ArrowRight: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT, - getItemFromIndex - }); - }, - ArrowLeft: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT, - getItemFromIndex - }); - }, - ArrowDown: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN, - getItemFromIndex - }); - }, - ArrowUp: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_ARROW_UP, - getItemFromIndex - }); - }, - Home: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_HOME, - getItemFromIndex - }); - }, - End: () => { - dispatch({ - type: BL.ComboboxActions.INPUT_KEYDOWN_END, - getItemFromIndex - }); - } - }; - - /** - * ************** - * - * Prop Getters - * - * ************** - * - * The combobox implements the ARIA 1.1 pattern - * https://www.w3.org/TR/wai-aria-practices/#wai-aria-roles-states-and-properties-6 - * - */ - function getLabelProps() { - return { - id: elementIds.labelId, - htmlFor: elementIds.inputId - }; - } - - /** - * @param {ComboboxAriaPopup} = {} as ComboboxGetterProps} { ariaPopup } - * in the ugliest, most-verbose way possible, we're saying - * if the user has defined a specific type of popup, then destructure - * otherwise, the user may have not passed any args, so default to an empty object - */ - function getComboboxProps( - { ariaPopup }: { ariaPopup?: BL.ComboboxAriaPopup } = {} as BL.ComboboxGetterProps - ) { - let ariaHasPopup = ariaPopup ? ariaPopup : ('grid' as BL.ComboboxAriaPopup); - // Implement: if the combobox has a visible label, the element with role combobox has aria-labelledby - // set to a value that refers to the labelling element. - // Otherwise, the combobox element has a label provided by aria-label. - return { - role: 'combobox', - 'aria-expanded': isOpen ? true : false, - 'aria-haspopup': ariaHasPopup, - 'aria-labelledby': elementIds.labelId, - onClick: setComboboxFocus - }; - } - - function getInputProps() { - const inputKeyDownHandler = (e: React.KeyboardEvent) => { - const keyEvt = normalizeKey(e); - if (keyEvt.name in inputKeyDownHandlers) { - inputKeyDownHandlers[keyEvt.name](); - } - }; - - const inputBlurHandler = (e: React.FocusEvent) => { - dispatch({ - type: BL.ComboboxActions.INPUT_BLUR - }); - }; - - const inputChangeHandler = (e: React.ChangeEvent) => { - dispatch({ - type: BL.ComboboxActions.INPUT_VALUE_CHANGE, - text: e.currentTarget.value - }); - }; - - const eventHandlers = { - onKeyDown: inputKeyDownHandler - }; - - return { - ref: inputRef, - value: inputValue, - onChange: inputChangeHandler, - onBlur: inputBlurHandler, - role: 'textbox', - 'aria-controls': elementIds.menuId, - 'aria-multiline': false, - 'aria-autocomplete': 'list' as BL.ComboboxAriaAutoComplete, - // fancy way of saying: assign the id of the item that is stored in the list of items - // as the aria-activedescendant, otherwise, merge a "null" (false) value - ...(isOpen && - highlightedIndex > -1 && { - 'aria-activedescendant': elementIds.getItemId(highlightedIndex) - }), - // event handlers - ...eventHandlers - }; - } - function getPopupProps( - { - role, - ariaLabel - }: { ariaLabel?: string; role?: string } = {} as BL.ComboboxPopupProps - ) { - let popupRole = 'grid'; - if (role) popupRole = role; - - return { - role: popupRole, - id: elementIds.menuId, - 'aria-labelledby': elementIds.labelId - }; - } - - // items are excluded from the tab sequence - function getItemProps(index: number) { - const handleClick = () => { - dispatch({ - type: BL.ComboboxActions.INPUT_ITEM_CLICK, - getItemFromIndex, - index - }); - }; - - const selected = index === highlightedIndex; - return { - role: 'gridcell', - id: elementIds.getItemId(index), - 'aria-selected': selected, - onClick: handleClick - }; - } - - function getGridPopupRowProps() { - return { role: 'row' }; - } - - return { - isOpen, - highlightedIndex, - // combobox-specific prop getters - getLabelProps, - getComboboxProps, - getInputProps, - getItemProps, - getPopupProps, - // combobox-grid prop getters - getGridPopupRowProps - }; -} diff --git a/proof-of-concepts/features/stories/combobox/hooks/useMultiSelection.ts b/proof-of-concepts/features/stories/combobox/hooks/useMultiSelection.ts deleted file mode 100644 index d0b570b..0000000 --- a/proof-of-concepts/features/stories/combobox/hooks/useMultiSelection.ts +++ /dev/null @@ -1,11 +0,0 @@ -interface Item {} - -type MultiSelectionState = { - selectedItems: Map; -}; - -const multiSelectionState = { - selectedItems: new Map() -} as MultiSelectionState; - -function useMultiSelection() {} diff --git a/proof-of-concepts/features/stories/combobox/hooks/combobox/reducer.ts b/proof-of-concepts/features/stories/combobox/reducer.ts similarity index 52% rename from proof-of-concepts/features/stories/combobox/hooks/combobox/reducer.ts rename to proof-of-concepts/features/stories/combobox/reducer.ts index bb919ed..9612907 100644 --- a/proof-of-concepts/features/stories/combobox/hooks/combobox/reducer.ts +++ b/proof-of-concepts/features/stories/combobox/reducer.ts @@ -1,81 +1,84 @@ -import { BL } from './types'; +import { ComboboxState, ComboboxAction, ComboboxActions } from './types'; import { getNewIndex, initialState } from './utils'; -export default function bottomlineComboboxReducer( - state: BL.ComboboxState, - action: BL.ComboboxAction -): BL.ComboboxState { - const { type, getItemFromIndex, props, index, text } = action; +export default function bottomlineComboboxReducer( + state: ComboboxState, + action: ComboboxAction +): ComboboxState { + // console.log('[STATE_REDUCER]', action.type); + const { type, getItemFromIndex, props, index, inputValue } = action; const { isOpen, highlightedIndex } = state; switch (type) { - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_UP: { - if (!props) return state; + case ComboboxActions.INPUT_KEYDOWN_ARROW_UP: { + if (!props || !props.items) return state; const newState = { ...state }; const nextIndex = getNewIndex( state.highlightedIndex, props.items.length, - BL.ComboboxActions.INPUT_KEYDOWN_ARROW_UP + ComboboxActions.INPUT_KEYDOWN_ARROW_UP ); newState.highlightedIndex = nextIndex; return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN: { - if (!props) return state; + case ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN: { + if (!props || !props.items) return state; const newState = { ...state }; const nextIndex = getNewIndex( state.highlightedIndex, props.items.length, - BL.ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN + ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN ); newState.highlightedIndex = nextIndex; return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT: { - if (!props) return state; + case ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT: { + if (!props || !props.items) return state; let newState = { ...state }; const prevIndex = getNewIndex( state.highlightedIndex, props.items.length, - BL.ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT + ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT ); newState.highlightedIndex = prevIndex; return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT: { - if (!props) return state; + case ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT: { + if (!props || !props.items) return state; const newState = { ...state }; const nextIndex = getNewIndex( state.highlightedIndex, props.items.length, - BL.ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT + ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT ); newState.highlightedIndex = nextIndex; return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_ESCAPE: { + case ComboboxActions.INPUT_KEYDOWN_ESCAPE: { // close popup, set to initial state return initialState; } - case BL.ComboboxActions.INPUT_KEYDOWN_ENTER: { + case ComboboxActions.INPUT_KEYDOWN_ENTER: { // select the item with the highlighted index - if (!props) return state; + if (!props || !props.items) return state; const newState = { ...state }; if (newState.highlightedIndex !== -1) { newState.selectedItem = props.items[newState.highlightedIndex]; + newState.inputValue = props.itemToString(newState.selectedItem); } return newState; } - case BL.ComboboxActions.INPUT_VALUE_CHANGE: { + case ComboboxActions.INPUT_VALUE_CHANGE: { // does nothing, does not move the input cursor, only when the highlightedIndex is -1 // combobox can be open const newState = { ...state }; - if (newState.highlightedIndex === -1 && text) { - newState.inputValue = text; + if (newState.highlightedIndex === -1 && inputValue) { + newState.inputValue = inputValue; } + newState.isOpen = true; return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_HOME: { + case ComboboxActions.INPUT_KEYDOWN_HOME: { // only if highlightedIndex >= 0, goes to 0 const newState = { ...state }; if (newState.highlightedIndex !== -1) { @@ -83,45 +86,33 @@ export default function bottomlineComboboxReducer( } return newState; } - case BL.ComboboxActions.INPUT_KEYDOWN_END: { + case ComboboxActions.INPUT_KEYDOWN_END: { // only if highlightedIndex >= 0, goes to items.length-1 - if (!props) return state; + if (!props || !props.items) return state; const newState = { ...state }; if (newState.highlightedIndex !== -1) { newState.highlightedIndex = props.items.length - 1; } return newState; } - case BL.ComboboxActions.INPUT_ITEM_CLICK: { + case ComboboxActions.ITEM_CLICK: { // select an item, selected item onchange event and highlight index on change + // console.log('[COMBOBOX_REDUCER]'); + // console.log('action', action); + // console.log('state', state); + const newState = { ...state }; - if (index && getItemFromIndex) { - newState.highlightedIndex = index; - newState.selectedItem = getItemFromIndex(index); - } + newState.highlightedIndex = index; + newState.selectedItem = getItemFromIndex(index); return newState; } - case BL.ComboboxActions.INPUT_BLUR: { + case ComboboxActions.INPUT_BLUR: { + // may need props in order to truly reset to initial state + // and call computeInitialState() from utils + const newState = { ...initialState }; + newState.inputValue = state.inputValue; return initialState; } - // case BL.ComboboxActions.INPUT_KEYDOWN_DELETE: { - // // does nothing, does not move the input cursor, only when the highlightedIndex is -1 - // // combobox can be open - // const newState = { ...state }; - // return newState; - // } - // case BL.ComboboxActions.FUNCTION_OPEN_POPUP: { - // return state; - // } - // case BL.ComboboxActions.FUNCTION_CLOSE_POPUP: { - // return state; - // } - // case BL.ComboboxActions.FUNCTION_SET_HIGHLIGHTED_INDEX: { - // return state; - // } - // case BL.ComboboxActions.FUNCTION_SELECT_ITEM: { - // return state; - // } default: throw new TypeError(`Unhandled action: ${action.type} for Tag Editor Reducer`); } diff --git a/proof-of-concepts/features/stories/combobox/types.ts b/proof-of-concepts/features/stories/combobox/types.ts new file mode 100644 index 0000000..fec477e --- /dev/null +++ b/proof-of-concepts/features/stories/combobox/types.ts @@ -0,0 +1,101 @@ +import React from 'react'; +import { ComponentProps } from '../types'; +// Combobox +export type ComboboxProps = { + id?: string; + labelId?: string; + inputId?: string; + menuId?: string; + initialIsOpen?: boolean; + initialInputValue?: string; + initialHighlightedIndex?: number; + selectedItem?: Item; + items?: Item[]; + itemToString: (item: any) => string; + onSelectedItemChange?: (changes: Partial>) => void; + onInputValueChange?: (changes: Partial>) => void; + onHighightedIndexChange?: (changes: Partial>) => void; + onIsOpenChange?: (changes: Partial>) => void; +} & ComponentProps, ComboboxActionAndChanges>; + +export type ComboboxState = { + isOpen: boolean; + inputValue: string; + highlightedIndex: number; + selectedItem: Item | null; +}; + +// Prop Getters +export interface ComboboxLabelGetterProps { + id?: string; + inputId?: string; +} + +export interface ComboboxInputGetterProps { + controlDispatch?: (...args: any[]) => any; + ref?: React.MutableRefObject; + onFocus?: (...args: any[]) => any; + onBlur?: (...args: any[]) => any; + onKeyDown?: (...args: any[]) => any; + ariaDescribedBy: string; + ariaLabelledBy: string; +} + +export interface ComboboxGetterProps { + ariaLabelledBy: string; + ariaDescribedBy: string; + ariaPopup?: ComboboxAriaPopup; +} + +export type ComboboxAriaAutoComplete = 'none' | 'list' | 'both'; +export type ComboboxAriaPopup = + | boolean + | 'dialog' + | 'menu' + | 'grid' + | 'false' + | 'true' + | 'listbox' + | 'tree'; + +export interface ComboboxPopupProps { + ariaLabel?: string; + role?: string; + ref?: React.MutableRefObject; +} + +// Combobox reducer +export enum ComboboxActions { + INPUT_KEYDOWN_ARROW_UP = '[input-keydown-arrow-up]', + INPUT_KEYDOWN_ARROW_DOWN = '[input-keydown-arrow-down]', + INPUT_KEYDOWN_ARROW_LEFT = '[input-keydown-arrow-left]', + INPUT_KEYDOWN_ARROW_RIGHT = '[input-keydown-arrow-right]', + INPUT_KEYDOWN_ESCAPE = '[input-keydown-escape]', + INPUT_KEYDOWN_DELETE = '[input-keydown-delete]', + INPUT_KEYDOWN_ENTER = '[input-keydown-enter]', + INPUT_KEYDOWN_HOME = '[input-keydown-home]', + INPUT_KEYDOWN_END = '[input-keydown-end]', + ITEM_CLICK = '[item-click]', + INPUT_BLUR = '[input-blur]', + INPUT_VALUE_CHANGE = '[input-value-change]', + FUNCTION_OPEN_POPUP = '[function-open-popup]', + FUNCTION_CLOSE_POPUP = '[function-close-popup]', + FUNCTION_SET_HIGHLIGHTED_INDEX = '[function-set-highlighted-index]', + FUNCTION_SELECT_ITEM = '[function-select-item]' +} + +export type ComboBoxStateChangeTypes = ComboboxActions; + +export type ComboboxActionAndChanges = { + changes: ComboboxState; + action: ComboboxAction; +}; + +export interface ComboboxAction { + type: ComboBoxStateChangeTypes; + getItemFromIndex?: (index: number) => any; + index?: number; + inputValue?: string; + props?: ComboboxProps; + selectedItem?: boolean; +} diff --git a/proof-of-concepts/features/stories/combobox/useCombobox.ts b/proof-of-concepts/features/stories/combobox/useCombobox.ts new file mode 100644 index 0000000..0666f26 --- /dev/null +++ b/proof-of-concepts/features/stories/combobox/useCombobox.ts @@ -0,0 +1,379 @@ +import * as React from 'react'; +import bottomlineComboboxReducer from './reducer'; +import { useElementId, computeInitialState } from './utils'; +import { + normalizeKey, + useControlledReducer, + mergeRefs, + callAllEventHandlers, + noop, + useMouseAndTracker +} from '../utils'; +import { + ComboboxProps, + ComboboxState, + ComboboxAction, + ComboboxActions, + ComboBoxStateChangeTypes, + ComboboxActionAndChanges, + ComboboxLabelGetterProps, + ComboboxGetterProps, + ComboboxAriaPopup, + ComboboxInputGetterProps, + ComboboxAriaAutoComplete, + ComboboxPopupProps +} from './types'; +import { ItemsList } from '../types'; + +export function useCombobox(props: ComboboxProps = {}) { + /** + * **************** + * + * Combobox State + * + * **************** + * + */ + const [state, dispatch] = useControlledReducer< + (state: ComboboxState, action: ComboboxAction) => ComboboxState, + ComboboxState, + ComboboxProps, + ComboBoxStateChangeTypes, + ComboboxActionAndChanges + >(bottomlineComboboxReducer, computeInitialState(props), props); + const { isOpen, highlightedIndex, inputValue } = state; + + /** + * ****** + * + * Refs + * + * ****** + * // stores the DOM reference to the label for the combobox + * // const labelRef = React.useRef(null); + * // stores the DOM reference to the popup box + * // const popupRef = React.useRef(null); + * // stores the DOM reference to the current item + * // const latestItem = React.useRef(null); + * + */ + // stores the DOM reference to the input element + const inputRef = React.useRef(null); + // stores the reference to the list of items + const itemsListRef = React.useRef({}); + // stores the DOM reference to the popup element + const popupRef = React.useRef(); + + /** + * ************************* + * + * Accessibility Identifiers + * + * ************************* + * + */ + // Returns accessibility identifiers and identifiers for items + const elementIds = useElementId(props); + + /** + * ************************* + * + * Items List (Combobox Items) + * + * ************************* + * + */ + if (props.items) { + props.items.forEach((item, index) => { + itemsListRef.current[elementIds.getItemId(index)] = item; + }); + } + // fetches the item based on the index as argument + const getItemFromIndex = React.useCallback( + (index: number) => itemsListRef.current[elementIds.getItemId(index)], + [elementIds] + ); + + /** + * ****************** + * + * Focus Management + * + * ****************** + * + * + */ + const mouseTrackerRef = useMouseAndTracker(isOpen, [inputRef, popupRef], () => { + dispatch({ + type: ComboboxActions.INPUT_BLUR + }); + }); + + React.useEffect(() => { + if (inputRef.current && isOpen) { + console.log('[USE_COMBOBOX_EFFECT] input focus'); + inputRef.current.focus(); + } + }, [isOpen]); + + // Area of concern: when a combobox receives focus, DOM focus is placed on the textbox element + // const setComboboxFocus = () => { + // console.log('[SET_COMBO_FOCUS] setComboboxFocus function'); + // if (document.activeElement !== inputRef.current && inputRef.current) { + // inputRef.current.focus(); + // } + // }; + // implement: when an item of the popup is box focused, DOM focus remains on thextbox + + /** + * ************** + * + * Event Handlers + * + * ************** + */ + const inputKeyDownHandlers: { + [eventHandler: string]: (e: React.KeyboardEvent) => void; + } = { + Enter: (e: React.KeyboardEvent) => { + e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ENTER, + getItemFromIndex + }); + }, + Escape: (e: React.KeyboardEvent) => { + e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ESCAPE, + getItemFromIndex + }); + }, + Delete: (e: React.KeyboardEvent) => { + e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_DELETE, + getItemFromIndex + }); + }, + ArrowRight: (e: React.KeyboardEvent) => { + // e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT, + getItemFromIndex + }); + }, + ArrowLeft: (e: React.KeyboardEvent) => { + // e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT, + getItemFromIndex + }); + }, + ArrowDown: (e: React.KeyboardEvent) => { + // e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN, + getItemFromIndex + }); + }, + ArrowUp: (e: React.KeyboardEvent) => { + // e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_ARROW_UP, + getItemFromIndex + }); + }, + Home: (e: React.KeyboardEvent) => { + e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_HOME, + getItemFromIndex + }); + }, + End: (e: React.KeyboardEvent) => { + e.preventDefault(); + dispatch({ + type: ComboboxActions.INPUT_KEYDOWN_END, + getItemFromIndex + }); + } + }; + + /** + * ************** + * + * Prop Getters + * + * ************** + * + * The combobox implements the ARIA 1.1 pattern + * https://www.w3.org/TR/wai-aria-practices/#wai-aria-roles-states-and-properties-6 + * + */ + + /** + * Use label props on the element that describes the combobox + */ + function getLabelProps(props?: ComboboxLabelGetterProps) { + const id = props?.id || elementIds.labelId; + const inputId = props?.inputId || elementIds.inputId; + return { + id, + htmlFor: inputId + }; + } + + /** + * @param {ComboboxAriaPopup} = {} as ComboboxGetterProps} { ariaPopup } + * in the ugliest, most-verbose way possible, we're saying + * if the user has defined a specific type of popup, then destructure + * otherwise, the user may have not passed any args, so default to an empty object + */ + function getComboboxProps(props: ComboboxGetterProps) { + const ariaHasPopup = props?.ariaPopup || ('grid' as ComboboxAriaPopup); + const ariaLabelledBy = props?.ariaLabelledBy || elementIds.labelId; + + // Implement: if the combobox has a visible label, the element with role combobox has aria-labelledby + // set to a value that refers to the labelling element. + // Otherwise, the combobox element has a label provided by aria-label. + return { + role: 'combobox', + + 'aria-expanded': isOpen ? true : false, + 'aria-owns': elementIds.menuId, + 'aria-haspopup': ariaHasPopup, + 'aria-labelledby': ariaLabelledBy + // 'aria-describedby': props.ariaDescribedBy + // onClick: setComboboxFocus + }; + } + + // here is the area that we need to focus on + // this is the entry point for handling the change event for the input value + // I think that we can add a debounce function here as a prop + // onchange prop will let the user control when the state updates + function getInputProps({ + onBlur = noop, + onFocus = noop, + controlDispatch, + onKeyDown = noop, + ...rest + }: ComboboxInputGetterProps) { + const inputKeyDownHandler = (e: React.KeyboardEvent) => { + const keyEvt = normalizeKey(e); + if (keyEvt.name in inputKeyDownHandlers) { + inputKeyDownHandlers[keyEvt.name](e); + } + }; + + const inputChangeHandler = (e: React.ChangeEvent) => { + const val = e.currentTarget.value; + if (controlDispatch) { + const fn = () => { + dispatch({ + type: ComboboxActions.INPUT_VALUE_CHANGE, + inputValue: val + }); + }; + controlDispatch(fn); + } else { + dispatch({ + type: ComboboxActions.INPUT_VALUE_CHANGE, + inputValue: val + }); + } + }; + + let eventHandlers = { + onKeyDown: callAllEventHandlers(inputKeyDownHandler, onKeyDown), + onChange: inputChangeHandler, + onBlur: onBlur, + onFocus + }; + return { + ref: mergeRefs(rest.ref, inputRef), + role: 'textbox', + 'aria-labelledby': `${elementIds.labelId}`, + 'aria-controls': elementIds.menuId, + 'aria-multiline': false, + 'aria-autocomplete': 'list' as ComboboxAriaAutoComplete, + // fancy way of saying: assign the id of the item that is stored in the list of items + // as the aria-activedescendant, otherwise, merge a "null" (false) value + ...(isOpen && + highlightedIndex > -1 && { + 'aria-activedescendant': elementIds.getItemId(highlightedIndex) + }), + // event handlers + ...eventHandlers + }; + } + + function getPopupProps( + { + role, + ariaLabel, + ref + }: { + ariaLabel?: string; + role?: string; + ref?: React.MutableRefObject; + } = {} as ComboboxPopupProps + ) { + let popupRole = 'grid'; + if (role) popupRole = role; + + return { + ref: mergeRefs(ref, popupRef), + role: popupRole, + id: elementIds.menuId, + 'aria-labelledby': elementIds.labelId + }; + } + + // items are excluded from the tab sequence + function getItemProps(index: number) { + const handleClick = () => { + // console.log('[GET_ITEM_PROPS] handle click'); + dispatch({ + type: ComboboxActions.ITEM_CLICK, + getItemFromIndex, + index, + selectedItem: true + }); + if (inputRef.current) { + // console.log('[GET_ITEM_PROPS] focus input'); + inputRef.current.focus(); + } + }; + + const selected = index === highlightedIndex; + return { + role: 'gridcell', + id: elementIds.getItemId(index), + 'aria-selected': selected, + onClick: handleClick + }; + } + + function getGridPopupRowProps() { + return { + role: 'row' + }; + } + + return { + // state + isOpen, + highlightedIndex, + inputValue, + // combobox-specific prop getters + getLabelProps, + getComboboxProps, + getInputProps, + getItemProps, + getPopupProps, + // combobox-grid prop getters + getGridPopupRowProps + }; +} diff --git a/proof-of-concepts/features/stories/combobox/utils.ts b/proof-of-concepts/features/stories/combobox/utils.ts new file mode 100644 index 0000000..0c10dc5 --- /dev/null +++ b/proof-of-concepts/features/stories/combobox/utils.ts @@ -0,0 +1,105 @@ +import React from 'react'; +import { ComboboxProps, ComboboxState, ComboboxActions } from './types'; +import { ComponentProps } from '../types'; +import { capitalizeString, generateId } from '../utils'; +export const initialState = { + selectedItem: null, + isOpen: false, + highlightedIndex: -1, + inputValue: '' +}; + +export function computeInitialState( + props: ComboboxProps +): ComboboxState { + const isOpen = getInitialValue(props, 'isOpen'); + const inputValue = getInitialValue(props, 'inputValue'); + const selectedItem = getInitialValue(props, 'selectedItem'); + let highlightedIndex = getInitialValue(props, 'highlightedIndex'); + // console.log('[COMPUTE_INITIAL_STATE]:highlightedIndex', highlightedIndex); + // console.log('items length:', props.items.length - 1); + if (props.items && highlightedIndex > props.items.length - 1) { + highlightedIndex = -1 as Partial>; + } + return { + isOpen, + inputValue, + highlightedIndex, + selectedItem + } as ComboboxState; +} + +/** + * Use the keys of state to get the initial values of properties defined in props + * Props and State share the same keys. + */ +export function getInitialValue( + props: ComboboxProps, + propKey: keyof ComboboxState +): Partial> { + if (propKey in props) { + return props[propKey as keyof ComboboxProps] as Partial< + ComboboxState + >; + } + + // get the user-provided initial prop state, it is a piece of state + const initialPropKey = `initial${capitalizeString(propKey)}` as keyof ComboboxProps< + Item + >; + if (initialPropKey in props) { + // console.log('initialPropKey', initialPropKey); + // console.log('initialPropKey', props[initialPropKey]); + return props[initialPropKey] as Partial>; + } + + // return values from statically defined initial state object + return initialState[propKey] as Partial>; +} + +/** + * Generates unique ids for the combobox component to avoid naming conflicts w/ other ids + */ +export function useElementId({ + id = `bottomline-${generateId()}`, + labelId, + inputId, + menuId +}: Partial>) { + const elementIds = React.useRef({ + id, + labelId: labelId || `${id}-label`, + inputId: inputId || `${id}-input`, + menuId: menuId || `${id}-menu`, + // computes id relative to other indices of items + getItemId: (index: number) => `${id}-item-${index}` + }); + return elementIds.current; +} + +export function getNewIndex( + currentIndex: number, + length: number, + action: ComboboxActions +): number { + switch (action) { + case ComboboxActions.INPUT_KEYDOWN_ARROW_DOWN: { + if (currentIndex === length - 1) return currentIndex; + return currentIndex + 1; + } + case ComboboxActions.INPUT_KEYDOWN_ARROW_UP: { + if (currentIndex === -1) return currentIndex; + return currentIndex - 1; + } + case ComboboxActions.INPUT_KEYDOWN_ARROW_LEFT: { + if (currentIndex === -1 || currentIndex === 0) return currentIndex; + return currentIndex - 1; + } + case ComboboxActions.INPUT_KEYDOWN_ARROW_RIGHT: { + if (currentIndex === length - 1 || currentIndex === -1) return currentIndex; + return currentIndex + 1; + } + default: + return 0; + } +} diff --git a/proof-of-concepts/features/stories/loader/Loader.scss b/proof-of-concepts/features/stories/loader/Loader.scss new file mode 100644 index 0000000..454736a --- /dev/null +++ b/proof-of-concepts/features/stories/loader/Loader.scss @@ -0,0 +1,38 @@ +@use '../styles/colors.scss'; +@use '../styles/spacing.scss'; + +.bottomline-search-loader { + display: inline-block; + margin: 2px; + width: spacing.$Size-0; + height: spacing.$Size-0; + border-radius: spacing.$Size-0; + background-color: colors.$Gray-6; + + opacity: 0.33; + animation-name: loader; + animation-duration: 0.8s; + animation-iteration-count: infinite; +} +.bottomline-search-loader:nth-child(1) { +} + +.bottomline-search-loader:nth-child(2) { + animation-delay: 0.12s; +} + +.bottomline-search-loader:nth-child(3) { + animation-delay: 0.24s; +} + +@keyframes loader { + 0% { + opacity: 0.33; + } + 25% { + opacity: 1; + } + 100% { + opacity: 0.33; + } +} diff --git a/proof-of-concepts/features/stories/loader/SearchLoader.tsx b/proof-of-concepts/features/stories/loader/SearchLoader.tsx new file mode 100644 index 0000000..e1c96d1 --- /dev/null +++ b/proof-of-concepts/features/stories/loader/SearchLoader.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import './Loader.scss'; +export function SearchLoader() { + return ( + + + + + + ); +} diff --git a/proof-of-concepts/features/stories/q/Editor.tsx b/proof-of-concepts/features/stories/q/Editor.tsx new file mode 100644 index 0000000..58cb176 --- /dev/null +++ b/proof-of-concepts/features/stories/q/Editor.tsx @@ -0,0 +1,226 @@ +import React, { KeyboardEvent } from 'react'; +import { ContentBlock, getDefaultKeyBinding, KeyBindingUtil } from 'draft-js'; +import { Tooltip } from '@chakra-ui/react'; +import { + BoldOutlinedIcon, + ItalicOutlinedIcon, + StrikethroughOutlinedIcon, + InlineCode, + Heading, + OrderedListOutlinedIcon, + UnorderedListOutlinedIcon, + DoubleQuotes, + TableOutlinedIcon, + FileImageOutlinedIcon +} from './assets/Icons'; + +export interface EditorIconProps { + className?: string; + isSelected?: boolean; + onClick?: () => void; + // ariaLabel: string; + label: string; + children: React.ReactNode; +} + +let INLINE_STYLE_FORMAT_CONTROLS = [ + { + label: 'Bold', + ariaLabel: 'Format text with a Bold Outline', + style: 'BOLD', + component: BoldOutlinedIcon + }, + { + label: 'Italic', + ariaLabel: 'Format text with an Italic Emphasis', + style: 'ITALIC', + component: ItalicOutlinedIcon + }, + { + label: 'Inline Code', + ariaLabel: 'Format text as an inline snippet of code', + style: 'CODE', + component: InlineCode + }, + { + label: 'Strikethrough', + ariaLabel: + 'Format text with a Strikethrough, a horizontal line through the center of text', + style: 'STRIKETHROUGH', + component: StrikethroughOutlinedIcon + } +]; + +export function InlineStyleControls(props) { + const currentStyle = props.editorState.getCurrentInlineStyle(); + return ( + + {INLINE_STYLE_FORMAT_CONTROLS.map((formatControl) => ( + + {formatControl.component()} + + ))} + + ); +} + +function FormatControl(props) { + const onToggle = (e: React.SyntheticEvent) => { + e.preventDefault(); + props.onToggle(props.style); + }; + + let className = 'format-option'; + if (props.active) { + className += ' format-option-active'; + } + + return ( + + ); +} + +let BLOCK_STYLE_FORMAT_CONTROLS = [ + { + label: 'Heading', + ariaLabel: 'Format text as a title', + style: 'header-one', + component: Heading + }, + { + label: 'Quote Block', + ariaLabel: 'Format text as a quote block', + style: 'blockquote', + component: DoubleQuotes + }, + { + label: 'Bullet Point List', + ariaLabel: 'Format text as a bullet point list', + style: 'unordered-list-item', + component: UnorderedListOutlinedIcon + }, + { + label: 'Numbered List', + ariaLabel: 'Format text as a numbered list', + style: 'ordered-list-item', + component: OrderedListOutlinedIcon + }, + { + label: 'Table', + ariaLabel: 'Insert a table', + style: null, + component: TableOutlinedIcon + }, + { + label: 'Add an Image', + ariaLabel: 'Add an image', + style: null, + component: FileImageOutlinedIcon + } +]; + +export function BlockStyleControls(props) { + const { editorState } = props; + const selection = editorState.getSelection(); + const blockType = editorState + .getCurrentContent() + .getBlockForKey(selection.getStartKey()) + .getType(); + // console.log(blockType); + return ( + + {BLOCK_STYLE_FORMAT_CONTROLS.map((formatControl) => ( + + {formatControl.component()} + + ))} + + ); +} + +export const styleMap = { + CODE: { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace', + fontSize: 16, + padding: 2 + }, + LINK: { + color: '#189AEF', + textDecoration: 'underline' + }, + HIGHLIGHT_LINK: { + backgroundColor: 'rgba(172,206,247, 0.8)' + }, + STRIKETHROUGH: { + textDecoration: 'line-through' + }, + blockquote: { + background: 'colors.$Gray-2', + 'background-color': 'colors.$Gray-2', + 'border-left': '5px solid colors.$Gray-4', + color: '#666', + 'font-family': + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + 'font-style': 'italic', + margin: '16px 0', + padding: '10px 20px' + } +}; + +export function blockStyleFn(contentBlock: ContentBlock) { + const type = contentBlock.getType(); + if (type === 'blockquote') { + return 'bottomline-editor__blockquote'; + } +} + +export function bottomlineEditorKeyBindingFn(e: KeyboardEvent): string | null { + const { hasCommandModifier } = KeyBindingUtil; + if (e.keyCode === 66 && hasCommandModifier(e)) { + return 'bold'; + } + if (e.keyCode === 75 && hasCommandModifier(e)) { + return 'link'; + } + if (e.keyCode === 73 && hasCommandModifier(e)) { + return 'italic'; + } + if (e.keyCode === 222 && e.shiftKey && hasCommandModifier(e)) { + return 'blockquote'; + } + if (e.keyCode === 56 && e.shiftKey && hasCommandModifier(e)) { + return 'bulleted-list'; + } + if (e.keyCode === 55 && e.shiftKey && hasCommandModifier(e)) { + return 'numbered-list'; + } + if (e.keyCode === 13 && e.shiftKey) { + return 'newline'; + } + if (e.keyCode === 8 || e.keyCode === 46) { + return 'backspace'; + } + return getDefaultKeyBinding(e); +} diff --git a/proof-of-concepts/features/stories/q/Q.scss b/proof-of-concepts/features/stories/q/Q.scss new file mode 100644 index 0000000..4994a7d --- /dev/null +++ b/proof-of-concepts/features/stories/q/Q.scss @@ -0,0 +1,432 @@ +@use '../styles/fonts'; +@use '../styles/colors'; +@use '../styles/spacing'; + +.ask-question { + width: 100%; + max-width: 610px; + border: 1px solid colors.$Gray-3; + border-radius: 4px; + box-shadow: 0px 4px 6px rgba(17, 24, 39, 0.2); +} + +.ask-question__section { + border-bottom: 1px solid colors.$Gray-3; + padding: spacing.$Size-5 spacing.$Size-5 spacing.$Size-2 spacing.$Size-5; +} + +.ask-question__section:not(:nth-child(1)) { + margin-top: spacing.$Size-1; +} + +.ask-question__section-header { + display: flex; +} + +.ask-question__section-heading { + display: flex; + flex-direction: column; +} + +.ask-question__section-title { + font-size: fonts.$Size-1; + font-weight: fonts.$Medium; +} + +.ask-question__section-info { + font-size: fonts.$Size-0; + margin: 0; +} + +.ask-question__section-icon { + width: 30px; + height: 30px; + color: colors.$Blue-6; + margin: 0; +} + +.ask-question__section-input { + margin-top: 8px; +} + +/************************* + * Section: Create Title * + *************************/ + +/* Change the white to any color */ +.ask-question__title-input, +.ask-question__title-input-error { + box-sizing: border-box; + width: 100%; + border-radius: 4px; + padding: spacing.$Size-2 spacing.$Size-4 spacing.$Size-2 spacing.$Size-4; + border: 1px solid colors.$Gray-4; + font-size: fonts.$Size-2; + color: colors.$Gray-9; +} + +.ask-question__title-input:focus { + outline: none; + border: 1px solid #0978d9; + box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3); +} + +.ask-question__title-input-error { + border: 1px solid colors.$Red-7; +} + +.ask-question__title-input-error:focus { + outline: none; + box-shadow: 0px 0px 0px 2px rgba(207, 19, 34, 0.3); +} + +.ask-question__title-input::selection { + background: #b4d5ff; +} + +.ask-question__title-input[type='text']:focus, +.ask-question__title-input[type='text']:active { + background-color: white; +} + +/*********************************** + * Section: Formatting Bar, Editor * + ***********************************/ + +.editor__formatting-bar { + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + border-top-right-radius: 4px; + border-top-left-radius: 4px; + border-top: 1px solid colors.$Gray-4; + border-left: 1px solid colors.$Gray-4; + border-right: 1px solid colors.$Gray-4; + overflow-x: scroll; + padding: 8px; +} + +.format-option { + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + width: 28px; + height: 28px; + padding: 0 2px 0 2px; + font-size: fonts.$Size-4; + margin-right: spacing.$Size-1; + cursor: pointer; + /* Color property is for styling the stroke of the Icons */ + color: colors.$Gray-5; + background-color: white; + border: none; +} + +.format-option:focus { + border: 1px solid #0978d9; + outline: none; +} + +.format-option-active { + color: colors.$Gray-9; + border-radius: 4px; + outline: none; +} + +.format-option:hover { + background-color: colors.$Gray-2; +} + +/******************* + * Section: Answer * + *******************/ +.answer-question { + display: flex; + align-items: center; +} + +.answer-question__flag { + margin-right: spacing.$Size-2; + cursor: pointer; +} + +.answer-question__button-icon { + vertical-align: text-top !important; +} + +.answer-question__heading { + font-size: fonts.$Size-2; + font-weight: fonts.$Regular; + cursor: pointer; +} + +/***************** + * Section: Post * + *****************/ +.post-question { + display: flex; + justify-content: flex-end; +} + +.post-question__button { + font-size: fonts.$Size-2; + position: static !important; + border: 1px solid colors.$Blue-5; + background-color: colors.$Blue-5; + color: white; + height: 48px !important; + width: 166px !important; + border-radius: 4px; +} + +// .post-question__button:hover, +// .post-question__button:focus { +// background-color: colors.$Blue-6; +// border: 1px solid colors.$Blue-6; +// } + +.unselectable { + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + + /* + Introduced in Internet Explorer 10. + See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/ + */ + -ms-user-select: none; + user-select: none; +} + +/******************************** + * Section: Field Error Message * + ********************************/ +.field-error { + display: flex; + align-items: center; + margin-top: spacing.$Size-1; +} + +.field-error__icon { + color: colors.$Red-7; + padding-right: spacing.$Size-1; + height: 18px; + width: 18px; +} + +.field-error__message, +.field-error__group-message { + color: colors.$Red-7; + font-size: fonts.$Size-1; +} + +.field-error-group { + display: flex; + align-items: center; + flex-wrap: wrap; + margin-top: spacing.$Size-1; + row-gap: spacing.$Size-0; +} + +.field-error__group-message { + display: inline-flex; + align-items: center; +} + +/** + * ********************************* + * Draft JS Styling + * ********************************* + * + * Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. + * + * This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.RichEditor-root { + background: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 14px; + padding: 15px; +} + +.RichEditor-editor { + cursor: text; + font-size: 16px; + border: 1px solid #9ca3af; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} +.RichEditor-editor:focus-within { + border: 1px solid #0978d9 !important; + border-radius: 4px !important; + box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3) !important; +} + +.RichEditor-editor .public-DraftEditorPlaceholder-root, +.RichEditor-editor .public-DraftEditor-content, +.RichEditor-editor-error .public-DraftEditorPlaceholder-root, +.RichEditor-editor-error .public-DraftEditor-content { + padding: 15px; +} + +.RichEditor-editor, +.public-DraftEditor-content, +.RichEditor-editor-error { + min-height: 18rem; +} + +.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root { + display: none; +} + +.public-DraftEditor-content, +.public-DraftEditor-content-error { + border: none; + border-radius: none; + color: colors.$Gray-9; +} + +.RichEditor-editor-error { + border: 1px solid colors.$Red-7; +} +.RichEditor-editor-error:focus-within { + outline: none; + border-radius: 4px; + box-shadow: 0px 0px 0px 2px rgba(207, 19, 34, 0.3); +} + +.bottomline-editor__blockquote { + border-left: 5px solid #85a5ff; + background-color: #f0f5ff; + color: colors.$Gray-7; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-style: italic; + padding: spacing.$Size-1 spacing.$Size-5; +} + +.RichEditor-editor .public-DraftStyleDefault-pre { + background-color: rgba(0, 0, 0, 0.05); + font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; + font-size: 16px; + padding: 20px; +} + +.RichEditor-controls { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 14px; + margin-bottom: 5px; + user-select: none; +} + +.RichEditor-styleButton { + color: #999; + cursor: pointer; + margin-right: 16px; + padding: 2px 0; + display: inline-block; +} + +.RichEditor-activeButton { + color: #5890ff; +} + +.bottomline-link { + font-family: system-ui; + text-decoration: underline; + font-size: 1rem; + color: #189afe; +} + +.draft-text-highlight { + background-color: rgba(172, 206, 247, 0.8); +} + +/** + * ********************** + * Top-Level Page Styling + * ********************** + * + * The following styles are for the ask question page layout. + * The layout involves separate components. + * If the component structure of these components should change, + * the layout may perhaps change. + */ + +.question-container { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: spacing.$Size-8; +} + +.question-ask { + grid-column-start: 1; + grid-row-start: 1; +} + +.question-review { + grid-column-start: 2; + height: fit-content; +} + +@media only screen and (max-width: 650px) { + .question-container { + max-width: 100%; + } + .ask-question { + width: 100vw; + } + .question-container { + display: grid; + grid-template-columns: 1fr; + row-gap: spacing.$Size-6; + } + .question-ask { + grid-row-start: 2; + } + .question-review { + grid-row-start: 1; + grid-column-start: 1; + max-width: 610px; + width: 100vw !important; + } + .review-section__button { + display: grid; + grid-template-columns: 1fr 18fr 2fr !important; + } + .review-section__info { + margin: spacing.$Size-2 spacing.$Size-2 spacing.$Size-2 spacing.$Size-8 !important; + } + .review-error-container { + grid-template-columns: 1fr 14fr !important; + } + .review-section-container { + grid-template-columns: 1fr 14fr !important; + } +} + +.tag-error { + display: inline-flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-radius: 6px; + border: 1px solid colors.$Red-7; +} +.tag-error:focus-within { + outline: none; + box-shadow: 0px 0px 0px 2px rgba(207, 19, 34, 0.3); +} diff --git a/proof-of-concepts/features/stories/q/Q.stories.tsx b/proof-of-concepts/features/stories/q/Q.stories.tsx new file mode 100644 index 0000000..9eb5fca --- /dev/null +++ b/proof-of-concepts/features/stories/q/Q.stories.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import Q from './Q'; +export default { + title: 'Example/Q', + component: Q, + argTypes: { + backgroundColor: { control: 'color' } + } +} as ComponentMeta; + +export const Mockup: ComponentStory = (args) => { + return ; +}; diff --git a/proof-of-concepts/features/stories/q/Q.tsx b/proof-of-concepts/features/stories/q/Q.tsx new file mode 100644 index 0000000..fe652f1 --- /dev/null +++ b/proof-of-concepts/features/stories/q/Q.tsx @@ -0,0 +1,456 @@ +import * as React from 'react'; +import { + Editor, + EditorState, + CompositeDecorator, + RichUtils, + DraftHandleValue, + convertToRaw +} from 'draft-js'; +import { stateFromMarkdown } from 'draft-js-import-markdown'; +import { stateToMarkdown } from 'draft-js-export-markdown'; +import { Input, Tooltip, Box } from '@chakra-ui/react'; +import { TagEditor } from '../tags/TagEditor/TagEditor'; +import { AddIcon } from '@chakra-ui/icons'; +import { AiOutlineExclamationCircle } from 'react-icons/ai'; +import { + BlockStyleControls, + InlineStyleControls, + styleMap, + blockStyleFn, + bottomlineEditorKeyBindingFn +} from './Editor'; +import { useLinkEditor } from './components/link/Link'; +import { findLinkEntities, cursorIsOnSingleBlock } from './components/link/utils'; +import { + RegularLink, + LinkEditor, + LinkDetails, + LinkInlineControl +} from './components/link/BottomlineLink'; +import { linkStateReducer } from './components/link/reducer'; +import './components/link/link.scss'; +import './Q.scss'; +import { + Form, + Field, + useField, + useFormState, + FieldInputProps +} from 'react-final-form'; +import createDecorator from 'final-form-focus'; +import { Review } from './components/reviewSteps/Review'; +import { Question, QuestionError, QuestionProps } from './types'; + +const Error = ({ name }: { name: string }) => { + const { meta } = useField(name, { + subscription: { error: true, submitFailed: true, valid: true } + }); + const { error, submitFailed, valid } = meta; + + return submitFailed && !valid && error ? ( + Array.isArray(error) ? ( + + ) : ( + + ) + ) : null; +}; + +const GroupError = ({ errors, name }: { errors: string[]; name: string }) => { + return ( +
    + {errors.map((error) => ( + + ))} +
    + ); +}; + +const SingleError = ({ error, name }: { error: string; name: string }) => ( +
    + + +
    +); + +export default function Q({ + tagsEndpoint = 'http://localhost:3000/tags', + ...questionProps +}: QuestionProps) { + const decorator = new CompositeDecorator([ + { + strategy: findLinkEntities, + component: RegularLink + } + ]); + // Draft-js editor state + const [state, setState] = React.useState( + EditorState.createEmpty(decorator) + ); + + const { + showDetails: linkShowDetails, + showEditor: linkShowEditor, + url, + text, + coords, + openLinkEditor, + removeLink, + closeLinkEditor, + updateLink, + // getLinkProps, + toggleLinkEditor + } = useLinkEditor({ editorState: state, editorSetState: setState }); + + // for transition animation + const [inProp, setInProp] = React.useState(false); + + const onChange = ( + editorState: EditorState, + rffProps: FieldInputProps + ) => { + setState(editorState); + // rffProps.onChange(editorState.getCurrentContent().getPlainText('\u0001')); + rffProps.onChange(stateToMarkdown(editorState.getCurrentContent()).trim()); + }; + + const handleKeyCommand = (command: string): DraftHandleValue => { + let newState; + switch (command) { + case 'bold': + toggleInlineStyle('BOLD'); + break; + case 'link': + toggleLinkEditor(); + break; + case 'italic': + toggleInlineStyle('ITALIC'); + break; + case 'blockquote': + toggleBlockType('blockquote'); + break; + case 'bulleted-list': + toggleBlockType('unordered-list-item'); + break; + case 'numbered-list': + toggleBlockType('ordered-list-item'); + break; + case 'newline': + newState = RichUtils.insertSoftNewline(state); + setState(newState); + break; + case 'backspace': + let startKey = state.getSelection().getStartKey(); + let selectedBlock = state.getCurrentContent().getBlockForKey(startKey); + const blockType = selectedBlock.getType(); + const text = selectedBlock.getText(); + if ( + text.length === 0 && + (blockType === 'blockquote' || + blockType === 'bulleted-list' || + blockType === 'numbered-list') + ) { + toggleBlockType(blockType); + break; + } else { + return 'not-handled'; + } + break; + default: + return 'not-handled'; + } + + return 'handled'; + }; + const toggleBlockType = (blockType: string) => { + setState(RichUtils.toggleBlockType(state, blockType)); + }; + const toggleInlineStyle = (inlineStyle: string) => { + setState(RichUtils.toggleInlineStyle(state, inlineStyle)); + }; + const toggleLink = () => { + setState(RichUtils.toggleLink(state, state.getSelection(), '')); + }; + // State for enabling/disabling the formatting bar link button + const [disableLinkControl, setDisableLinkControl] = React.useState(false); + // State for signaling when the editor has/doesn't have focus + const [editorFocus, setEditorFocus] = React.useState(false); + // Ref for focusing the editor + const editorRef = React.useRef(null); + const editorErrorRef = React.useRef(false); + + // We need this effect because when the link-editor is closed, we want to refocus the editor + React.useEffect(() => { + if (linkShowEditor === false) { + onEditorFocus(); + } + }, [linkShowEditor]); + + /** + * Enables/disables the formatting bar link button based on user cursor position in editor + * Disabled: user's cursor range starts and ends on different lines ("blocks" in draft-js terms) + * @dependency state.getSelection().getStartOffset() - the anchor index of the user's cursor + * @dependency state.getSelection().getEndOffset() - the focus index of the user's cursor + */ + React.useEffect(() => { + const selectionBlockID = state.getSelection().getStartKey(); + const selectionBlockEndID = state.getSelection().getEndKey(); + if (cursorIsOnSingleBlock(selectionBlockID, selectionBlockEndID) === false) { + setDisableLinkControl(true); + } else { + setDisableLinkControl(false); + } + }, [state.getSelection().getStartOffset(), state.getSelection().getEndOffset()]); + + /* FIX: Need refactor? */ + const onEditorFocus = () => { + setEditorFocus(true); + }; + + const handleOpen = () => { + // if the editor isnt focused and has content + // the link control button does nothing - move to be an effect + if (editorFocus === false && state.getCurrentContent().hasText()) { + return; + } else if ( + editorFocus === false && + state.getCurrentContent().hasText() === false + ) { + onEditorFocus(); + } + }; + + const onSubmit = (formData: Question) => { + if (questionProps && questionProps.onSubmit) { + questionProps.onSubmit(formData); + } + }; + + const minLength = (field: string, requiredLen: number) => + field.length >= requiredLen; + const exceeds = (field: string, requiredLen: number) => + minLength(field, requiredLen); + + const validate = (values: Question) => { + const { title, body, tags } = values; + const errors: QuestionError = {}; + if (!title) { + errors.title = 'Title is missing.'; + } else if (!exceeds(title, 15)) { + errors.title = `Title must be at least 15 characters. You entered ${title.length} characters.`; + } + + if (!body) { + errors.body = 'Body is missing.'; + } else if (!exceeds(body, 30)) { + errors.body = `Body must be at least 30 characters long, you entered ${body.length} characters.`; + } + + if (tags && tags.length > 0) { + let tagsWithManyChars = tags.filter((tag) => exceeds(tag.name, 35)); + let tagErrs = tagsWithManyChars.map( + (tag) => + `The tag '${tag.name}' is too long. The maximum length is 35 characters.` + ); + if (tagErrs.length > 0) errors.tags = tagErrs; + } else { + errors.tags = 'Please enter at least one tag.'; + } + return errors; + }; + + const focusOnErrors = React.useMemo(() => createDecorator(), []); + + return ( +
    + {({ submitting, submitFailed, handleSubmit, hasValidationErrors, errors }) => { + return ( +
    + + +
    +
    +
    +
    + +

    + Be specific and try to imagine that you’re asking another + person your question. +

    +
    +
    +
    + + {(props) => ( + + )} + +
    + +
    + +
    +
    +
    + +

    + Include all context and information one would need to answer + your question. +

    +
    +
    +
    +
    +
    + + + +
    + + + {(props) => ( +
    + + onChange(editorState, props.input) + } + blockStyleFn={blockStyleFn} + /> +
    + )} +
    + + +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + + {(props) => ( + + )} + +
    + +
    +
    +
    + +
    +
    +
    + +
    + ); + }} + + ); +} diff --git a/proof-of-concepts/features/stories/q/__tests__/Q.test.ts b/proof-of-concepts/features/stories/q/__tests__/Q.test.ts new file mode 100644 index 0000000..357963b --- /dev/null +++ b/proof-of-concepts/features/stories/q/__tests__/Q.test.ts @@ -0,0 +1,166 @@ +import React from 'react'; +import { Question } from '../types'; +import { BottomlineTag, BottomlineTags } from '../../tags/TagEditor/types'; +import { renderQuestionForm } from './utils'; +import userEvent from '@testing-library/user-event'; +import { createEvent } from '@testing-library/dom'; +import { screen, fireEvent } from '@testing-library/react'; +import { build, fake, oneOf, sequence } from '@jackfranklin/test-data-bot'; +import { server, rest } from '../../test/server'; + +// simulate network request/mock server and response +const tagsMockEndpoint = 'http://localhost:3000/tags'; + +const buildTag = build({ + fields: { + name: oneOf( + 'materialist-theory', + 'prison-abolition', + 'east-asian-american-history' + ) + } +}); + +const buildQuestionForm = build({ + fields: { + title: 'some_random_title', + tags: [] + }, + postBuild: (question) => { + question.tags = Array(3) + .fill(undefined) + .map(() => buildTag()); + return question; + } +}); + +describe('bottomline ask a question', () => { + test('submitting the form calls onSubmit with the title, body, and tags', () => { + const handleSubmit = jest.fn(); + const body = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const { tagsElement, titleElement, bodyElement } = renderQuestionForm({ + tagsEndpoint: tagsMockEndpoint, + onSubmit: handleSubmit + }); + + const { title, tags } = buildQuestionForm(); + userEvent.type(titleElement, title); + tags.forEach((tag) => { + userEvent.type(tagsElement, tag.name); + userEvent.type(tagsElement, '{enter}'); + }); + + const event = createEvent.paste(bodyElement, { + clipboardData: { + types: ['text/plain'], + getData: () => { + return body; + } + } + }); + + fireEvent(bodyElement, event); + userEvent.click(screen.getByTestId('submit')); + expect(handleSubmit).toHaveBeenCalledTimes(1); + expect(handleSubmit).toHaveBeenCalledWith({ title, body, tags }); + }); + + test('omitting the title results in an error', () => { + const handleSubmit = jest.fn(); + const body = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const { tagsElement, titleElement, bodyElement } = renderQuestionForm({ + tagsEndpoint: tagsMockEndpoint, + onSubmit: handleSubmit + }); + + const { title, tags } = buildQuestionForm(); + + // OMIT: the title + // userEvent.type(titleElement, title); + + tags.forEach((tag) => { + userEvent.type(tagsElement, tag.name); + userEvent.type(tagsElement, '{enter}'); + }); + + const event = createEvent.paste(bodyElement, { + clipboardData: { + types: ['text/plain'], + getData: () => { + return body; + } + } + }); + + fireEvent(bodyElement, event); + userEvent.click(screen.getByTestId('submit')); + const titleError = screen.getByText('Title is missing.'); + expect(titleError).toBeDefined(); + }); + test('omitting the body results in an error', () => { + const handleSubmit = jest.fn(); + const body = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const { tagsElement, titleElement, bodyElement } = renderQuestionForm({ + tagsEndpoint: tagsMockEndpoint, + onSubmit: handleSubmit + }); + + const { title, tags } = buildQuestionForm(); + + userEvent.type(titleElement, title); + + tags.forEach((tag) => { + userEvent.type(tagsElement, tag.name); + userEvent.type(tagsElement, '{enter}'); + }); + + // OMIT: the body + // const event = createEvent.paste(bodyElement, { + // clipboardData: { + // types: ['text/plain'], + // getData: () => { + // return body; + // } + // } + // }); + + // fireEvent(bodyElement, event); + + userEvent.click(screen.getByTestId('submit')); + const titleError = screen.getByText('Body is missing.'); + expect(titleError).toBeDefined(); + }); + test('omitting tags results in an error', () => { + const handleSubmit = jest.fn(); + const body = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const { tagsElement, titleElement, bodyElement } = renderQuestionForm({ + tagsEndpoint: tagsMockEndpoint, + onSubmit: handleSubmit + }); + + const { title, tags } = buildQuestionForm(); + + userEvent.type(titleElement, title); + + // OMIT: tags + // tags.forEach((tag) => { + // userEvent.type(tagsElement, tag.name); + // userEvent.type(tagsElement, '{enter}'); + // }); + + const event = createEvent.paste(bodyElement, { + clipboardData: { + types: ['text/plain'], + getData: () => { + return body; + } + } + }); + + fireEvent(bodyElement, event); + + userEvent.click(screen.getByTestId('submit')); + const titleError = screen.getByText('Please enter at least one tag.'); + expect(titleError).toBeDefined(); + }); +}); diff --git a/proof-of-concepts/features/stories/q/__tests__/utils.tsx b/proof-of-concepts/features/stories/q/__tests__/utils.tsx new file mode 100644 index 0000000..02655fc --- /dev/null +++ b/proof-of-concepts/features/stories/q/__tests__/utils.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Q from '../Q'; +import { QuestionProps } from '../types'; +import { render, screen } from '@testing-library/react'; + +const labelId = { + title: 'Title', + body: 'Body', + tags: 'Tags' +}; + +export function renderQuestionForm(props: QuestionProps) { + const container = render(); + + const titleElement = screen.getByLabelText(labelId.title); + const bodyElement = screen.getByLabelText(labelId.body); + const tagsElement = screen.getByLabelText(labelId.tags); + + return { + titleElement, + tagsElement, + bodyElement + }; +} diff --git a/proof-of-concepts/features/stories/q/assets/CircleOne.tsx b/proof-of-concepts/features/stories/q/assets/CircleOne.tsx new file mode 100644 index 0000000..fa96f3c --- /dev/null +++ b/proof-of-concepts/features/stories/q/assets/CircleOne.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import {QuestionIconProps} from './QuestionIcon' + +export default function CircleOne(props: QuestionIconProps) { + return ( +
    + + + + + +
    + ) +} diff --git a/proof-of-concepts/features/stories/q/assets/CircleThree.tsx b/proof-of-concepts/features/stories/q/assets/CircleThree.tsx new file mode 100644 index 0000000..590949e --- /dev/null +++ b/proof-of-concepts/features/stories/q/assets/CircleThree.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import {QuestionIconProps} from './QuestionIcon' + +export default function CircleThree(props: QuestionIconProps) { + return ( +
    + + + + + + +
    + ) +} diff --git a/proof-of-concepts/features/stories/q/assets/CircleTwo.tsx b/proof-of-concepts/features/stories/q/assets/CircleTwo.tsx new file mode 100644 index 0000000..c68179e --- /dev/null +++ b/proof-of-concepts/features/stories/q/assets/CircleTwo.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import {QuestionIconProps} from './QuestionIcon' + +export default function CircleTwo(props: QuestionIconProps) { + return ( +
    + + + + + +
    + ) +} diff --git a/proof-of-concepts/features/stories/q/assets/Icons.tsx b/proof-of-concepts/features/stories/q/assets/Icons.tsx new file mode 100644 index 0000000..5d1d589 --- /dev/null +++ b/proof-of-concepts/features/stories/q/assets/Icons.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import {Icon} from '@chakra-ui/react' + +export const BoldOutlinedIcon = () => ( + + + +) + +export const ItalicOutlinedIcon = () => ( + + + +) + +export const LinkOutlinedIcon = () => ( + + + +) + +export const StrikethroughOutlinedIcon = () => ( + + + +) + +export const InlineCode = () => ( + + + +) + +export const Heading = () => ( + + + +) + +export const OrderedListOutlinedIcon = () => ( + + + +) + +export const UnorderedListOutlinedIcon = () => ( + + + +) + +export const DoubleQuotes = () => ( + + + +) + +export const CodeOutlinedIcon = () => ( + + + +) + +export const TableOutlinedIcon = () => ( + + + +) + +export const FileImageOutlinedIcon = () => ( + + + +) +export const QuestionCircleOutlinedIcon = () => ( + + + +) diff --git a/proof-of-concepts/features/stories/q/components/link/BottomlineLink.tsx b/proof-of-concepts/features/stories/q/components/link/BottomlineLink.tsx new file mode 100644 index 0000000..65be97a --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/BottomlineLink.tsx @@ -0,0 +1,242 @@ +import React from 'react'; +import { LinkOutlinedIcon } from '../../assets/Icons'; +import { LinkCoordinates } from './types'; +export interface LinkDetailsProps { + shouldShow: boolean; + onDetailsChange: (control: LinkControl) => void; + removeLink: (e: React.SyntheticEvent) => void; + control: LinkControl; + coords: LinkCoordinates | undefined; + url: string; +} + +export function LinkDetails({ + shouldShow, + onDetailsChange, + removeLink, + control, + coords, + url +}: LinkDetailsProps) { + if (!shouldShow) return null; + const position = { + top: coords!.bottom + 16 + window.scrollY, + left: coords!.left - 20, + position: 'absolute' + } as React.CSSProperties; + + return ( +
    + {url} + - + + | + +
    + ); +} + +export interface LinkEditorProps { + onBlur: (event: React.SyntheticEvent) => void; + ref: React.RefObject; + shouldShow: boolean; + updateLink: (obj: any) => void; + coords: LinkCoordinates | undefined; + url: string; + text: string; +} + +export function LinkEditor({ + onBlur, + // ref, + shouldShow, + updateLink, + coords, + url, + text +}: LinkEditorProps) { + if (!shouldShow) return null; + + const position = { + top: coords!.bottom + 16 + window.scrollY, + left: coords!.left - 20, + position: 'absolute' + } as React.CSSProperties; + + // Ref for trapping focus in the editor when the user opens the editor + const linkEditorRef = React.useRef(null); + + React.useEffect(() => { + if (shouldShow && linkEditorRef.current) { + linkEditorRef.current.focus(); + } + }, [shouldShow]); + + // add validation for a correct url and show an error message + // use chakra design here + const [state, setState] = React.useState({ url: '', text }); + const [error, setError] = React.useState(false); + // const [focusFromDraftToEditor, setFocusFromDraftToEditor] = React.useState(true); + const urlInputRef = React.useRef(null); + React.useEffect(() => { + if (error && urlInputRef.current) { + urlInputRef.current.focus(); + } + }, [error]); + + const handleOnChange = (event: React.ChangeEvent) => { + let name = event.currentTarget.name; + let value = event.currentTarget.value; + setState((state) => ({ + ...state, + [name]: value + })); + + if (error) { + const res = urlIsValid(state.url); + if (res) { + setError(false); + } + } + }; + + const handleUpdate = (e: React.SyntheticEvent) => { + e.preventDefault(); + if (urlIsValid(state.url)) { + updateLink(state); + } else { + setError(true); + } + }; + + const handleBlur = (e: React.FocusEvent) => { + // the behavior that we're trying to capture here is + // the link editor should call the callback if the user leaves focus from the component + if (!e.currentTarget.contains(e.relatedTarget as HTMLElement)) { + console.log('[BOTTOMLINE_LINK] handle blur'); + onBlur(e); + } + }; + + const editorInputButtonClassname = error + ? 'link-editor__button--disabled' + : 'link-editor__button'; + + return ( +
    +
    +
    + + +
    +
    + + +
    +
    + {error ? ( + The url doesn't look right + ) : null} + +
    +
    +
    + ); +} +/** + * Determines whether or not the url is valid, excluding the url-scheme + * Regex test + */ +function urlIsValid(url: string): boolean { + const validURL = /^(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i; + const urlExp = new RegExp(validURL); + return urlExp.test(url); +} + +export type LinkControl = 'EDITOR_CONTROL' | 'LINK_DETAILS'; + +export interface LinkControlProps { + editorState: EditorState; + onToggle: (control: LinkControl) => void; + control: LinkControl; + active?: boolean; + disabled?: boolean; +} + +export function LinkInlineControl(props: LinkControlProps) { + const onToggle = (e: React.SyntheticEvent) => { + e.preventDefault(); + props.onToggle(props.control); + }; + const ariaLabel = 'Insert Link'; + const key = 'link-inline-control' + ariaLabel; + let className = 'format-option'; + if (props.active) { + className += ' format-option-active'; + } + + return ( + + ); +} + +export interface RegularLinkProps { + contentState: ContentState; + children: React.ReactChildren; + entityKey: string; +} + +export function RegularLink(props: RegularLinkProps) { + const { url } = props.contentState.getEntity(props.entityKey).getData(); + + return ( + + {props.children} + + ); +} diff --git a/proof-of-concepts/features/stories/q/components/link/Link.tsx b/proof-of-concepts/features/stories/q/components/link/Link.tsx new file mode 100644 index 0000000..6462e69 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/Link.tsx @@ -0,0 +1,414 @@ +import React from 'react'; +import { + LinkState, + LinkAction, + LinkProps, + LinkEditorStateChangeTypes, + LinkActionAndChanges +} from './types'; +import { useControlledReducer } from '../../../utils'; +import { + cursorMatchesSingleLinkRange, + cursorIsOnSingleBlock, + hasHTTPS, + cursorIsSelectingOriginalText, + getSelectionIndices, + getSelectionBlockProps, + initialLinkState, + findLinkEntities +} from './utils'; +import { linkStateReducer } from './reducer'; +import { Modifier, SelectionState, RichUtils, EditorState } from 'draft-js'; + +export function useLinkEditor(props: LinkProps) { + const [state, controlledDispatch] = useControlledReducer< + (state: LinkState, action: LinkAction) => LinkState, + LinkState, + LinkProps, + LinkEditorStateChangeTypes, + LinkActionAndChanges + >(linkStateReducer, initialLinkState, props); + + const { editorState, editorSetState } = props; + const { + linkSelection, + textSelection, + showDetails, + showEditor, + url, + text, + coords + } = state; + + /** + * ******************************** + * + * Detects if the user's cursor is still on the ~Text~ DOM Node that they wanted to convert into a link + * + * ******************************** + * + * Side-effects: + * - Transforms Text to a Link + * - Closes Editor For TEXT + * - Closes the editor if the text no longer has selection + */ + React.useEffect(() => { + const userSelection = editorState.getSelection(); + // check if the user's cursor has moved i.e. it is on a different piece of text + if ( + showEditor && + textSelection && + !cursorIsSelectingOriginalText(state, textSelection, userSelection) + ) { + console.log('[CLOSE_EDITOR_FOR_TEXT EFFECT]'); + const contentState = Modifier.removeInlineStyle( + editorState.getCurrentContent(), + textSelection, + 'HIGHLIGHT_LINK' + ); + const stateWithoutTextHighlight = EditorState.push( + editorState, + contentState, + 'change-inline-style' + ); + const stateWithUserSelection = EditorState.forceSelection( + stateWithoutTextHighlight, + userSelection + ); + editorSetState(stateWithUserSelection); + controlledDispatch({ type: LinkEditorStateChangeTypes.CLOSE_EDITOR }); + } + }, [ + controlledDispatch, + editorState, + editorSetState, + showEditor, + state, + textSelection + ]); + + /** + * *************************************** + * + * Show or Close Link Details Popup + * + * *************************************** + * + * - Show Details for Link Entities + * - Detects if the user's cursor is still on the ~Link~ Entity + * - Closes Detail Popup for links if the user is no longer selecting the link range + */ + React.useEffect(() => { + if (showEditor === false) { + // move down under the findLinkEntities, it'll be part of the cb fn to run + const { cursorStart, cursorEnd } = getSelectionIndices(editorState); + const { + selectionBlock, + selectionBlockID, + selectionBlockEndID + } = getSelectionBlockProps(editorState); + let cursorIsOnLink = false; + if (cursorIsOnSingleBlock(selectionBlockID, selectionBlockEndID)) { + findLinkEntities( + selectionBlock, + (start: number, end: number) => { + if (cursorMatchesSingleLinkRange(cursorStart, cursorEnd, start, end)) { + controlledDispatch({ + type: LinkEditorStateChangeTypes.OPEN_DETAILS, + editorState, + linkRange: [start, end] + }); + cursorIsOnLink = true; + } + }, + editorState.getCurrentContent() + ); + } + // If the current cursor is no longer on a link + // and the link details popup is open -> close the detail popup + if (cursorIsOnLink === false) + controlledDispatch({ type: LinkEditorStateChangeTypes.CLOSE_DETAILS }); + } + }, [controlledDispatch, editorState, showEditor]); + + function closeLinkEditor() { + console.log('[CLOSE_EDITOR]'); + let contentState = editorState.getCurrentContent(); + if (linkSelection) { + contentState = Modifier.removeInlineStyle( + editorState.getCurrentContent(), + linkSelection, + 'HIGHLIGHT_LINK' + ); + } else if (textSelection) { + contentState = Modifier.removeInlineStyle( + editorState.getCurrentContent(), + textSelection, + 'HIGHLIGHT_LINK' + ); + } + const newState = EditorState.push( + editorState, + contentState, + 'change-inline-style' + ); + editorSetState(newState); + controlledDispatch({ type: LinkEditorStateChangeTypes.CLOSE_EDITOR }); + } + + /** + * Explicitly remove the url from an existing link + * without the url, the link becomes a piece of text + * @param {[type]} e [description] + * @return {[type]} [description] + */ + function removeLink() { + const { cursorStart, cursorEnd } = getSelectionIndices(editorState); + const { selectionBlock, selectionBlockID } = getSelectionBlockProps(editorState); + // capture the original selection state + // after removing the link + // place the selectionstate back on the link + findLinkEntities( + selectionBlock, + (start: number, end: number) => { + if (cursorMatchesSingleLinkRange(cursorStart, cursorEnd, start, end)) { + let selectionState = SelectionState.createEmpty('link-to-remove'); + let updatedSelection = selectionState.merge({ + anchorKey: selectionBlockID, + anchorOffset: start, + focusKey: selectionBlockID, + focusOffset: end + }); + + const contentState = Modifier.removeInlineStyle( + editorState.getCurrentContent(), + updatedSelection, + 'HIGHLIGHT_LINK' + ); + + const newState = EditorState.push( + editorState, + contentState, + 'change-inline-style' + ); + + controlledDispatch({ type: LinkEditorStateChangeTypes.REMOVE_LINK }); + editorSetState(RichUtils.toggleLink(newState, updatedSelection, null)); + } + }, + editorState.getCurrentContent() + ); + } + + /** + * ********************** + * + * Update the Link + * + * ********************** + * + * Update the existing link meaning: + * - change the link's URL or, + * - change the link's text + */ + function updateLink({ url, text }: any) { + console.log('[UPDATE_LINK]'); + if (!hasHTTPS(url)) { + url = 'https://'.concat(url); + } + if (text === '') { + text = url; + } + let contentState = editorState.getCurrentContent(); + let selectionState = SelectionState.createEmpty('link-to-update'); + let updatedSelection = editorState.getSelection(); + if (linkSelection) { + updatedSelection = selectionState.merge({ + anchorKey: linkSelection.getAnchorKey(), + anchorOffset: linkSelection.getAnchorOffset(), + focusKey: linkSelection.getFocusKey(), + focusOffset: linkSelection.getFocusOffset() + }); + } else if (textSelection) { + updatedSelection = textSelection; + } + + let updatedContent = Modifier.removeInlineStyle( + contentState, + updatedSelection, + 'HIGHLIGHT_LINK' + ); + let replacedContent = updatedContent.createEntity('LINK', 'MUTABLE', { + url: url, + text + }); + + const entityKey = replacedContent.getLastCreatedEntityKey(); + let newContentState = Modifier.replaceText( + replacedContent, + updatedSelection, + text, + undefined, + entityKey + ); + + // const blockType = editorState + // .getCurrentContent() + // .getBlockForKey(updatedSelection.getStartKey()) + // .getType(); + + editorSetState( + EditorState.push(editorState, newContentState, 'insert-characters') + ); + controlledDispatch({ type: LinkEditorStateChangeTypes.UPDATE_LINK }); + } + + const toggleLinkEditor = () => { + if (showEditor) { + closeLinkEditor(); + } else { + openLinkEditor('EDITOR_CONTROL'); + } + }; + + /** + * ********************** + * + * Open the Link editor + * + * ********************** + * + */ + const openLinkEditor = React.useCallback( + (control: LinkControl) => { + switch (control) { + case 'LINK_DETAILS': + const { cursorStart, cursorEnd } = getSelectionIndices(editorState); + const { selectionBlock } = getSelectionBlockProps(editorState); + const currentLinkSelection = editorState.getSelection(); + findLinkEntities( + selectionBlock, + (start: number, end: number) => { + if (cursorMatchesSingleLinkRange(cursorStart, cursorEnd, start, end)) { + console.log('[APPLY_LINK_HIGHLIGHT EFFECT]'); + let selectionState = SelectionState.createEmpty('foo'); + let updatedSelection = selectionState.merge({ + anchorKey: currentLinkSelection.getAnchorKey(), + anchorOffset: start, + focusKey: currentLinkSelection.getFocusKey(), + focusOffset: end + }); + const editorWithLinkSelection = EditorState.forceSelection( + editorState, + updatedSelection + ); + const contentState = Modifier.applyInlineStyle( + editorWithLinkSelection.getCurrentContent(), + updatedSelection, + 'HIGHLIGHT_LINK' + ); + + const newState = EditorState.push( + editorWithLinkSelection, + contentState, + 'change-inline-style' + ); + + editorSetState(newState); + controlledDispatch({ + type: LinkEditorStateChangeTypes.OPEN_EDITOR_BY_LINK_DETAILS, + linkRange: [start, end], + editorState + }); + } + }, + editorState.getCurrentContent() + ); + break; + case 'EDITOR_CONTROL': + console.log('[OPEN_EDITOR: EDITOR_CONTROL]'); + + const cursorIsOnLink = showDetails; + if (cursorIsOnLink) { + const { cursorStart, cursorEnd } = getSelectionIndices(editorState); + const { selectionBlock } = getSelectionBlockProps(editorState); + + findLinkEntities( + selectionBlock, + (start, end) => { + if ( + cursorMatchesSingleLinkRange(cursorStart, cursorEnd, start, end) + ) { + controlledDispatch({ + type: + LinkEditorStateChangeTypes.OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_LINK, + linkRange: [start, end], + editorState + }); + } + }, + editorState.getCurrentContent() + ); + break; + } else { + // cursor is on text + const { cursorStart, cursorEnd } = getSelectionIndices(editorState); + const hasText = editorState.getCurrentContent().hasText(); + if (hasText) { + const currentTextSelection = editorState.getSelection(); + const editorStateWithTextSelection = EditorState.forceSelection( + editorState, + currentTextSelection + ); + const contentState = Modifier.applyInlineStyle( + editorStateWithTextSelection.getCurrentContent(), + currentTextSelection, + 'HIGHLIGHT_LINK' + ); + + const newState = EditorState.push( + editorStateWithTextSelection, + contentState, + 'change-inline-style' + ); + editorSetState(newState); + } + controlledDispatch({ + type: LinkEditorStateChangeTypes.OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_TEXT, + textRange: [cursorStart, cursorEnd], + editorState, + hasText + }); + } + break; + default: + throw new TypeError('Unhandled control to open editor '); + } + }, + [controlledDispatch, editorState, editorSetState, showDetails, showEditor] + ); + + // function getLinkProps() { + // return { + // ref: linkEditorRef + // }; + // } + + return { + // state + linkSelection, + textSelection, + showDetails, + showEditor, + url, + text, + coords, + // functions + toggleLinkEditor, + closeLinkEditor, + removeLink, + updateLink, + openLinkEditor + // getLinkProps + }; +} diff --git a/proof-of-concepts/features/stories/q/components/link/LinkIcon.tsx b/proof-of-concepts/features/stories/q/components/link/LinkIcon.tsx new file mode 100644 index 0000000..8a6eb20 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/LinkIcon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Icon } from '@chakra-ui/react'; + +export const LinkOutlinedIcon = () => ( + + + +); diff --git a/proof-of-concepts/features/stories/link/src/App.css b/proof-of-concepts/features/stories/q/components/link/demo/src/App.css similarity index 100% rename from proof-of-concepts/features/stories/link/src/App.css rename to proof-of-concepts/features/stories/q/components/link/demo/src/App.css diff --git a/proof-of-concepts/features/stories/link/src/App.js b/proof-of-concepts/features/stories/q/components/link/demo/src/App.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/App.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/App.js diff --git a/proof-of-concepts/features/stories/link/src/App.test.js b/proof-of-concepts/features/stories/q/components/link/demo/src/App.test.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/App.test.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/App.test.js diff --git a/proof-of-concepts/features/stories/link/src/FormatControls.js b/proof-of-concepts/features/stories/q/components/link/demo/src/FormatControls.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/FormatControls.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/FormatControls.js diff --git a/proof-of-concepts/features/stories/link/src/Link/link.css b/proof-of-concepts/features/stories/q/components/link/demo/src/Link/link.css similarity index 100% rename from proof-of-concepts/features/stories/link/src/Link/link.css rename to proof-of-concepts/features/stories/q/components/link/demo/src/Link/link.css diff --git a/proof-of-concepts/features/stories/link/src/Link/link.js b/proof-of-concepts/features/stories/q/components/link/demo/src/Link/link.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/Link/link.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/Link/link.js diff --git a/proof-of-concepts/features/stories/link/src/stub.js b/proof-of-concepts/features/stories/q/components/link/demo/src/stub.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/stub.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/stub.js diff --git a/proof-of-concepts/features/stories/link/src/todos.md b/proof-of-concepts/features/stories/q/components/link/demo/src/todos.md similarity index 100% rename from proof-of-concepts/features/stories/link/src/todos.md rename to proof-of-concepts/features/stories/q/components/link/demo/src/todos.md diff --git a/proof-of-concepts/features/stories/link/src/useFocusWithin.js b/proof-of-concepts/features/stories/q/components/link/demo/src/useFocusWithin.js similarity index 100% rename from proof-of-concepts/features/stories/link/src/useFocusWithin.js rename to proof-of-concepts/features/stories/q/components/link/demo/src/useFocusWithin.js diff --git a/proof-of-concepts/features/stories/link/docs/link.md b/proof-of-concepts/features/stories/q/components/link/docs/link.md similarity index 100% rename from proof-of-concepts/features/stories/link/docs/link.md rename to proof-of-concepts/features/stories/q/components/link/docs/link.md diff --git a/proof-of-concepts/features/stories/link/docs/table.md b/proof-of-concepts/features/stories/q/components/link/docs/table.md similarity index 100% rename from proof-of-concepts/features/stories/link/docs/table.md rename to proof-of-concepts/features/stories/q/components/link/docs/table.md diff --git a/proof-of-concepts/features/stories/q/components/link/link.scss b/proof-of-concepts/features/stories/q/components/link/link.scss new file mode 100644 index 0000000..e5f92c8 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/link.scss @@ -0,0 +1,153 @@ +@use '../../../styles/colors'; +.link-details { + position: absolute; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.5rem; + font-size: 0.875rem; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + background-color: white; + box-shadow: 0 2px 8px 0 hsla(0, 0%, 0%, 0.05); +} + +.link-details:after { + content: ''; + position: absolute; + width: 0.3rem; + height: 0.3rem; + border-top: 1px solid #d1d5db; + border-right: 1px solid #d1d5db; + background-color: white; + top: calc(-0.2rem - 1px); + left: 1rem; + transform: rotate(-45deg); +} + +.link-details__spacer { + padding: 0 0.25rem 0px 0.25rem; + font-size: 0.875rem; + /*font-weight: 500;*/ +} +.link-details__url { + color: #189afe; + cursor: pointer; +} + +.link-details__url:hover, +.link-details__button:hover { + text-decoration: underline; +} + +.link-details__button { + font-size: inherit; + font-family: inherit; + cursor: pointer; + background-color: white; + padding: 0; + border: none; + outline: none; + color: #189afe; +} + +.link-editor { + display: flex; + flex-direction: column; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 0.25rem; + background-color: white; + box-shadow: 0 2px 8px 0 hsla(0, 0%, 0%, 0.05); +} + +.link-editor:after { + background-color: white; + content: ''; + position: absolute; + width: 0.3rem; + height: 0.3rem; + border-top: 1px solid #d1d5db; + border-right: 1px solid #d1d5db; + top: calc(-0.2rem - 1px); + left: 1rem; + transform: rotate(-45deg); +} + +.link-editor__field { + padding: 0.25rem; +} + +.link-editor__label { + padding-right: 12px; +} + +.link-editor__input { + padding: 0.55rem; + border: 1px solid #d1d5db; + border-radius: 4px; + background-color: #f9fafb; + height: 24px; + width: 20rem; +} + +.link-editor__input:focus { + border: 1px solid #0978d9; + outline: none; + /*0px 0px 2px 1px rgba(24,154,254,0.75)*/ + + box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3); +} + +.link-editor__input::placeholder { + color: #6b7280; +} + +.link-editor__actions { + display: inline-flex; + justify-content: space-between; + align-items: center; +} + +.link-editor__error { + color: #ff4d4f; + margin-left: 0.25rem; +} + +.link-editor__button { + margin: 0.5rem 0.5rem 0.5rem auto; /* align button to right of container */ + color: #f9fafb; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + /*letter-spacing: 0.03rem;*/ + font-size: 1rem; + background-color: #189afe; + border-radius: 4px; + border: 1px solid #0978d9; + height: 2rem; + width: 5.5rem; +} +.link-editor__button:focus { + outline: none; + -webkit-box-shadow: 0px 0px 0px 3px rgba(186, 234, 255, 1); + box-shadow: 0px 0px 0px 3px rgba(186, 234, 255, 1); +} + +.link-editor__button--disabled { + cursor: not-allowed; + color: #374151; + margin: 0.5rem 0.5rem 0.5rem auto; /* align button to right of container */ + background-color: #9ca3af; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + border: 1px solid #6b7280; + border-radius: 4px; + height: 2rem; + width: 5.5rem; +} diff --git a/proof-of-concepts/features/stories/q/components/link/reducer.ts b/proof-of-concepts/features/stories/q/components/link/reducer.ts new file mode 100644 index 0000000..baeaf15 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/reducer.ts @@ -0,0 +1,133 @@ +import { LinkEditorStateChangeTypes, LinkState, LinkAction } from './types'; +import { + initialLinkState, + getFirstBlockDOMNode, + getSelectedLinkElement, + getLinkCoordinates, + getTextDOMNode +} from './utils'; + +export function linkStateReducer(state: LinkState, action: LinkAction) { + const { type } = action; + switch (type) { + case LinkEditorStateChangeTypes.UPDATE_LINK: { + return initialLinkState; + } + case LinkEditorStateChangeTypes.REMOVE_LINK: { + return initialLinkState; + } + case LinkEditorStateChangeTypes.OPEN_EDITOR_BY_LINK_DETAILS: { + const newLinkState = { ...state }; + const { linkRange, editorState } = action; + const start = linkRange[0]; + const end = linkRange[1]; + const selectionBlockID = editorState.getSelection().getStartKey(); + const selectionBlock = editorState + .getCurrentContent() + .getBlockForKey(selectionBlockID); + // Get the entire link entity range + let selectionState = editorState.getSelection(); + let linkSelectionRange = selectionState.merge({ + anchorKey: selectionBlockID, + anchorOffset: start, + focusKey: selectionBlockID, + focusOffset: end + }); + // we need the link selection because we need to clean the selection up later + newLinkState.linkSelection = linkSelectionRange; + newLinkState.showDetails = false; + newLinkState.showEditor = true; + return newLinkState; + } + case LinkEditorStateChangeTypes.OPEN_DETAILS: { + const newLinkState = { ...state }; + const { linkRange, editorState } = action; + const start = linkRange[0]; + const end = linkRange[1]; + const selectionBlockID = editorState.getSelection().getStartKey(); + const selectionBlock = editorState + .getCurrentContent() + .getBlockForKey(selectionBlockID); + + const linkElement = getSelectedLinkElement(); + // determine if we need this + if (linkElement) { + // get current coords of link entity + // in order to calculate link-details popover placement + const linkDimensions = linkElement.getBoundingClientRect(); + newLinkState.coords = { ...getLinkCoordinates(linkDimensions) }; + + // update link state + const linkKey = selectionBlock.getEntityAt(start); + const linkInstance = editorState.getCurrentContent().getEntity(linkKey); + let url = linkInstance.getData().url; + newLinkState.url = url; + newLinkState.text = selectionBlock.getText().slice(start, end); + newLinkState.showDetails = true; + newLinkState.showEditor = false; + } + return newLinkState; + } + case LinkEditorStateChangeTypes.CLOSE_EDITOR: { + return initialLinkState; + } + case LinkEditorStateChangeTypes.CLOSE_DETAILS: { + return initialLinkState; + } + case LinkEditorStateChangeTypes.OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_TEXT: { + const newLinkState = { ...state }; + const { textRange, editorState, hasText } = action; + const start = textRange[0]; + const end = textRange[1]; + const selectionState = editorState.getSelection(); // Get the text content range + const selectionBlockID = editorState.getSelection().getStartKey(); + const firstContentBlock = editorState.getCurrentContent().getFirstBlock(); + + let textDOMNode: Range | Element | null = hasText + ? getTextDOMNode() + : getFirstBlockDOMNode(firstContentBlock.getKey()); + + const textBlockDimensions = textDOMNode!.getBoundingClientRect(); + newLinkState.coords = getLinkCoordinates(textBlockDimensions); + + const selectionBlock = editorState + .getCurrentContent() + .getBlockForKey(selectionBlockID); + const selectionBlockText = selectionBlock.getText(); + const text = selectionBlockText.substring(start, end); + + // we need the link selection because we need to clean the selection up later + newLinkState.text = text; + newLinkState.textSelection = selectionState; + newLinkState.showEditor = true; + newLinkState.showDetails = false; + return newLinkState; + } + case LinkEditorStateChangeTypes.OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_LINK: { + const newLinkState = { ...state }; + const { linkRange, editorState } = action; + const start = linkRange[0]; + const end = linkRange[1]; + const selectionBlockID = editorState.getSelection().getStartKey(); + const selectionBlock = editorState + .getCurrentContent() + .getBlockForKey(selectionBlockID); + // Get the entire link entity range + let selectionState = editorState.getSelection(); + let linkSelectionRange = selectionState.merge({ + anchorKey: selectionBlockID, + anchorOffset: start, + focusKey: selectionBlockID, + focusOffset: end + }); + // we need the link selection because we need to clean the selection up later + newLinkState.linkSelection = linkSelectionRange; + newLinkState.showDetails = false; + newLinkState.showEditor = true; + return newLinkState; + } + default: { + throw new Error('Unhandled Link Entity action type'); + } + } +} diff --git a/proof-of-concepts/features/stories/q/components/link/types.ts b/proof-of-concepts/features/stories/q/components/link/types.ts new file mode 100644 index 0000000..7ca28ee --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/types.ts @@ -0,0 +1,49 @@ +import { EditorState, SelectionState } from 'draft-js'; +import { ComponentProps } from '../../../types'; +export type LinkState = { + linkSelection?: SelectionState | null; + textSelection?: SelectionState | null; + showDetails: boolean; + showEditor: boolean; + url: string; + text: string; + coords?: LinkCoordinates; +}; + +export type LinkProps = { + editorState: EditorState; + editorSetState: React.Dispatch; +} & ComponentProps; + +export interface LinkCoordinates { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; + x: number; + y: number; +} + +export enum LinkEditorStateChangeTypes { + LINK_CLOSE_DETAILS = '[link_editor_close_details]', + CLOSE_EDITOR = '[link_editor_close_editor]', + CLOSE_DETAILS = '[link_editor_close_details]', + REMOVE_LINK = '[link_editor_remove_link]', + UPDATE_LINK = '[link_editor_update_link]', + OPEN_EDITOR_BY_LINK_DETAILS = '[link_editor_open_editor_by_link_details]', + OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_TEXT = '[link_editor_open_editor_control_for_text]', + OPEN_DETAILS = '[link_editor_open_details]', + OPEN_EDITOR_BY_EDITOR_CONTROL_FOR_LINK = '[link_editor_open_editor_control_for_link]' +} + +export type LinkAction = { + type: LinkEditorStateChangeTypes; + editorState?: EditorState; + linkRange?: [number, number]; + textRange?: [number, number]; + hasText?: boolean; +}; + +export type LinkActionAndChanges = {}; diff --git a/proof-of-concepts/features/stories/q/components/link/utils.ts b/proof-of-concepts/features/stories/q/components/link/utils.ts new file mode 100644 index 0000000..3aec089 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/link/utils.ts @@ -0,0 +1,167 @@ +import { + EditorState, + SelectionState, + ContentBlock, + ContentState, + CharacterMetadata +} from 'draft-js'; +import { LinkState, LinkCoordinates } from './types'; + +/** + * Iterates over the content block and determines the ranges of link entities + * For example, suppose the following content block: + * "Hello world I'm going.com to PEE.com" + * The string would be considered a content block. 'going.com' and 'PEE.com' + * would be considered Link entities. We call the callback on the two link ranges + */ +export const findLinkEntities = ( + contentBlock: ContentBlock, + callback: (start: number, end: number) => void, + contentState: ContentState +) => { + contentBlock.findEntityRanges((character: CharacterMetadata): boolean => { + const entityKey = character.getEntity(); + const test = + entityKey !== null && contentState.getEntity(entityKey).getType('LINK'); + return test; + }, callback); +}; + +export const initialLinkState = { + linkSelection: null, + textSelection: null, + showDetails: false, + showEditor: false, + url: '', + text: '', + coords: {} +} as LinkState; + +/** + * Adds window location of the DOM element to the link state + */ +export function getLinkCoordinates(linkDimensions: DOMRect): LinkCoordinates { + return { + bottom: linkDimensions.bottom, + height: linkDimensions.height, + left: linkDimensions.left, + right: linkDimensions.right, + top: linkDimensions.top, + width: linkDimensions.width, + x: linkDimensions.x, + y: linkDimensions.y + }; +} + +/** + * Returns the DOM node of the first content block, useful for when the editor state is empty + */ +export function getFirstBlockDOMNode(firstContentBlock: string): Element | null { + const node = document.querySelector(`[data-offset-key='${firstContentBlock}-0-0']`); + return node; +} + +/** + * Returns the DOM node of the text range selection, useful for when converting a text to link + */ +export function getTextDOMNode(): Range | null { + let selection = window.getSelection() && window.getSelection(); + return selection ? selection.getRangeAt(0) : null; +} + +/** + * Grabs the DOM node of the selected link + */ +export function getSelectedLinkElement(): (Node & Element) | null { + let selection = window.getSelection() && window.getSelection(); + if (!selection || selection.rangeCount == 0) return null; + let node = selection.getRangeAt(0).startContainer as Node & Element; + while (node != null) { + if (node.getAttribute && node.getAttribute('class') === 'bottomline-link') + return node; + node = node.parentNode as Node & Element; + } + return null; +} + +/** + * Determine if the user's cursor selection is within the start and end range of a piece of text + * @param cursorStart - the anchor index of user's cursor selection + * @param cursorEnd - the focus index of the user's cursor selection + * @param start - the start index of the text range + * @param end - the end index of the text range + */ +export function cursorMatchesSingleLinkRange( + cursorStart: number, + cursorEnd: number, + start: number, + end: number +): boolean { + return ( + cursorStart >= start && + cursorStart <= end && + cursorEnd >= start && + cursorEnd <= end + ); +} + +/** + * Checks if the user's selected range is on a single selection block + * i.e. whether or not the selected range spans multiple lines + * Definition: Block + * - a block is synonymous with line of text + * @param selectionBlockID - the selection block that the user's cursor begins on + * @param selectionBlockEndID - the selection block that the user's cursor ends on + */ +export function cursorIsOnSingleBlock( + selectionBlockID: string, + selectionBlockEndID: string +): boolean { + return selectionBlockID === selectionBlockEndID; +} + +/** + * Regex test to detect whether or not the url string contains an https scheme + */ +export function hasHTTPS(url: string): boolean { + const httpsCheck = /^(?:(?:(?:https?):)?\/\/)/i; + const urlExp = new RegExp(httpsCheck); + return urlExp.test(url); +} + +/** + * + * @param state - link + * @param textSelection + * @param userSelection + */ +export function cursorIsSelectingOriginalText( + state: LinkState, + textSelection: SelectionState | null, + userSelection: SelectionState +): boolean { + if (textSelection === null) return false; + return ( + state.showEditor && + textSelection.getAnchorKey() === userSelection.getAnchorKey() && + textSelection.getFocusKey() === userSelection.getFocusKey() && + textSelection.getAnchorOffset() === userSelection.getAnchorOffset() && + textSelection.getFocusOffset() === userSelection.getFocusOffset() + ); +} + +export function getSelectionIndices(editorState: EditorState) { + return { + cursorStart: editorState.getSelection().getStartOffset(), + cursorEnd: editorState.getSelection().getEndOffset() + }; +} + +export function getSelectionBlockProps(editorState: EditorState) { + let selectionBlockID = editorState.getSelection().getStartKey(); + return { + selectionBlockID, + selectionBlockEndID: editorState.getSelection().getEndKey(), + selectionBlock: editorState.getCurrentContent().getBlockForKey(selectionBlockID) + }; +} diff --git a/proof-of-concepts/features/stories/q/components/reviewSteps/Review.scss b/proof-of-concepts/features/stories/q/components/reviewSteps/Review.scss new file mode 100644 index 0000000..22f86fd --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/reviewSteps/Review.scss @@ -0,0 +1,129 @@ +@use '../../../styles/fonts'; +@use '../../../styles/colors'; +@use '../../../styles/spacing'; + +.review-container { + display: flex; + flex-direction: column; + background: white; + border: 1px solid colors.$Gray-3; + border-radius: 4px; + width: 310px; + box-shadow: 0px 4px 6px rgba(17, 24, 39, 0.2); + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.review-header { + background: colors.$Gray-0; + padding: spacing.$Size-4; + color: colors.$Gray-6; + font-size: fonts.$Size-1; + font-weight: fonts.$Regular; + letter-spacing: 0.2px; + border-bottom: 1px solid colors.$Gray-3; + margin: 0; +} + +.guideline-header__info { + padding: spacing.$Size-4 spacing.$Size-4 0 spacing.$Size-4; + font-size: fonts.$Size-1; + font-weight: fonts.$Regular; + line-height: spacing.$Size-7; + color: colors.$Gray-8; +} + +.guideline-section-container { + padding: spacing.$Size-4; +} + +.guideline-section { + margin-top: spacing.$Size-2; +} + +.guideline-section:first-child { + padding-bottom: spacing.$Size-1; + border-bottom: 1px solid colors.$Gray-2; +} + +.guideline-section__button { + display: inline-grid !important; + grid-template-rows: 1fr; + grid-template-columns: 1fr 10fr 2fr; + width: 100%; + background: none; + border: none; + padding: 0; +} + +.guideline-section__title { + font-weight: fonts.$Semibold; + color: colors.$Gray-8; + justify-self: start; + letter-spacing: 0.1px; +} + +.guideline-section__title-number { + justify-self: start; + font-weight: fonts.$Semibold; + font-size: fonts.$Size-2; + color: colors.$Blue-6; + text-shadow: 1px 1px colors.$Gray-4; +} + +.guideline-section__toggle-button { + grid-column-start: 3; + justify-self: end; + height: fonts.$Size-4; + width: fonts.$Size-4; +} + +.guideline-section__info { + display: inline-block; + margin: spacing.$Size-2 spacing.$Size-2 spacing.$Size-2 spacing.$Size-6; + font-size: fonts.$Size-1; + line-height: spacing.$Size-7; +} + +.review-section-container, +.review-error-container { + display: grid; + grid-template-columns: 1fr 10fr; + color: colors.$Gray-8; + padding: spacing.$Size-4; +} +.review-section-container { + grid-template-rows: 1fr; +} + +.review-error-container { + grid-template-rows: 1fr 1fr; +} + +.review-header-result { + font-weight: fonts.$Semibold; + color: colors.$Gray-7; + justify-self: start; + letter-spacing: 0.1px; + font-size: fonts.$Size-1; +} + +.review-header__error-icon { + grid-row-start: 1; + grid-column-start: 1; + color: colors.$Red-7; + height: spacing.$Size-5; + width: spacing.$Size-5; + filter: drop-shadow(2px 2px 1px rgb(255, 120, 117)); +} + +.review-header__result-resolution { + grid-row-start: 2; + grid-column-start: 2; + margin-top: spacing.$Size-0; + font-size: fonts.$Size-1; +} + +.review-header__no-errors-icon { + color: #389e0d; + filter: drop-shadow(2px 2px 1px rgb(183, 235, 143)); +} diff --git a/proof-of-concepts/features/stories/q/components/reviewSteps/Review.tsx b/proof-of-concepts/features/stories/q/components/reviewSteps/Review.tsx new file mode 100644 index 0000000..2908b95 --- /dev/null +++ b/proof-of-concepts/features/stories/q/components/reviewSteps/Review.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { + Accordion, + AccordionItem, + AccordionButton, + AccordionPanel, + AccordionIcon, + Box +} from '@chakra-ui/react'; +import { FaRegThumbsUp } from 'react-icons/fa'; +import { HiOutlineChevronDown } from 'react-icons/hi'; +import { RiNumber1, RiNumber2 } from 'react-icons/ri'; +import { AiOutlineExclamationCircle } from 'react-icons/ai'; +import './Review.scss'; +import { useFormState } from 'react-final-form'; + +// internally, useFormState uses React.Context maintained by react-final-form +// we choose not to pass props from parent to grandchild for readability +const ReviewMessage = () => { + const { submitFailed, hasValidationErrors, errors } = useFormState(); + let numErrors = errors + ? Object.keys(errors).reduce((accum, key) => (errors[key] ? accum + 1 : accum), 0) + : 0; + let reviewMessage = ''; + + if (numErrors === 0) { + reviewMessage = 'Your question is ready for posting'; + } else if (numErrors === 1) { + reviewMessage = 'You have 1 error'; + } else { + reviewMessage = `You have ${numErrors} errors`; + } + + const ErrorMessage = () => { + return ( +
    + + + Your question couldn't be submitted + + + Resolve {numErrors} issues before posting + +
    + ); + }; + + const NoErrors = () => { + return ( +
    + + + Your question is ready for posting + +
    + ); + }; + + return ( + +

    Step 2: Review Your Question

    + {submitFailed && hasValidationErrors && numErrors !== 0 ? ( + + ) : ( + + )} +
    + ); +}; +const ContentGuidelines = () => { + return ( + <> +

    Step 1: Draft Your Question

    + + The community is here to answer questions that you have about organizing, + historical events, radical left theory, and left politics. + + + + + 1. + Summarize your problem + + + + + Provide any background detail and context necessary for understanding + the question you’re asking. + + + + + + + 2. + + Show some context or research + + + + +
    + + You can get better answers when you provide research and context + +
      +
    • + When appropriate, share links or author, title, and page number to + content that you reference. +
    • +
    • + Clarify any terms, histories, or concepts that you use as you + understand them. This helps others be on the same page as you. +
    • +
    +
    +
    +
    +
    + + ); +}; +export function Review({ + className, + displayErrors, + transitionIn +}: { + className: string; + displayErrors: boolean; + transitionIn: boolean; +}) { + const reviewContainerClassName = 'review-container ' + className; + + return ( +
    + {/**/} + {displayErrors ? : } + {/**/} +
    + ); +} diff --git a/proof-of-concepts/features/stories/q/types.ts b/proof-of-concepts/features/stories/q/types.ts new file mode 100644 index 0000000..005ac72 --- /dev/null +++ b/proof-of-concepts/features/stories/q/types.ts @@ -0,0 +1,17 @@ +import { BottomlineTag } from '../tags/TagEditor/types'; +export type QuestionProps = { + onSubmit?: (...args: any) => any; + tagsEndpoint?: string; + markdown?: string; +}; +export type Question = { + title?: string; + body?: string; + tags?: BottomlineTag[]; +}; + +export type QuestionError = { + title?: string; + body?: string; + tags?: string[] | string; +}; diff --git a/proof-of-concepts/features/stories/styles/colors.scss b/proof-of-concepts/features/stories/styles/colors.scss index 39f7fd8..ab274f7 100644 --- a/proof-of-concepts/features/stories/styles/colors.scss +++ b/proof-of-concepts/features/stories/styles/colors.scss @@ -25,3 +25,17 @@ $Gray-6: #4b5563; $Gray-7: #374151; $Gray-8: #1f2937; $Gray-9: #111827; + +/******* + * Red * + *******/ +$Red-1: #fff1f0; +$Red-2: #ffccc7; +$Red-3: #ffa39e; +$Red-4: #ff7875; +$Red-5: #ff4d4f; +$Red-6: #f5222d; +$Red-7: #cf1322; +$Red-8: #a8071a; +$Red-9: #820014; +$Red-10: #5c0011; diff --git a/proof-of-concepts/features/stories/styles/screen.scss b/proof-of-concepts/features/stories/styles/screen.scss new file mode 100644 index 0000000..c1b0f35 --- /dev/null +++ b/proof-of-concepts/features/stories/styles/screen.scss @@ -0,0 +1,4 @@ +$Phone: 400px; // 72px +$Medium: 800px; // 72px +$Desktop: 1280px; // 72px +$Large: 1600px; // 72px diff --git a/proof-of-concepts/features/stories/styles/spacing.scss b/proof-of-concepts/features/stories/styles/spacing.scss new file mode 100644 index 0000000..14e3f53 --- /dev/null +++ b/proof-of-concepts/features/stories/styles/spacing.scss @@ -0,0 +1,13 @@ +$Size-12: 4.5rem; // 72px +$Size-11: 3.75rem; // 60px +$Size-10: 3rem; // 48px +$Size-9: 2.25rem; // 36px +$Size-8: 1.875rem; // 30px +$Size-7: 1.5rem; // 24px +$Size-6: 1.25rem; // 20px +$Size-5: 1rem; // 16px +$Size-4: 0.875rem; // 14px +$Size-3: 0.75rem; // 12px +$Size-2: 0.625rem; // 10px +$Size-1: 0.5rem; // 8px +$Size-0: 0.25rem; // 4px diff --git a/proof-of-concepts/features/stories/EditorTags/Tag.scss b/proof-of-concepts/features/stories/tags/Tag.scss similarity index 100% rename from proof-of-concepts/features/stories/EditorTags/Tag.scss rename to proof-of-concepts/features/stories/tags/Tag.scss diff --git a/proof-of-concepts/features/stories/EditorTags/Tag.stories.tsx b/proof-of-concepts/features/stories/tags/Tag.stories.tsx similarity index 95% rename from proof-of-concepts/features/stories/EditorTags/Tag.stories.tsx rename to proof-of-concepts/features/stories/tags/Tag.stories.tsx index 0168104..dbce26f 100644 --- a/proof-of-concepts/features/stories/EditorTags/Tag.stories.tsx +++ b/proof-of-concepts/features/stories/tags/Tag.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { Tag, TagIcon, TagCloseButton } from './Tag'; -import { TagEditor } from './TagEditor'; +import { TagEditor } from './TagEditor/TagEditor'; import { GoPlus } from 'react-icons/go'; export default { @@ -68,7 +68,6 @@ const UseCaseTemplate: ComponentStory = (args) => { return (
    -
    ); }; diff --git a/proof-of-concepts/features/stories/EditorTags/Tag.tsx b/proof-of-concepts/features/stories/tags/Tag.tsx similarity index 100% rename from proof-of-concepts/features/stories/EditorTags/Tag.tsx rename to proof-of-concepts/features/stories/tags/Tag.tsx diff --git a/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.scss b/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.scss new file mode 100644 index 0000000..065727f --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.scss @@ -0,0 +1,223 @@ +@use '../../styles/colors.scss'; +@use '../../styles/fonts.scss'; +@use '../../styles/spacing.scss'; + +.tag-editor-section { + padding: 0; +} + +.tag-editor { + display: flex; + flex-direction: column; +} + +/** + * ******************* + * + * Header + * + * ******************* + */ + +.tag-header-container { + display: inline-grid; + grid-template-columns: 10fr 1fr; +} + +.tag-header-title { + font-size: fonts.$Size-0; + flex: 1; +} + +.tag-header-description { + justify-self: right; + height: spacing.$Size-5; + width: spacing.$Size-5; + color: white; + cursor: pointer; +} + +.tag-header-description path { + fill: colors.$Blue-6; +} + +/** + * ******************* + * + * Selected Tags + * + * ******************* + */ + +.selected-tags { + display: flex; + flex-wrap: wrap; + list-style-type: none; + padding: 0; + margin: spacing.$Size-2 0 spacing.$Size-2 0; +} + +.selected-tag { + padding: spacing.$Size-0; +} + +.selected-tag:first-child { + padding-left: 0; +} +/** + * ******************* + * + * Search/input + * + * ******************* + */ + +.tag-search-container { + display: inline-flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border: 1px solid colors.$Gray-4; + border-radius: 6px; +} + +.tag-search-container:focus-within { + border: 1px solid #0978d9; + box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 0.3); + outline: none; +} + +.tag-search-input { + margin: spacing.$Size-1; + font-size: fonts.$Size-1; + border: none; + width: 70%; +} + +.tag-search-input:focus-within { + outline: none; +} + +.tag-search-loader { + display: inline-block; + margin-right: 4px; +} + +/** + * ******************* + * + * Results + * + * ******************* + */ + +.tag-results-container { + display: flex; + box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); + border-radius: 4px; +} + +.tag-no-results { + display: flex; + color: colors.$Gray-7; + padding: spacing.$Size-0 spacing.$Size-2 spacing.$Size-0 spacing.$Size-2; +} + +.tag-results { + list-style-type: none; + padding: 0; + margin: 0; + + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 150px 150px; +} + +.tag-result { + padding: spacing.$Size-1; + margin: spacing.$Size-0; + border-radius: 4px; + display: inline-flex; + flex-direction: column; + cursor: pointer; +} + +.tag-result:hover { + background-color: rgba(236, 238, 241, 0.7); +} +.tag-result--focused { + outline: none; + box-shadow: 0px 0px 0px 2px rgba(9, 120, 217, 1); + background-color: colors.$Gray-1; +} + +.tag-result-header { + display: inline-grid; + width: 100%; + grid-template-rows: 1fr; + // tag should be allowed to have 100% of its content visible, the rest of the space is distributed equally + grid-template-columns: max-content 1fr 1fr; + align-items: center; +} + +.tag-result-count { + justify-self: start; + overflow: hidden; + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: spacing.$Size-1; + font-size: fonts.$Size-1; +} + +.tag-result-details { + justify-self: flex-end; +} +.tag-result-details path { + fill: colors.$Gray-5; +} + +.tag-result-excerpt { + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + margin: 0; +} + +@media screen and (max-width: 500px) { + .tag-header-title { + font-size: fonts.$Size-1; + } + .tag-results { + display: inline; + padding: 0; + } + .tag-result { + display: inline-block; + padding: spacing.$Size-1; + margin: spacing.$Size-0; + } + .tag-result-header { + // just want to show the tags, no other info so we only need 1 column + grid-template-columns: 1fr; + } + .tag-result-details { + display: none; + } + .tag-result-excerpt { + display: none; + } +} + +@media screen and (max-width: 600px) { + .tag-result-header { + grid-template-columns: 8fr 1fr; + } + .tag-result-details { + justify-self: end; + } + .tag-result-count { + display: none; + } +} diff --git a/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.tsx b/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.tsx new file mode 100644 index 0000000..c20c1aa --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/TagEditor.tsx @@ -0,0 +1,357 @@ +import React from 'react'; +import { Tag, TagIcon, TagCloseButton, TagProps } from '../Tag'; +import { + getTagAttributes, + getDuplicateTagAlert, + isWhitespace, + getTags, + cleanText, + isDuplicate, + isEmpty, + noop, + fetchTags +} from './utils'; +import { useCombobox } from '../../combobox/useCombobox'; +import { useMultipleSelection } from '../../useMultipleSelection/useMultipleSelection'; +import { useAbortController } from '../../useAbortController/useAbortController'; +import useDebouncedCallback from '../../useDebounce/src/hooks/useDebouncedCallback'; +import useAsync from '../../useDebounce/src/hooks/useAsync'; +import { SearchLoader } from '../../loader/SearchLoader'; +import { UseAsyncStatus, UseAsyncState } from '../../useDebounce/src/types'; +import { NavigationKeys } from '../../useMultipleSelection/types'; +import { + ComboboxState, + ComboboxActionAndChanges, + ComboboxActions +} from '../../combobox/types'; +import { BottomlineTag, BottomlineTags, TagEditorProps } from './types'; +import { AiFillQuestionCircle } from 'react-icons/ai'; +import classNames from 'classnames'; +import './TagEditor.scss'; + +/** + * ******************* + * + * Tag Editor + * + * ******************* + * + * Use Case: + * - user inputs text in input + * - user confirms text via an event (click, keyboard) + * - tag is created and inserted in the box of tags + * - user can remove tags + * - user has option to use the editor as an autocomplete + * + * See: https://github.com/krfong916/bottomline/issues/6 for a formal spec on the use case + */ + +export const TagEditor = ({ + onTagsChanged = noop, + endpoint = '', + ...props +}: TagEditorProps) => { + const [input, setInput] = React.useState(''); + const prevInput = React.useRef(''); + const [selectedTags, setSelectedTags] = React.useState(); + const [tagSuggestions, setTagSuggestions] = React.useState< + BottomlineTag[] | undefined + >(); + // we define state and change handler callbacks instead of a ref because we don't need to handle + // we need the "appearance" of focus handling for the container when the input element is focused + const [inputFocused, setInputFocused] = React.useState(false); + const inputOnFocus = () => setInputFocused(true); + const inputOnBlur = () => { + setInputFocused(false); + if (props.onBlur) props.onBlur(); + }; + let derivedLoaderState = false; + const inputRef = React.useRef(); + const cancelRequestRef = React.useRef(false); + const debounce = useDebouncedCallback( + (dispatch) => { + if (cancelRequestRef.current === false) { + dispatch(); + } else { + cancelRequestRef.current = false; + } + }, + 1000, + { trailing: true } + ); + + const { data: tags, error, status, run } = useAsync({ + initialState: { + status: UseAsyncStatus.IDLE + } as UseAsyncState + }); + if (error) { + // console.log('[APP] error:', error); + } + + const { getSignal, forceAbort } = useAbortController(); + + React.useEffect(() => { + if (!input || (prevInput.current === '' && input === '')) return; + run(fetchTags(input, endpoint, getSignal)); + }, [input, run]); + + if (status === UseAsyncStatus.PENDING) derivedLoaderState = true; + + React.useEffect(() => { + if (tags) setTagSuggestions(tags); + }, [tags]); + + const { + getSelectedItemProps, + removeSelectedItem, + addSelectedItem, + getDropdownProps, + currentSelectedItemIndex, + items + } = useMultipleSelection({ + items: [], + itemToString: (item: BottomlineTag) => item.name, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT, + onItemsChange: (tags: BottomlineTag[]) => { + if (props.onChange) { + console.log('[TagEditor ON_ITEMS_CHANGE]', tags); + props.onChange(tags); + } + onTagsChanged(tags); + setSelectedTags(tags); + } + }); + + const handleRemove = (item: BottomlineTag, index: number) => + removeSelectedItem(item, index); + + function stateReducer( + state: ComboboxState, + actionAndChanges: ComboboxActionAndChanges + ) { + const { action, changes } = actionAndChanges; + const recommendations = { ...changes }; + switch (action.type) { + case ComboboxActions.ITEM_CLICK: { + // console.log('[CUSTOM_REDUCER] item click'); + // console.log('recommendations', recommendations); + // console.log('actionAndChanges', actionAndChanges); + inputRef.current.value = ''; + const newTags = { ...selectedTags }; + if (recommendations.selectedItem) { + if (!newTags[recommendations.selectedItem.name]) { + newTags[recommendations.selectedItem.name as keyof BottomlineTags] = + recommendations.selectedItem; + addSelectedItem(recommendations.selectedItem); + setSelectedTags(newTags); + // issue a warning, item already selected + } + recommendations.isOpen = false; + } + + return recommendations; + } + case ComboboxActions.INPUT_BLUR: { + // console.log('[CUSTOM_REDUCER] input blur'); + forceAbort(); + recommendations.inputValue = state.inputValue; + return recommendations; + } + case ComboboxActions.INPUT_VALUE_CHANGE: { + // if we've deleted all text from the input, close the popup + if (action.inputValue === '' && changes.inputValue !== '') { + recommendations.isOpen = false; + setTagSuggestions(undefined); + } + // console.log('[CUSTOM_REDUCER] input value change'); + // console.log('input', action.inputValue); + recommendations.inputValue = action.inputValue; + return recommendations; + } + case ComboboxActions.INPUT_KEYDOWN_ENTER: { + const newTags = { ...selectedTags }; + const textboxValue = inputRef.current.value; + let newSelectedItem; + + inputRef.current.value = ''; + cancelRequestRef.current = true; + + if (changes.selectedItem) { + newSelectedItem = changes.selectedItem; + } else { + newSelectedItem = { name: textboxValue }; + } + + if (!newTags[newSelectedItem.name]) { + newTags[newSelectedItem.name] = newSelectedItem; + addSelectedItem(newSelectedItem); + setSelectedTags(newTags); + } + + recommendations.isOpen = false; + return recommendations; + } + case ComboboxActions.FUNCTION_CLOSE_POPUP: { + if (inputRef.current) inputRef.current.focus(); + return recommendations; + } + default: { + return changes; + } + } + } + + const { + isOpen, + highlightedIndex, + getLabelProps, + getComboboxProps, + getInputProps, + getItemProps, + getPopupProps + } = useCombobox({ + onInputValueChange: (changes: Partial>) => { + // piggy-back on the state change + // set our own input value change + // console.log('[ON_INPUT_VALUE_CHANGE_CALLBACK]', changes); + prevInput.current = changes; + setInput(changes as string); + }, + itemToString: (tag: BottomlineTag) => tag.name, + stateReducer, + items: tagSuggestions, + initialIsOpen: tagSuggestions ? true : false + }); + + const noResultsFound = isOpen && tagSuggestions && tagSuggestions.length == 0; + const resultsFound = isOpen && tagSuggestions && tagSuggestions.length >= 1; + + const handleOnPaste = (e: React.ClipboardEvent) => { + console.log('[ON_PASTE]'); + console.log(e); + let pastedText = e.clipboardData.getData('text'); + pastedText = cleanText(pastedText); + cancelRequestRef.current = true; + + if (pastedText.length > 1) { + // create tags from the user's pasted text + pastedText.split(' ').forEach((pieceOfText) => { + addSelectedItem({ name: pieceOfText }); + }); + } + inputRef.current.value = ''; + setInput(''); + }; + + return ( +
    +
    +
    + + +
    +
    + {items ? ( +
      + {Object.keys(items).map((tagIndex, index) => { + const tag = items[tagIndex]; + const key = `${tag.name} ${index}`; + { + /*const active = currentSelectedItemIndex === index ? true : false;*/ + } + return ( +
    • + + handleRemove(tag, index)} + /> + +
    • + ); + })} +
    + ) : null} +
    +
    + ({ + controlDispatch: debounce, + onFocus: inputOnFocus, + onBlur: inputOnBlur, + ...getDropdownProps({ ref: inputRef }) + })} + onPaste={handleOnPaste} + type="text" + aria-labelledby={props.ariaLabelledBy} + aria-describedby={`tags-description ${props.ariaDescribedBy}`} + data-testid="tags-input" + autoComplete="off" + className="tag-search-input" + /> + {derivedLoaderState ? ( + + + + ) : null} +
    +
    + {noResultsFound ? ( + + No results found + + ) : null} + {resultsFound ? ( +
      + {tagSuggestions.map((tag, index: number) => { + const key = `${tag.name} ${index}`; + return ( +
    • +
      + + + {tag.count} + + +
      +

      {tag.excerpt}

      +
    • + ); + })} +
    + ) : null} +
    +
    +
    + ); +}; diff --git a/proof-of-concepts/features/stories/tags/TagEditor/__tests__/TagEditor.test.tsx b/proof-of-concepts/features/stories/tags/TagEditor/__tests__/TagEditor.test.tsx new file mode 100644 index 0000000..b85a0c9 --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/__tests__/TagEditor.test.tsx @@ -0,0 +1,107 @@ +import { + renderTagEditor, + getSelectedItem, + getAllSelectedItems, + getPopup, + getPopupItem, + getPopupItems +} from './utils'; +import { + render, + screen, + fireEvent, + waitFor, + waitForElementToBeRemoved +} from '@testing-library/react'; +import { server, rest } from '../../../test/server'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import { tags } from '../../../test/bottomline-data'; + +const tagsMockEndpoint = 'http://localhost:3000/tags'; + +describe('Tag Editor', () => { + /** + * Two tests that we cannot test for: + * - whitespace is treated as an enter for each word separated by whitespace + * - Input that has multiple white space is not allowed + */ + + test('When the input is focused and the input has text, the enter keydown event creates the selected tag ', () => { + const { container, input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + userEvent.type(input, 'material'); + expect(input).toHaveValue('material'); + userEvent.type(input, '{enter}'); + const createdSelectedItem = getSelectedItem(0); + expect(createdSelectedItem).toBeDefined(); + expect(input).toHaveValue(''); + }); + test('When a tag is created, the input is cleared', async () => { + const { container, input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + userEvent.type(input, 'material'); + expect(input).toHaveValue('material'); + userEvent.type(input, '{enter}'); + await waitFor(() => expect(input).toHaveValue('')); + + input.focus(); + await userEvent.type(input, 'material', { delay: 300 }); + const item = getPopupItem(1); + expect(item).toBeDefined(); + userEvent.click(item); + await waitFor(() => expect(input).toHaveValue('')); + }); + test('When a tag is created, any onTagsChanged callbacks are called', async () => { + const { onTagsChanged, input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + userEvent.type(input, 'w/e random text'); + userEvent.type(input, '{enter}'); + await waitFor(() => expect(getAllSelectedItems).toBeDefined()); + expect(onTagsChanged).toHaveBeenCalled(); + }); + test('When the user pastes text, we handle on paste', async () => { + // see: https://github.com/testing-library/react-testing-library/issues/499 + const { input, onPaste } = renderTagEditor(tagsMockEndpoint); + const text = 'this sentence will be created into 9 selected tags'; + input.focus(); + userEvent.paste(input, text); + expect(onPaste).toHaveBeenCalled(); + }); + + test('Clicking a suggested tag creates the tag and closes the popup, clears the input, and the input continues to be focused', async () => { + const { input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + await userEvent.type(input, 'material', { delay: 300 }); + const item = getPopupItem(0); + userEvent.click(item); + const suggestedItems = screen.queryAllByRole('gridcell'); + expect(suggestedItems).toHaveLength(0); + }); + test('A user is able to delete a created tag', async () => { + const { input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + userEvent.type(input, 'material'); + userEvent.type(input, '{enter}'); + await waitFor(() => expect(getAllSelectedItems).toBeDefined()); + const removeSelectedTagButton = screen.getByLabelText('remove'); + removeSelectedTagButton.click(); + const removedSelectedItem = getSelectedItem(0); + expect(removedSelectedItem).toBeUndefined(); + }); + test('Duplicate created tags are not allowed', async () => { + const { container, input } = renderTagEditor(tagsMockEndpoint); + input.focus(); + userEvent.type(input, 'material'); + expect(input).toHaveValue('material'); + userEvent.type(input, '{enter}'); + await waitFor(() => expect(input).toHaveValue('')); + + input.focus(); + userEvent.type(input, 'material'); + expect(input).toHaveValue('material'); + userEvent.type(input, '{enter}'); + + expect(getAllSelectedItems()).toHaveLength(1); + }); +}); diff --git a/proof-of-concepts/features/stories/tags/TagEditor/__tests__/utils.tsx b/proof-of-concepts/features/stories/tags/TagEditor/__tests__/utils.tsx new file mode 100644 index 0000000..f3cfda1 --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/__tests__/utils.tsx @@ -0,0 +1,338 @@ +import React from 'react'; +import { render, screen, getAllByRole } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { Tag, TagIcon, TagCloseButton, TagProps } from '../../Tag'; +import { + getTagAttributes, + getDuplicateTagAlert, + isWhitespace, + getTags, + cleanText, + isDuplicate, + isEmpty, + noop, + fetchTags +} from '../utils'; +import { useCombobox } from '../../../combobox/useCombobox'; +import { useMultipleSelection } from '../../../useMultipleSelection/useMultipleSelection'; +import { useAbortController } from '../../../useAbortController/useAbortController'; +import useDebouncedCallback from '../../../useDebounce/src/hooks/useDebouncedCallback'; +import useAsync from '../../../useDebounce/src/hooks/useAsync'; +import { SearchLoader } from '../../../loader/SearchLoader'; +import { UseAsyncStatus, UseAsyncState } from '../../../useDebounce/src/types'; +import { NavigationKeys } from '../../../useMultipleSelection/types'; +import { + ComboboxState, + ComboboxActionAndChanges, + ComboboxActions +} from '../../../combobox/types'; +import { BottomlineTag, BottomlineTags, TagEditorProps } from '../types'; +import { AiFillQuestionCircle } from 'react-icons/ai'; + +const dataTestId = { + input: 'input-testid', + popup: 'popup-testid', + selectedItem: 'selected-item-testid', + suggestedItem: 'suggested-item-testid' +}; + +export function renderTagEditor(endpoint: string) { + const onTagsChanged = jest.fn(() => true); + // on paste shouldn't be passed as a prop, but for testing purposes we need it + // additionally, jsdom doesn't implement the clipboardEvent API so we need to mock the onPaste event + const onPaste = jest.fn(() => true); + const container = render( + + ); + const input = screen.getByRole('textbox'); + return { + container, + input, + onTagsChanged, + onPaste + }; +} + +export function getSelectedItem(index: number) { + const items = screen.queryAllByTestId(dataTestId.selectedItem); + return items[index]; +} +export function getAllSelectedItems() { + return screen.getAllByTestId(dataTestId.selectedItem); +} +export function getPopup() { + return screen.getByRole('grid'); +} +export function getPopupItem(index: number) { + const items = screen.getAllByTestId(dataTestId.suggestedItem); + return items[index]; +} +export function getPopupItems() { + return screen.getAllByTestId(dataTestId.suggestedItem); +} + +function TagEditor({ endpoint = '', ...props }: TagEditorProps) { + const [input, setInput] = React.useState(''); + const prevInput = React.useRef(''); + const [selectedTags, setSelectedTags] = React.useState(); + const [tagSuggestions, setTagSuggestions] = React.useState< + BottomlineTag[] | undefined + >(); + // we define state and change handler callbacks instead of a ref because we don't need to handle + // we need the "appearance" of focus handling for the container when the input element is focused + const [inputFocused, setInputFocused] = React.useState(false); + const inputOnFocus = () => setInputFocused(true); + const inputOnBlur = () => setInputFocused(false); + let derivedLoaderState = false; + const inputRef = React.useRef(); + const cancelDebounceCallback = React.useRef(false); + const debounce = useDebouncedCallback( + (dispatch) => { + if (cancelDebounceCallback.current === false) { + dispatch(); + } else { + cancelDebounceCallback.current = false; + } + }, + 200, + { trailing: true } + ); + + const { data: tags, error, status, run } = useAsync({ + initialState: { + status: UseAsyncStatus.IDLE + } as UseAsyncState + }); + if (error) { + console.log('[APP] error:', error); + } + + const { getSignal, forceAbort } = useAbortController(); + + React.useEffect(() => { + if (!input || (prevInput.current === '' && input === '')) return; + run(fetchTags(input, endpoint, getSignal)); + }, [input, run]); + + if (status === UseAsyncStatus.PENDING) derivedLoaderState = true; + + React.useEffect(() => { + if (tags) setTagSuggestions(tags); + }, [tags]); + + const { + getSelectedItemProps, + removeSelectedItem, + addSelectedItem, + getDropdownProps, + currentSelectedItemIndex, + items + } = useMultipleSelection({ + // items: presetSelectedItems, + itemToString: (item: BottomlineTag) => item.name, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT, + onItemsChange: (items: Item[]) => { + props.onTagsChanged(items); + } + }); + + const handleRemove = (item: BottomlineTag, index: number) => + removeSelectedItem(item, index); + + function stateReducer( + state: ComboboxState, + actionAndChanges: ComboboxActionAndChanges + ) { + const { action, changes } = actionAndChanges; + const recommendations = { ...changes }; + switch (action.type) { + case ComboboxActions.ITEM_CLICK: { + inputRef.current.value = ''; + const newTags = { ...selectedTags }; + if (recommendations.selectedItem) { + if (!newTags[recommendations.selectedItem.name]) { + newTags[recommendations.selectedItem.name as keyof BottomlineTags] = + recommendations.selectedItem; + addSelectedItem(recommendations.selectedItem); + setSelectedTags(newTags); + // issue a warning, item already selected + } + recommendations.isOpen = false; + } + return recommendations; + } + case ComboboxActions.INPUT_BLUR: { + // forceAbort(); + // recommendations.inputValue = state.inputValue; + return recommendations; + } + case ComboboxActions.INPUT_VALUE_CHANGE: { + if (action.inputValue === '' && changes.inputValue !== '') { + recommendations.isOpen = false; + setTagSuggestions(undefined); + } + recommendations.inputValue = action.inputValue; + return recommendations; + } + case ComboboxActions.INPUT_KEYDOWN_ENTER: { + cancelDebounceCallback.current = true; + inputRef.current.value = ''; + const newTags = { ...selectedTags }; + const value = inputRef.current.value; + const newSelectedItem = { + name: value + }; + if (!newTags[newSelectedItem.name]) { + newTags[newSelectedItem.name] = newSelectedItem; + addSelectedItem(newSelectedItem); + setSelectedTags(newTags); + } + recommendations.isOpen = false; + return recommendations; + } + default: { + return changes; + } + } + } + + const { + isOpen, + highlightedIndex, + getLabelProps, + getComboboxProps, + getInputProps, + getItemProps, + getPopupProps + } = useCombobox({ + onInputValueChange: (changes: Partial>) => { + // piggy-back on the state change + // set our own input value change + prevInput.current = input; + setInput(changes as string); + }, + stateReducer, + items: tagSuggestions, + initialIsOpen: tagSuggestions ? true : false + }); + + const noResultsFound = isOpen && tagSuggestions && tagSuggestions.length == 0; + const resultsFound = isOpen && tagSuggestions && tagSuggestions.length >= 1; + + const handleOnPaste = () => props.onPaste(); + + return ( +
    +
    +
    + + +
    + +
    + {items ? ( +
      + {Object.keys(items).map((tagIndex, index) => { + const tag = items[tagIndex]; + const key = `${tag.name} ${index}`; + const removeLabel = `remove ${tag.name}`; + const active = currentSelectedItemIndex === index ? true : false; + return ( +
    • + + handleRemove(tag, index)} + /> + +
    • + ); + })} +
    + ) : null} +
    +
    + ({ + controlDispatch: debounce, + onFocus: inputOnFocus, + onBlur: inputOnBlur, + ...getDropdownProps({ ref: inputRef }) + })} + onPaste={handleOnPaste} + data-testid={dataTestId.input} + type="text" + autoComplete="off" + className="tag-search-input" + /> + {derivedLoaderState ? ( + + + + ) : null} +
    +
    + {noResultsFound ? ( + + No results found + + ) : null} + {resultsFound ? ( +
      + {tagSuggestions.map((tag, index: number) => { + const key = `${tag.name} ${index}`; + return ( +
    • +
      + + + {tag.count} + + +
      +

      {tag.excerpt}

      +
    • + ); + })} +
    + ) : null} +
    +
    +
    + ); +} diff --git a/proof-of-concepts/features/stories/tags/TagEditor/types.ts b/proof-of-concepts/features/stories/tags/TagEditor/types.ts new file mode 100644 index 0000000..2d694d5 --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/types.ts @@ -0,0 +1,18 @@ +export type TagEditorProps = { + onTagsChanged?: (tags: BottomlineTag[]) => void; + name?: string; + onBlur?: (args: any) => any; + onFocus?: (args: any) => any; + onChange?: (args: any) => any; + endpoint?: string; + ariaDescribedBy: string; + ariaLabelledBy: string; +}; +export type BottomlineTag = { + id?: string; + name: string; + count?: number; + excerpt?: string; +}; + +export type BottomlineTags = Record; diff --git a/proof-of-concepts/features/stories/tags/TagEditor/utils.ts b/proof-of-concepts/features/stories/tags/TagEditor/utils.ts new file mode 100644 index 0000000..a5ea754 --- /dev/null +++ b/proof-of-concepts/features/stories/tags/TagEditor/utils.ts @@ -0,0 +1,76 @@ +// debounce hook +// useInteractOutside - to close the popup? +import { delayRandomly, delayControlled, throwRandomly } from '../../utils'; +const props = { + size: 'small', + type: 'no-outline', + text: '', + orientation: 'right' +} as TagProps; + +export const noop = () => {}; + +export function fetchTags(tag: string, url: string, getSignal: () => AbortSignal) { + // change using env variable for url endpoint + if (process.env.PRODUCTION) { + // url = 'production endpoint'; + } + return fetch(url, { + signal: getSignal(), + method: 'GET', + headers: { + 'content-type': 'application/json' + } + }) + .then(async (res) => { + // await delayControlled(); + // throwRandomly(); + return res.json(); + }) + .catch((error) => { + return Promise.reject(error); + }); +} + +export function getTagAttributes(target: HTMLElement) { + return { + name: target.getAttribute('data-name'), + index: parseInt(target.getAttribute('data-index')), + tagContainer: target.getAttribute('data-container') + }; +} + +export function getDuplicateTagAlert(text: string): string { + return `Unable to create the tag "${text}", duplicate of a tag that you've already created. Text cleared, please continue typing as you were`; +} + +export function isWhitespace(str: string): boolean { + const whitespaceTest = new RegExp(/\s/); + return whitespaceTest.test(str); +} + +export function getTags(state: TagEditor): string[] { + let tags = state.tags; + if (tags.size === 0) return []; + let tagsList = []; + tags.forEach((_, tagName) => tagsList.push(tagName)); + return tagsList; +} + +export function cleanText(text) { + let regex = /[a-z]|[A-Z]|[0-9]|[\-]/g; + let cleaned = text.match(regex); + if (!cleaned) return ''; + const res = cleaned.join('').toLowerCase(); + return res; +} + +export function isDuplicate(tagText: string, state: TagEditor): boolean { + let tags: string[] = getTags(state); + if (tags.length === 0) return false; + return tags.includes(tagText); +} + +export function isEmpty(text: string): boolean { + return text === ''; +} diff --git a/proof-of-concepts/features/stories/test/bottomline-data.ts b/proof-of-concepts/features/stories/test/bottomline-data.ts new file mode 100644 index 0000000..c237a0b --- /dev/null +++ b/proof-of-concepts/features/stories/test/bottomline-data.ts @@ -0,0 +1,44 @@ +export const tags = [ + { + id: 1, + name: 'material-analysis', + count: 5, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + }, + { + id: 2, + name: 'class-analysis', + count: 33, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + }, + { + id: 3, + name: 'materialism', + count: 12, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + }, + { + id: 4, + name: 'dialectical-materialism', + count: 9, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + }, + { + id: 5, + name: 'historical-materialism', + count: 5, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + }, + { + id: 6, + name: 'materialist-theory', + count: 2, + excerpt: + "What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." + } +]; diff --git a/proof-of-concepts/features/stories/test/server-handlers.ts b/proof-of-concepts/features/stories/test/server-handlers.ts new file mode 100644 index 0000000..1dce4c2 --- /dev/null +++ b/proof-of-concepts/features/stories/test/server-handlers.ts @@ -0,0 +1,27 @@ +import 'whatwg-fetch'; +import { rest } from 'msw'; +import { tags } from './bottomline-data'; + +const delay = 0; + +const handlers = [ + rest.get('http://localhost:3000/tags', async (req, res, ctx) => { + const query = req.url.searchParams; + const like = query.get('like'); + return res(ctx.delay(delay), ctx.status(200), ctx.json(tags)); + }), + rest.post('https://localhost:3000/question', async (req, res, ctx) => { + if (!req.body.title) { + return res(ctx.status(400), ctx.json({ message: 'title required' })); + } + if (!req.body.body) { + return res(ctx.status(400), ctx.json({ message: 'body required' })); + } + if (!req.body.tags) { + return res(ctx.status(400), ctx.json({ message: 'tags required' })); + } + return res(ctx.status(201)); + }) +]; + +export { handlers }; diff --git a/proof-of-concepts/features/stories/test/server.ts b/proof-of-concepts/features/stories/test/server.ts new file mode 100644 index 0000000..309f571 --- /dev/null +++ b/proof-of-concepts/features/stories/test/server.ts @@ -0,0 +1,5 @@ +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { handlers } from './server-handlers'; +const server = setupServer(...handlers); +export { server, rest }; diff --git a/proof-of-concepts/features/stories/types.ts b/proof-of-concepts/features/stories/types.ts new file mode 100644 index 0000000..0398fb0 --- /dev/null +++ b/proof-of-concepts/features/stories/types.ts @@ -0,0 +1,5 @@ +export interface ComponentProps { + stateReducer?: (state: State, actionAndChanges: ActionAndChanges) => State; +} + +export type ItemsList = Record; diff --git a/proof-of-concepts/features/stories/useAbortController/useAbortController.ts b/proof-of-concepts/features/stories/useAbortController/useAbortController.ts new file mode 100644 index 0000000..1034688 --- /dev/null +++ b/proof-of-concepts/features/stories/useAbortController/useAbortController.ts @@ -0,0 +1,45 @@ +import React from 'react'; +export function useAbortController() { + const abortControllerRef = React.useRef(); + + // our abort controller is declared once on initial render + const getAbortController = React.useCallback(() => { + if (!abortControllerRef.current) { + abortControllerRef.current = new AbortController(); + } + return abortControllerRef.current; + }, []); + + // callback ran when we need to create a new abort controller + const createNewAbortController = React.useCallback(() => { + abortControllerRef.current = new AbortController(); + }, []); + + const forceAbort = React.useCallback(() => { + if (getAbortController()) { + // abort the previous request + getAbortController().abort(); + } + }, []); + + // when the component unmounts/re-renders, abort any existing requests + // to prevent memory leaks + React.useEffect(() => { + return () => getAbortController().abort(); + }, [getAbortController]); + + // when we call getSignal, we cancel any outstanding requests + // and create a new instance of an abort controller + // then return the signal for the requesting code + const getSignal = React.useCallback(() => { + if (getAbortController()) { + // abort the previous request + getAbortController().abort(); + createNewAbortController(); + } + + return getAbortController().signal; + }, [getAbortController, createNewAbortController]); + + return { getSignal, forceAbort }; +} diff --git a/proof-of-concepts/features/stories/useDebounce/src/App.tsx b/proof-of-concepts/features/stories/useDebounce/src/App.tsx new file mode 100755 index 0000000..c36fd81 --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/App.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import useDebouncedCallback from './hooks/useDebouncedCallback'; +import useAsync from './hooks/useAsync'; +import { UseAsyncStatus, UseAsyncResponse } from './types'; +import { fetchData } from './fetchData'; +const endpointAPI = 'http://localhost:3001/results'; + +export default function App() { + const [value, setValue] = React.useState(''); + const debounce = useDebouncedCallback( + (value: string) => { + setValue(value); + }, + 4000, + { trailing: true } + ); + React.useEffect(() => { + console.log('useEffect'); + }); + + const fetchResults: UseAsyncResponse = useAsync( + () => { + if (value && value !== '') { + return fetchData(endpointAPI); + } + }, + { status: UseAsyncStatus.IDLE }, + [value] + ); + + const { data: items, error, status } = fetchResults; + if (status === UseAsyncStatus.IDLE) { + console.log('we are idle'); + } else if (status === UseAsyncStatus.PENDING) { + console.log('we are pending'); + } else if (status === UseAsyncStatus.RESOLVED) { + console.log('we are resolved'); + } else { + console.log('we are rejected'); + } + + return ( +
    + ) => + debounce(e.currentTarget.value) + } + > +
    + ); +} diff --git a/proof-of-concepts/features/stories/useDebounce/src/data/stub-data.js b/proof-of-concepts/features/stories/useDebounce/src/data/stub-data.js new file mode 100644 index 0000000..f1f9560 --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/data/stub-data.js @@ -0,0 +1,805 @@ +const results = { + results: [ + { name: 'bulbasaur', url: 'https://pokeapi.co/api/v2/pokemon/1/' }, + { name: 'ivysaur', url: 'https://pokeapi.co/api/v2/pokemon/2/' }, + { name: 'venusaur', url: 'https://pokeapi.co/api/v2/pokemon/3/' }, + { name: 'charmander', url: 'https://pokeapi.co/api/v2/pokemon/4/' }, + { name: 'charmeleon', url: 'https://pokeapi.co/api/v2/pokemon/5/' }, + { name: 'charizard', url: 'https://pokeapi.co/api/v2/pokemon/6/' }, + { name: 'squirtle', url: 'https://pokeapi.co/api/v2/pokemon/7/' }, + { name: 'wartortle', url: 'https://pokeapi.co/api/v2/pokemon/8/' }, + { name: 'blastoise', url: 'https://pokeapi.co/api/v2/pokemon/9/' }, + { name: 'caterpie', url: 'https://pokeapi.co/api/v2/pokemon/10/' }, + { name: 'metapod', url: 'https://pokeapi.co/api/v2/pokemon/11/' }, + { name: 'butterfree', url: 'https://pokeapi.co/api/v2/pokemon/12/' }, + { name: 'weedle', url: 'https://pokeapi.co/api/v2/pokemon/13/' }, + { name: 'kakuna', url: 'https://pokeapi.co/api/v2/pokemon/14/' }, + { name: 'beedrill', url: 'https://pokeapi.co/api/v2/pokemon/15/' }, + { name: 'pidgey', url: 'https://pokeapi.co/api/v2/pokemon/16/' }, + { name: 'pidgeotto', url: 'https://pokeapi.co/api/v2/pokemon/17/' }, + { name: 'pidgeot', url: 'https://pokeapi.co/api/v2/pokemon/18/' }, + { name: 'rattata', url: 'https://pokeapi.co/api/v2/pokemon/19/' }, + { name: 'raticate', url: 'https://pokeapi.co/api/v2/pokemon/20/' }, + { name: 'spearow', url: 'https://pokeapi.co/api/v2/pokemon/21/' }, + { name: 'fearow', url: 'https://pokeapi.co/api/v2/pokemon/22/' }, + { name: 'ekans', url: 'https://pokeapi.co/api/v2/pokemon/23/' }, + { name: 'arbok', url: 'https://pokeapi.co/api/v2/pokemon/24/' }, + { name: 'pikachu', url: 'https://pokeapi.co/api/v2/pokemon/25/' }, + { name: 'raichu', url: 'https://pokeapi.co/api/v2/pokemon/26/' }, + { name: 'sandshrew', url: 'https://pokeapi.co/api/v2/pokemon/27/' }, + { name: 'sandslash', url: 'https://pokeapi.co/api/v2/pokemon/28/' }, + { name: 'nidoran-f', url: 'https://pokeapi.co/api/v2/pokemon/29/' }, + { name: 'nidorina', url: 'https://pokeapi.co/api/v2/pokemon/30/' }, + { name: 'nidoqueen', url: 'https://pokeapi.co/api/v2/pokemon/31/' }, + { name: 'nidoran-m', url: 'https://pokeapi.co/api/v2/pokemon/32/' }, + { name: 'nidorino', url: 'https://pokeapi.co/api/v2/pokemon/33/' }, + { name: 'nidoking', url: 'https://pokeapi.co/api/v2/pokemon/34/' }, + { name: 'clefairy', url: 'https://pokeapi.co/api/v2/pokemon/35/' }, + { name: 'clefable', url: 'https://pokeapi.co/api/v2/pokemon/36/' }, + { name: 'vulpix', url: 'https://pokeapi.co/api/v2/pokemon/37/' }, + { name: 'ninetales', url: 'https://pokeapi.co/api/v2/pokemon/38/' }, + { name: 'jigglypuff', url: 'https://pokeapi.co/api/v2/pokemon/39/' }, + { name: 'wigglytuff', url: 'https://pokeapi.co/api/v2/pokemon/40/' }, + { name: 'zubat', url: 'https://pokeapi.co/api/v2/pokemon/41/' }, + { name: 'golbat', url: 'https://pokeapi.co/api/v2/pokemon/42/' }, + { name: 'oddish', url: 'https://pokeapi.co/api/v2/pokemon/43/' }, + { name: 'gloom', url: 'https://pokeapi.co/api/v2/pokemon/44/' }, + { name: 'vileplume', url: 'https://pokeapi.co/api/v2/pokemon/45/' }, + { name: 'paras', url: 'https://pokeapi.co/api/v2/pokemon/46/' }, + { name: 'parasect', url: 'https://pokeapi.co/api/v2/pokemon/47/' }, + { name: 'venonat', url: 'https://pokeapi.co/api/v2/pokemon/48/' }, + { name: 'venomoth', url: 'https://pokeapi.co/api/v2/pokemon/49/' }, + { name: 'diglett', url: 'https://pokeapi.co/api/v2/pokemon/50/' }, + { name: 'dugtrio', url: 'https://pokeapi.co/api/v2/pokemon/51/' }, + { name: 'meowth', url: 'https://pokeapi.co/api/v2/pokemon/52/' }, + { name: 'persian', url: 'https://pokeapi.co/api/v2/pokemon/53/' }, + { name: 'psyduck', url: 'https://pokeapi.co/api/v2/pokemon/54/' }, + { name: 'golduck', url: 'https://pokeapi.co/api/v2/pokemon/55/' }, + { name: 'mankey', url: 'https://pokeapi.co/api/v2/pokemon/56/' }, + { name: 'primeape', url: 'https://pokeapi.co/api/v2/pokemon/57/' }, + { name: 'growlithe', url: 'https://pokeapi.co/api/v2/pokemon/58/' }, + { name: 'arcanine', url: 'https://pokeapi.co/api/v2/pokemon/59/' }, + { name: 'poliwag', url: 'https://pokeapi.co/api/v2/pokemon/60/' }, + { name: 'poliwhirl', url: 'https://pokeapi.co/api/v2/pokemon/61/' }, + { name: 'poliwrath', url: 'https://pokeapi.co/api/v2/pokemon/62/' }, + { name: 'abra', url: 'https://pokeapi.co/api/v2/pokemon/63/' }, + { name: 'kadabra', url: 'https://pokeapi.co/api/v2/pokemon/64/' }, + { name: 'alakazam', url: 'https://pokeapi.co/api/v2/pokemon/65/' }, + { name: 'machop', url: 'https://pokeapi.co/api/v2/pokemon/66/' }, + { name: 'machoke', url: 'https://pokeapi.co/api/v2/pokemon/67/' }, + { name: 'machamp', url: 'https://pokeapi.co/api/v2/pokemon/68/' }, + { name: 'bellsprout', url: 'https://pokeapi.co/api/v2/pokemon/69/' }, + { name: 'weepinbell', url: 'https://pokeapi.co/api/v2/pokemon/70/' }, + { name: 'victreebel', url: 'https://pokeapi.co/api/v2/pokemon/71/' }, + { name: 'tentacool', url: 'https://pokeapi.co/api/v2/pokemon/72/' }, + { name: 'tentacruel', url: 'https://pokeapi.co/api/v2/pokemon/73/' }, + { name: 'geodude', url: 'https://pokeapi.co/api/v2/pokemon/74/' }, + { name: 'graveler', url: 'https://pokeapi.co/api/v2/pokemon/75/' }, + { name: 'golem', url: 'https://pokeapi.co/api/v2/pokemon/76/' }, + { name: 'ponyta', url: 'https://pokeapi.co/api/v2/pokemon/77/' }, + { name: 'rapidash', url: 'https://pokeapi.co/api/v2/pokemon/78/' }, + { name: 'slowpoke', url: 'https://pokeapi.co/api/v2/pokemon/79/' }, + { name: 'slowbro', url: 'https://pokeapi.co/api/v2/pokemon/80/' }, + { name: 'magnemite', url: 'https://pokeapi.co/api/v2/pokemon/81/' }, + { name: 'magneton', url: 'https://pokeapi.co/api/v2/pokemon/82/' }, + { name: 'farfetchd', url: 'https://pokeapi.co/api/v2/pokemon/83/' }, + { name: 'doduo', url: 'https://pokeapi.co/api/v2/pokemon/84/' }, + { name: 'dodrio', url: 'https://pokeapi.co/api/v2/pokemon/85/' }, + { name: 'seel', url: 'https://pokeapi.co/api/v2/pokemon/86/' }, + { name: 'dewgong', url: 'https://pokeapi.co/api/v2/pokemon/87/' }, + { name: 'grimer', url: 'https://pokeapi.co/api/v2/pokemon/88/' }, + { name: 'muk', url: 'https://pokeapi.co/api/v2/pokemon/89/' }, + { name: 'shellder', url: 'https://pokeapi.co/api/v2/pokemon/90/' }, + { name: 'cloyster', url: 'https://pokeapi.co/api/v2/pokemon/91/' }, + { name: 'gastly', url: 'https://pokeapi.co/api/v2/pokemon/92/' }, + { name: 'haunter', url: 'https://pokeapi.co/api/v2/pokemon/93/' }, + { name: 'gengar', url: 'https://pokeapi.co/api/v2/pokemon/94/' }, + { name: 'onix', url: 'https://pokeapi.co/api/v2/pokemon/95/' }, + { name: 'drowzee', url: 'https://pokeapi.co/api/v2/pokemon/96/' }, + { name: 'hypno', url: 'https://pokeapi.co/api/v2/pokemon/97/' }, + { name: 'krabby', url: 'https://pokeapi.co/api/v2/pokemon/98/' }, + { name: 'kingler', url: 'https://pokeapi.co/api/v2/pokemon/99/' }, + { name: 'voltorb', url: 'https://pokeapi.co/api/v2/pokemon/100/' }, + { name: 'electrode', url: 'https://pokeapi.co/api/v2/pokemon/101/' }, + { name: 'exeggcute', url: 'https://pokeapi.co/api/v2/pokemon/102/' }, + { name: 'exeggutor', url: 'https://pokeapi.co/api/v2/pokemon/103/' }, + { name: 'cubone', url: 'https://pokeapi.co/api/v2/pokemon/104/' }, + { name: 'marowak', url: 'https://pokeapi.co/api/v2/pokemon/105/' }, + { name: 'hitmonlee', url: 'https://pokeapi.co/api/v2/pokemon/106/' }, + { name: 'hitmonchan', url: 'https://pokeapi.co/api/v2/pokemon/107/' }, + { name: 'lickitung', url: 'https://pokeapi.co/api/v2/pokemon/108/' }, + { name: 'koffing', url: 'https://pokeapi.co/api/v2/pokemon/109/' }, + { name: 'weezing', url: 'https://pokeapi.co/api/v2/pokemon/110/' }, + { name: 'rhyhorn', url: 'https://pokeapi.co/api/v2/pokemon/111/' }, + { name: 'rhydon', url: 'https://pokeapi.co/api/v2/pokemon/112/' }, + { name: 'chansey', url: 'https://pokeapi.co/api/v2/pokemon/113/' }, + { name: 'tangela', url: 'https://pokeapi.co/api/v2/pokemon/114/' }, + { name: 'kangaskhan', url: 'https://pokeapi.co/api/v2/pokemon/115/' }, + { name: 'horsea', url: 'https://pokeapi.co/api/v2/pokemon/116/' }, + { name: 'seadra', url: 'https://pokeapi.co/api/v2/pokemon/117/' }, + { name: 'goldeen', url: 'https://pokeapi.co/api/v2/pokemon/118/' }, + { name: 'seaking', url: 'https://pokeapi.co/api/v2/pokemon/119/' }, + { name: 'staryu', url: 'https://pokeapi.co/api/v2/pokemon/120/' }, + { name: 'starmie', url: 'https://pokeapi.co/api/v2/pokemon/121/' }, + { name: 'mr-mime', url: 'https://pokeapi.co/api/v2/pokemon/122/' }, + { name: 'scyther', url: 'https://pokeapi.co/api/v2/pokemon/123/' }, + { name: 'jynx', url: 'https://pokeapi.co/api/v2/pokemon/124/' }, + { name: 'electabuzz', url: 'https://pokeapi.co/api/v2/pokemon/125/' }, + { name: 'magmar', url: 'https://pokeapi.co/api/v2/pokemon/126/' }, + { name: 'pinsir', url: 'https://pokeapi.co/api/v2/pokemon/127/' }, + { name: 'tauros', url: 'https://pokeapi.co/api/v2/pokemon/128/' }, + { name: 'magikarp', url: 'https://pokeapi.co/api/v2/pokemon/129/' }, + { name: 'gyarados', url: 'https://pokeapi.co/api/v2/pokemon/130/' }, + { name: 'lapras', url: 'https://pokeapi.co/api/v2/pokemon/131/' }, + { name: 'ditto', url: 'https://pokeapi.co/api/v2/pokemon/132/' }, + { name: 'eevee', url: 'https://pokeapi.co/api/v2/pokemon/133/' }, + { name: 'vaporeon', url: 'https://pokeapi.co/api/v2/pokemon/134/' }, + { name: 'jolteon', url: 'https://pokeapi.co/api/v2/pokemon/135/' }, + { name: 'flareon', url: 'https://pokeapi.co/api/v2/pokemon/136/' }, + { name: 'porygon', url: 'https://pokeapi.co/api/v2/pokemon/137/' }, + { name: 'omanyte', url: 'https://pokeapi.co/api/v2/pokemon/138/' }, + { name: 'omastar', url: 'https://pokeapi.co/api/v2/pokemon/139/' }, + { name: 'kabuto', url: 'https://pokeapi.co/api/v2/pokemon/140/' }, + { name: 'kabutops', url: 'https://pokeapi.co/api/v2/pokemon/141/' }, + { name: 'aerodactyl', url: 'https://pokeapi.co/api/v2/pokemon/142/' }, + { name: 'snorlax', url: 'https://pokeapi.co/api/v2/pokemon/143/' }, + { name: 'articuno', url: 'https://pokeapi.co/api/v2/pokemon/144/' }, + { name: 'zapdos', url: 'https://pokeapi.co/api/v2/pokemon/145/' }, + { name: 'moltres', url: 'https://pokeapi.co/api/v2/pokemon/146/' }, + { name: 'dratini', url: 'https://pokeapi.co/api/v2/pokemon/147/' }, + { name: 'dragonair', url: 'https://pokeapi.co/api/v2/pokemon/148/' }, + { name: 'dragonite', url: 'https://pokeapi.co/api/v2/pokemon/149/' }, + { name: 'mewtwo', url: 'https://pokeapi.co/api/v2/pokemon/150/' }, + { name: 'mew', url: 'https://pokeapi.co/api/v2/pokemon/151/' }, + { name: 'chikorita', url: 'https://pokeapi.co/api/v2/pokemon/152/' }, + { name: 'bayleef', url: 'https://pokeapi.co/api/v2/pokemon/153/' }, + { name: 'meganium', url: 'https://pokeapi.co/api/v2/pokemon/154/' }, + { name: 'cyndaquil', url: 'https://pokeapi.co/api/v2/pokemon/155/' }, + { name: 'quilava', url: 'https://pokeapi.co/api/v2/pokemon/156/' }, + { name: 'typhlosion', url: 'https://pokeapi.co/api/v2/pokemon/157/' }, + { name: 'totodile', url: 'https://pokeapi.co/api/v2/pokemon/158/' }, + { name: 'croconaw', url: 'https://pokeapi.co/api/v2/pokemon/159/' }, + { name: 'feraligatr', url: 'https://pokeapi.co/api/v2/pokemon/160/' }, + { name: 'sentret', url: 'https://pokeapi.co/api/v2/pokemon/161/' }, + { name: 'furret', url: 'https://pokeapi.co/api/v2/pokemon/162/' }, + { name: 'hoothoot', url: 'https://pokeapi.co/api/v2/pokemon/163/' }, + { name: 'noctowl', url: 'https://pokeapi.co/api/v2/pokemon/164/' }, + { name: 'ledyba', url: 'https://pokeapi.co/api/v2/pokemon/165/' }, + { name: 'ledian', url: 'https://pokeapi.co/api/v2/pokemon/166/' }, + { name: 'spinarak', url: 'https://pokeapi.co/api/v2/pokemon/167/' }, + { name: 'ariados', url: 'https://pokeapi.co/api/v2/pokemon/168/' }, + { name: 'crobat', url: 'https://pokeapi.co/api/v2/pokemon/169/' }, + { name: 'chinchou', url: 'https://pokeapi.co/api/v2/pokemon/170/' }, + { name: 'lanturn', url: 'https://pokeapi.co/api/v2/pokemon/171/' }, + { name: 'pichu', url: 'https://pokeapi.co/api/v2/pokemon/172/' }, + { name: 'cleffa', url: 'https://pokeapi.co/api/v2/pokemon/173/' }, + { name: 'igglybuff', url: 'https://pokeapi.co/api/v2/pokemon/174/' }, + { name: 'togepi', url: 'https://pokeapi.co/api/v2/pokemon/175/' }, + { name: 'togetic', url: 'https://pokeapi.co/api/v2/pokemon/176/' }, + { name: 'natu', url: 'https://pokeapi.co/api/v2/pokemon/177/' }, + { name: 'xatu', url: 'https://pokeapi.co/api/v2/pokemon/178/' }, + { name: 'mareep', url: 'https://pokeapi.co/api/v2/pokemon/179/' }, + { name: 'flaaffy', url: 'https://pokeapi.co/api/v2/pokemon/180/' }, + { name: 'ampharos', url: 'https://pokeapi.co/api/v2/pokemon/181/' }, + { name: 'bellossom', url: 'https://pokeapi.co/api/v2/pokemon/182/' }, + { name: 'marill', url: 'https://pokeapi.co/api/v2/pokemon/183/' }, + { name: 'azumarill', url: 'https://pokeapi.co/api/v2/pokemon/184/' }, + { name: 'sudowoodo', url: 'https://pokeapi.co/api/v2/pokemon/185/' }, + { name: 'politoed', url: 'https://pokeapi.co/api/v2/pokemon/186/' }, + { name: 'hoppip', url: 'https://pokeapi.co/api/v2/pokemon/187/' }, + { name: 'skiploom', url: 'https://pokeapi.co/api/v2/pokemon/188/' }, + { name: 'jumpluff', url: 'https://pokeapi.co/api/v2/pokemon/189/' }, + { name: 'aipom', url: 'https://pokeapi.co/api/v2/pokemon/190/' }, + { name: 'sunkern', url: 'https://pokeapi.co/api/v2/pokemon/191/' }, + { name: 'sunflora', url: 'https://pokeapi.co/api/v2/pokemon/192/' }, + { name: 'yanma', url: 'https://pokeapi.co/api/v2/pokemon/193/' }, + { name: 'wooper', url: 'https://pokeapi.co/api/v2/pokemon/194/' }, + { name: 'quagsire', url: 'https://pokeapi.co/api/v2/pokemon/195/' }, + { name: 'espeon', url: 'https://pokeapi.co/api/v2/pokemon/196/' }, + { name: 'umbreon', url: 'https://pokeapi.co/api/v2/pokemon/197/' }, + { name: 'murkrow', url: 'https://pokeapi.co/api/v2/pokemon/198/' }, + { name: 'slowking', url: 'https://pokeapi.co/api/v2/pokemon/199/' }, + { name: 'misdreavus', url: 'https://pokeapi.co/api/v2/pokemon/200/' }, + { name: 'unown', url: 'https://pokeapi.co/api/v2/pokemon/201/' }, + { name: 'wobbuffet', url: 'https://pokeapi.co/api/v2/pokemon/202/' }, + { name: 'girafarig', url: 'https://pokeapi.co/api/v2/pokemon/203/' }, + { name: 'pineco', url: 'https://pokeapi.co/api/v2/pokemon/204/' }, + { name: 'forretress', url: 'https://pokeapi.co/api/v2/pokemon/205/' }, + { name: 'dunsparce', url: 'https://pokeapi.co/api/v2/pokemon/206/' }, + { name: 'gligar', url: 'https://pokeapi.co/api/v2/pokemon/207/' }, + { name: 'steelix', url: 'https://pokeapi.co/api/v2/pokemon/208/' }, + { name: 'snubbull', url: 'https://pokeapi.co/api/v2/pokemon/209/' }, + { name: 'granbull', url: 'https://pokeapi.co/api/v2/pokemon/210/' }, + { name: 'qwilfish', url: 'https://pokeapi.co/api/v2/pokemon/211/' }, + { name: 'scizor', url: 'https://pokeapi.co/api/v2/pokemon/212/' }, + { name: 'shuckle', url: 'https://pokeapi.co/api/v2/pokemon/213/' }, + { name: 'heracross', url: 'https://pokeapi.co/api/v2/pokemon/214/' }, + { name: 'sneasel', url: 'https://pokeapi.co/api/v2/pokemon/215/' }, + { name: 'teddiursa', url: 'https://pokeapi.co/api/v2/pokemon/216/' }, + { name: 'ursaring', url: 'https://pokeapi.co/api/v2/pokemon/217/' }, + { name: 'slugma', url: 'https://pokeapi.co/api/v2/pokemon/218/' }, + { name: 'magcargo', url: 'https://pokeapi.co/api/v2/pokemon/219/' }, + { name: 'swinub', url: 'https://pokeapi.co/api/v2/pokemon/220/' }, + { name: 'piloswine', url: 'https://pokeapi.co/api/v2/pokemon/221/' }, + { name: 'corsola', url: 'https://pokeapi.co/api/v2/pokemon/222/' }, + { name: 'remoraid', url: 'https://pokeapi.co/api/v2/pokemon/223/' }, + { name: 'octillery', url: 'https://pokeapi.co/api/v2/pokemon/224/' }, + { name: 'delibird', url: 'https://pokeapi.co/api/v2/pokemon/225/' }, + { name: 'mantine', url: 'https://pokeapi.co/api/v2/pokemon/226/' }, + { name: 'skarmory', url: 'https://pokeapi.co/api/v2/pokemon/227/' }, + { name: 'houndour', url: 'https://pokeapi.co/api/v2/pokemon/228/' }, + { name: 'houndoom', url: 'https://pokeapi.co/api/v2/pokemon/229/' }, + { name: 'kingdra', url: 'https://pokeapi.co/api/v2/pokemon/230/' }, + { name: 'phanpy', url: 'https://pokeapi.co/api/v2/pokemon/231/' }, + { name: 'donphan', url: 'https://pokeapi.co/api/v2/pokemon/232/' }, + { name: 'porygon2', url: 'https://pokeapi.co/api/v2/pokemon/233/' }, + { name: 'stantler', url: 'https://pokeapi.co/api/v2/pokemon/234/' }, + { name: 'smeargle', url: 'https://pokeapi.co/api/v2/pokemon/235/' }, + { name: 'tyrogue', url: 'https://pokeapi.co/api/v2/pokemon/236/' }, + { name: 'hitmontop', url: 'https://pokeapi.co/api/v2/pokemon/237/' }, + { name: 'smoochum', url: 'https://pokeapi.co/api/v2/pokemon/238/' }, + { name: 'elekid', url: 'https://pokeapi.co/api/v2/pokemon/239/' }, + { name: 'magby', url: 'https://pokeapi.co/api/v2/pokemon/240/' }, + { name: 'miltank', url: 'https://pokeapi.co/api/v2/pokemon/241/' }, + { name: 'blissey', url: 'https://pokeapi.co/api/v2/pokemon/242/' }, + { name: 'raikou', url: 'https://pokeapi.co/api/v2/pokemon/243/' }, + { name: 'entei', url: 'https://pokeapi.co/api/v2/pokemon/244/' }, + { name: 'suicune', url: 'https://pokeapi.co/api/v2/pokemon/245/' }, + { name: 'larvitar', url: 'https://pokeapi.co/api/v2/pokemon/246/' }, + { name: 'pupitar', url: 'https://pokeapi.co/api/v2/pokemon/247/' }, + { name: 'tyranitar', url: 'https://pokeapi.co/api/v2/pokemon/248/' }, + { name: 'lugia', url: 'https://pokeapi.co/api/v2/pokemon/249/' }, + { name: 'ho-oh', url: 'https://pokeapi.co/api/v2/pokemon/250/' }, + { name: 'celebi', url: 'https://pokeapi.co/api/v2/pokemon/251/' }, + { name: 'treecko', url: 'https://pokeapi.co/api/v2/pokemon/252/' }, + { name: 'grovyle', url: 'https://pokeapi.co/api/v2/pokemon/253/' }, + { name: 'sceptile', url: 'https://pokeapi.co/api/v2/pokemon/254/' }, + { name: 'torchic', url: 'https://pokeapi.co/api/v2/pokemon/255/' }, + { name: 'combusken', url: 'https://pokeapi.co/api/v2/pokemon/256/' }, + { name: 'blaziken', url: 'https://pokeapi.co/api/v2/pokemon/257/' }, + { name: 'mudkip', url: 'https://pokeapi.co/api/v2/pokemon/258/' }, + { name: 'marshtomp', url: 'https://pokeapi.co/api/v2/pokemon/259/' }, + { name: 'swampert', url: 'https://pokeapi.co/api/v2/pokemon/260/' }, + { name: 'poochyena', url: 'https://pokeapi.co/api/v2/pokemon/261/' }, + { name: 'mightyena', url: 'https://pokeapi.co/api/v2/pokemon/262/' }, + { name: 'zigzagoon', url: 'https://pokeapi.co/api/v2/pokemon/263/' }, + { name: 'linoone', url: 'https://pokeapi.co/api/v2/pokemon/264/' }, + { name: 'wurmple', url: 'https://pokeapi.co/api/v2/pokemon/265/' }, + { name: 'silcoon', url: 'https://pokeapi.co/api/v2/pokemon/266/' }, + { name: 'beautifly', url: 'https://pokeapi.co/api/v2/pokemon/267/' }, + { name: 'cascoon', url: 'https://pokeapi.co/api/v2/pokemon/268/' }, + { name: 'dustox', url: 'https://pokeapi.co/api/v2/pokemon/269/' }, + { name: 'lotad', url: 'https://pokeapi.co/api/v2/pokemon/270/' }, + { name: 'lombre', url: 'https://pokeapi.co/api/v2/pokemon/271/' }, + { name: 'ludicolo', url: 'https://pokeapi.co/api/v2/pokemon/272/' }, + { name: 'seedot', url: 'https://pokeapi.co/api/v2/pokemon/273/' }, + { name: 'nuzleaf', url: 'https://pokeapi.co/api/v2/pokemon/274/' }, + { name: 'shiftry', url: 'https://pokeapi.co/api/v2/pokemon/275/' }, + { name: 'taillow', url: 'https://pokeapi.co/api/v2/pokemon/276/' }, + { name: 'swellow', url: 'https://pokeapi.co/api/v2/pokemon/277/' }, + { name: 'wingull', url: 'https://pokeapi.co/api/v2/pokemon/278/' }, + { name: 'pelipper', url: 'https://pokeapi.co/api/v2/pokemon/279/' }, + { name: 'ralts', url: 'https://pokeapi.co/api/v2/pokemon/280/' }, + { name: 'kirlia', url: 'https://pokeapi.co/api/v2/pokemon/281/' }, + { name: 'gardevoir', url: 'https://pokeapi.co/api/v2/pokemon/282/' }, + { name: 'surskit', url: 'https://pokeapi.co/api/v2/pokemon/283/' }, + { name: 'masquerain', url: 'https://pokeapi.co/api/v2/pokemon/284/' }, + { name: 'shroomish', url: 'https://pokeapi.co/api/v2/pokemon/285/' }, + { name: 'breloom', url: 'https://pokeapi.co/api/v2/pokemon/286/' }, + { name: 'slakoth', url: 'https://pokeapi.co/api/v2/pokemon/287/' }, + { name: 'vigoroth', url: 'https://pokeapi.co/api/v2/pokemon/288/' }, + { name: 'slaking', url: 'https://pokeapi.co/api/v2/pokemon/289/' }, + { name: 'nincada', url: 'https://pokeapi.co/api/v2/pokemon/290/' }, + { name: 'ninjask', url: 'https://pokeapi.co/api/v2/pokemon/291/' }, + { name: 'shedinja', url: 'https://pokeapi.co/api/v2/pokemon/292/' }, + { name: 'whismur', url: 'https://pokeapi.co/api/v2/pokemon/293/' }, + { name: 'loudred', url: 'https://pokeapi.co/api/v2/pokemon/294/' }, + { name: 'exploud', url: 'https://pokeapi.co/api/v2/pokemon/295/' }, + { name: 'makuhita', url: 'https://pokeapi.co/api/v2/pokemon/296/' }, + { name: 'hariyama', url: 'https://pokeapi.co/api/v2/pokemon/297/' }, + { name: 'azurill', url: 'https://pokeapi.co/api/v2/pokemon/298/' }, + { name: 'nosepass', url: 'https://pokeapi.co/api/v2/pokemon/299/' }, + { name: 'skitty', url: 'https://pokeapi.co/api/v2/pokemon/300/' }, + { name: 'delcatty', url: 'https://pokeapi.co/api/v2/pokemon/301/' }, + { name: 'sableye', url: 'https://pokeapi.co/api/v2/pokemon/302/' }, + { name: 'mawile', url: 'https://pokeapi.co/api/v2/pokemon/303/' }, + { name: 'aron', url: 'https://pokeapi.co/api/v2/pokemon/304/' }, + { name: 'lairon', url: 'https://pokeapi.co/api/v2/pokemon/305/' }, + { name: 'aggron', url: 'https://pokeapi.co/api/v2/pokemon/306/' }, + { name: 'meditite', url: 'https://pokeapi.co/api/v2/pokemon/307/' }, + { name: 'medicham', url: 'https://pokeapi.co/api/v2/pokemon/308/' }, + { name: 'electrike', url: 'https://pokeapi.co/api/v2/pokemon/309/' }, + { name: 'manectric', url: 'https://pokeapi.co/api/v2/pokemon/310/' }, + { name: 'plusle', url: 'https://pokeapi.co/api/v2/pokemon/311/' }, + { name: 'minun', url: 'https://pokeapi.co/api/v2/pokemon/312/' }, + { name: 'volbeat', url: 'https://pokeapi.co/api/v2/pokemon/313/' }, + { name: 'illumise', url: 'https://pokeapi.co/api/v2/pokemon/314/' }, + { name: 'roselia', url: 'https://pokeapi.co/api/v2/pokemon/315/' }, + { name: 'gulpin', url: 'https://pokeapi.co/api/v2/pokemon/316/' }, + { name: 'swalot', url: 'https://pokeapi.co/api/v2/pokemon/317/' }, + { name: 'carvanha', url: 'https://pokeapi.co/api/v2/pokemon/318/' }, + { name: 'sharpedo', url: 'https://pokeapi.co/api/v2/pokemon/319/' }, + { name: 'wailmer', url: 'https://pokeapi.co/api/v2/pokemon/320/' }, + { name: 'wailord', url: 'https://pokeapi.co/api/v2/pokemon/321/' }, + { name: 'numel', url: 'https://pokeapi.co/api/v2/pokemon/322/' }, + { name: 'camerupt', url: 'https://pokeapi.co/api/v2/pokemon/323/' }, + { name: 'torkoal', url: 'https://pokeapi.co/api/v2/pokemon/324/' }, + { name: 'spoink', url: 'https://pokeapi.co/api/v2/pokemon/325/' }, + { name: 'grumpig', url: 'https://pokeapi.co/api/v2/pokemon/326/' }, + { name: 'spinda', url: 'https://pokeapi.co/api/v2/pokemon/327/' }, + { name: 'trapinch', url: 'https://pokeapi.co/api/v2/pokemon/328/' }, + { name: 'vibrava', url: 'https://pokeapi.co/api/v2/pokemon/329/' }, + { name: 'flygon', url: 'https://pokeapi.co/api/v2/pokemon/330/' }, + { name: 'cacnea', url: 'https://pokeapi.co/api/v2/pokemon/331/' }, + { name: 'cacturne', url: 'https://pokeapi.co/api/v2/pokemon/332/' }, + { name: 'swablu', url: 'https://pokeapi.co/api/v2/pokemon/333/' }, + { name: 'altaria', url: 'https://pokeapi.co/api/v2/pokemon/334/' }, + { name: 'zangoose', url: 'https://pokeapi.co/api/v2/pokemon/335/' }, + { name: 'seviper', url: 'https://pokeapi.co/api/v2/pokemon/336/' }, + { name: 'lunatone', url: 'https://pokeapi.co/api/v2/pokemon/337/' }, + { name: 'solrock', url: 'https://pokeapi.co/api/v2/pokemon/338/' }, + { name: 'barboach', url: 'https://pokeapi.co/api/v2/pokemon/339/' }, + { name: 'whiscash', url: 'https://pokeapi.co/api/v2/pokemon/340/' }, + { name: 'corphish', url: 'https://pokeapi.co/api/v2/pokemon/341/' }, + { name: 'crawdaunt', url: 'https://pokeapi.co/api/v2/pokemon/342/' }, + { name: 'baltoy', url: 'https://pokeapi.co/api/v2/pokemon/343/' }, + { name: 'claydol', url: 'https://pokeapi.co/api/v2/pokemon/344/' }, + { name: 'lileep', url: 'https://pokeapi.co/api/v2/pokemon/345/' }, + { name: 'cradily', url: 'https://pokeapi.co/api/v2/pokemon/346/' }, + { name: 'anorith', url: 'https://pokeapi.co/api/v2/pokemon/347/' }, + { name: 'armaldo', url: 'https://pokeapi.co/api/v2/pokemon/348/' }, + { name: 'feebas', url: 'https://pokeapi.co/api/v2/pokemon/349/' }, + { name: 'milotic', url: 'https://pokeapi.co/api/v2/pokemon/350/' }, + { name: 'castform', url: 'https://pokeapi.co/api/v2/pokemon/351/' }, + { name: 'kecleon', url: 'https://pokeapi.co/api/v2/pokemon/352/' }, + { name: 'shuppet', url: 'https://pokeapi.co/api/v2/pokemon/353/' }, + { name: 'banette', url: 'https://pokeapi.co/api/v2/pokemon/354/' }, + { name: 'duskull', url: 'https://pokeapi.co/api/v2/pokemon/355/' }, + { name: 'dusclops', url: 'https://pokeapi.co/api/v2/pokemon/356/' }, + { name: 'tropius', url: 'https://pokeapi.co/api/v2/pokemon/357/' }, + { name: 'chimecho', url: 'https://pokeapi.co/api/v2/pokemon/358/' }, + { name: 'absol', url: 'https://pokeapi.co/api/v2/pokemon/359/' }, + { name: 'wynaut', url: 'https://pokeapi.co/api/v2/pokemon/360/' }, + { name: 'snorunt', url: 'https://pokeapi.co/api/v2/pokemon/361/' }, + { name: 'glalie', url: 'https://pokeapi.co/api/v2/pokemon/362/' }, + { name: 'spheal', url: 'https://pokeapi.co/api/v2/pokemon/363/' }, + { name: 'sealeo', url: 'https://pokeapi.co/api/v2/pokemon/364/' }, + { name: 'walrein', url: 'https://pokeapi.co/api/v2/pokemon/365/' }, + { name: 'clamperl', url: 'https://pokeapi.co/api/v2/pokemon/366/' }, + { name: 'huntail', url: 'https://pokeapi.co/api/v2/pokemon/367/' }, + { name: 'gorebyss', url: 'https://pokeapi.co/api/v2/pokemon/368/' }, + { name: 'relicanth', url: 'https://pokeapi.co/api/v2/pokemon/369/' }, + { name: 'luvdisc', url: 'https://pokeapi.co/api/v2/pokemon/370/' }, + { name: 'bagon', url: 'https://pokeapi.co/api/v2/pokemon/371/' }, + { name: 'shelgon', url: 'https://pokeapi.co/api/v2/pokemon/372/' }, + { name: 'salamence', url: 'https://pokeapi.co/api/v2/pokemon/373/' }, + { name: 'beldum', url: 'https://pokeapi.co/api/v2/pokemon/374/' }, + { name: 'metang', url: 'https://pokeapi.co/api/v2/pokemon/375/' }, + { name: 'metagross', url: 'https://pokeapi.co/api/v2/pokemon/376/' }, + { name: 'regirock', url: 'https://pokeapi.co/api/v2/pokemon/377/' }, + { name: 'regice', url: 'https://pokeapi.co/api/v2/pokemon/378/' }, + { name: 'registeel', url: 'https://pokeapi.co/api/v2/pokemon/379/' }, + { name: 'latias', url: 'https://pokeapi.co/api/v2/pokemon/380/' }, + { name: 'latios', url: 'https://pokeapi.co/api/v2/pokemon/381/' }, + { name: 'kyogre', url: 'https://pokeapi.co/api/v2/pokemon/382/' }, + { name: 'groudon', url: 'https://pokeapi.co/api/v2/pokemon/383/' }, + { name: 'rayquaza', url: 'https://pokeapi.co/api/v2/pokemon/384/' }, + { name: 'jirachi', url: 'https://pokeapi.co/api/v2/pokemon/385/' }, + { + name: 'deoxys-normal', + url: 'https://pokeapi.co/api/v2/pokemon/386/' + }, + { name: 'turtwig', url: 'https://pokeapi.co/api/v2/pokemon/387/' }, + { name: 'grotle', url: 'https://pokeapi.co/api/v2/pokemon/388/' }, + { name: 'torterra', url: 'https://pokeapi.co/api/v2/pokemon/389/' }, + { name: 'chimchar', url: 'https://pokeapi.co/api/v2/pokemon/390/' }, + { name: 'monferno', url: 'https://pokeapi.co/api/v2/pokemon/391/' }, + { name: 'infernape', url: 'https://pokeapi.co/api/v2/pokemon/392/' }, + { name: 'piplup', url: 'https://pokeapi.co/api/v2/pokemon/393/' }, + { name: 'prinplup', url: 'https://pokeapi.co/api/v2/pokemon/394/' }, + { name: 'empoleon', url: 'https://pokeapi.co/api/v2/pokemon/395/' }, + { name: 'starly', url: 'https://pokeapi.co/api/v2/pokemon/396/' }, + { name: 'staravia', url: 'https://pokeapi.co/api/v2/pokemon/397/' }, + { name: 'staraptor', url: 'https://pokeapi.co/api/v2/pokemon/398/' }, + { name: 'bidoof', url: 'https://pokeapi.co/api/v2/pokemon/399/' }, + { name: 'bibarel', url: 'https://pokeapi.co/api/v2/pokemon/400/' }, + { name: 'kricketot', url: 'https://pokeapi.co/api/v2/pokemon/401/' }, + { name: 'kricketune', url: 'https://pokeapi.co/api/v2/pokemon/402/' }, + { name: 'shinx', url: 'https://pokeapi.co/api/v2/pokemon/403/' }, + { name: 'luxio', url: 'https://pokeapi.co/api/v2/pokemon/404/' }, + { name: 'luxray', url: 'https://pokeapi.co/api/v2/pokemon/405/' }, + { name: 'budew', url: 'https://pokeapi.co/api/v2/pokemon/406/' }, + { name: 'roserade', url: 'https://pokeapi.co/api/v2/pokemon/407/' }, + { name: 'cranidos', url: 'https://pokeapi.co/api/v2/pokemon/408/' }, + { name: 'rampardos', url: 'https://pokeapi.co/api/v2/pokemon/409/' }, + { name: 'shieldon', url: 'https://pokeapi.co/api/v2/pokemon/410/' }, + { name: 'bastiodon', url: 'https://pokeapi.co/api/v2/pokemon/411/' }, + { name: 'burmy', url: 'https://pokeapi.co/api/v2/pokemon/412/' }, + { + name: 'wormadam-plant', + url: 'https://pokeapi.co/api/v2/pokemon/413/' + }, + { name: 'mothim', url: 'https://pokeapi.co/api/v2/pokemon/414/' }, + { name: 'combee', url: 'https://pokeapi.co/api/v2/pokemon/415/' }, + { name: 'vespiquen', url: 'https://pokeapi.co/api/v2/pokemon/416/' }, + { name: 'pachirisu', url: 'https://pokeapi.co/api/v2/pokemon/417/' }, + { name: 'buizel', url: 'https://pokeapi.co/api/v2/pokemon/418/' }, + { name: 'floatzel', url: 'https://pokeapi.co/api/v2/pokemon/419/' }, + { name: 'cherubi', url: 'https://pokeapi.co/api/v2/pokemon/420/' }, + { name: 'cherrim', url: 'https://pokeapi.co/api/v2/pokemon/421/' }, + { name: 'shellos', url: 'https://pokeapi.co/api/v2/pokemon/422/' }, + { name: 'gastrodon', url: 'https://pokeapi.co/api/v2/pokemon/423/' }, + { name: 'ambipom', url: 'https://pokeapi.co/api/v2/pokemon/424/' }, + { name: 'drifloon', url: 'https://pokeapi.co/api/v2/pokemon/425/' }, + { name: 'drifblim', url: 'https://pokeapi.co/api/v2/pokemon/426/' }, + { name: 'buneary', url: 'https://pokeapi.co/api/v2/pokemon/427/' }, + { name: 'lopunny', url: 'https://pokeapi.co/api/v2/pokemon/428/' }, + { name: 'mismagius', url: 'https://pokeapi.co/api/v2/pokemon/429/' }, + { name: 'honchkrow', url: 'https://pokeapi.co/api/v2/pokemon/430/' }, + { name: 'glameow', url: 'https://pokeapi.co/api/v2/pokemon/431/' }, + { name: 'purugly', url: 'https://pokeapi.co/api/v2/pokemon/432/' }, + { name: 'chingling', url: 'https://pokeapi.co/api/v2/pokemon/433/' }, + { name: 'stunky', url: 'https://pokeapi.co/api/v2/pokemon/434/' }, + { name: 'skuntank', url: 'https://pokeapi.co/api/v2/pokemon/435/' }, + { name: 'bronzor', url: 'https://pokeapi.co/api/v2/pokemon/436/' }, + { name: 'bronzong', url: 'https://pokeapi.co/api/v2/pokemon/437/' }, + { name: 'bonsly', url: 'https://pokeapi.co/api/v2/pokemon/438/' }, + { name: 'mime-jr', url: 'https://pokeapi.co/api/v2/pokemon/439/' }, + { name: 'happiny', url: 'https://pokeapi.co/api/v2/pokemon/440/' }, + { name: 'chatot', url: 'https://pokeapi.co/api/v2/pokemon/441/' }, + { name: 'spiritomb', url: 'https://pokeapi.co/api/v2/pokemon/442/' }, + { name: 'gible', url: 'https://pokeapi.co/api/v2/pokemon/443/' }, + { name: 'gabite', url: 'https://pokeapi.co/api/v2/pokemon/444/' }, + { name: 'garchomp', url: 'https://pokeapi.co/api/v2/pokemon/445/' }, + { name: 'munchlax', url: 'https://pokeapi.co/api/v2/pokemon/446/' }, + { name: 'riolu', url: 'https://pokeapi.co/api/v2/pokemon/447/' }, + { name: 'lucario', url: 'https://pokeapi.co/api/v2/pokemon/448/' }, + { name: 'hippopotas', url: 'https://pokeapi.co/api/v2/pokemon/449/' }, + { name: 'hippowdon', url: 'https://pokeapi.co/api/v2/pokemon/450/' }, + { name: 'skorupi', url: 'https://pokeapi.co/api/v2/pokemon/451/' }, + { name: 'drapion', url: 'https://pokeapi.co/api/v2/pokemon/452/' }, + { name: 'croagunk', url: 'https://pokeapi.co/api/v2/pokemon/453/' }, + { name: 'toxicroak', url: 'https://pokeapi.co/api/v2/pokemon/454/' }, + { name: 'carnivine', url: 'https://pokeapi.co/api/v2/pokemon/455/' }, + { name: 'finneon', url: 'https://pokeapi.co/api/v2/pokemon/456/' }, + { name: 'lumineon', url: 'https://pokeapi.co/api/v2/pokemon/457/' }, + { name: 'mantyke', url: 'https://pokeapi.co/api/v2/pokemon/458/' }, + { name: 'snover', url: 'https://pokeapi.co/api/v2/pokemon/459/' }, + { name: 'abomasnow', url: 'https://pokeapi.co/api/v2/pokemon/460/' }, + { name: 'weavile', url: 'https://pokeapi.co/api/v2/pokemon/461/' }, + { name: 'magnezone', url: 'https://pokeapi.co/api/v2/pokemon/462/' }, + { name: 'lickilicky', url: 'https://pokeapi.co/api/v2/pokemon/463/' }, + { name: 'rhyperior', url: 'https://pokeapi.co/api/v2/pokemon/464/' }, + { name: 'tangrowth', url: 'https://pokeapi.co/api/v2/pokemon/465/' }, + { name: 'electivire', url: 'https://pokeapi.co/api/v2/pokemon/466/' }, + { name: 'magmortar', url: 'https://pokeapi.co/api/v2/pokemon/467/' }, + { name: 'togekiss', url: 'https://pokeapi.co/api/v2/pokemon/468/' }, + { name: 'yanmega', url: 'https://pokeapi.co/api/v2/pokemon/469/' }, + { name: 'leafeon', url: 'https://pokeapi.co/api/v2/pokemon/470/' }, + { name: 'glaceon', url: 'https://pokeapi.co/api/v2/pokemon/471/' }, + { name: 'gliscor', url: 'https://pokeapi.co/api/v2/pokemon/472/' }, + { name: 'mamoswine', url: 'https://pokeapi.co/api/v2/pokemon/473/' }, + { name: 'porygon-z', url: 'https://pokeapi.co/api/v2/pokemon/474/' }, + { name: 'gallade', url: 'https://pokeapi.co/api/v2/pokemon/475/' }, + { name: 'probopass', url: 'https://pokeapi.co/api/v2/pokemon/476/' }, + { name: 'dusknoir', url: 'https://pokeapi.co/api/v2/pokemon/477/' }, + { name: 'froslass', url: 'https://pokeapi.co/api/v2/pokemon/478/' }, + { name: 'rotom', url: 'https://pokeapi.co/api/v2/pokemon/479/' }, + { name: 'uxie', url: 'https://pokeapi.co/api/v2/pokemon/480/' }, + { name: 'mesprit', url: 'https://pokeapi.co/api/v2/pokemon/481/' }, + { name: 'azelf', url: 'https://pokeapi.co/api/v2/pokemon/482/' }, + { name: 'dialga', url: 'https://pokeapi.co/api/v2/pokemon/483/' }, + { name: 'palkia', url: 'https://pokeapi.co/api/v2/pokemon/484/' }, + { name: 'heatran', url: 'https://pokeapi.co/api/v2/pokemon/485/' }, + { name: 'regigigas', url: 'https://pokeapi.co/api/v2/pokemon/486/' }, + { + name: 'giratina-altered', + url: 'https://pokeapi.co/api/v2/pokemon/487/' + }, + { name: 'cresselia', url: 'https://pokeapi.co/api/v2/pokemon/488/' }, + { name: 'phione', url: 'https://pokeapi.co/api/v2/pokemon/489/' }, + { name: 'manaphy', url: 'https://pokeapi.co/api/v2/pokemon/490/' }, + { name: 'darkrai', url: 'https://pokeapi.co/api/v2/pokemon/491/' }, + { name: 'shaymin-land', url: 'https://pokeapi.co/api/v2/pokemon/492/' }, + { name: 'arceus', url: 'https://pokeapi.co/api/v2/pokemon/493/' }, + { name: 'victini', url: 'https://pokeapi.co/api/v2/pokemon/494/' }, + { name: 'snivy', url: 'https://pokeapi.co/api/v2/pokemon/495/' }, + { name: 'servine', url: 'https://pokeapi.co/api/v2/pokemon/496/' }, + { name: 'serperior', url: 'https://pokeapi.co/api/v2/pokemon/497/' }, + { name: 'tepig', url: 'https://pokeapi.co/api/v2/pokemon/498/' }, + { name: 'pignite', url: 'https://pokeapi.co/api/v2/pokemon/499/' }, + { name: 'emboar', url: 'https://pokeapi.co/api/v2/pokemon/500/' }, + { name: 'oshawott', url: 'https://pokeapi.co/api/v2/pokemon/501/' }, + { name: 'dewott', url: 'https://pokeapi.co/api/v2/pokemon/502/' }, + { name: 'samurott', url: 'https://pokeapi.co/api/v2/pokemon/503/' }, + { name: 'patrat', url: 'https://pokeapi.co/api/v2/pokemon/504/' }, + { name: 'watchog', url: 'https://pokeapi.co/api/v2/pokemon/505/' }, + { name: 'lillipup', url: 'https://pokeapi.co/api/v2/pokemon/506/' }, + { name: 'herdier', url: 'https://pokeapi.co/api/v2/pokemon/507/' }, + { name: 'stoutland', url: 'https://pokeapi.co/api/v2/pokemon/508/' }, + { name: 'purrloin', url: 'https://pokeapi.co/api/v2/pokemon/509/' }, + { name: 'liepard', url: 'https://pokeapi.co/api/v2/pokemon/510/' }, + { name: 'pansage', url: 'https://pokeapi.co/api/v2/pokemon/511/' }, + { name: 'simisage', url: 'https://pokeapi.co/api/v2/pokemon/512/' }, + { name: 'pansear', url: 'https://pokeapi.co/api/v2/pokemon/513/' }, + { name: 'simisear', url: 'https://pokeapi.co/api/v2/pokemon/514/' }, + { name: 'panpour', url: 'https://pokeapi.co/api/v2/pokemon/515/' }, + { name: 'simipour', url: 'https://pokeapi.co/api/v2/pokemon/516/' }, + { name: 'munna', url: 'https://pokeapi.co/api/v2/pokemon/517/' }, + { name: 'musharna', url: 'https://pokeapi.co/api/v2/pokemon/518/' }, + { name: 'pidove', url: 'https://pokeapi.co/api/v2/pokemon/519/' }, + { name: 'tranquill', url: 'https://pokeapi.co/api/v2/pokemon/520/' }, + { name: 'unfezant', url: 'https://pokeapi.co/api/v2/pokemon/521/' }, + { name: 'blitzle', url: 'https://pokeapi.co/api/v2/pokemon/522/' }, + { name: 'zebstrika', url: 'https://pokeapi.co/api/v2/pokemon/523/' }, + { name: 'roggenrola', url: 'https://pokeapi.co/api/v2/pokemon/524/' }, + { name: 'boldore', url: 'https://pokeapi.co/api/v2/pokemon/525/' }, + { name: 'gigalith', url: 'https://pokeapi.co/api/v2/pokemon/526/' }, + { name: 'woobat', url: 'https://pokeapi.co/api/v2/pokemon/527/' }, + { name: 'swoobat', url: 'https://pokeapi.co/api/v2/pokemon/528/' }, + { name: 'drilbur', url: 'https://pokeapi.co/api/v2/pokemon/529/' }, + { name: 'excadrill', url: 'https://pokeapi.co/api/v2/pokemon/530/' }, + { name: 'audino', url: 'https://pokeapi.co/api/v2/pokemon/531/' }, + { name: 'timburr', url: 'https://pokeapi.co/api/v2/pokemon/532/' }, + { name: 'gurdurr', url: 'https://pokeapi.co/api/v2/pokemon/533/' }, + { name: 'conkeldurr', url: 'https://pokeapi.co/api/v2/pokemon/534/' }, + { name: 'tympole', url: 'https://pokeapi.co/api/v2/pokemon/535/' }, + { name: 'palpitoad', url: 'https://pokeapi.co/api/v2/pokemon/536/' }, + { name: 'seismitoad', url: 'https://pokeapi.co/api/v2/pokemon/537/' }, + { name: 'throh', url: 'https://pokeapi.co/api/v2/pokemon/538/' }, + { name: 'sawk', url: 'https://pokeapi.co/api/v2/pokemon/539/' }, + { name: 'sewaddle', url: 'https://pokeapi.co/api/v2/pokemon/540/' }, + { name: 'swadloon', url: 'https://pokeapi.co/api/v2/pokemon/541/' }, + { name: 'leavanny', url: 'https://pokeapi.co/api/v2/pokemon/542/' }, + { name: 'venipede', url: 'https://pokeapi.co/api/v2/pokemon/543/' }, + { name: 'whirlipede', url: 'https://pokeapi.co/api/v2/pokemon/544/' }, + { name: 'scolipede', url: 'https://pokeapi.co/api/v2/pokemon/545/' }, + { name: 'cottonee', url: 'https://pokeapi.co/api/v2/pokemon/546/' }, + { name: 'whimsicott', url: 'https://pokeapi.co/api/v2/pokemon/547/' }, + { name: 'petilil', url: 'https://pokeapi.co/api/v2/pokemon/548/' }, + { name: 'lilligant', url: 'https://pokeapi.co/api/v2/pokemon/549/' }, + { + name: 'basculin-red-striped', + url: 'https://pokeapi.co/api/v2/pokemon/550/' + }, + { name: 'sandile', url: 'https://pokeapi.co/api/v2/pokemon/551/' }, + { name: 'krokorok', url: 'https://pokeapi.co/api/v2/pokemon/552/' }, + { name: 'krookodile', url: 'https://pokeapi.co/api/v2/pokemon/553/' }, + { name: 'darumaka', url: 'https://pokeapi.co/api/v2/pokemon/554/' }, + { + name: 'darmanitan-standard', + url: 'https://pokeapi.co/api/v2/pokemon/555/' + }, + { name: 'maractus', url: 'https://pokeapi.co/api/v2/pokemon/556/' }, + { name: 'dwebble', url: 'https://pokeapi.co/api/v2/pokemon/557/' }, + { name: 'crustle', url: 'https://pokeapi.co/api/v2/pokemon/558/' }, + { name: 'scraggy', url: 'https://pokeapi.co/api/v2/pokemon/559/' }, + { name: 'scrafty', url: 'https://pokeapi.co/api/v2/pokemon/560/' }, + { name: 'sigilyph', url: 'https://pokeapi.co/api/v2/pokemon/561/' }, + { name: 'yamask', url: 'https://pokeapi.co/api/v2/pokemon/562/' }, + { name: 'cofagrigus', url: 'https://pokeapi.co/api/v2/pokemon/563/' }, + { name: 'tirtouga', url: 'https://pokeapi.co/api/v2/pokemon/564/' }, + { name: 'carracosta', url: 'https://pokeapi.co/api/v2/pokemon/565/' }, + { name: 'archen', url: 'https://pokeapi.co/api/v2/pokemon/566/' }, + { name: 'archeops', url: 'https://pokeapi.co/api/v2/pokemon/567/' }, + { name: 'trubbish', url: 'https://pokeapi.co/api/v2/pokemon/568/' }, + { name: 'garbodor', url: 'https://pokeapi.co/api/v2/pokemon/569/' }, + { name: 'zorua', url: 'https://pokeapi.co/api/v2/pokemon/570/' }, + { name: 'zoroark', url: 'https://pokeapi.co/api/v2/pokemon/571/' }, + { name: 'minccino', url: 'https://pokeapi.co/api/v2/pokemon/572/' }, + { name: 'cinccino', url: 'https://pokeapi.co/api/v2/pokemon/573/' }, + { name: 'gothita', url: 'https://pokeapi.co/api/v2/pokemon/574/' }, + { name: 'gothorita', url: 'https://pokeapi.co/api/v2/pokemon/575/' }, + { name: 'gothitelle', url: 'https://pokeapi.co/api/v2/pokemon/576/' }, + { name: 'solosis', url: 'https://pokeapi.co/api/v2/pokemon/577/' }, + { name: 'duosion', url: 'https://pokeapi.co/api/v2/pokemon/578/' }, + { name: 'reuniclus', url: 'https://pokeapi.co/api/v2/pokemon/579/' }, + { name: 'ducklett', url: 'https://pokeapi.co/api/v2/pokemon/580/' }, + { name: 'swanna', url: 'https://pokeapi.co/api/v2/pokemon/581/' }, + { name: 'vanillite', url: 'https://pokeapi.co/api/v2/pokemon/582/' }, + { name: 'vanillish', url: 'https://pokeapi.co/api/v2/pokemon/583/' }, + { name: 'vanilluxe', url: 'https://pokeapi.co/api/v2/pokemon/584/' }, + { name: 'deerling', url: 'https://pokeapi.co/api/v2/pokemon/585/' }, + { name: 'sawsbuck', url: 'https://pokeapi.co/api/v2/pokemon/586/' }, + { name: 'emolga', url: 'https://pokeapi.co/api/v2/pokemon/587/' }, + { name: 'karrablast', url: 'https://pokeapi.co/api/v2/pokemon/588/' }, + { name: 'escavalier', url: 'https://pokeapi.co/api/v2/pokemon/589/' }, + { name: 'foongus', url: 'https://pokeapi.co/api/v2/pokemon/590/' }, + { name: 'amoonguss', url: 'https://pokeapi.co/api/v2/pokemon/591/' }, + { name: 'frillish', url: 'https://pokeapi.co/api/v2/pokemon/592/' }, + { name: 'jellicent', url: 'https://pokeapi.co/api/v2/pokemon/593/' }, + { name: 'alomomola', url: 'https://pokeapi.co/api/v2/pokemon/594/' }, + { name: 'joltik', url: 'https://pokeapi.co/api/v2/pokemon/595/' }, + { name: 'galvantula', url: 'https://pokeapi.co/api/v2/pokemon/596/' }, + { name: 'ferroseed', url: 'https://pokeapi.co/api/v2/pokemon/597/' }, + { name: 'ferrothorn', url: 'https://pokeapi.co/api/v2/pokemon/598/' }, + { name: 'klink', url: 'https://pokeapi.co/api/v2/pokemon/599/' }, + { name: 'klang', url: 'https://pokeapi.co/api/v2/pokemon/600/' }, + { name: 'klinklang', url: 'https://pokeapi.co/api/v2/pokemon/601/' }, + { name: 'tynamo', url: 'https://pokeapi.co/api/v2/pokemon/602/' }, + { name: 'eelektrik', url: 'https://pokeapi.co/api/v2/pokemon/603/' }, + { name: 'eelektross', url: 'https://pokeapi.co/api/v2/pokemon/604/' }, + { name: 'elgyem', url: 'https://pokeapi.co/api/v2/pokemon/605/' }, + { name: 'beheeyem', url: 'https://pokeapi.co/api/v2/pokemon/606/' }, + { name: 'litwick', url: 'https://pokeapi.co/api/v2/pokemon/607/' }, + { name: 'lampent', url: 'https://pokeapi.co/api/v2/pokemon/608/' }, + { name: 'chandelure', url: 'https://pokeapi.co/api/v2/pokemon/609/' }, + { name: 'axew', url: 'https://pokeapi.co/api/v2/pokemon/610/' }, + { name: 'fraxure', url: 'https://pokeapi.co/api/v2/pokemon/611/' }, + { name: 'haxorus', url: 'https://pokeapi.co/api/v2/pokemon/612/' }, + { name: 'cubchoo', url: 'https://pokeapi.co/api/v2/pokemon/613/' }, + { name: 'beartic', url: 'https://pokeapi.co/api/v2/pokemon/614/' }, + { name: 'cryogonal', url: 'https://pokeapi.co/api/v2/pokemon/615/' }, + { name: 'shelmet', url: 'https://pokeapi.co/api/v2/pokemon/616/' }, + { name: 'accelgor', url: 'https://pokeapi.co/api/v2/pokemon/617/' }, + { name: 'stunfisk', url: 'https://pokeapi.co/api/v2/pokemon/618/' }, + { name: 'mienfoo', url: 'https://pokeapi.co/api/v2/pokemon/619/' }, + { name: 'mienshao', url: 'https://pokeapi.co/api/v2/pokemon/620/' }, + { name: 'druddigon', url: 'https://pokeapi.co/api/v2/pokemon/621/' }, + { name: 'golett', url: 'https://pokeapi.co/api/v2/pokemon/622/' }, + { name: 'golurk', url: 'https://pokeapi.co/api/v2/pokemon/623/' }, + { name: 'pawniard', url: 'https://pokeapi.co/api/v2/pokemon/624/' }, + { name: 'bisharp', url: 'https://pokeapi.co/api/v2/pokemon/625/' }, + { name: 'bouffalant', url: 'https://pokeapi.co/api/v2/pokemon/626/' }, + { name: 'rufflet', url: 'https://pokeapi.co/api/v2/pokemon/627/' }, + { name: 'braviary', url: 'https://pokeapi.co/api/v2/pokemon/628/' }, + { name: 'vullaby', url: 'https://pokeapi.co/api/v2/pokemon/629/' }, + { name: 'mandibuzz', url: 'https://pokeapi.co/api/v2/pokemon/630/' }, + { name: 'heatmor', url: 'https://pokeapi.co/api/v2/pokemon/631/' }, + { name: 'durant', url: 'https://pokeapi.co/api/v2/pokemon/632/' }, + { name: 'deino', url: 'https://pokeapi.co/api/v2/pokemon/633/' }, + { name: 'zweilous', url: 'https://pokeapi.co/api/v2/pokemon/634/' }, + { name: 'hydreigon', url: 'https://pokeapi.co/api/v2/pokemon/635/' }, + { name: 'larvesta', url: 'https://pokeapi.co/api/v2/pokemon/636/' }, + { name: 'volcarona', url: 'https://pokeapi.co/api/v2/pokemon/637/' }, + { name: 'cobalion', url: 'https://pokeapi.co/api/v2/pokemon/638/' }, + { name: 'terrakion', url: 'https://pokeapi.co/api/v2/pokemon/639/' }, + { name: 'virizion', url: 'https://pokeapi.co/api/v2/pokemon/640/' }, + { + name: 'tornadus-incarnate', + url: 'https://pokeapi.co/api/v2/pokemon/641/' + }, + { + name: 'thundurus-incarnate', + url: 'https://pokeapi.co/api/v2/pokemon/642/' + }, + { name: 'reshiram', url: 'https://pokeapi.co/api/v2/pokemon/643/' }, + { name: 'zekrom', url: 'https://pokeapi.co/api/v2/pokemon/644/' }, + { + name: 'landorus-incarnate', + url: 'https://pokeapi.co/api/v2/pokemon/645/' + }, + { name: 'kyurem', url: 'https://pokeapi.co/api/v2/pokemon/646/' }, + { + name: 'keldeo-ordinary', + url: 'https://pokeapi.co/api/v2/pokemon/647/' + }, + { + name: 'meloetta-aria', + url: 'https://pokeapi.co/api/v2/pokemon/648/' + }, + { name: 'genesect', url: 'https://pokeapi.co/api/v2/pokemon/649/' }, + { name: 'chespin', url: 'https://pokeapi.co/api/v2/pokemon/650/' }, + { name: 'quilladin', url: 'https://pokeapi.co/api/v2/pokemon/651/' }, + { name: 'chesnaught', url: 'https://pokeapi.co/api/v2/pokemon/652/' }, + { name: 'fennekin', url: 'https://pokeapi.co/api/v2/pokemon/653/' }, + { name: 'braixen', url: 'https://pokeapi.co/api/v2/pokemon/654/' }, + { name: 'delphox', url: 'https://pokeapi.co/api/v2/pokemon/655/' }, + { name: 'froakie', url: 'https://pokeapi.co/api/v2/pokemon/656/' }, + { name: 'frogadier', url: 'https://pokeapi.co/api/v2/pokemon/657/' }, + { name: 'greninja', url: 'https://pokeapi.co/api/v2/pokemon/658/' }, + { name: 'bunnelby', url: 'https://pokeapi.co/api/v2/pokemon/659/' }, + { name: 'diggersby', url: 'https://pokeapi.co/api/v2/pokemon/660/' }, + { name: 'fletchling', url: 'https://pokeapi.co/api/v2/pokemon/661/' }, + { name: 'fletchinder', url: 'https://pokeapi.co/api/v2/pokemon/662/' }, + { name: 'talonflame', url: 'https://pokeapi.co/api/v2/pokemon/663/' }, + { name: 'scatterbug', url: 'https://pokeapi.co/api/v2/pokemon/664/' }, + { name: 'spewpa', url: 'https://pokeapi.co/api/v2/pokemon/665/' }, + { name: 'vivillon', url: 'https://pokeapi.co/api/v2/pokemon/666/' }, + { name: 'litleo', url: 'https://pokeapi.co/api/v2/pokemon/667/' }, + { name: 'pyroar', url: 'https://pokeapi.co/api/v2/pokemon/668/' }, + { name: 'flabebe', url: 'https://pokeapi.co/api/v2/pokemon/669/' }, + { name: 'floette', url: 'https://pokeapi.co/api/v2/pokemon/670/' }, + { name: 'florges', url: 'https://pokeapi.co/api/v2/pokemon/671/' }, + { name: 'skiddo', url: 'https://pokeapi.co/api/v2/pokemon/672/' }, + { name: 'gogoat', url: 'https://pokeapi.co/api/v2/pokemon/673/' }, + { name: 'pancham', url: 'https://pokeapi.co/api/v2/pokemon/674/' }, + { name: 'pangoro', url: 'https://pokeapi.co/api/v2/pokemon/675/' }, + { name: 'furfrou', url: 'https://pokeapi.co/api/v2/pokemon/676/' }, + { name: 'espurr', url: 'https://pokeapi.co/api/v2/pokemon/677/' }, + { + name: 'meowstic-male', + url: 'https://pokeapi.co/api/v2/pokemon/678/' + }, + { name: 'honedge', url: 'https://pokeapi.co/api/v2/pokemon/679/' }, + { name: 'doublade', url: 'https://pokeapi.co/api/v2/pokemon/680/' }, + { + name: 'aegislash-shield', + url: 'https://pokeapi.co/api/v2/pokemon/681/' + }, + { name: 'spritzee', url: 'https://pokeapi.co/api/v2/pokemon/682/' }, + { name: 'aromatisse', url: 'https://pokeapi.co/api/v2/pokemon/683/' }, + { name: 'swirlix', url: 'https://pokeapi.co/api/v2/pokemon/684/' }, + { name: 'slurpuff', url: 'https://pokeapi.co/api/v2/pokemon/685/' }, + { name: 'inkay', url: 'https://pokeapi.co/api/v2/pokemon/686/' }, + { name: 'malamar', url: 'https://pokeapi.co/api/v2/pokemon/687/' }, + { name: 'binacle', url: 'https://pokeapi.co/api/v2/pokemon/688/' }, + { name: 'barbaracle', url: 'https://pokeapi.co/api/v2/pokemon/689/' }, + { name: 'skrelp', url: 'https://pokeapi.co/api/v2/pokemon/690/' }, + { name: 'dragalge', url: 'https://pokeapi.co/api/v2/pokemon/691/' }, + { name: 'clauncher', url: 'https://pokeapi.co/api/v2/pokemon/692/' }, + { name: 'clawitzer', url: 'https://pokeapi.co/api/v2/pokemon/693/' }, + { name: 'helioptile', url: 'https://pokeapi.co/api/v2/pokemon/694/' }, + { name: 'heliolisk', url: 'https://pokeapi.co/api/v2/pokemon/695/' }, + { name: 'tyrunt', url: 'https://pokeapi.co/api/v2/pokemon/696/' }, + { name: 'tyrantrum', url: 'https://pokeapi.co/api/v2/pokemon/697/' }, + { name: 'amaura', url: 'https://pokeapi.co/api/v2/pokemon/698/' }, + { name: 'aurorus', url: 'https://pokeapi.co/api/v2/pokemon/699/' }, + { name: 'sylveon', url: 'https://pokeapi.co/api/v2/pokemon/700/' }, + { name: 'hawlucha', url: 'https://pokeapi.co/api/v2/pokemon/701/' }, + { name: 'dedenne', url: 'https://pokeapi.co/api/v2/pokemon/702/' }, + { name: 'carbink', url: 'https://pokeapi.co/api/v2/pokemon/703/' }, + { name: 'goomy', url: 'https://pokeapi.co/api/v2/pokemon/704/' }, + { name: 'sliggoo', url: 'https://pokeapi.co/api/v2/pokemon/705/' }, + { name: 'goodra', url: 'https://pokeapi.co/api/v2/pokemon/706/' }, + { name: 'klefki', url: 'https://pokeapi.co/api/v2/pokemon/707/' }, + { name: 'phantump', url: 'https://pokeapi.co/api/v2/pokemon/708/' }, + { name: 'trevenant', url: 'https://pokeapi.co/api/v2/pokemon/709/' }, + { + name: 'pumpkaboo-average', + url: 'https://pokeapi.co/api/v2/pokemon/710/' + }, + { + name: 'gourgeist-average', + url: 'https://pokeapi.co/api/v2/pokemon/711/' + }, + { name: 'bergmite', url: 'https://pokeapi.co/api/v2/pokemon/712/' }, + { name: 'avalugg', url: 'https://pokeapi.co/api/v2/pokemon/713/' }, + { name: 'noibat', url: 'https://pokeapi.co/api/v2/pokemon/714/' }, + { name: 'noivern', url: 'https://pokeapi.co/api/v2/pokemon/715/' }, + { name: 'xerneas', url: 'https://pokeapi.co/api/v2/pokemon/716/' }, + { name: 'yveltal', url: 'https://pokeapi.co/api/v2/pokemon/717/' }, + { name: 'zygarde', url: 'https://pokeapi.co/api/v2/pokemon/718/' }, + { name: 'diancie', url: 'https://pokeapi.co/api/v2/pokemon/719/' }, + { name: 'hoopa', url: 'https://pokeapi.co/api/v2/pokemon/720/' }, + { name: 'volcanion', url: 'https://pokeapi.co/api/v2/pokemon/721/' }, + { name: 'rowlet', url: 'https://pokeapi.co/api/v2/pokemon/722/' }, + { name: 'dartrix', url: 'https://pokeapi.co/api/v2/pokemon/723/' }, + { name: 'decidueye', url: 'https://pokeapi.co/api/v2/pokemon/724/' }, + { name: 'litten', url: 'https://pokeapi.co/api/v2/pokemon/725/' }, + { name: 'torracat', url: 'https://pokeapi.co/api/v2/pokemon/726/' }, + { name: 'incineroar', url: 'https://pokeapi.co/api/v2/pokemon/727/' }, + { name: 'popplio', url: 'https://pokeapi.co/api/v2/pokemon/728/' }, + { name: 'brionne', url: 'https://pokeapi.co/api/v2/pokemon/729/' }, + { name: 'primarina', url: 'https://pokeapi.co/api/v2/pokemon/730/' }, + { name: 'pikipek', url: 'https://pokeapi.co/api/v2/pokemon/731/' }, + { name: 'trumbeak', url: 'https://pokeapi.co/api/v2/pokemon/732/' }, + { name: 'toucannon', url: 'https://pokeapi.co/api/v2/pokemon/733/' }, + { name: 'yungoos', url: 'https://pokeapi.co/api/v2/pokemon/734/' }, + { name: 'gumshoos', url: 'https://pokeapi.co/api/v2/pokemon/735/' }, + { name: 'grubbin', url: 'https://pokeapi.co/api/v2/pokemon/736/' }, + { name: 'charjabug', url: 'https://pokeapi.co/api/v2/pokemon/737/' }, + { name: 'vikavolt', url: 'https://pokeapi.co/api/v2/pokemon/738/' }, + { name: 'crabrawler', url: 'https://pokeapi.co/api/v2/pokemon/739/' }, + { name: 'crabominable', url: 'https://pokeapi.co/api/v2/pokemon/740/' }, + { + name: 'oricorio-baile', + url: 'https://pokeapi.co/api/v2/pokemon/741/' + }, + { name: 'cutiefly', url: 'https://pokeapi.co/api/v2/pokemon/742/' }, + { name: 'ribombee', url: 'https://pokeapi.co/api/v2/pokemon/743/' }, + { name: 'rockruff', url: 'https://pokeapi.co/api/v2/pokemon/744/' }, + { + name: 'lycanroc-midday', + url: 'https://pokeapi.co/api/v2/pokemon/745/' + }, + { + name: 'wishiwashi-solo', + url: 'https://pokeapi.co/api/v2/pokemon/746/' + }, + { name: 'mareanie', url: 'https://pokeapi.co/api/v2/pokemon/747/' }, + { name: 'toxapex', url: 'https://pokeapi.co/api/v2/pokemon/748/' }, + { name: 'mudbray', url: 'https://pokeapi.co/api/v2/pokemon/749/' }, + { name: 'mudsdale', url: 'https://pokeapi.co/api/v2/pokemon/750/' } + ] +}; diff --git a/proof-of-concepts/features/stories/useDebounce/src/fetchData.ts b/proof-of-concepts/features/stories/useDebounce/src/fetchData.ts new file mode 100755 index 0000000..e711550 --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/fetchData.ts @@ -0,0 +1,16 @@ +// const abortController = React.useRef(); +// const signal = abortController.signal; +// will delay calling a function +// the function to delay is the fetch request +// fetch(endpoint, { signal }) +export function fetchData(endpoint: string) { + return window + .fetch(endpoint, { + method: 'GET', + headers: { + Accept: 'application/json' + } + }) + .then((res) => res.json()) + .catch((error) => Promise.reject(error)); +} diff --git a/proof-of-concepts/features/stories/useDebounce/src/hooks/useAsync.ts b/proof-of-concepts/features/stories/useDebounce/src/hooks/useAsync.ts new file mode 100755 index 0000000..008d59d --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/hooks/useAsync.ts @@ -0,0 +1,55 @@ +import React from 'react'; +import { + UseAsyncStatus, + UseAsyncResponse, + UseAsyncState, + UseAsyncAction, + UseAsyncProps +} from '../types'; + +function asyncReducer(state: UseAsyncState, action: UseAsyncAction) { + switch (action.type) { + case UseAsyncStatus.PENDING: { + return { status: UseAsyncStatus.PENDING, data: null, error: null }; + } + case UseAsyncStatus.RESOLVED: { + return { status: UseAsyncStatus.RESOLVED, data: action.data, error: null }; + } + case UseAsyncStatus.REJECTED: { + return { status: UseAsyncStatus.REJECTED, data: null, error: action.error }; + } + default: { + throw new TypeError(`Unhandled Action Type. Received ${action.type}`); + } + } +} + +export default function useAsync(props: UseAsyncProps): UseAsyncResponse { + const [state, dispatch] = React.useReducer(asyncReducer, { + status: UseAsyncStatus.PENDING, + data: null, + error: null, + ...props.initialState + }); + + const { status, data, error } = state; + + const run = React.useCallback((promise) => { + dispatch({ type: UseAsyncStatus.PENDING }); + promise.then( + (onFulfilled: any) => { + dispatch({ type: UseAsyncStatus.RESOLVED, data: onFulfilled }); + }, + (onRejected: any) => { + dispatch({ type: UseAsyncStatus.REJECTED, error: onRejected }); + } + ); + }, []); + + return { + data, + status, + error, + run + }; +} diff --git a/proof-of-concepts/features/stories/useDebounce/src/hooks/useDebouncedCallback.ts b/proof-of-concepts/features/stories/useDebounce/src/hooks/useDebouncedCallback.ts new file mode 100755 index 0000000..c5689b2 --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/hooks/useDebouncedCallback.ts @@ -0,0 +1,66 @@ +import React, { useRef } from 'react'; + +const noop = () => {}; + +export type UseDebounceOptions = { + leading?: boolean; + trailing?: boolean; +}; + +export interface DebouncedReturnFunction< + T extends (...args: any[]) => ReturnType +> { + (...args: Parameters): ReturnType | void; +} +/** + * Debounce function callback that invokes your function that you passed in as argument to `useDebouncedCallback` + * + * More details: the function callback uses a TypeVariable `T` to capture the type of function you provide + * and the return type of your function callback + * The function callback also uses the utility type Parameters to capture the types of parameters that your function callback expects + */ + +export default function useDebouncedCallback any>( + fn: T, + delay: number, + options: UseDebounceOptions +) { + const { leading = false, trailing = true } = options; + const fnRef = useRef(fn); + const delayRef = useRef(delay); + const timerIDRef = useRef(null); + const isLeading = useRef(leading); + const isTrailing = useRef(trailing); + + if (typeof fn !== 'function') { + throw new TypeError( + `Must define a function as callback to use this hook. Expected function, received ${typeof fn}` + ); + } + + /** + * Re-computes the memoized value when one of the dependencies have changed + */ + const debounce = React.useMemo(() => { + const func: DebouncedReturnFunction = (...args: Parameters) => { + if (isLeading.current) { + fnRef.current(...args); + } + + if (timerIDRef.current) { + clearTimeout(timerIDRef.current); + } + + timerIDRef.current = setTimeout(() => { + if (isTrailing) { + fnRef.current(...args); + } + }, delayRef.current); + }; + + return func; + }, []); + + // debounce is a function that encapsulates the memo call it will accepts a variable amount of arguments + return debounce; +} diff --git a/proof-of-concepts/features/stories/useDebounce/src/index.tsx b/proof-of-concepts/features/stories/useDebounce/src/index.tsx new file mode 100755 index 0000000..ce85da4 --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/index.tsx @@ -0,0 +1,5 @@ +import { render } from "react-dom"; +import App from "./App"; + +const rootElement = document.getElementById("root"); +render(, rootElement); diff --git a/proof-of-concepts/features/stories/useDebounce/src/types.ts b/proof-of-concepts/features/stories/useDebounce/src/types.ts new file mode 100755 index 0000000..ca0270f --- /dev/null +++ b/proof-of-concepts/features/stories/useDebounce/src/types.ts @@ -0,0 +1,29 @@ +export interface UseAsyncProps { + initialState: UseAsyncState; +} + +export enum UseAsyncStatus { + IDLE = '[idle]', + RESOLVED = '[resolved]', + REJECTED = '[rejected]', + PENDING = '[pending]' +} + +export type UseAsyncAction = { + type: UseAsyncStatus; + data?: any; + error?: any; +}; + +export type UseAsyncState = { + status: UseAsyncStatus; + data?: any; + error?: any; +}; + +export type UseAsyncResponse = { + status: UseAsyncStatus; + data: any; + error: any; + run: (promise: any) => any; +}; diff --git a/proof-of-concepts/features/stories/useMultipleSelection/__tests__/useMultipleSelection.test.tsx b/proof-of-concepts/features/stories/useMultipleSelection/__tests__/useMultipleSelection.test.tsx new file mode 100644 index 0000000..b4a5c02 --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/__tests__/useMultipleSelection.test.tsx @@ -0,0 +1,185 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { NavigationKeys } from '../types'; +import { + renderMultipleSelection, + sampleSelectedItems, + sampleItemToString, + SelectedItem, + getItem, + getAllItems +} from './utils'; + +describe('useMultipleSelection hook', () => { + const lastItem = sampleSelectedItems.length - 1; + + test('we can transition focus from the textbox to the last item in the list of selected items', () => { + let { container, textbox } = renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + }); + + textbox.focus(); + fireEvent.keyDown(textbox, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + let currentSelectedItem = getItem(lastItem); + expect(currentSelectedItem.classList).toContain( + 'current-selected-item-highlight' + ); + }); + + test('if we are at the start of the list, pressing the key to go to the prev element does nothing', () => { + let { container, textbox, selectedItems } = renderMultipleSelection( + { + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + } + ); + + textbox.focus(); + fireEvent.keyDown(textbox, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + + let currentSelectedItem; + let i = 0; + while (i < sampleSelectedItems.length) { + currentSelectedItem = getItem(i); + fireEvent.keyDown(currentSelectedItem, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + i++; + } + fireEvent.keyDown(currentSelectedItem, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + + currentSelectedItem = getItem(0); + + expect(currentSelectedItem.classList).toContain( + 'current-selected-item-highlight' + ); + }); + + test('all items have a -1 tabindex when not highlighted', () => { + let { container, textbox } = renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + }); + + const allItems = getAllItems(); + allItems.forEach((item) => { + expect(item.getAttribute('tabindex')).toEqual('-1'); + }); + }); + + test('if the current index === -1, we place focus back on the textbox', () => { + let { container, textbox, selectedItems } = renderMultipleSelection( + { + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + } + ); + + textbox.focus(); + fireEvent.keyDown(textbox, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + + let currentSelectedItem = getItem(lastItem); + expect(currentSelectedItem.classList).toContain( + 'current-selected-item-highlight' + ); + fireEvent.keyDown(selectedItems, { + key: 'ArrowRight', + code: 'ArrowRight', + charCode: 39 + }); + + expect(currentSelectedItem).not.toContain('current-selected-item-highlight'); + userEvent.type(selectedItems, '{arrowright}'); + + textbox.focus(); + expect(textbox).toHaveFocus(); + }); + + test('removing a currently active item maintains focus on the item in the given index', () => { + let { container, textbox } = renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + }); + + textbox.focus(); + // place focus on a current item + fireEvent.keyDown(textbox, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37 + }); + + // remove an item + const item = getItem(lastItem); + userEvent.click(item.querySelector('[aria-label="Close"]')); + expect(item).not.toBeInTheDocument(); + expect(getItem(lastItem - 1)).toHaveClass('current-selected-item-highlight'); + }); + + test('onclick places focus on the item', () => { + const { container, textbox } = renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + }); + const allItems = getAllItems(); + const itemToClick = allItems[allItems.length / 2]; + fireEvent.click(itemToClick); + expect(itemToClick.classList).toContain('current-selected-item-highlight'); + }); + + test('adding a new element is inserted at the end of the list', () => { + renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString + }); + + const addItem = screen.getByText(/add item/i); + fireEvent.click(addItem); + const allItems = getAllItems(); + expect(allItems.length).toBe(sampleSelectedItems.length + 1); + }); + + test('backspace deletes the current item and moves the focus to the next element, if it exists', () => { + renderMultipleSelection({ + items: sampleSelectedItems, + itemToString: sampleItemToString, + nextKey: NavigationKeys.ARROW_RIGHT, + prevKey: NavigationKeys.ARROW_LEFT + }); + const item = getItem(lastItem); + fireEvent.keyDown(item, { key: 'Backspace', code: 'Backspace', charCode: 8 }); + expect(getAllItems().length).toBe(sampleSelectedItems.length - 1); + }); +}); diff --git a/proof-of-concepts/features/stories/useMultipleSelection/__tests__/utils.tsx b/proof-of-concepts/features/stories/useMultipleSelection/__tests__/utils.tsx new file mode 100644 index 0000000..56d3404 --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/__tests__/utils.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { useMultipleSelection } from '../useMultipleSelection'; +import { + MultipleSelectionProps, + MultipleSelectionState, + MultipleSelectionActionAndChanges, + MultipleSelectionStateChangeTypes +} from '../types'; +import { render, screen, getAllByRole } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; + +const dataTestIds = { + selectedItems: 'selectedItems-testid', + item: 'item-testid', + input: 'input-testid' +}; + +export function getItem(itemNum: number) { + return screen.getByTestId(`item-testid-${itemNum}`); +} + +export function getAllItems() { + return screen.getAllByTestId(/item-testid/); +} + +export function renderMultipleSelection(props: MultipleSelectionProps) { + const container = render(); + const selectedItems = screen.getByTestId(dataTestIds.selectedItems); + const textbox = screen.getByTestId(dataTestIds.input); + return { + container, + selectedItems, + textbox + }; +} +const dummyItem = { + name: 'dummy-item', + count: -1, + contents: '' +}; + +function MultipleSelection(props: MultipleSelectionProps) { + const { items: initialItems, itemToString } = props; + const [selectedItems, setSelectedItems] = React.useState(initialItems); + + function stateReducer( + state: MultipleSelectionState, + actionAndChanges: MultipleSelectionActionAndChanges + ) { + const { action, changes } = actionAndChanges; + const recommendations = { ...changes }; + switch (action.type) { + case MultipleSelectionStateChangeTypes.FUNCTION_REMOVE_SELECTED_ITEM: { + setSelectedItems([...recommendations.items]); + return recommendations; + } + case MultipleSelectionStateChangeTypes.FUNCTION_ADD_SELECTED_ITEM: { + setSelectedItems([...recommendations.items]); + return recommendations; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_BACKSPACE: { + setSelectedItems([...recommendations.items]); + return recommendations; + } + default: { + return recommendations; + } + } + } + + const { + getSelectedItemProps, + currentSelectedItemIndex, + getDropdownProps, + removeSelectedItem, + addSelectedItem + } = useMultipleSelection({ + ...props, + items: selectedItems, + stateReducer + }); + + const handleAddItem = (e) => { + e.stopPropagation(); + addSelectedItem((dummyItem as unknown) as Item); + }; + + return ( +
    + +
    + {selectedItems && + selectedItems.map((item, index) => { + return ( + + {itemToString(item)} + + + ); + })} + +
    +
    + ); +} + +/** + * ******************* + * + * Mock Data + * + * ******************* + * Not used by the functions above^ + * Used in the actual test suite 'useMultipleSelection.test.tsx' + */ +export const sampleSelectedItems = [ + { name: 'material-analysis', count: 5, contents: '' }, + { name: 'class-analysis', count: 33, contents: '' }, + { name: 'materialism', count: 12, contents: '' }, + { name: 'dialectical-materialism', count: 9, contents: '' }, + { name: 'historical-materialism', count: 5, contents: '' }, + { name: 'materialist-theory', count: 2, contents: '' } +] as SelectedItem[]; + +export const sampleItemToString = (item: SelectedItem) => item.name; + +export interface SelectedItem { + name: string; + count: number; + contents: string; +} diff --git a/proof-of-concepts/features/stories/useMultipleSelection/reducer.ts b/proof-of-concepts/features/stories/useMultipleSelection/reducer.ts new file mode 100644 index 0000000..4824a9d --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/reducer.ts @@ -0,0 +1,109 @@ +import { + MultipleSelectionState, + MultipleSelectionAction, + MultipleSelectionStateChangeTypes +} from './types'; +import { getNextItemIndex } from './utils'; + +export function multipleSelectionReducer( + state: MultipleSelectionState, + action: MultipleSelectionAction +) { + const { type } = action; + const newState = { ...state }; + const currState = { ...state }; + switch (type) { + case MultipleSelectionStateChangeTypes.NAVIGATION_NEXT: { + newState.currentSelectedItemIndex = getNextItemIndex( + MultipleSelectionStateChangeTypes.NAVIGATION_NEXT, + currState.currentSelectedItemIndex, + currState.items + ); + newState.currentSelectedItem = + newState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.NAVIGATION_PREV: { + newState.currentSelectedItemIndex = getNextItemIndex( + MultipleSelectionStateChangeTypes.NAVIGATION_PREV, + currState.currentSelectedItemIndex, + currState.items + ); + newState.currentSelectedItem = + newState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.DROPDOWN_NAVIGATION_TO_ITEMS: { + newState.currentSelectedItemIndex = currState.items.length - 1; + newState.currentSelectedItem = + currState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_CLICK: { + newState.currentSelectedItemIndex = action.index; + return newState; + } + case MultipleSelectionStateChangeTypes.FUNCTION_ADD_SELECTED_ITEM: { + newState.items = [...currState.items, action.item]; + return newState; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_BACKSPACE: { + newState.items = [ + ...currState.items.slice(0, action.index), + ...currState.items.slice(action.index + 1) + ]; + // check if items still exist + newState.currentSelectedItemIndex = getNextItemIndex( + MultipleSelectionStateChangeTypes.KEYDOWN_BACKSPACE, + currState.currentSelectedItemIndex, + currState.items, + newState.items, + action.index + ); + newState.currentSelectedItem = + newState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.FUNCTION_REMOVE_SELECTED_ITEM: { + const { item: removeItem, itemToString, index } = action; + const itemToRemove = itemToString(removeItem); + newState.items = newState.items.filter( + (curr) => itemToString(curr) !== itemToRemove + ); + // check if items still exist + newState.currentSelectedItemIndex = getNextItemIndex( + MultipleSelectionStateChangeTypes.FUNCTION_REMOVE_SELECTED_ITEM, + currState.currentSelectedItemIndex, + currState.items, + newState.items, + index + ); + newState.currentSelectedItem = + newState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.FUNCTION_SET_CURRENT_INDEX: { + newState.currentSelectedItemIndex = action.index; + newState.currentSelectedItem = + newState.items[newState.currentSelectedItemIndex]; + return newState; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_ENTER: { + return newState; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_SPACEBAR: { + return newState; + } + case MultipleSelectionStateChangeTypes.MULTIPLE_SELECTION_GROUP_BLUR: { + return newState; + } + case MultipleSelectionStateChangeTypes.MULTIPLE_SELECTION_GROUP_FOCUS: { + return newState; + } + default: { + throw new TypeError( + `Unhandled action in useMultipleSelection. Received ${type}` + ); + } + } +} diff --git a/proof-of-concepts/features/stories/useMultipleSelection/types.ts b/proof-of-concepts/features/stories/useMultipleSelection/types.ts new file mode 100644 index 0000000..3f2ae2c --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/types.ts @@ -0,0 +1,61 @@ +import React from 'react'; +import { ComponentProps } from '../types'; +export type MultipleSelectionProps = { + items?: Item[]; + initialCurrentSelectedItemIndex?: number; + nextKey?: NavigationKeys; + prevKey?: NavigationKeys; + itemToString: (item: Item) => string; + onCurrentSelectedItemIndexChange?: (index: number) => void; + onCurrentSelectedItemChange?: (item: Item) => void; + onItemsChange?: (items: Item[]) => void; + onHasSelectedItemsChange?: () => void; +} & ComponentProps< + MultipleSelectionState, + MultipleSelectionActionAndChanges +>; + +export enum NavigationKeys { + ARROW_RIGHT = 'ArrowRight', + ARROW_LEFT = 'ArrowLeft', + ARROW_UP = 'ArrowUp', + ARROW_DOWN = 'ArrowDown' +} + +export interface DropdownGetterProps { + ref?: React.MutableRefObject; +} + +export type MultipleSelectionState = { + items: Item[]; + currentSelectedItem: Item; + currentSelectedItemIndex: number; + hasSelectedItems: boolean; +}; + +export enum MultipleSelectionStateChangeTypes { + NAVIGATION_NEXT = '[multiple_selection_next_item]', + NAVIGATION_PREV = '[multiple_selection_prev_item]', + DROPDOWN_NAVIGATION_TO_ITEMS = '[multiple_selection_navigate_to_items]', + KEYDOWN_ENTER = '[multiple_selection_keydown_enter]', + KEYDOWN_SPACEBAR = '[multiple_selection_keydown_spacebar]', + KEYDOWN_BACKSPACE = '[multiple_selection_keydown_backspace]', + KEYDOWN_CLICK = '[multiple_selection_keydown_click]', + MULTIPLE_SELECTION_GROUP_BLUR = '[multiple_selection_group_blur]', + MULTIPLE_SELECTION_GROUP_FOCUS = '[multiple_selection_group_focus]', + FUNCTION_SET_CURRENT_INDEX = '[multiple_selection_set_current_index]', + FUNCTION_ADD_SELECTED_ITEM = '[multiple_selection_function_add_selected_item]', + FUNCTION_REMOVE_SELECTED_ITEM = '[multiple_selection_function_remove_selected_item]' +} + +export type MultipleSelectionAction = { + type: MultipleSelectionStateChangeTypes; + index?: number; + item?: Item; + itemToString?: (item: Item) => string; +}; + +export type MultipleSelectionActionAndChanges = { + changes: MultipleSelectionState; + action: MultipleSelectionAction; +}; diff --git a/proof-of-concepts/features/stories/useMultipleSelection/useMultipleSelection.ts b/proof-of-concepts/features/stories/useMultipleSelection/useMultipleSelection.ts new file mode 100644 index 0000000..b64b01e --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/useMultipleSelection.ts @@ -0,0 +1,182 @@ +import React from 'react'; +import { multipleSelectionReducer } from './reducer'; +import { computeInitialState, canNavigateToItems } from './utils'; +import { normalizeKey, useControlledReducer, mergeRefs } from '../utils'; +import { + MultipleSelectionProps, + MultipleSelectionState, + MultipleSelectionAction, + MultipleSelectionStateChangeTypes, + MultipleSelectionActionAndChanges, + DropdownGetterProps, + NavigationKeys +} from './types'; +import { ItemsList } from '../types'; + +export function useMultipleSelection(props: MultipleSelectionProps) { + const [state, dispatch] = useControlledReducer< + ( + state: MultipleSelectionState, + action: MultipleSelectionAction + ) => MultipleSelectionState, + MultipleSelectionState, + MultipleSelectionProps, + MultipleSelectionStateChangeTypes, + MultipleSelectionActionAndChanges + >(multipleSelectionReducer, computeInitialState(props), props); + const { items, currentSelectedItem, currentSelectedItemIndex } = state; + const hasSelectedItems = items.length > 0; + const { + nextKey = NavigationKeys.ARROW_LEFT, + prevKey = NavigationKeys.ARROW_RIGHT + } = props; + const currentSelectedItemsRef = React.useRef(); + // reinitializes array on every re-render to "gc" unmounted components + currentSelectedItemsRef.current = []; + + React.useEffect(() => { + if ( + currentSelectedItemIndex >= 0 && + currentSelectedItemsRef.current && + currentSelectedItemsRef.current[currentSelectedItemIndex] + ) { + currentSelectedItemsRef.current[currentSelectedItemIndex].focus(); + } + }, [currentSelectedItemIndex, currentSelectedItem]); + + const itemsList = React.useRef({}); + if (props.items) { + props.items.forEach((item) => itemsList.current[props.itemToString(item)]); + } + + const itemKeydownHandlers: { + [eventHandler: string]: (e: React.KeyboardEvent, index: number) => void; + } = { + [nextKey]: () => { + dispatch({ + type: MultipleSelectionStateChangeTypes.NAVIGATION_NEXT + }); + }, + [prevKey]: () => { + dispatch({ + type: MultipleSelectionStateChangeTypes.NAVIGATION_PREV + }); + }, + Enter: (e: React.KeyboardEvent) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.KEYDOWN_ENTER + }); + }, + Spacebar: (e: React.KeyboardEvent) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.KEYDOWN_SPACEBAR + }); + }, + Backspace: (e: React.KeyboardEvent, index: number) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.KEYDOWN_BACKSPACE, + index + }); + } + }; + + const dropdownKeydownHandlers: { + [eventHandler: string]: (e: React.KeyboardEvent) => void; + } = { + [prevKey]: (e: React.KeyboardEvent) => { + // e.stopPropagation(); + if (canNavigateToItems(e)) { + dispatch({ + type: MultipleSelectionStateChangeTypes.DROPDOWN_NAVIGATION_TO_ITEMS + }); + } + } + }; + + function getDropdownProps({ ref = null, ...rest }: DropdownGetterProps) { + const handleKeydown = (e: React.KeyboardEvent) => { + const { name: keyName } = normalizeKey(e); + if (keyName in dropdownKeydownHandlers) { + dropdownKeydownHandlers[keyName](e); + } + }; + return { + ref, + onKeyDown: handleKeydown, + ...rest + }; + } + + function getSelectedItemProps(selectedItem: Item, index: number) { + const handleKeydown = (e: React.KeyboardEvent) => { + const { name: keyName } = normalizeKey(e); + if (keyName in itemKeydownHandlers) { + itemKeydownHandlers[keyName](e, index); + } + }; + + const handleClick = (e: React.KeyboardEvent) => { + e.stopPropagation(); + dispatch({ + type: MultipleSelectionStateChangeTypes.KEYDOWN_CLICK, + index + }); + }; + + return { + tabIndex: index === currentSelectedItemIndex ? 0 : -1, + onClick: handleClick, + onKeyDown: handleKeydown, + ref: mergeRefs((node) => { + if (node) currentSelectedItemsRef.current.push(node); + }) + }; + } + + const removeSelectedItem = React.useCallback( + (item: Item, index: number) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.FUNCTION_REMOVE_SELECTED_ITEM, + item, + itemToString: props.itemToString, + index + }); + }, + [dispatch, props.itemToString] + ); + + const addSelectedItem = React.useCallback( + (item: Item) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.FUNCTION_ADD_SELECTED_ITEM, + item + }); + }, + [dispatch] + ); + + const setCurrentIndex = React.useCallback( + (index: number) => { + dispatch({ + type: MultipleSelectionStateChangeTypes.FUNCTION_SET_CURRENT_INDEX, + index + }); + }, + [dispatch] + ); + + return { + // state + items, + currentSelectedItem, + currentSelectedItemIndex, + hasSelectedItems, + // functions + removeSelectedItem, + addSelectedItem, + setCurrentIndex, + // prop-getters + getSelectedItemProps, + getDropdownProps + }; +} diff --git a/proof-of-concepts/features/stories/useMultipleSelection/utils.ts b/proof-of-concepts/features/stories/useMultipleSelection/utils.ts new file mode 100644 index 0000000..1b0453d --- /dev/null +++ b/proof-of-concepts/features/stories/useMultipleSelection/utils.ts @@ -0,0 +1,149 @@ +import { + MultipleSelectionProps, + MultipleSelectionState, + MultipleSelectionStateChangeTypes +} from './types'; +import { capitalizeString } from '../utils'; + +export const initialState = { + items: [], + currentSelectedItem: undefined, + currentSelectedItemIndex: -1 +}; + +export function computeInitialState( + props: MultipleSelectionProps +): MultipleSelectionState { + const items = getInitialValue(props, 'items'); + let currentSelectedItem = getInitialValue(props, 'currentSelectedItem'); + let currentSelectedItemIndex = getInitialValue(props, 'currentSelectedItemIndex'); + + if (currentSelectedItemIndex > -1) + currentSelectedItem = items[currentSelectedItemIndex]; + + return { + items, + currentSelectedItem, + currentSelectedItemIndex + } as MultipleSelectionState; +} + +/** + * Use the keys of state to get the initial values of properties defined in props + * Props and State share the same keys. + */ +export function getInitialValue( + props: MultipleSelectionProps, + propKey: keyof MultipleSelectionState +): any { + if (propKey in props) { + return props[propKey as keyof MultipleSelectionProps] as Partial< + MultipleSelectionState + >; + } + + // get the user-provided initial prop state, it is a piece of state + const initialPropKey = `initial${capitalizeString( + propKey + )}` as keyof MultipleSelectionProps; + if (initialPropKey in props) { + // console.log('initialPropKey', initialPropKey); + // console.log('initialPropKey', props[initialPropKey]); + return props[initialPropKey] as Partial>; + } + + // return values from statically defined initial state object + return initialState[propKey] as Partial>; +} + +export function canNavigateToItems( + e: React.SyntheticEvent +): boolean { + const element = e.target as HTMLInputElement; + console.log('element', element); + console.log('element value', element.value); + console.log('element selectionStart', element.selectionStart); + console.log('element selectionEnd', element.selectionEnd); + + if ( + (element && element.value !== '') || + (element.selectionStart !== 0 && element.selectionEnd !== 0) + ) { + return false; + } else { + return true; + } +} + +export function getNextItemIndex( + stateChangeType: MultipleSelectionStateChangeTypes, + currItemIdx: number, + currentItems: Item[], + newItems?: Item[], + index?: number +): number { + const end = currentItems.length; + switch (stateChangeType) { + case MultipleSelectionStateChangeTypes.NAVIGATION_NEXT: { + if (currItemIdx === end - 1) return -1; + return currItemIdx + 1; + } + case MultipleSelectionStateChangeTypes.NAVIGATION_PREV: { + if (currItemIdx === 0) return currItemIdx; + return currItemIdx - 1; + } + case MultipleSelectionStateChangeTypes.KEYDOWN_BACKSPACE: { + if (isHead(index) && !isEmpty(newItems)) { + return 0; + } else if (isHead(index) && isEmpty(newItems)) { + return -1; + } + + if (isTail(currItemIdx, currentItems) && !isEmpty(newItems)) { + return newItems.length - 1; + } else if (isTail(currItemIdx, currentItems) && isEmpty(newItems)) { + return -1; + } + + return currItemIdx - 1; + } + case MultipleSelectionStateChangeTypes.FUNCTION_REMOVE_SELECTED_ITEM: { + if (isHead(index) && !isEmpty(newItems)) { + return 0; + } else if (isHead(index) && isEmpty(newItems)) { + return -1; + // if the user is not currently focused on a selected item and deletes an item + } else if (currItemIdx === -1) { + return -1; + } + + if ( + isTail(currItemIdx, currentItems) && + isTail(index, currentItems) && + !isEmpty(newItems) + ) { + return newItems.length - 1; + } else if ( + isTail(currItemIdx, currentItems) && + isTail(index, currentItems) && + isEmpty(newItems) + ) { + return -1; + } + return currItemIdx - 1; + } + default: { + return currItemIdx; + } + } +} + +function isHead(index: number) { + return index === 0; +} +function isTail(currentIndex: number, itemsList: any[]) { + return currentIndex === itemsList.length - 1; +} +function isEmpty(itemsList: any[]) { + return itemsList.length > 0; +} diff --git a/proof-of-concepts/features/stories/utils.ts b/proof-of-concepts/features/stories/utils.ts new file mode 100644 index 0000000..c6c869e --- /dev/null +++ b/proof-of-concepts/features/stories/utils.ts @@ -0,0 +1,244 @@ +import React from 'react'; +import { ComponentProps } from './types'; + +export function useControlledReducer< + ComponentReducer extends React.Reducer, + ComponentState extends React.ReducerState, + Props extends ComponentProps, + StateChangeType, + ActionAndChanges +>( + reducer: ComponentReducer, + initialState: ComponentState, + props: Props +): [ + React.ReducerState, + React.Dispatch> +] { + // store and track dispatched actions + const actionRef = React.useRef(); + + // store and track the "previous" state, as a ref. updating as a side-effect + // allows us to choose when to update this ref + const prevStateRef = React.useRef(); + + // const propsRef = React.useRef(props); + + // return either the internal changes based on our state reducer, + // or the internal changes based on the user's recommendations + const controlledReducer = React.useCallback( + (state: ComponentState, action) => { + actionRef.current = action; + const internalChanges = reducer(state, action); + if (props && props.stateReducer) { + const userRecommendedChanges = props.stateReducer(state, ({ + action, + changes: internalChanges + } as unknown) as ActionAndChanges); + return userRecommendedChanges; + } + return internalChanges; + }, + [props, reducer] + ); + + const [state, dispatch] = React.useReducer(controlledReducer, initialState); + + // our component calls this dispatch function with props added as a convenience + // this function saves us from having to declare props on every dispatch, + // if we declared useReducer within the component itself + const dispatchWithProps = React.useCallback( + ({ type, ...rest }: { type: StateChangeType }) => { + dispatch({ type, props, ...rest }); + }, + [props] + ); + + // recall: useEffect runs after the render phase, therefore + // the reference of state is the most up-to-date state + // and prevStateRef references the state from the previous render + React.useEffect(() => { + if (actionRef.current && prevStateRef.current && prevStateRef.current !== state) { + onStateChange(props, prevStateRef.current, state); + } + prevStateRef.current = state; + }, [props, state]); + + return [state, dispatchWithProps]; +} + +// Calls any callback the users' of our component have registered when a piece of state +// has changed +export function onStateChange( + props: ComponentProps, + state: ComponentState, + newState: ComponentState +) { + for (let pieceOfState in state) { + const stateValue = state[pieceOfState]; + const newStateValue = newState[pieceOfState]; + if (stateValue !== newStateValue) { + invokeOnStateChange(pieceOfState, props, state, newState); + } + } +} + +export function invokeOnStateChange< + ComponentProps extends Record, + ComponentState +>( + pieceOfState: keyof ComponentState, + props: ComponentProps, + state: ComponentState, + newState: ComponentState +) { + const statePiece = capitalizeString(pieceOfState as string); + const stateChangeCallback = `on${statePiece}Change`; + // console.log('[STATE_CHANGE_CALLBACK]', stateChangeCallback); + if (stateChangeCallback in props) { + props[stateChangeCallback](newState[pieceOfState]); + } +} + +export function capitalizeString(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function generateId() { + return Math.floor(Math.random() * 1000); +} + +export function normalizeKey(e: React.KeyboardEvent) { + return { + name: e.key, + code: e.charCode + }; +} + +// the ref property refers to? +// ref is a property on a JSX element +// react is a UI runtime that creates predictable UI +// so how is a ref actually assigned? +// well, we wait until all the elements are rendered on the page +// then the ref is assigned that node +// ref={fn()} +export function mergeRefs(...refs: (React.MutableRefObject | undefined)[]) { + return function(node: React.ReactElement) { + // iterate over every ref + // assign the node to the current ref + refs.forEach((ref) => { + if (typeof ref === 'function') { + ref(node); + } else if (ref) { + ref.current = node; + } + }); + }; +} + +export function callAllEventHandlers(...fns: ((...args: any[]) => any)[]) { + return function(...args: any[]) { + fns.forEach((fn) => { + if (typeof fn === 'function') { + fn(...args); + } + }); + }; +} + +/** + * Credits to Downshift + * https://github.com/downshift-js/downshift/blob/26c93a539dad09e41adba69ddc3a7d7ecccfc8bb/src/hooks/utils.js#L316 + */ +export function useMouseAndTracker( + isOpen: boolean, + refs: React.MutableRefObject[], + handleBlur: (...args: any) => any +) { + const mouseAndTrackerRef = React.useRef({ + isMouseDown: false, + isTouchMove: false + }); + + React.useEffect(() => { + const onMouseDown = () => { + mouseAndTrackerRef.current.isMouseDown = true; + }; + const onMouseUp = (e: React.SyntheticEvent) => { + mouseAndTrackerRef.current.isMouseDown = false; + if (isOpen && !isWithinBottomline(refs, e)) { + handleBlur(); + } + }; + const onTouchStart = () => { + mouseAndTrackerRef.current.isTouchMove = true; + }; + + const onTouchMove = () => { + mouseAndTrackerRef.current.isTouchMove = true; + }; + + const onTouchEnd = (e: React.SyntheticEvent) => { + mouseAndTrackerRef.current.isTouchMove = false; + if (isOpen && !isWithinBottomline(refs, e)) { + handleBlur(); + } + }; + + window.addEventListener('mousedown', onMouseDown); + window.addEventListener('mouseup', onMouseUp); + window.addEventListener('touchstart', onTouchStart); + window.addEventListener('touchmove', onTouchMove); + window.addEventListener('touchend', onTouchEnd); + + return function() { + window.removeEventListener('mousedown', onMouseDown); + window.removeEventListener('mouseup', onMouseUp); + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchmove', onTouchMove); + window.removeEventListener('touchend', onTouchEnd); + }; + }, [isOpen]); + return mouseAndTrackerRef; +} + +export function isWithinBottomline( + refs: React.MutableRefObject[], + event: React.SyntheticEvent +) { + return refs.some((ref) => { + if (ref.current && event.target) { + return ref.current.contains(event.target); + } else { + return false; + } + }); +} + +export const noop = () => {}; + +// chooses a random delay time before sending a request +export function delayRandomly() { + const timeout = sample([1000, 2000, 5000, 7000, 10000]); + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +export function throwRandomly() { + const shouldThrow = sample([true, false, false, false]); + if (shouldThrow) { + throw new Error('simulated async failure'); + } +} + +export function delayControlled() { + const timeout = sample([5000, 10000]); + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +function sample(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} diff --git a/proof-of-concepts/features/tsconfig.json b/proof-of-concepts/features/tsconfig.json index 1aa9cf7..d9eae2a 100644 --- a/proof-of-concepts/features/tsconfig.json +++ b/proof-of-concepts/features/tsconfig.json @@ -13,7 +13,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "baseUrl": "." + }, - "exclude": ["node_modules"] + } diff --git a/proof-of-concepts/usedebounce/.eslintrc b/proof-of-concepts/usedebounce/.eslintrc new file mode 100644 index 0000000..9df80b0 --- /dev/null +++ b/proof-of-concepts/usedebounce/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["react-app", "plugin:react-hooks/recommended"] +} \ No newline at end of file diff --git a/proof-of-concepts/usedebounce/package-lock.json b/proof-of-concepts/usedebounce/package-lock.json index 3aa5a49..721925d 100644 --- a/proof-of-concepts/usedebounce/package-lock.json +++ b/proof-of-concepts/usedebounce/package-lock.json @@ -9,18 +9,20 @@ "version": "1.0.0", "dependencies": { "@babel/core": "7.16.0", - "@babel/plugin-transform-typescript": "7.16.1", - "@babel/preset-typescript": "7.16.0", + "@types/jest": "^27.0.2", + "@types/node": "^16.11.7", + "@types/react": "^17.0.34", + "@types/react-dom": "^17.0.11", "json-server": "0.17.0", "nodemon": "2.0.15", "react": "17.0.2", "react-dom": "17.0.2", - "react-scripts": "4.0.3" + "react-scripts": "4.0.3", + "typescript": "^4.4.4" }, "devDependencies": { - "@types/react": "17.0.20", - "@types/react-dom": "17.0.9", - "typescript": "4.4.2" + "@babel/plugin-transform-typescript": "^7.16.1", + "@babel/preset-typescript": "^7.16.0" } }, "node_modules/@babel/code-frame": { @@ -1572,6 +1574,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.0.tgz", "integrity": "sha512-txegdrZYgO9DlPbv+9QOVpMnKbOtezsLHWsnsRF4AjbSIsVaujrq1qg8HK0mxQpWv0jnejt0yEoW1uWpvbrDTg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", @@ -3066,6 +3069,157 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", + "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", + "dependencies": { + "jest-diff": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "27.2.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz", + "integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/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==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@types/jest/node_modules/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==" + }, + "node_modules/@types/jest/node_modules/diff-sequences": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", + "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.3.1.tgz", + "integrity": "sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.0.6", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-get-type": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.3.1.tgz", + "integrity": "sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg==", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz", + "integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==", + "dependencies": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/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==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -3104,8 +3258,7 @@ "node_modules/@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -3113,10 +3266,9 @@ "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" }, "node_modules/@types/react": { - "version": "17.0.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", - "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", - "dev": true, + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz", + "integrity": "sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3124,10 +3276,9 @@ } }, "node_modules/@types/react-dom": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", - "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", - "dev": true, + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", + "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", "dependencies": { "@types/react": "*" } @@ -3143,8 +3294,7 @@ "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@types/source-list-map": { "version": "0.1.2", @@ -6525,8 +6675,7 @@ "node_modules/csstype": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", - "dev": true + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, "node_modules/cyclist": { "version": "1.0.1", @@ -20145,9 +20294,9 @@ } }, "node_modules/typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23659,6 +23808,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.0.tgz", "integrity": "sha512-txegdrZYgO9DlPbv+9QOVpMnKbOtezsLHWsnsRF4AjbSIsVaujrq1qg8HK0mxQpWv0jnejt0yEoW1uWpvbrDTg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", @@ -24743,6 +24893,119 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", + "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", + "requires": { + "jest-diff": "^27.0.0", + "pretty-format": "^27.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "27.2.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz", + "integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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==", + "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==" + }, + "diff-sequences": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", + "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "jest-diff": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.3.1.tgz", + "integrity": "sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.0.6", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + } + }, + "jest-get-type": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.3.1.tgz", + "integrity": "sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg==" + }, + "pretty-format": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz", + "integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==", + "requires": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "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==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -24781,8 +25044,7 @@ "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/q": { "version": "1.5.5", @@ -24790,10 +25052,9 @@ "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" }, "@types/react": { - "version": "17.0.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", - "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", - "dev": true, + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz", + "integrity": "sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -24801,10 +25062,9 @@ } }, "@types/react-dom": { - "version": "17.0.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz", - "integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==", - "dev": true, + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", + "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", "requires": { "@types/react": "*" } @@ -24820,8 +25080,7 @@ "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/source-list-map": { "version": "0.1.2", @@ -27491,8 +27750,7 @@ "csstype": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", - "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==", - "dev": true + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" }, "cyclist": { "version": "1.0.1", @@ -37958,9 +38216,9 @@ } }, "typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==" + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==" }, "unbox-primitive": { "version": "1.0.1", diff --git a/proof-of-concepts/usedebounce/package.json b/proof-of-concepts/usedebounce/package.json index 006a9cf..2f813a7 100755 --- a/proof-of-concepts/usedebounce/package.json +++ b/proof-of-concepts/usedebounce/package.json @@ -2,22 +2,24 @@ "name": "react-typescript", "version": "1.0.0", "description": "React and TypeScript example starter project", - "keywords": ["typescript", "react", "starter"], + "keywords": [ + "typescript", + "react", + "starter" + ], "main": "src/index.tsx", "dependencies": { "@babel/core": "7.16.0", - "@babel/plugin-transform-typescript": "7.16.1", - "@babel/preset-typescript": "7.16.0", + "@types/jest": "^27.0.2", + "@types/node": "^16.11.7", + "@types/react": "^17.0.34", + "@types/react-dom": "^17.0.11", "json-server": "0.17.0", "nodemon": "2.0.15", "react": "17.0.2", "react-dom": "17.0.2", - "react-scripts": "4.0.3" - }, - "devDependencies": { - "@types/react": "17.0.20", - "@types/react-dom": "17.0.9", - "typescript": "4.4.2" + "react-scripts": "4.0.3", + "typescript": "^4.4.4" }, "scripts": { "start": "react-scripts start", @@ -26,5 +28,14 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, - "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"] + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "devDependencies": { + "@babel/plugin-transform-typescript": "^7.16.1", + "@babel/preset-typescript": "^7.16.0" + } } diff --git a/proof-of-concepts/usedebounce/src/App.tsx b/proof-of-concepts/usedebounce/src/App.tsx index 59037d9..c36fd81 100755 --- a/proof-of-concepts/usedebounce/src/App.tsx +++ b/proof-of-concepts/usedebounce/src/App.tsx @@ -1,23 +1,26 @@ import React from 'react'; -import useDebounce from './hooks/useDebouncedCallback'; +import useDebouncedCallback from './hooks/useDebouncedCallback'; import useAsync from './hooks/useAsync'; -import { UseAsyncStatus, UseAsyncProps, UseAsyncResponse } from './types'; +import { UseAsyncStatus, UseAsyncResponse } from './types'; import { fetchData } from './fetchData'; const endpointAPI = 'http://localhost:3001/results'; export default function App() { const [value, setValue] = React.useState(''); const debounce = useDebouncedCallback( - (userValue: string) => { - setValue(userValue); + (value: string) => { + setValue(value); }, - 3000, + 4000, { trailing: true } ); + React.useEffect(() => { + console.log('useEffect'); + }); const fetchResults: UseAsyncResponse = useAsync( () => { - if (value) { + if (value && value !== '') { return fetchData(endpointAPI); } }, @@ -27,14 +30,18 @@ export default function App() { const { data: items, error, status } = fetchResults; if (status === UseAsyncStatus.IDLE) { + console.log('we are idle'); } else if (status === UseAsyncStatus.PENDING) { + console.log('we are pending'); } else if (status === UseAsyncStatus.RESOLVED) { + console.log('we are resolved'); + } else { + console.log('we are rejected'); } return (
    ) => debounce(e.currentTarget.value) } diff --git a/proof-of-concepts/usedebounce/src/hooks/__tests__/useAsync.test.ts b/proof-of-concepts/usedebounce/src/hooks/__tests__/useAsync.test.ts new file mode 100644 index 0000000..887c34e --- /dev/null +++ b/proof-of-concepts/usedebounce/src/hooks/__tests__/useAsync.test.ts @@ -0,0 +1 @@ +useAsync.test.ts; diff --git a/proof-of-concepts/usedebounce/src/hooks/__tests__/useDebouncedCallback.test.ts b/proof-of-concepts/usedebounce/src/hooks/__tests__/useDebouncedCallback.test.ts new file mode 100644 index 0000000..435318b --- /dev/null +++ b/proof-of-concepts/usedebounce/src/hooks/__tests__/useDebouncedCallback.test.ts @@ -0,0 +1 @@ +useDebouncedCallback.test.ts; diff --git a/proof-of-concepts/usedebounce/src/hooks/useAsync.ts b/proof-of-concepts/usedebounce/src/hooks/useAsync.ts index 89dcde1..9786982 100755 --- a/proof-of-concepts/usedebounce/src/hooks/useAsync.ts +++ b/proof-of-concepts/usedebounce/src/hooks/useAsync.ts @@ -1,8 +1,6 @@ import React from 'react'; -import useDebounce from './useDebounce'; import { UseAsyncStatus, - UseAsyncProps, UseAsyncResponse, UseAsyncState, UseAsyncAction @@ -12,15 +10,32 @@ function asyncReducer(state: UseAsyncState, action: UseAsyncAction) { const newState = { ...state }; switch (action.type) { case UseAsyncStatus.IDLE: { + console.log('[IDLE]'); + console.log('action:', action); + console.log('state:', newState); + newState.status = action.type; return newState; } case UseAsyncStatus.PENDING: { + console.log('[PENDING]'); + console.log('action:', action); + console.log('state:', newState); + newState.status = action.type; return newState; } case UseAsyncStatus.RESOLVED: { + console.log('[RESOLVED]'); + console.log('action:', action); + console.log('state:', newState); + newState.status = action.type; + newState.data = action.data; return newState; } case UseAsyncStatus.REJECTED: { + console.log('[REJECTED]'); + console.log('action:', action); + console.log('state:', newState); + newState.status = action.type; return newState; } default: { @@ -29,8 +44,10 @@ function asyncReducer(state: UseAsyncState, action: UseAsyncAction) { } } -export default function useAsync( - asyncCallback: () => Promise, +export default function useAsync< + T extends (...args: any[]) => Promise | undefined +>( + asyncCallback: T, initialState: UseAsyncState, dependencies: any[] ): UseAsyncResponse { @@ -59,9 +76,3 @@ export default function useAsync( error: state.error }; } -/** -debounce making a fetch request - -if there is a pending request when a new request is made - cancel the pending request -*/ diff --git a/proof-of-concepts/usedebounce/src/hooks/useDebouncedCallback.ts b/proof-of-concepts/usedebounce/src/hooks/useDebouncedCallback.ts index d05ae8f..c5689b2 100755 --- a/proof-of-concepts/usedebounce/src/hooks/useDebouncedCallback.ts +++ b/proof-of-concepts/usedebounce/src/hooks/useDebouncedCallback.ts @@ -7,6 +7,11 @@ export type UseDebounceOptions = { trailing?: boolean; }; +export interface DebouncedReturnFunction< + T extends (...args: any[]) => ReturnType +> { + (...args: Parameters): ReturnType | void; +} /** * Debounce function callback that invokes your function that you passed in as argument to `useDebouncedCallback` * @@ -14,21 +19,16 @@ export type UseDebounceOptions = { * and the return type of your function callback * The function callback also uses the utility type Parameters to capture the types of parameters that your function callback expects */ -export interface DebouncedReturnFunction< - T extends (...args: any[]) => ReturnType -> { - (...args: Parameters): ReturnType | undefined; -} -export default function useDebouncedCallback< - T extends (...args: any[]) => ReturnType ->(fn: T, delay: number, options: UseDebounceOptions) { +export default function useDebouncedCallback any>( + fn: T, + delay: number, + options: UseDebounceOptions +) { const { leading = false, trailing = true } = options; const fnRef = useRef(fn); const delayRef = useRef(delay); - const resultRef = useRef>(); - const startTimeRef = useRef(); - const timerIDRef = useRef(); + const timerIDRef = useRef(null); const isLeading = useRef(leading); const isTrailing = useRef(trailing); @@ -42,71 +42,25 @@ export default function useDebouncedCallback< * Re-computes the memoized value when one of the dependencies have changed */ const debounce = React.useMemo(() => { - const timersDontExist = () => !timerIDRef.current && !startTimeRef.current; - - const clearTimer = () => { - startTimeRef.current = undefined; - timerIDRef.current = undefined; - }; + const func: DebouncedReturnFunction = (...args: Parameters) => { + if (isLeading.current) { + fnRef.current(...args); + } - const resetTimer = () => { if (timerIDRef.current) { clearTimeout(timerIDRef.current); - setTimeout(() => {}, delayRef.current); } - }; - const shouldInvoke = (currentTime: number) => { - if (timersDontExist()) { - return true; - } else if (startTimeRef.current) { - const elapsed = currentTime - startTimeRef.current; - if (elapsed >= delayRef.current) return true; - } - return false; + timerIDRef.current = setTimeout(() => { + if (isTrailing) { + fnRef.current(...args); + } + }, delayRef.current); }; - // run the fn the user specified - // based on the arguments - const func: DebouncedReturnFunction = ( - ...args: Parameters - ): ReturnType | undefined => { - const currentTime = Date.now(); - const invocable = shouldInvoke(currentTime); - let scheduledFn = noop; - - if (invocable) { - if (isLeading) fnRef.current(...args); - - if (isTrailing) scheduledFn = () => fnRef.current(...args); - - timerIDRef.current = setTimeout(() => { - scheduledFn(); - clearTimer(); - }, delayRef.current); - } else { - resetTimer(); - } - return resultRef.current; - }; return func; }, []); // debounce is a function that encapsulates the memo call it will accepts a variable amount of arguments return debounce; } - -// invocable means -// either time has passed and we can invoke -// invoke, then reset and start timer -// or the option is leading -// invoke, then set and start the timer -// trailing -// set and start the timer, invoke after -// if it's not invocable does that mean that we run the timer -// what happens if it is not invocable? -// do we have a timeout currently running? -// if so, reset the timeout -// if not, then start the timeout -// and assign the id -// this sounds the same diff --git a/proof-of-concepts/usedebounce/src/types.ts b/proof-of-concepts/usedebounce/src/types.ts index 48219ba..1eaa92f 100755 --- a/proof-of-concepts/usedebounce/src/types.ts +++ b/proof-of-concepts/usedebounce/src/types.ts @@ -10,13 +10,6 @@ export type UseAsyncAction = { data?: any; }; -export interface UseAsyncProps { - asyncCallback: (...args: any) => Promise; - initialState: any; - endpoint: string; - dependencies: any[]; -} - export type UseAsyncState = { status: UseAsyncStatus; data?: any;