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 - - {numbers.map((v) => (v < 10 ? `0${v}` : v)).join(LOTTO_NUMBER_SEPARATOR)} - -
- ); - } -} + return ( +
+ lotto + + {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 ( + + {children} +
+ ); +} + +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==