Skip to content
Open

XST #58

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Building a user friendly interface with React.js, Redux/Mobx and LESS/SCSS.

- Step1: clone this repository to your account.
- Step2: finish the following tasks.
- Step3: Send Pull Request to repository *bridge5/react-examination*.
- Step3: Send Pull Request to repository _bridge5/react-examination_.

## Tasks

- Please add pagination support to the list when there are more than *5* entries.
- Please add pagination support to the list when there are more than _5_ entries.
- Please add option to select position of a player SF/PG and display it.
- Please add tests using your preferred testing tool (jest, enzyme, mocha, ...).
- Please add some features that could help you show your personal abilities.
Expand All @@ -24,4 +24,41 @@ Building a user friendly interface with React.js, Redux/Mobx and LESS/SCSS.

- 第一步:克隆这个仓库到你自己的账号里。
- 第二步:完成下列任务。
- 第三步:发送 * Pull Request* 到仓库 *bridge5/react-examination*。
- 第三步:发送 _ Pull Request_ 到仓库 _bridge5/react-examination_。

## 修改

1. 启动项目报错,经排查是 webpack 内部 createHash 函数有问题,更新 react-scripts 包可解决
2. 整体思路
- 由于该功能类似于留言板功能,因此增删改查、分页等为必要功能。这里将 PlayerListApp 组件作为容器组件,负责逻辑处理
- 其他 添加、搜索、查看等 UI 组件负责渲染即可,不参与逻辑处理
- 将 store 当做数据层,数据层需要返回两个数据:需要显示的列表数据 showPlayersById 和 总数据量 total,分页、过滤等逻辑都应在数据层做处理
- 添加逻辑:当输入名称、定位、球队信息提交时,将数据传给 store,成功后清空输入框,并重新请求最新的列表数据
- 删除逻辑:当点击删除按钮时,需要将 id 传给 数据处理层,删除成功后请求最新的列表数据。(这里需要注意:如果此时列表数据为空 且 page > 1,则使用 page-1 重新请求列表数据)
- 查找逻辑:封装 useMount 处理第一次渲染的逻辑请求,封装 useUpdateEffect 和 useUpdateDebounceEffect,当重新渲染时处理其中的逻辑
- 分页器:按照 antd 用法封装的简易分页器
- 加入 form 必填检验,自动生成 id 等,筛选框支持过滤收藏的等
3. 样式修复
4. 这里面遇到的卡点:select 中的 onChange 触发时,获取到的 event.target.value 为空,经研究,跟 react 的版本有关系,16.12.0 版本应该用 e.nativeEvent.target.value 来取值
5. 用户体验优化:封装 useStorageState,该 hook 可用来缓存用户的查询参数,防止用户刷新或再次进入时查询数据丢失

## 自测用例

1. 添加球员
- 当输入球员姓名、位置、团队时,列表有无正常显示
- 当球员姓名为空时,应添加失败
2. 删除球员
- 当点击删除按钮,列表是否正常显示
- 先选择第二页,将第二页删完,此时是否重置到了第一页且正常显示
3. 查询列表
- 输入球员姓名、位置、团队时,列表有无正常过滤
- 当输入大小写时,列表有无正常过滤
- 当改变搜索条件时,分页是否重置并正常显示数据
- 当输入搜索条件后,刷新列表,搜索条件是否依然保留
4. 收藏球员
- 点击收藏按钮时,列表有无正常显示
- 点击取消收藏按钮时,列表有无正常显示

## 单元测试

由于单元测试只是了解,而且在写的过程中库报错,简单判断下是因为 react-dom 包变更所致,因此只写了一个 useFirstMount.test.js
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
"redux": "^4.0.5"
},
"devDependencies": {
"react-scripts": "3.4.0"
"react-scripts": "^5.0.1"
},
"browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"]
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
10 changes: 5 additions & 5 deletions src/App.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

it('renders without crashing', () => {
const div = document.createElement('div');
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
});
14 changes: 11 additions & 3 deletions src/actions/PlayersActions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as types from '../constants/ActionTypes';
import * as types from "../constants/ActionTypes";

export function addPlayer(name) {
export function addPlayer(playerInfo) {
return {
type: types.ADD_PLAYER,
name,
playerInfo,
};
}

Expand All @@ -20,3 +20,11 @@ export function starPlayer(id) {
id,
};
}

// 获取最新球员的信息
export const getLatestPlayerData = (params) => {
return {
type: types.GET_LATEST_PLAYER_LIST,
...params,
};
};
6 changes: 0 additions & 6 deletions src/components/AddPlayerInput.css

This file was deleted.

119 changes: 84 additions & 35 deletions src/components/AddPlayerInput.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,94 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import styles from './AddPlayerInput.css';
import React, { useState } from "react";
import classnames from "classnames";
import PropTypes from "prop-types";

class AddPlayerInput extends Component {
render() {
return (
<input
type="text"
autoFocus={true}
className={classnames('form-control', styles.addPlayerInput)}
placeholder="Type the name of a player"
value={this.state.name}
onChange={this.handleChange.bind(this)}
onKeyDown={this.handleSubmit.bind(this)}
/>
);
}
import { PLAYER_POSITION, PLAYER_TEAM } from "../constants";

constructor(props, context) {
super(props, context);
this.state = {
name: this.props.name || '',
};
}
import styles from "./AddPlayerInput.module.css";

handleChange(e) {
this.setState({ name: e.target.value });
}
const AddPlayerInput = ({ handleAddPlayer, addPlayerSuccessCallback }) => {
// 球员基本信息
const [playerInfo, setPlayerInfo] = useState({
name: "",
position: PLAYER_POSITION[0].value,
team: PLAYER_TEAM[0].value,
});

handleSubmit(e) {
const name = e.target.value.trim();
if (e.which === 13) {
this.props.addPlayer(name);
this.setState({ name: '' });
const handleSubmit = (e) => {
if (e.which === 13 && playerInfo?.name !== "") {
handleAddPlayer(playerInfo);
// 添加成功后初始化数据、并重新请求数据
setPlayerInfo({
name: "",
position: PLAYER_POSITION[0].value,
team: PLAYER_TEAM[0].value,
});
addPlayerSuccessCallback();
}
}
}
};

return (
<>
<h3>add player:</h3>
<div className={styles["add-player-layout"]}>
<span className={styles["label"]}>球员姓名:</span>
<input
type="text"
autoFocus={true}
className={classnames("form-control", styles.addPlayerInput)}
placeholder="请输入球员姓名"
value={playerInfo?.name}
onChange={(e) =>
setPlayerInfo({ ...playerInfo, name: e.target.value.trim() })
}
onKeyDown={handleSubmit}
/>
<span className={styles["label"]}>球员位置:</span>
<select
className={styles["select"]}
value={playerInfo?.position}
onChange={(e) => {
// 这里不能用 e.target.value
setPlayerInfo({
...playerInfo,
position: e.nativeEvent.target.value,
});
}}
onKeyDown={handleSubmit}
>
{PLAYER_POSITION.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<span className={styles["label"]}>所属团队:</span>
<select
className={styles["select"]}
value={playerInfo?.position}
onChange={(e) => {
// 这里不能用 e.target.value
setPlayerInfo({
...playerInfo,
team: e.nativeEvent.target.value,
});
}}
onKeyDown={handleSubmit}
>
{PLAYER_TEAM.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</>
);
};

AddPlayerInput.propTypes = {
addPlayer: PropTypes.func.isRequired,
handleAddPlayer: PropTypes.func.isRequired,
addPlayerSuccessCallback: PropTypes.func.isRequired,
};

export default AddPlayerInput;
32 changes: 32 additions & 0 deletions src/components/AddPlayerInput.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
h3 {
color: #fff;
font-size: 16px;
}

.add-player-layout {
display: flex;
align-items: center;
}

.label {
margin-left: 20px;
color: #e9eada;
}

.add-player-layout :first-child {
margin-left: 0;
}

.addPlayerInput {
width: 200px;
border-radius: 0;
border-color: #abaaaa;
border-left: 0;
border-right: 0;
}

.select {
width: 200px;
height: 34px;
margin: "0 20px";
}
28 changes: 28 additions & 0 deletions src/components/Pagination.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
align-items: center;
}

button {
padding: 8px;
margin: 0 5px;
border: 1px solid #ddd;
background-color: #f9f9f9;
cursor: pointer;
}

button:hover {
background-color: #ddd;
}

button:disabled {
color: #999;
cursor: not-allowed;
}

button.active {
background-color: #007bff;
color: #fff;
}
Loading