diff --git a/package.json b/package.json index f6c7705..1101d14 100755 --- a/package.json +++ b/package.json @@ -9,16 +9,26 @@ "eject": "react-scripts eject" }, "dependencies": { + "antd": "^4.15.2", "classnames": "^2.2.6", "lodash": "^4.17.15", + "node-sass": "4.14.1", "prop-types": "^15.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", "react-redux": "^7.2.0", - "redux": "^4.0.5" + "redux": "^4.0.5", + "typescript": "^4.2.4", + "uuid": "^8.3.2", + "yarn": "^1.22.10" }, "devDependencies": { - "react-scripts": "3.4.0" + "react-scripts": "^3.4.0" }, - "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"] + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] } diff --git a/src/actions/PlayersActions.js b/src/actions/PlayersActions.js index 8f427c2..01bab15 100755 --- a/src/actions/PlayersActions.js +++ b/src/actions/PlayersActions.js @@ -1,9 +1,9 @@ -import * as types from '../constants/ActionTypes'; +import * as types from "../constants/ActionTypes"; -export function addPlayer(name) { +export function addPlayer(newPlayer) { return { type: types.ADD_PLAYER, - name, + newPlayer, }; } @@ -20,3 +20,10 @@ export function starPlayer(id) { id, }; } + +export const filterPlayer = (position) => { + return { + type: types.FILTER_PLAYER, + position, + }; +}; diff --git a/src/components/AddPlayerInput.css b/src/components/AddPlayerInput.css deleted file mode 100755 index 1507c6e..0000000 --- a/src/components/AddPlayerInput.css +++ /dev/null @@ -1,6 +0,0 @@ -:local(.addPlayerInput) { - border-radius: 0; - border-color: #abaaaa; - border-left: 0; - border-right: 0; -} diff --git a/src/components/AddPlayerInput.js b/src/components/AddPlayerInput.js deleted file mode 100755 index 5d914d8..0000000 --- a/src/components/AddPlayerInput.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component } from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import styles from './AddPlayerInput.css'; - -class AddPlayerInput extends Component { - render() { - return ( - - ); - } - - constructor(props, context) { - super(props, context); - this.state = { - name: this.props.name || '', - }; - } - - handleChange(e) { - this.setState({ name: e.target.value }); - } - - handleSubmit(e) { - const name = e.target.value.trim(); - if (e.which === 13) { - this.props.addPlayer(name); - this.setState({ name: '' }); - } - } -} - -AddPlayerInput.propTypes = { - addPlayer: PropTypes.func.isRequired, -}; - -export default AddPlayerInput; diff --git a/src/components/PlayerList.js b/src/components/PlayerList.js deleted file mode 100755 index 7b40246..0000000 --- a/src/components/PlayerList.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import styles from './PlayerList.css'; -import PlayerListItem from './PlayerListItem'; - -class PlayerList extends Component { - render() { - return ( - - ); - } -} - -PlayerList.propTypes = { - players: PropTypes.array.isRequired, - actions: PropTypes.object.isRequired, -}; - -export default PlayerList; diff --git a/src/components/add-player-input/AddPlayerInput.js b/src/components/add-player-input/AddPlayerInput.js new file mode 100644 index 0000000..de5c9ef --- /dev/null +++ b/src/components/add-player-input/AddPlayerInput.js @@ -0,0 +1,54 @@ +import React, { Component } from "react"; +import classnames from "classnames"; +import PropTypes from "prop-types"; +import { POS_TYPES, Options } from "../../constants/posTypes"; + +import styles from "./AddPlayerInput.module.scss"; + +class AddPlayerInput extends Component { + render() { + return ( +
+ + +
+ ); + } + + constructor(props) { + super(props); + this.state = { + name: "", + position: POS_TYPES.SF, + }; + } + + handleChange(e) { + this.setState({ name: e.target.value }); + } + handlePositionSelect = (e) => { + this.setState({ position: e.target.value }); + }; + handleSubmit(e) { + const name = e.target.value.trim(); + if (e.which === 13) { + console.log(this.state.position); + this.props.addPlayer({ name, position: this.state.position }); + this.setState({ name: "" }); + } + } +} + +AddPlayerInput.propTypes = { + addPlayer: PropTypes.func.isRequired, +}; + +export default AddPlayerInput; diff --git a/src/components/add-player-input/AddPlayerInput.module.scss b/src/components/add-player-input/AddPlayerInput.module.scss new file mode 100644 index 0000000..2d7deef --- /dev/null +++ b/src/components/add-player-input/AddPlayerInput.module.scss @@ -0,0 +1,14 @@ +.addPlayerFeild { + display: flex; + margin-bottom: 4px; + select { + padding: 0 10px; + } + .addPlayerInput { + border-radius: 0; + border-color: #abaaaa; + border-left: 0; + border-right: 0; + width: 100%; + } +} diff --git a/src/components/index.js b/src/components/index.js index 12ce117..cd34e2c 100755 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,3 +1,3 @@ -export { default as AddPlayerInput } from './AddPlayerInput'; -export { default as PlayerList } from './PlayerList'; -export { default as PlayerListItem } from './PlayerListItem'; +export { default as AddPlayerInput } from './add-player-input/AddPlayerInput'; +export { default as PlayerList } from './player-list/PlayerList'; +export { default as PlayerListItem } from './player-list-item/PlayerListItem'; diff --git a/src/components/pagination/index.module.scss b/src/components/pagination/index.module.scss new file mode 100644 index 0000000..1861af3 --- /dev/null +++ b/src/components/pagination/index.module.scss @@ -0,0 +1,11 @@ +.paginationWrapper { + width: 100%; + padding: 5px 0; + text-align: center; + color: antiquewhite; + font-size: larger; + + .arrow { + cursor: pointer; + } +} diff --git a/src/components/pagination/index.tsx b/src/components/pagination/index.tsx new file mode 100644 index 0000000..a5f597f --- /dev/null +++ b/src/components/pagination/index.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react' +import styles from './index.module.scss' + +interface IPagination { + pageNo: number + pageSize: number +} + +interface IProps { + initPagination: IPagination + total: number + onChange: Function +} + +export default (props: IProps) => { + const { total, initPagination, onChange } = props + const [pageNo, setNo] = useState(initPagination.pageNo) + const [pageSize] = useState(initPagination.pageSize) + const [maxPages, setMaxPages] = useState(Math.ceil(total / pageSize) || 1) + + useEffect(() => { + setMaxPages(Math.ceil(total / pageSize) || 1) + setNo(initPagination.pageNo) + onChange({ pageNo: initPagination.pageNo, pageSize }) + }, [total, pageSize]) + + const handlePaginationChange = (accumlation: number) => { + let no = accumlation + pageNo + if (no > maxPages) no = maxPages + else if (no < 1) no = 1 + + onChange({ pageNo: no, pageSize }) + setNo(no) + } + + return ( +
+ handlePaginationChange(-1)}> + ← + + {pageNo} / {maxPages} + handlePaginationChange(1)}> + → + +
+ ) +} diff --git a/src/components/PlayerListItem.js b/src/components/player-list-item/PlayerListItem.js old mode 100755 new mode 100644 similarity index 77% rename from src/components/PlayerListItem.js rename to src/components/player-list-item/PlayerListItem.js index ec9758c..145aabf --- a/src/components/PlayerListItem.js +++ b/src/components/player-list-item/PlayerListItem.js @@ -1,7 +1,7 @@ -import React, { Component } from 'react'; -import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import styles from './PlayerListItem.css'; +import React, { Component } from "react"; +import classnames from "classnames"; +import PropTypes from "prop-types"; +import styles from "./PlayerListItem.module.scss"; class PlayerListItem extends Component { render() { @@ -23,9 +23,9 @@ class PlayerListItem extends Component { onClick={() => this.props.starPlayer(this.props.id)} > @@ -42,7 +42,7 @@ class PlayerListItem extends Component { } PlayerListItem.propTypes = { - id: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, team: PropTypes.string.isRequired, position: PropTypes.string.isRequired, diff --git a/src/components/PlayerListItem.css b/src/components/player-list-item/PlayerListItem.module.scss old mode 100755 new mode 100644 similarity index 58% rename from src/components/PlayerListItem.css rename to src/components/player-list-item/PlayerListItem.module.scss index 4ed1654..d8f2c07 --- a/src/components/PlayerListItem.css +++ b/src/components/player-list-item/PlayerListItem.module.scss @@ -1,4 +1,4 @@ -:local(.playerListItem) { +.playerListItem { list-style: none; padding: 20px 10px 20px 20px; background-color: white; @@ -6,22 +6,22 @@ display: flex; } -:local(.playerInfos) { +.playerInfos { flex: 1 0 auto; } -:local(.playerInfos span) { +.playerInfos span { font-weight: bold; } -:local(.playerActions) { +.playerActions { flex: 0 0 90px; } -:local(.btnAction), -:local(.btnAction):active, -:local(.btnAction):focus, -:local(.btnAction):hover { +.btnAction, +.btnAction:active, +.btnAction:focus, +.btnAction:hover { margin-right: 5px; color: #5c75b0; } diff --git a/src/components/player-list/PlayerList.js b/src/components/player-list/PlayerList.js new file mode 100644 index 0000000..35ce34b --- /dev/null +++ b/src/components/player-list/PlayerList.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import styles from './PlayerList.module.scss' +import Pagination from '../pagination' +import PlayerListItem from '../player-list-item/PlayerListItem' + +const initPagination = { + pageSize: 5, + pageNo: 1, +} + +class PlayerList extends Component { + constructor() { + super() + this.state = { + ...initPagination, + } + } + + getFilteredPlayer = (players) => { + const result = [] + const { showPosition } = this.props + const { pageNo, pageSize } = this.state + const currentLastPage = pageNo * pageSize + const filteredByPosition = players.filter( + (player) => player.position === showPosition || !showPosition + ) + + result.push(filteredByPosition.length) + + const filteredByPagination = filteredByPosition.filter( + (_player, index) => + index < currentLastPage && index >= currentLastPage - pageSize + ) + + result.push(filteredByPagination) + return result + } + + render() { + const { players } = this.props + const [total, filtered] = this.getFilteredPlayer(players) + return ( + + ) + } +} + +PlayerList.propTypes = { + players: PropTypes.array.isRequired, + actions: PropTypes.object.isRequired, +} + +export default PlayerList diff --git a/src/components/PlayerList.css b/src/components/player-list/PlayerList.module.scss old mode 100755 new mode 100644 similarity index 57% rename from src/components/PlayerList.css rename to src/components/player-list/PlayerList.module.scss index b8d6f00..5e9ecab --- a/src/components/PlayerList.css +++ b/src/components/player-list/PlayerList.module.scss @@ -1,4 +1,5 @@ -:local(.playerList) { +.playerList { padding-left: 0; margin-bottom: 0; + height: auto; } diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index b796fae..3df755b 100755 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -1,3 +1,4 @@ export const ADD_PLAYER = 'ADD_PLAYER'; export const STAR_PLAYER = 'STAR_PLAYER'; export const DELETE_PLAYER = 'DELETE_PLAYER'; +export const FILTER_PLAYER = 'FILTER_PLAYER' diff --git a/src/constants/posTypes.tsx b/src/constants/posTypes.tsx new file mode 100644 index 0000000..42f6d2c --- /dev/null +++ b/src/constants/posTypes.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +export const POS_TYPES = { + SF:'SF', + SG:'SG', + PF:'PF', + PG:'PG' +} + +export const Options = Object.keys(POS_TYPES).map((postion, index) => { + return ( + + ); + }); + \ No newline at end of file diff --git a/src/containers/PlayerListApp.css b/src/containers/PlayerListApp.css deleted file mode 100755 index f438bf1..0000000 --- a/src/containers/PlayerListApp.css +++ /dev/null @@ -1,24 +0,0 @@ -body { - background-color: #f4f3f0; - display: flex; - align-items: center; - justify-content: center; -} - -:local(.playerListApp) { - width: 540px; - margin-top: 10px; - padding-top: 18px; - background-color: #5c75b0; - border: 1px solid #e3e3e3; -} - -:local(.playerListApp h1) { - color: white; - font-size: 16px; - line-height: 20px; - margin-bottom: 10px; - margin-top: 0; - padding-left: 10px; - font-family: Helvetica; -} diff --git a/src/containers/PlayerListApp.js b/src/containers/PlayerListApp.js index 0e4bfa5..3966b74 100755 --- a/src/containers/PlayerListApp.js +++ b/src/containers/PlayerListApp.js @@ -1,16 +1,25 @@ -import React, { Component } from 'react'; -import styles from './PlayerListApp.css'; -import { connect } from 'react-redux'; +import React, { Component } from "react"; +import styles from "./PlayerListApp.module.scss"; +import { connect } from "react-redux"; -import { addPlayer, deletePlayer, starPlayer } from '../actions/PlayersActions'; -import { PlayerList, AddPlayerInput } from '../components'; +import { + addPlayer, + deletePlayer, + starPlayer, + filterPlayer, +} from "../actions/PlayersActions"; +import { PlayerList, AddPlayerInput } from "../components"; +import { Options } from "../constants/posTypes"; class PlayerListApp extends Component { + handleOnPostionChange = (e) => { + this.props.filterPlayer(e.target.value); + }; + render() { const { - playerlist: { playersById }, + playerlist: { playersById, showPosition }, } = this.props; - const actions = { addPlayer: this.props.addPlayer, deletePlayer: this.props.deletePlayer, @@ -19,9 +28,19 @@ class PlayerListApp extends Component { return (
-

NBA Players

+
+

NBA Players

+ +
- +
); } @@ -31,11 +50,9 @@ function mapStateToProps(state) { return state; } -export default connect( - mapStateToProps, - { - addPlayer, - deletePlayer, - starPlayer, - }, -)(PlayerListApp); +export default connect(mapStateToProps, { + addPlayer, + deletePlayer, + starPlayer, + filterPlayer, +})(PlayerListApp); diff --git a/src/containers/PlayerListApp.module.scss b/src/containers/PlayerListApp.module.scss new file mode 100644 index 0000000..a96cf24 --- /dev/null +++ b/src/containers/PlayerListApp.module.scss @@ -0,0 +1,24 @@ +body { + background-color: #f4f3f0; + display: flex; + align-items: center; + justify-content: center; + + .playerListApp { + width: 540px; + margin-top: 10px; + padding-top: 18px; + background-color: #5c75b0; + border: 1px solid #e3e3e3; + + h1 { + color: white; + font-size: 16px; + line-height: 20px; + margin-bottom: 10px; + margin-top: 0; + padding-left: 10px; + font-family: Helvetica; + } + } +} diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/reducers/initData.ts b/src/reducers/initData.ts new file mode 100644 index 0000000..08adafb --- /dev/null +++ b/src/reducers/initData.ts @@ -0,0 +1,48 @@ +import { POS_TYPES } from "../constants/posTypes"; +export const initialState = { + playersById: [ + { + id:'asjdlk1jlkjd1i1j', + name: 'LeBron James', + team: 'LOS ANGELES LAKERS', + position: POS_TYPES.SF, + starred: true, + }, + { + id:'lksajdkjdadshasdsjlk', + name: 'Kevin Duran', + team: 'GOLDEN STATE WARRIORS', + position: POS_TYPES.SF, + starred: false, + }, + { + id:'asi12e12klj313oiu12u31o2ij3kl', + name: 'Anthony Davis', + team: 'NEW ORLEANS PELICANS', + position: POS_TYPES.PF, + starred: false, + }, + { + id:'79h97hu8youij80uioiuo68ty', + name: 'Stephen Curry', + team: 'GOLDEN STATE WARRIORS', + position: POS_TYPES.PG, + starred: false, + }, + { + id:'812d8j182oud2186diweuhfw7832y', + name: 'James Harden', + team: 'HOUSTON ROCKETS', + position: POS_TYPES.SG, + starred: false, + }, + { + id:'s8dusdasdnj1829218712', + name: 'Kawhi Leonard', + team: 'TORONTO RAPTORS', + position: POS_TYPES.SF, + starred: false, + }, + ], + }; + \ No newline at end of file diff --git a/src/reducers/playerlist.js b/src/reducers/playerlist.js index 1bc7457..fb55cc2 100755 --- a/src/reducers/playerlist.js +++ b/src/reducers/playerlist.js @@ -1,45 +1,7 @@ -import * as types from '../constants/ActionTypes'; - -const initialState = { - playersById: [ - { - name: 'LeBron James', - team: 'LOS ANGELES LAKERS', - position: 'SF', - starred: true, - }, - { - name: 'Kevin Duran', - team: 'GOLDEN STATE WARRIORS', - position: 'SF', - starred: false, - }, - { - name: 'Anthony Davis', - team: 'NEW ORLEANS PELICANS', - position: 'PF', - starred: false, - }, - { - name: 'Stephen Curry', - team: 'GOLDEN STATE WARRIORS', - position: 'PG', - starred: false, - }, - { - name: 'James Harden', - team: 'HOUSTON ROCKETS', - position: 'SG', - starred: false, - }, - { - name: 'Kawhi Leonard', - team: 'TORONTO RAPTORS', - position: 'SF', - starred: false, - }, - ], -}; +import { v4 as uuidv4 } from 'uuid' +import * as types from '../constants/ActionTypes' +import { POS_TYPES } from '../constants/posTypes' +import { initialState } from './initData' export default function players(state = initialState, action) { switch (action.type) { @@ -49,29 +11,35 @@ export default function players(state = initialState, action) { playersById: [ ...state.playersById, { - name: action.name, + id: uuidv4(), + name: action.newPlayer.name, team: 'LOS ANGELES LAKERS', - position: 'SF', + position: action.newPlayer.position || POS_TYPES.SF, + starred: false, }, ], - }; + } case types.DELETE_PLAYER: return { ...state, - playersById: state.playersById.filter( - (item, index) => index !== action.id, - ), - }; + playersById: state.playersById.filter((item) => item.id !== action.id), + } case types.STAR_PLAYER: - let players = [...state.playersById]; - let player = players.find((item, index) => index === action.id); - player.starred = !player.starred; + let players = [...state.playersById] + let player = players.find((item) => item.id === action.id) + player.starred = !player.starred return { ...state, playersById: players, - }; + } + + case types.FILTER_PLAYER: + return { + ...state, + showPosition: action.position, + } default: - return state; + return state } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f2850b7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": [ + "src" + ] +}