diff --git a/.eslintrc.json b/.eslintrc.json
index 0b2ddb11..17e4159a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -2,26 +2,18 @@
"env": {
"browser": true
},
- "extends": ["airbnb", "airbnb/hooks", "plugin:prettier/recommended"],
- "plugins": ["prettier"],
+ "extends": [
+ "semistandard",
+ "standard-jsx",
+ "standard-react",
+ "plugin:react/recommended",
+ "plugin:jsx-a11y/recommended",
+ "prettier"
+ ],
+ "plugins": ["react", "jsx-a11y"],
"parserOptions": {
"sourceType": "module"
},
"parser": "babel-eslint",
- "rules": {
- "no-new": "off",
- "no-alert": "off",
- "no-param-reassign": "off",
- "no-return-assign": "off",
- "import/extensions": "off",
- "import/prefer-default-export": "off",
- "max-depth": ["error", 1],
- "react/react-in-jsx-scope": "off",
- "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
- "react/jsx-one-expression-per-line": "off",
- "react/prefer-stateless-function": "off",
- "react/prop-types": "off",
- "react/destructuring-assignment": "off",
- "max-classes-per-file": ["error", 3]
- }
+ "rules": {}
}
diff --git a/README.md b/README.md
index 5110c9fb..463a2fe7 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@
### Step2
-- [ ] Step1의 클래스 컴포넌트를 함수형 컴포넌트로 마이그레이션 한다.
+- [x] Step1의 클래스 컴포넌트를 함수형 컴포넌트로 마이그레이션 한다.
## 👏 Contributing
diff --git a/cspell.json b/cspell.json
new file mode 100644
index 00000000..a4a830ec
--- /dev/null
+++ b/cspell.json
@@ -0,0 +1,19 @@
+{
+ "version": "0.1",
+ "language": "en",
+ "words": [
+ "theads",
+ "noninteractive",
+ "labelledby",
+ "describedby",
+ "semistandard",
+ "noopener",
+ "noreferrer",
+ "Viewports",
+ "Parens",
+ "Strapi",
+ "classname",
+ "browserslist"
+ ],
+ "flagWords": []
+}
diff --git a/package.json b/package.json
index 5cf66dcb..8ce11d6b 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"@testing-library/user-event": "^12.1.10",
"babel-eslint": "^10.1.0",
"classnames": "^2.3.1",
+ "prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-lottie": "^1.2.3",
@@ -40,9 +41,17 @@
},
"devDependencies": {
"eslint": "^7.24.0",
- "eslint-config-airbnb": "^18.2.1",
- "eslint-config-prettier": "^8.1.0",
- "eslint-plugin-prettier": "^3.3.1",
+ "eslint-config-prettier": "^8.2.0",
+ "eslint-config-semistandard": "15.0.1",
+ "eslint-config-standard": ">=14.1.0",
+ "eslint-config-standard-jsx": "^10.0.0",
+ "eslint-config-standard-react": "^11.0.1",
+ "eslint-plugin-import": ">=2.18.0",
+ "eslint-plugin-jsx-a11y": "^6.4.1",
+ "eslint-plugin-node": ">=9.1.0",
+ "eslint-plugin-promise": ">=4.2.1",
+ "eslint-plugin-react": "^7.23.2",
+ "eslint-plugin-standard": ">=4.0.0",
"prettier": "^2.2.1"
}
}
diff --git a/src/components/App/index.js b/src/components/App/index.js
index 1463be9f..2cc09220 100644
--- a/src/components/App/index.js
+++ b/src/components/App/index.js
@@ -1,88 +1,71 @@
-/* eslint-disable react/sort-comp */
-import { Component } from 'react';
-import PurchaseForm from '../containers/PurchaseForm';
-import UserLotto from '../containers/UserLotto';
-import WinningNumbers from '../containers/WinningNumbers';
-import UserResult from '../containers/UserResult';
-import { createLotto } from './service';
+import React, { useState } from 'react';
+import { PurchaseForm } from '../containers/PurchaseForm';
+import { UserLotto } from '../containers/UserLotto';
+import { WinningNumbers } from '../containers/WinningNumbers';
+import { UserResult } from '../containers/UserResult';
+import { Title } from '../shared';
+import { useModal } from '../../hooks';
import './style.css';
const initialState = {
lottoBundle: [],
winningNumber: {},
shouldReset: false,
- isShowingUserResult: false,
};
-export default class App extends Component {
- constructor() {
- super();
- this.state = { ...initialState };
- this.onPurchaseLotto = this.onPurchaseLotto.bind(this);
- this.setWinningNumber = this.setWinningNumber.bind(this);
- this.onShowUserResult = this.onShowUserResult.bind(this);
- this.onCloseUserResult = this.onCloseUserResult.bind(this);
- this.onReset = this.onReset.bind(this);
- this.didReset = this.didReset.bind(this);
- }
+export const App = () => {
+ const [lottoBundle, setLottoBundle] = useState(initialState.lottoBundle);
+ const isPurchased = lottoBundle.length > 0;
+ const [winningNumber, setWinningNumber] = useState(initialState.winningNumber);
+ const [shouldReset, setShouldReset] = useState(initialState.shouldReset);
+ const {
+ isOpen: isUserResultOpen,
+ open: showUserResult,
+ close: hideUserResult,
+ ...restUseModal
+ } = useModal();
- onPurchaseLotto({ numOfLotto }) {
- this.setState({ lottoBundle: [...Array(numOfLotto)].map(() => createLotto()) });
- }
+ const onReset = () => {
+ setLottoBundle(initialState.lottoBundle);
+ setWinningNumber(initialState.winningNumber);
+ hideUserResult();
+ setShouldReset(true);
+ };
- setWinningNumber({ winningNumber }) {
- this.setState({ winningNumber });
- }
+ const finishReset = () => {
+ setShouldReset(false);
+ };
- onShowUserResult() {
- this.setState({ isShowingUserResult: true });
- }
-
- onCloseUserResult() {
- this.setState({ isShowingUserResult: false });
- }
-
- onReset() {
- this.setState({ ...initialState, shouldReset: true });
- }
-
- didReset() {
- this.setState({ shouldReset: false });
- }
-
- render() {
- const { lottoBundle, winningNumber, isShowingUserResult, shouldReset } = this.state;
- const isPurchased = Boolean(lottoBundle.length);
-
- return (
- <>
-
- 행운의 로또
-
- {isPurchased && (
- <>
-
-
- >
- )}
-
- {isShowingUserResult && (
-
+ return (
+ <>
+
+
+ 행운의 로또
+
+
+ {isPurchased && (
+ <>
+
+
+ >
)}
- >
- );
- }
-}
+
+ {isUserResultOpen && (
+
+ )}
+ >
+ );
+};
diff --git a/src/components/App/service.js b/src/components/App/service.js
deleted file mode 100644
index e822ece0..00000000
--- a/src/components/App/service.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { getRandomNumber } from '../../utils';
-import { LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_NUMBERS_LENGTH } from '../../constants';
-
-export const createLotto = (array = []) => {
- const number = getRandomNumber({ min: LOTTO_MIN_NUMBER, max: LOTTO_MAX_NUMBER });
-
- if (array.length === LOTTO_NUMBERS_LENGTH) {
- return array.sort((a, b) => a - b);
- }
- if (!array.includes(number)) {
- array.push(number);
- }
-
- return createLotto(array);
-};
diff --git a/src/components/App/style.css b/src/components/App/style.css
index 4e7f1472..9951c42b 100644
--- a/src/components/App/style.css
+++ b/src/components/App/style.css
@@ -10,10 +10,3 @@
min-height: 440px;
box-shadow: 6px 10px 20px rgb(0, 0, 0, 0.15);
}
-
-.App__title {
- text-align: center;
- color: #333;
- font-size: 1.5rem;
- margin: 0;
-}
diff --git a/src/components/containers/PurchaseForm/index.js b/src/components/containers/PurchaseForm/index.js
index 3b06f1fc..37faeac5 100644
--- a/src/components/containers/PurchaseForm/index.js
+++ b/src/components/containers/PurchaseForm/index.js
@@ -1,38 +1,32 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
-import React, { Component } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
import { Button } from '../../shared';
-import { validatePurchaseAmount, payForLotto } from './service';
+import { validatePurchaseAmount, payForLotto, getLottoBundle } from './service';
import { MESSAGE } from '../../../constants';
import './style.css';
-export default class PurchaseForm extends Component {
- constructor(props) {
- super(props);
+const initialState = {
+ inputStatus: {
+ isValidAmount: false,
+ validationMessage: '',
+ isSubmitted: false,
+ },
+};
- this.state = {
- validationMessage: '',
- isInputDisabled: false,
- isSubmitButtonDisabled: true,
- };
- this.paymentInput = React.createRef();
- this.onChangeInput = this.onChangeInput.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- }
+export const PurchaseForm = (props) => {
+ const { setLottoBundle, shouldReset, finishReset } = props;
+ const [inputStatus, setInputStatus] = useState(initialState.inputStatus);
+ const { isValidAmount, validationMessage, isSubmitted } = inputStatus;
- componentDidMount() {
- this.paymentInput.current.focus();
- }
-
- componentDidUpdate() {
- if (this.props.shouldReset) {
- this.paymentInput.current.value = '';
- this.paymentInput.current.disabled = false;
- this.paymentInput.current.focus();
- this.props.didReset();
- }
- }
+ const paymentInputRef = useRef(null);
+ const onChangeInput = (e) => {
+ const money = e.target.value;
+ const { isValidAmount, validationMessage } = validatePurchaseAmount(money);
- onSubmit(e) {
+ setInputStatus((prevState) => ({ ...prevState, isValidAmount, validationMessage }));
+ };
+ const onSubmit = (e) => {
e.preventDefault();
const money = e.target.input.value;
@@ -41,48 +35,52 @@ export default class PurchaseForm extends Component {
if (change > 0) {
alert(MESSAGE.PURCHASE_AMOUNT_HAS_CHANGE(change));
}
+ setLottoBundle(getLottoBundle(numOfLotto));
+ setInputStatus((prevState) => ({ ...prevState, isSubmitted: true }));
+ };
- this.props.onPurchaseLotto({ numOfLotto });
- this.setState({ isInputDisabled: true, isSubmitButtonDisabled: true });
- }
-
- onChangeInput(e) {
- const money = e.target.value;
- const { validationMessage, isSubmitButtonDisabled } = validatePurchaseAmount(money);
-
- this.setState({ validationMessage, isSubmitButtonDisabled });
- }
+ useEffect(() => {
+ paymentInputRef.current.focus();
+ paymentInputRef.current.value = '';
+ setInputStatus(() => initialState.inputStatus);
+ finishReset();
+ }, [shouldReset]);
- render() {
- const { validationMessage, isInputDisabled, isSubmitButtonDisabled } = this.state;
+ return (
+
+
+
{validationMessage}
+
+ );
+};
- return (
-
-
-
{validationMessage}
-
- );
- }
-}
+PurchaseForm.propTypes = {
+ setLottoBundle: PropTypes.func.isRequired,
+ shouldReset: PropTypes.bool.isRequired,
+ finishReset: PropTypes.func.isRequired,
+};
diff --git a/src/components/containers/PurchaseForm/service.js b/src/components/containers/PurchaseForm/service.js
index 01154aa9..9ad758a7 100644
--- a/src/components/containers/PurchaseForm/service.js
+++ b/src/components/containers/PurchaseForm/service.js
@@ -1,23 +1,31 @@
-import { MESSAGE, LOTTO_UNIT_PRICE, MIN_MONETARY_UNIT } from '../../../constants';
+import { getRandomNumber } from '../../../utils';
+import {
+ MESSAGE,
+ LOTTO_UNIT_PRICE,
+ MIN_MONETARY_UNIT,
+ LOTTO_MIN_NUMBER,
+ LOTTO_MAX_NUMBER,
+ LOTTO_NUMBERS_LENGTH,
+} from '../../../constants';
export const validatePurchaseAmount = (money) => {
if (money % MIN_MONETARY_UNIT > 0) {
return {
validationMessage: MESSAGE.INVALID_PURCHASE_AMOUNT_UNDER_MONETARY_UNIT,
- isSubmitButtonDisabled: true,
+ isValidAmount: false,
};
}
if (money < LOTTO_UNIT_PRICE) {
return {
validationMessage: MESSAGE.INVALID_PURCHASE_AMOUNT_UNDER_LOTTO_UNIT_PRICE,
- isSubmitButtonDisabled: true,
+ isValidAmount: false,
};
}
return {
validationMessage: '',
- isSubmitButtonDisabled: false,
+ isValidAmount: true,
};
};
@@ -27,3 +35,18 @@ export const payForLotto = (money) => {
return { change, numOfLotto };
};
+
+const createLotto = (array = []) => {
+ const number = getRandomNumber({ min: LOTTO_MIN_NUMBER, max: LOTTO_MAX_NUMBER });
+
+ if (array.length === LOTTO_NUMBERS_LENGTH) {
+ return array.sort((a, b) => a - b);
+ }
+ if (!array.includes(number)) {
+ array.push(number);
+ }
+
+ return createLotto(array);
+};
+
+export const getLottoBundle = (numOfLotto) => [...Array(numOfLotto)].map(() => createLotto());
diff --git a/src/components/containers/UserLotto/Lotto.js b/src/components/containers/UserLotto/Lotto.js
index ee09ccda..7af920eb 100644
--- a/src/components/containers/UserLotto/Lotto.js
+++ b/src/components/containers/UserLotto/Lotto.js
@@ -1,18 +1,21 @@
-import { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import { lottoImage } from '../../../statics';
import { LOTTO_NUMBER_SEPARATOR } from '../../../constants';
-export default class Lotto extends Component {
- render() {
- const { numbers } = this.props;
+export const Lotto = (props) => {
+ const { numbers } = props;
- return (
-
-
![lotto]({lottoImage})
-
- {numbers.map((v) => (v < 10 ? `0${v}` : v)).join(LOTTO_NUMBER_SEPARATOR)}
-
-
- );
- }
-}
+ return (
+
+
![lotto]({lottoImage})
+
+ {numbers.map((v) => v.toString().padStart(2, '0')).join(LOTTO_NUMBER_SEPARATOR)}
+
+
+ );
+};
+
+Lotto.propTypes = {
+ numbers: PropTypes.array.isRequired,
+};
diff --git a/src/components/containers/UserLotto/index.js b/src/components/containers/UserLotto/index.js
index 36cc8456..36dabf18 100644
--- a/src/components/containers/UserLotto/index.js
+++ b/src/components/containers/UserLotto/index.js
@@ -1,44 +1,33 @@
-/* eslint-disable react/no-array-index-key */
-import { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
-import Lotto from './Lotto';
-import { ToggleButton } from '../../shared';
+import { Lotto } from './Lotto';
+import { useToggleButton } from '../../../hooks';
import styles from './style.css';
const cx = classNames.bind(styles);
-export default class UserLotto extends Component {
- constructor(props) {
- super(props);
+export const UserLotto = (props) => {
+ const { lottoBundle } = props;
+ const { HookedToggleButton: ToggleButton, isToggled } = useToggleButton(false);
- this.state = { isToggled: false };
- this.onChangeToggleButton = this.onChangeToggleButton.bind(this);
- }
+ return (
+
+
번호보기
+
+ 총 {lottoBundle.length}개 구매하였습니다.
+
+
+ {lottoBundle.map((lotto, index) => (
+ -
+
+
+ ))}
+
+
+ );
+};
- onChangeToggleButton(e) {
- this.setState({ isToggled: e.target.checked });
- }
-
- render() {
- const { isToggled } = this.state;
- const { lottoBundle } = this.props;
- const userLottoDisplayClass = cx({
- UserLotto__display: true,
- toggle: isToggled,
- });
-
- return (
-
-
번호보기
-
- 총 {lottoBundle.length}개 구매하였습니다.
-
-
- {lottoBundle.map((v, i) => (
-
- ))}
-
-
- );
- }
-}
+UserLotto.propTypes = {
+ lottoBundle: PropTypes.array.isRequired,
+};
diff --git a/src/components/containers/UserLotto/style.css b/src/components/containers/UserLotto/style.css
index 99285412..4375bb1e 100644
--- a/src/components/containers/UserLotto/style.css
+++ b/src/components/containers/UserLotto/style.css
@@ -7,21 +7,23 @@
padding: 0 0.25rem;
}
-.UserLotto__display {
+.UserLotto__list {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
+ list-style: none;
+ padding-left: 0;
}
-.UserLotto__display .Lotto__number {
+.UserLotto__list .Lotto__number {
display: none;
}
-.UserLotto__display.toggle {
+.UserLotto__list.toggle {
flex-direction: column;
}
-.UserLotto__display.toggle .Lotto__number {
+.UserLotto__list.toggle .Lotto__number {
display: inline-block;
}
diff --git a/src/components/containers/UserResult/ResultTable.js b/src/components/containers/UserResult/ResultTable.js
deleted file mode 100644
index 9380247e..00000000
--- a/src/components/containers/UserResult/ResultTable.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/* eslint-disable react/no-array-index-key */
-import { Component } from 'react';
-import { getNumOfMatch } from './service';
-import { LottoBall } from '../../shared';
-import { RESULT_TABLE_DATA } from '../../../constants';
-
-export default class ResultTable extends Component {
- render() {
- return (
-
-
-
-
- 구분 |
- 번호 |
-
-
-
- {this.props.lottoBundle.map((v, i) => (
-
- ))}
-
-
-
- );
- }
-}
-
-class ResultTableRow extends Component {
- render() {
- const { lotto, winningNumber } = this.props;
- const { winningNumbers, bonusNumber } = winningNumber;
- const numOfMatch = getNumOfMatch(lotto, winningNumber);
-
- return (
-
- {RESULT_TABLE_DATA[numOfMatch].DESCRIPTION} |
-
- {lotto.map((v, i) => (
-
- ))}
- |
-
- );
- }
-}
diff --git a/src/components/containers/UserResult/index.js b/src/components/containers/UserResult/index.js
index 0f44e476..0e54d825 100644
--- a/src/components/containers/UserResult/index.js
+++ b/src/components/containers/UserResult/index.js
@@ -1,67 +1,114 @@
-/* eslint-disable react/sort-comp */
-import { Component } from 'react';
-import ResultTable from './ResultTable';
-import { Animation, Button, XButton, Record } from '../../shared';
-import { getComputedResult } from './service';
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Animation,
+ Button,
+ Record,
+ Title,
+ XButton,
+ LottoBall,
+ Table,
+ Thead,
+ Tbody,
+ TbodyRow,
+} from '../../shared';
+import { getComputedResult, getNumOfMatch } from './service';
+import { RESULT_TABLE_DATA } from '../../../constants';
import { coin } from '../../../statics';
import './style.css';
const COIN_ANIMATION_DURATION = 1500;
+const initialState = {
+ profit: 0,
+ rateOfReturn: 0,
+};
-export default class UserResult extends Component {
- constructor(props) {
- super(props);
+export const UserResult = (props) => {
+ const { restUseModal, lottoBundle, winningNumber, onReset, ...rest } = props;
+ const { HookedModal: Modal, completeLoading, hideUserResult } = restUseModal;
+ const [result, setResult] = useState(initialState);
+ const { profit, rateOfReturn } = result;
+ const ARIA_LABEL = { TITLE: 'user-result-title', DESC: 'user-result-desc' };
- this.state = {
- isLoading: true,
- result: {
- profit: 0,
- rateOfReturn: 0,
- },
- };
- this.removeLoader = this.removeLoader.bind(this);
- }
+ useEffect(() => {
+ setResult(() => getComputedResult(lottoBundle, winningNumber));
+ setTimeout(completeLoading, COIN_ANIMATION_DURATION);
+ }, [lottoBundle]);
- componentDidMount() {
- const { lottoBundle, winningNumber } = this.props;
- const result = getComputedResult(lottoBundle, winningNumber);
+ return (
+ }
+ aria-labelledby={ARIA_LABEL.TITLE}
+ aria-describedby={ARIA_LABEL.DESC}
+ {...rest}
+ >
+ <>
+
+ 당첨결과
+
+
+ {profit}원
+ {rateOfReturn}%
+
+
+
+
+ >
+
+ );
+};
- this.setState({ result });
- setTimeout(this.removeLoader, COIN_ANIMATION_DURATION);
- }
+UserResult.propTypes = {
+ restUseModal: PropTypes.shape({
+ isUserResultOpen: PropTypes.bool,
+ showUserResult: PropTypes.func,
+ hideUserResult: PropTypes.func.isRequired,
+ HookedModal: PropTypes.elementType.isRequired,
+ completeLoading: PropTypes.func.isRequired,
+ }),
+ lottoBundle: PropTypes.array.isRequired,
+ winningNumber: PropTypes.object.isRequired,
+ onReset: PropTypes.func.isRequired,
+};
- removeLoader() {
- this.setState({ isLoading: false });
- }
+function UserResultTable(props) {
+ const { lottoBundle, winningNumber } = props;
+ const { winningNumbers, bonusNumber } = winningNumber;
- render() {
- const { isLoading, result } = this.state;
- const { profit, rateOfReturn } = result;
- const { lottoBundle, winningNumber, onCloseUserResult, onReset } = this.props;
+ const theadItems = ['구분', '번호'];
+ const getFirstCell = (lotto) => {
+ const numOfMatch = getNumOfMatch(lotto, winningNumber);
+ return RESULT_TABLE_DATA[numOfMatch].DESCRIPTION;
+ };
+ const LottoBalls = (lotto, rowIndex) =>
+ lotto.map((num, index) => (
+
+ ));
- return (
-
- {isLoading ? (
-
- ) : (
-
-
-
당첨결과
-
-
- {profit}원
- {rateOfReturn}%
-
-
-
-
-
- )}
-
- );
- }
+ return (
+
+ {theadItems}
+
+ {lottoBundle.map((lotto, index) => (
+
+ {[getFirstCell(lotto), LottoBalls(lotto, index)]}
+
+ ))}
+
+
+ );
}
+
+UserResultTable.propTypes = {
+ lottoBundle: PropTypes.array.isRequired,
+ winningNumber: PropTypes.exact({
+ winningNumbers: PropTypes.array,
+ bonusNumber: PropTypes.number,
+ }).isRequired,
+};
diff --git a/src/components/containers/UserResult/service.js b/src/components/containers/UserResult/service.js
index 8b65c7e2..ff2bd1e6 100644
--- a/src/components/containers/UserResult/service.js
+++ b/src/components/containers/UserResult/service.js
@@ -5,8 +5,7 @@ import {
RESULT_TABLE_DATA,
} from '../../../constants';
-export const getNumOfMatch = (lotto, winningNumber) => {
- const { winningNumbers, bonusNumber } = winningNumber;
+export const getNumOfMatch = (lotto, { winningNumbers = [], bonusNumber }) => {
const numOfMatch = lotto.reduce((acc, cur) => acc + Number(winningNumbers.includes(cur)), 0);
if (numOfMatch === BONUS_CHECK_REQUIRED_COUNT && lotto.includes(bonusNumber)) {
diff --git a/src/components/containers/UserResult/style.css b/src/components/containers/UserResult/style.css
index bc2edd25..335d2142 100644
--- a/src/components/containers/UserResult/style.css
+++ b/src/components/containers/UserResult/style.css
@@ -1,71 +1,3 @@
-.UserResult {
- opacity: 0;
- visibility: hidden;
- display: flex;
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background: rgba(0, 0, 0, 0.5);
- transition: opacity 0.25s ease;
-}
-
-.UserResult--open {
- opacity: 1;
- visibility: visible;
- padding: 2.5rem;
-}
-
-.UserResult--loading {
- margin: 10rem auto 0;
-}
-
-.UserResult__inner {
- transition: top 0.25s ease;
- max-width: 400px;
- margin: 0 auto;
- height: fit-content;
- max-height: 90%;
- padding: 2.5rem;
- background: #fff;
- border-radius: 0.5rem;
- position: relative;
- overflow: auto;
- z-index: 2;
- top: 3rem;
-}
-
-.UserResult__title {
- text-align: center;
-}
-
-.ResultTable {
- display: flex;
- justify-content: center;
-}
-
-.ResultTable__table {
- border-width: 1px;
- border-collapse: collapse;
- border-radius: 1em;
- border-color: black;
- overflow: hidden;
- font-size: 0.9rem;
-}
-
-.ResultTable__row {
- text-align: center;
-}
-
-.ResultTable__head,
-.ResultTable__data {
- min-width: 2rem;
- text-align: center;
- padding: 0.75rem;
- border-bottom: 1px solid gainsboro;
-}
-
.UserResult__reset_button_wrapper {
display: flex;
justify-content: center;
@@ -90,12 +22,3 @@
.Record__wrapper {
margin: 2rem 0 0.5rem;
}
-
-@media screen and (max-width: 768px) {
- .UserResult__inner {
- width: 90%;
- height: 90%;
- padding: 20px;
- box-sizing: border-box;
- }
-}
diff --git a/src/components/containers/WinningNumbers/WinningNumberList.js b/src/components/containers/WinningNumbers/WinningNumberList.js
index ed2a51b0..ecffe8cd 100644
--- a/src/components/containers/WinningNumbers/WinningNumberList.js
+++ b/src/components/containers/WinningNumbers/WinningNumberList.js
@@ -1,21 +1,27 @@
-import { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import { LottoBall } from '../../shared';
-export default class WinningNumberList extends Component {
- render() {
- const { winningNumbers, bonusNumber } = this.props.number;
+export const WinningNumberList = (props) => {
+ const { winningNumbers, bonusNumber } = props.number;
- return (
- <>
-
- {winningNumbers.map((v) => (
-
- ))}
- +
- 보너스번호
-
-
- >
- );
- }
-}
+ return (
+ <>
+
+ {winningNumbers.map((v) => (
+
+ ))}
+ +
+ 보너스번호
+
+
+ >
+ );
+};
+
+WinningNumberList.propTypes = {
+ number: PropTypes.exact({
+ winningNumbers: PropTypes.array,
+ bonusNumber: PropTypes.number,
+ }).isRequired,
+};
diff --git a/src/components/containers/WinningNumbers/index.js b/src/components/containers/WinningNumbers/index.js
index 9a123c7e..2d05c0a8 100644
--- a/src/components/containers/WinningNumbers/index.js
+++ b/src/components/containers/WinningNumbers/index.js
@@ -1,6 +1,8 @@
-import { Component } from 'react';
-import WinningNumberList from './WinningNumberList';
-import { Animation, Button } from '../../shared';
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useLoading } from '../../../hooks';
+import { WinningNumberList } from './WinningNumberList';
+import { Animation, Button, Title } from '../../shared';
import { getWinningNumber } from './service';
import { dummyDrawNumber } from '../../../constants';
import { countDown } from '../../../statics';
@@ -12,42 +14,32 @@ const DRAW_DATE_KEY = 'drwNoDate';
const drawNth = dummyDrawNumber[DRAW_NTH_KEY];
const drawDate = dummyDrawNumber[DRAW_DATE_KEY].split('-').join('.');
-export default class WinningNumbers extends Component {
- constructor(props) {
- super(props);
+export const WinningNumbers = (props) => {
+ const { winningNumber, setWinningNumber, onShowUserResult } = props;
+ const { isLoaded, completeLoading } = useLoading(false);
- this.state = {
- isLoading: true,
- };
- this.removeLoader = this.removeLoader.bind(this);
- this.winningNumber = getWinningNumber();
- this.props.setWinningNumber({ winningNumber: this.winningNumber });
- }
+ useEffect(() => {
+ setWinningNumber(getWinningNumber());
+ setTimeout(completeLoading, COUNT_DOWN_ANIMATION_DURATION);
+ }, []);
- componentDidMount() {
- setTimeout(this.removeLoader, COUNT_DOWN_ANIMATION_DURATION);
- }
+ return isLoaded ? (
+ <>
+
+ {drawNth}회차 당첨번호 {drawDate}
+
+
+
+ >
+ ) : (
+
+ );
+};
- removeLoader() {
- this.setState({ isLoading: false });
- }
-
- render() {
- const { onShowUserResult } = this.props;
- const { isLoading } = this.state;
-
- return isLoading ? (
-
- ) : (
- <>
-
- {drawNth}회차 당첨번호 {drawDate}
-
-
-
- >
- );
- }
-}
+WinningNumbers.propTypes = {
+ winningNumber: PropTypes.object.isRequired,
+ setWinningNumber: PropTypes.func.isRequired,
+ onShowUserResult: PropTypes.func.isRequired,
+};
diff --git a/src/components/containers/WinningNumbers/style.css b/src/components/containers/WinningNumbers/style.css
index 5748fa52..56b6427b 100644
--- a/src/components/containers/WinningNumbers/style.css
+++ b/src/components/containers/WinningNumbers/style.css
@@ -1,10 +1,3 @@
-.WinningNumbers__title {
- margin-bottom: 1.5rem;
- font-size: 0.9rem;
- text-align: center;
- color: #333;
-}
-
.WinningNumberList {
display: flex;
justify-content: center;
diff --git a/src/components/shared/Animation/index.js b/src/components/shared/Animation/index.js
index fb6dc073..00f6beac 100644
--- a/src/components/shared/Animation/index.js
+++ b/src/components/shared/Animation/index.js
@@ -1,19 +1,32 @@
-/* eslint-disable react/jsx-props-no-spreading */
-import { Component } from 'react';
+import React from 'react';
import Lottie from 'react-lottie';
+import PropTypes from 'prop-types';
-export default class Animation extends Component {
- render() {
- const { animationData, loop, ...others } = this.props;
+export default function Animation(props) {
+ const { animationData, loop, ...rest } = props;
- return (
-
- );
- }
+ return (
+
+ );
}
+
+Animation.propTypes = {
+ animationData: PropTypes.object.isRequired,
+ loop: PropTypes.bool,
+ speed: PropTypes.number,
+ width: PropTypes.string,
+ height: PropTypes.string,
+};
+
+Animation.defaultProps = {
+ loop: false,
+ speed: 1,
+ width: null,
+ height: null,
+};
diff --git a/src/components/shared/Ball/index.js b/src/components/shared/Ball/index.js
new file mode 100644
index 00000000..d38b50ab
--- /dev/null
+++ b/src/components/shared/Ball/index.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
+
+const cx = classNames.bind(styles);
+
+export default function Ball(props) {
+ const { className, children, ...rest } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
+Ball.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.string,
+};
\ No newline at end of file
diff --git a/src/components/shared/NDigitBall/style.css b/src/components/shared/Ball/style.css
similarity index 96%
rename from src/components/shared/NDigitBall/style.css
rename to src/components/shared/Ball/style.css
index ff828881..4055114d 100644
--- a/src/components/shared/NDigitBall/style.css
+++ b/src/components/shared/Ball/style.css
@@ -1,4 +1,4 @@
-.NDigitBall {
+.Ball {
display: inline-block;
width: 1rem;
height: 1rem;
diff --git a/src/components/shared/Button/index.js b/src/components/shared/Button/index.js
index 43926189..0ed8fd68 100644
--- a/src/components/shared/Button/index.js
+++ b/src/components/shared/Button/index.js
@@ -1,23 +1,39 @@
-/* eslint-disable react/jsx-props-no-spreading */
-/* eslint-disable react/button-has-type */
-import { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import styles from './style.css';
const cx = classNames.bind(styles);
-export default class Button extends Component {
- render() {
- const { className, children, ...props } = this.props;
- const buttonClass = cx({
- Button: true,
- [`${className}`]: true,
- });
+export default function Button(props) {
+ const { className, children, ...rest } = props;
- return (
-
- );
- }
+ return (
+
+ );
}
+
+Button.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.string,
+ type: PropTypes.oneOf(['submit', 'reset', 'button']),
+ onClick: PropTypes.func,
+ autofocus: PropTypes.bool,
+ disabled: PropTypes.bool,
+ form: PropTypes.string,
+ formAction: PropTypes.string,
+ formEncType: PropTypes.oneOf([
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data',
+ 'text/plain',
+ ]),
+ formMethod: PropTypes.oneOf(['get', 'post']),
+ name: PropTypes.string,
+ value: PropTypes.string,
+};
+
+Button.defaultProps = {
+ type: 'submit',
+};
diff --git a/src/components/shared/Button/style.css b/src/components/shared/Button/style.css
index 2cb3ef40..4d750c63 100644
--- a/src/components/shared/Button/style.css
+++ b/src/components/shared/Button/style.css
@@ -11,3 +11,7 @@
cursor: pointer;
background-color: #ee8791;
}
+
+.Button:disabled {
+ cursor: default;
+}
diff --git a/src/components/shared/LottoBall/index.js b/src/components/shared/LottoBall/index.js
index 6ff015ba..aca521c7 100644
--- a/src/components/shared/LottoBall/index.js
+++ b/src/components/shared/LottoBall/index.js
@@ -1,26 +1,31 @@
-import { Component } from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
-import TwoDigitBall from '../NDigitBall';
+import Ball from '../Ball';
import styles from './style.css';
const cx = classNames.bind(styles);
-export default class LottoBall extends Component {
- render() {
- const { targetNumber: num, winningNumbers } = this.props;
- const lottoBallClass = cx({
- 'LottoBall--zeros': num < 10,
- 'LottoBall--tens': num >= 10 && num < 20,
- 'LottoBall--twenties': num >= 20 && num < 30,
- 'LottoBall--thirties': num >= 30 && num < 40,
- 'LottoBall--forties': num >= 40,
- 'LottoBall--not_matched': winningNumbers && !winningNumbers.includes(num),
- });
+export default function LottoBall(props) {
+ const { className, targetNumber: num, winningNumbers, ...rest } = props;
+ const classnames = cx(className, {
+ 'LottoBall--zeros': num < 10,
+ 'LottoBall--tens': num >= 10 && num < 20,
+ 'LottoBall--twenties': num >= 20 && num < 30,
+ 'LottoBall--thirties': num >= 30 && num < 40,
+ 'LottoBall--forties': num >= 40,
+ 'LottoBall--not_matched': winningNumbers && !winningNumbers.includes(num),
+ });
- return (
-
- {num}
-
- );
- }
+ return (
+
+ {num.toString().padStart(2, '0')}
+
+ );
}
+
+LottoBall.propTypes = {
+ className: PropTypes.string,
+ targetNumber: PropTypes.number.isRequired,
+ winningNumbers: PropTypes.array,
+};
diff --git a/src/components/shared/Modal/index.js b/src/components/shared/Modal/index.js
new file mode 100644
index 00000000..9b5e0186
--- /dev/null
+++ b/src/components/shared/Modal/index.js
@@ -0,0 +1,40 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
+/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
+
+const cx = classNames.bind(styles);
+
+export default function Modal(props) {
+ const { className, isOpen, isLoaded, loading, children, onClickDimmedArea, ...rest } = props;
+ return (
+
+ {isLoaded ? (
+
{children}
+ ) : (
+
{loading}
+ )}
+
+ );
+}
+
+Modal.propTypes = {
+ className: PropTypes.string,
+ isOpen: PropTypes.bool,
+ isLoaded: PropTypes.bool,
+ loading: PropTypes.node,
+ children: PropTypes.node,
+ onClickDimmedArea: PropTypes.func,
+};
+
+Modal.defaultProps = {
+ isOpen: false,
+ isLoaded: false,
+};
diff --git a/src/components/shared/Modal/style.css b/src/components/shared/Modal/style.css
new file mode 100644
index 00000000..665f33ec
--- /dev/null
+++ b/src/components/shared/Modal/style.css
@@ -0,0 +1,46 @@
+.Modal {
+ opacity: 0;
+ visibility: hidden;
+ display: flex;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background: rgba(0, 0, 0, 0.5);
+ transition: opacity 0.25s ease;
+}
+
+.Modal--open {
+ opacity: 1;
+ visibility: visible;
+ padding: 2.5rem;
+}
+
+.Modal__Inner--loading {
+ margin: 10rem auto 0;
+}
+
+.Modal__Inner {
+ transition: top 0.25s ease;
+ max-width: 400px;
+ margin: 0 auto;
+ height: fit-content;
+ max-height: 90%;
+ padding: 2.5rem;
+ background: #fff;
+ border-radius: 0.5rem;
+ position: relative;
+ overflow: auto;
+ z-index: 2;
+ top: 3rem;
+}
+
+@media screen and (max-width: 768px) {
+ .Modal__Inner {
+ width: 90%;
+ height: 90%;
+ padding: 20px;
+ box-sizing: border-box;
+ }
+}
diff --git a/src/components/shared/NDigitBall/index.js b/src/components/shared/NDigitBall/index.js
deleted file mode 100644
index db35f69a..00000000
--- a/src/components/shared/NDigitBall/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component } from 'react';
-import classNames from 'classnames/bind';
-import styles from './style.css';
-
-const cx = classNames.bind(styles);
-export default class NDigitBall extends Component {
- render() {
- const { className, children, n } = this.props;
- const NDigitBallClass = cx({
- NDigitBall: true,
- [`${className}`]: true,
- });
-
- return {children.toString().padStart(n, '0')};
- }
-}
diff --git a/src/components/shared/Record/index.js b/src/components/shared/Record/index.js
index 137715f0..a4bcad8d 100644
--- a/src/components/shared/Record/index.js
+++ b/src/components/shared/Record/index.js
@@ -1,15 +1,23 @@
-import { Component } from 'react';
-import './style.css';
-
-export default class Record extends Component {
- render() {
- const { label, children } = this.props;
-
- return (
-
- {label}
- {children}
-
- );
- }
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
+
+const cx = classNames.bind(styles);
+
+export default function Record(props) {
+ const { className, label, children, ...rest } = props;
+
+ return (
+
+ {label}
+ {children}
+
+ );
}
+
+Record.propTypes = {
+ className: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+};
diff --git a/src/components/shared/Table/index.js b/src/components/shared/Table/index.js
new file mode 100644
index 00000000..07791fe4
--- /dev/null
+++ b/src/components/shared/Table/index.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
+
+const cx = classNames.bind(styles);
+
+export default function Table(props) {
+ const { className, children, ...rest } = props;
+
+ return (
+
+ );
+}
+
+Table.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export const Thead = (props) => {
+ const { className, children: theadItems, ...rest } = props;
+ return (
+
+
+ {theadItems.map((item, index) => (
+ {item} |
+ ))}
+
+
+ );
+};
+
+Thead.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export const Tbody = (props) => {
+ const { className, children, ...rest } = props;
+ return (
+
+ {children}
+
+ );
+};
+
+Tbody.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export const TbodyRow = (props) => {
+ const { className, children: trItems, rowIndex, ...rest } = props;
+ return (
+
+ {trItems.map((tdItem, index) => (
+ {tdItem} |
+ ))}
+
+ );
+};
+
+TbodyRow.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.array.isRequired,
+ rowIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+};
+
+export const Tr = (props) => {
+ const { className, children, ...rest } = props;
+ return (
+
+ {children}
+
+ );
+};
+
+Tr.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export const Th = (props) => {
+ const { className, children, ...rest } = props;
+ return (
+
+ {children}
+ |
+ );
+};
+
+Th.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export const Td = (props) => {
+ const { className, children, ...rest } = props;
+ return (
+
+ {children}
+ |
+ );
+};
+
+Td.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
diff --git a/src/components/shared/Table/style.css b/src/components/shared/Table/style.css
new file mode 100644
index 00000000..5bfca5c9
--- /dev/null
+++ b/src/components/shared/Table/style.css
@@ -0,0 +1,20 @@
+.Table {
+ border-width: 1px;
+ border-collapse: collapse;
+ border-radius: 1em;
+ border-color: black;
+ overflow: hidden;
+ font-size: 0.9rem;
+}
+
+.Tr {
+ text-align: center;
+}
+
+.Tr__Th,
+.Tr__Td {
+ min-width: 2rem;
+ text-align: center;
+ padding: 0.75rem;
+ border-bottom: 1px solid gainsboro;
+}
diff --git a/src/components/shared/Title/index.js b/src/components/shared/Title/index.js
new file mode 100644
index 00000000..bd400ab0
--- /dev/null
+++ b/src/components/shared/Title/index.js
@@ -0,0 +1,63 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
+
+const cx = classNames.bind(styles);
+
+export default function Title(props) {
+ const { className, as, size, children, ...rest } = props;
+ const classnames = cx('Title', className, { [`Title--${size}`]: size });
+
+ switch (as) {
+ case 'h1':
+ return (
+
+ {children}
+
+ );
+ case 'h2':
+ return (
+
+ {children}
+
+ );
+ case 'h3':
+ return (
+
+ {children}
+
+ );
+ case 'h4':
+ return (
+
+ {children}
+
+ );
+ case 'h5':
+ return (
+
+ {children}
+
+ );
+ case 'h6':
+ return (
+
+ {children}
+
+ );
+ default:
+ return (
+
+ {children}
+
+ );
+ }
+}
+
+Title.propTypes = {
+ className: PropTypes.string,
+ as: PropTypes.string,
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
+ children: PropTypes.node.isRequired,
+};
diff --git a/src/components/shared/Title/style.css b/src/components/shared/Title/style.css
new file mode 100644
index 00000000..88ab0526
--- /dev/null
+++ b/src/components/shared/Title/style.css
@@ -0,0 +1,17 @@
+.Title {
+ margin: 1.5rem 0;
+ text-align: center;
+ color: #333;
+}
+
+.Title--small {
+ font-size: 0.9rem;
+}
+
+.Title--medium {
+ font-size: 1.5rem;
+}
+
+.Title--large {
+ font-size: 2rem;
+}
diff --git a/src/components/shared/ToggleButton/index.js b/src/components/shared/ToggleButton/index.js
index a066a21e..32d415fa 100644
--- a/src/components/shared/ToggleButton/index.js
+++ b/src/components/shared/ToggleButton/index.js
@@ -1,18 +1,33 @@
-/* eslint-disable jsx-a11y/label-has-associated-control */
-import { Component } from 'react';
-import './style.css';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
-export default class ToggleButton extends Component {
- render() {
- const { onChange, children } = this.props;
+const cx = classNames.bind(styles);
- return (
-
-
-
- );
- }
+export default function ToggleButton(props) {
+ const { containerClassname, className, isToggled, onChange, children, ...rest } = props;
+
+ return (
+
+
+
+ );
}
+
+ToggleButton.propTypes = {
+ containerClassname: PropTypes.string,
+ className: PropTypes.string,
+ isToggled: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ children: PropTypes.node,
+};
diff --git a/src/components/shared/XButton/index.js b/src/components/shared/XButton/index.js
index 1cd0e693..a4384628 100644
--- a/src/components/shared/XButton/index.js
+++ b/src/components/shared/XButton/index.js
@@ -1,16 +1,23 @@
-import { Component } from 'react';
-import './style.css';
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames/bind';
+import styles from './style.css';
-export default class XButton extends Component {
- render() {
- const { onClick } = this.props;
+const cx = classNames.bind(styles);
- return (
-
- );
- }
+export default function XButton(props) {
+ const { className, onClick, ...rest } = props;
+
+ return (
+
+ );
}
+
+XButton.propTypes = {
+ className: PropTypes.string,
+ onClick: PropTypes.func,
+};
diff --git a/src/components/shared/index.js b/src/components/shared/index.js
index b7ed6890..8c1ef7f3 100644
--- a/src/components/shared/index.js
+++ b/src/components/shared/index.js
@@ -1,9 +1,29 @@
import Animation from './Animation';
+import Ball from './Ball';
import Button from './Button';
-import ToggleButton from './ToggleButton';
-import XButton from './XButton';
-import NDigitBall from './NDigitBall';
import LottoBall from './LottoBall';
+import Modal from './Modal';
import Record from './Record';
+import Table, { Thead, Tbody, TbodyRow, Tr, Th, Td } from './Table';
+import Title from './Title';
+import ToggleButton from './ToggleButton';
+import XButton from './XButton';
-export { Animation, Button, ToggleButton, XButton, NDigitBall, LottoBall, Record };
+export {
+ Animation,
+ Ball,
+ Button,
+ LottoBall,
+ Modal,
+ Record,
+ Table,
+ Thead,
+ Tbody,
+ TbodyRow,
+ Tr,
+ Th,
+ Td,
+ Title,
+ ToggleButton,
+ XButton,
+};
diff --git a/src/hooks/index.js b/src/hooks/index.js
new file mode 100644
index 00000000..92cfa157
--- /dev/null
+++ b/src/hooks/index.js
@@ -0,0 +1,5 @@
+import { useLoading } from './useLoading';
+import { useModal } from './useModal';
+import { useToggleButton } from './useToggleButton';
+
+export { useLoading, useModal, useToggleButton };
diff --git a/src/hooks/useLoading.js b/src/hooks/useLoading.js
new file mode 100644
index 00000000..65b0f94a
--- /dev/null
+++ b/src/hooks/useLoading.js
@@ -0,0 +1,8 @@
+import { useState } from 'react';
+
+export const useLoading = (initialState = false) => {
+ const [isLoaded, setIsLoaded] = useState(initialState);
+ const completeLoading = () => setIsLoaded(() => true);
+
+ return { isLoaded, completeLoading };
+};
diff --git a/src/hooks/useModal.js b/src/hooks/useModal.js
new file mode 100644
index 00000000..942f5b5c
--- /dev/null
+++ b/src/hooks/useModal.js
@@ -0,0 +1,63 @@
+import React, { useReducer } from 'react';
+import { PropTypes } from 'prop-types';
+import { Modal } from '../components/shared';
+
+const MODAL_ACTION = {
+ OPEN: 'modal/OPEN',
+ COMPLETE_LOADING: 'modal/COMPLETE_LOADING',
+ CLOSE: 'modal/CLOSE',
+};
+
+const modalReducer = (state, action) => {
+ switch (action.type) {
+ case MODAL_ACTION.OPEN:
+ return { ...state, isOpen: true };
+
+ case MODAL_ACTION.COMPLETE_LOADING:
+ return { ...state, isLoaded: true };
+
+ case MODAL_ACTION.CLOSE:
+ return { ...state, isOpen: false, isLoaded: false };
+
+ default:
+ return state;
+ }
+};
+
+export const useModal = (initialState = { isOpen: false, isLoaded: false }) => {
+ const [modalStatus, dispatch] = useReducer(modalReducer, initialState);
+ const { isOpen, isLoaded } = modalStatus;
+
+ const open = () => dispatch({ type: MODAL_ACTION.OPEN });
+ const completeLoading = () => dispatch({ type: MODAL_ACTION.COMPLETE_LOADING });
+ const close = () => dispatch({ type: MODAL_ACTION.CLOSE });
+
+ const onClickDimmedArea = ({ target, currentTarget }) => {
+ if (target !== currentTarget) {
+ return;
+ }
+ close();
+ };
+
+ const HookedModal = (props) => {
+ const { children, ...rest } = props;
+ return (
+
+ {children}
+
+ );
+ };
+
+ HookedModal.propTypes = {
+ children: PropTypes.node,
+ };
+
+ return {
+ HookedModal,
+ isOpen,
+ isLoaded,
+ open,
+ completeLoading,
+ close,
+ };
+};
diff --git a/src/hooks/useToggleButton.js b/src/hooks/useToggleButton.js
new file mode 100644
index 00000000..14e2225f
--- /dev/null
+++ b/src/hooks/useToggleButton.js
@@ -0,0 +1,23 @@
+import React, { useState } from 'react';
+import { PropTypes } from 'prop-types';
+import { ToggleButton } from '../components/shared';
+
+export const useToggleButton = (initialState = false) => {
+ const [isToggled, setIsToggled] = useState(initialState);
+ const toggle = (e) => setIsToggled((prevState) => !prevState);
+
+ const HookedToggleButton = (props) => {
+ const { children, ...rest } = props;
+ return (
+
+ {children}
+
+ );
+ };
+
+ HookedToggleButton.propTypes = {
+ children: PropTypes.node,
+ };
+
+ return { HookedToggleButton, toggle, isToggled };
+};
diff --git a/src/index.js b/src/index.js
index 5472b994..171f367c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import App from './components/App/index.js';
+import { App } from './components/App';
ReactDOM.render(
diff --git a/yarn.lock b/yarn.lock
index 17e6c74c..227794e4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2341,7 +2341,7 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
-array-includes@^3.1.1, array-includes@^3.1.2:
+array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a"
integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==
@@ -2383,7 +2383,7 @@ array.prototype.flat@^1.2.3:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
-array.prototype.flatmap@^1.2.3:
+array.prototype.flatmap@^1.2.3, array.prototype.flatmap@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz#94cfd47cc1556ec0747d97f7c7738c58122004c9"
integrity sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==
@@ -4408,28 +4408,10 @@ escodegen@^1.14.1:
optionalDependencies:
source-map "~0.6.1"
-eslint-config-airbnb-base@^14.2.1:
- version "14.2.1"
- resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e"
- integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==
- dependencies:
- confusing-browser-globals "^1.0.10"
- object.assign "^4.1.2"
- object.entries "^1.1.2"
-
-eslint-config-airbnb@^18.2.1:
- version "18.2.1"
- resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz#b7fe2b42f9f8173e825b73c8014b592e449c98d9"
- integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==
- dependencies:
- eslint-config-airbnb-base "^14.2.1"
- object.assign "^4.1.2"
- object.entries "^1.1.2"
-
-eslint-config-prettier@^8.1.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6"
- integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==
+eslint-config-prettier@^8.2.0:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.2.0.tgz#78de77d63bca8e9e59dae75a614b5299925bb7b3"
+ integrity sha512-dWV9EVeSo2qodOPi1iBYU/x6F6diHv8uujxbxr77xExs3zTAlNXvVZKiyLsQGNz7yPV2K49JY5WjPzNIuDc2Bw==
eslint-config-react-app@^6.0.0:
version "6.0.0"
@@ -4438,6 +4420,26 @@ eslint-config-react-app@^6.0.0:
dependencies:
confusing-browser-globals "^1.0.10"
+eslint-config-semistandard@15.0.1:
+ version "15.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-config-semistandard/-/eslint-config-semistandard-15.0.1.tgz#a868397af8fb26d1bb6b907aa9d575d385581099"
+ integrity sha512-sfV+qNBWKOmF0kZJll1VH5XqOAdTmLlhbOl9WKI11d2eMEe+Kicxnpm24PQWHOqAfk5pAWU2An0LjNCXKa4Usg==
+
+eslint-config-standard-jsx@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-10.0.0.tgz#dc24992661325a2e480e2c3091d669f19034e18d"
+ integrity sha512-hLeA2f5e06W1xyr/93/QJulN/rLbUVUmqTlexv9PRKHFwEC9ffJcH2LvJhMoEqYQBEYafedgGZXH2W8NUpt5lA==
+
+eslint-config-standard-react@^11.0.1:
+ version "11.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard-react/-/eslint-config-standard-react-11.0.1.tgz#1f488e0062c1e21c4c8584551619f11750658f55"
+ integrity sha512-4WlBynOqBZJRaX81CBcIGDHqUiqxvw4j/DbEIICz8QkMs3xEncoPgAoysiqCSsg71X92uhaBc8sgqB96smaMmg==
+
+eslint-config-standard@>=14.1.0:
+ version "16.0.2"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.2.tgz#71e91727ac7a203782d0a5ca4d1c462d14e234f6"
+ integrity sha512-fx3f1rJDsl9bY7qzyX8SAtP8GBSk6MfXFaTfaGgk12aAYW4gJSyRm7dM790L6cbXv63fvjY4XeSzXnb4WM+SKw==
+
eslint-import-resolver-node@^0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
@@ -4454,6 +4456,14 @@ eslint-module-utils@^2.6.0:
debug "^2.6.9"
pkg-dir "^2.0.0"
+eslint-plugin-es@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
+ integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
+ dependencies:
+ eslint-utils "^2.0.0"
+ regexpp "^3.0.0"
+
eslint-plugin-flowtype@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.2.tgz#c6e5dd2fad4e757a1c63e652da6cff597659554f"
@@ -4462,7 +4472,7 @@ eslint-plugin-flowtype@^5.2.0:
lodash "^4.17.15"
string-natural-compare "^3.0.1"
-eslint-plugin-import@^2.22.1:
+eslint-plugin-import@>=2.18.0, eslint-plugin-import@^2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702"
integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==
@@ -4488,7 +4498,7 @@ eslint-plugin-jest@^24.1.0:
dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1"
-eslint-plugin-jsx-a11y@^6.3.1:
+eslint-plugin-jsx-a11y@^6.3.1, eslint-plugin-jsx-a11y@^6.4.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==
@@ -4505,12 +4515,22 @@ eslint-plugin-jsx-a11y@^6.3.1:
jsx-ast-utils "^3.1.0"
language-tags "^1.0.5"
-eslint-plugin-prettier@^3.3.1:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
- integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
+eslint-plugin-node@>=9.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
+ integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
dependencies:
- prettier-linter-helpers "^1.0.0"
+ eslint-plugin-es "^3.0.0"
+ eslint-utils "^2.0.0"
+ ignore "^5.1.1"
+ minimatch "^3.0.4"
+ resolve "^1.10.1"
+ semver "^6.1.0"
+
+eslint-plugin-promise@>=4.2.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.0.tgz#fb2188fb734e4557993733b41aa1a688f46c6f24"
+ integrity sha512-NGmI6BH5L12pl7ScQHbg7tvtk4wPxxj8yPHH47NvSmMtFneC077PSeY3huFj06ZWZvtbfxSPt3RuOQD5XcR4ng==
eslint-plugin-react-hooks@^4.2.0:
version "4.2.0"
@@ -4534,6 +4554,29 @@ eslint-plugin-react@^7.21.5:
resolve "^1.18.1"
string.prototype.matchall "^4.0.2"
+eslint-plugin-react@^7.23.2:
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz#2d2291b0f95c03728b55869f01102290e792d494"
+ integrity sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==
+ dependencies:
+ array-includes "^3.1.3"
+ array.prototype.flatmap "^1.2.4"
+ doctrine "^2.1.0"
+ has "^1.0.3"
+ jsx-ast-utils "^2.4.1 || ^3.0.0"
+ minimatch "^3.0.4"
+ object.entries "^1.1.3"
+ object.fromentries "^2.0.4"
+ object.values "^1.1.3"
+ prop-types "^15.7.2"
+ resolve "^2.0.0-next.3"
+ string.prototype.matchall "^4.0.4"
+
+eslint-plugin-standard@>=4.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz#c43f6925d669f177db46f095ea30be95476b1ee4"
+ integrity sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==
+
eslint-plugin-testing-library@^3.9.2:
version "3.10.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.1.tgz#4dd02306d601c3238fdabf1d1dbc5f2a8e85d531"
@@ -4909,11 +4952,6 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-fast-diff@^1.1.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
- integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
-
fast-glob@^3.1.1:
version "3.2.5"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
@@ -5708,7 +5746,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
-ignore@^5.1.4:
+ignore@^5.1.1, ignore@^5.1.4:
version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@@ -7631,7 +7669,7 @@ object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
has-symbols "^1.0.1"
object-keys "^1.1.1"
-object.entries@^1.1.0, object.entries@^1.1.2:
+object.entries@^1.1.0, object.entries@^1.1.2, object.entries@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.3.tgz#c601c7f168b62374541a07ddbd3e2d5e4f7711a6"
integrity sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==
@@ -7641,7 +7679,7 @@ object.entries@^1.1.0, object.entries@^1.1.2:
es-abstract "^1.18.0-next.1"
has "^1.0.3"
-object.fromentries@^2.0.2:
+object.fromentries@^2.0.2, object.fromentries@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8"
integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==
@@ -7677,6 +7715,16 @@ object.values@^1.1.0, object.values@^1.1.1:
es-abstract "^1.18.0-next.1"
has "^1.0.3"
+object.values@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee"
+ integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.18.0-next.2"
+ has "^1.0.3"
+
obuf@^1.0.0, obuf@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
@@ -8783,13 +8831,6 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
-prettier-linter-helpers@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
- integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
- dependencies:
- fast-diff "^1.1.2"
-
prettier@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
@@ -9489,7 +9530,7 @@ resolve@1.18.1:
is-core-module "^2.0.0"
path-parse "^1.0.6"
-resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
+resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -9497,6 +9538,14 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.1
is-core-module "^2.2.0"
path-parse "^1.0.6"
+resolve@^2.0.0-next.3:
+ version "2.0.0-next.3"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
+ integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==
+ dependencies:
+ is-core-module "^2.2.0"
+ path-parse "^1.0.6"
+
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@@ -9738,7 +9787,7 @@ semver@7.3.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
-semver@^6.0.0, semver@^6.3.0:
+semver@^6.0.0, semver@^6.1.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -10226,7 +10275,7 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
-string.prototype.matchall@^4.0.2:
+string.prototype.matchall@^4.0.2, string.prototype.matchall@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.4.tgz#608f255e93e072107f5de066f81a2dfb78cf6b29"
integrity sha512-pknFIWVachNcyqRfaQSeu/FUfpvJTe4uskUSZ9Wc1RijsPuzbZ8TyYT8WCNnntCjUEqQ3vUHMAfVj2+wLAisPQ==