mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
fixing files
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,5 +2,5 @@
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="../../step2-06/demo/dist/step2-06/demo.js"></script><script type="text/javascript" src="../../step2-06/demo/step2-06/demo.js"></script></body>
|
||||
<script type="text/javascript" src="../../step2-06/demo/step2-06/demo.js"></script></body>
|
||||
</html>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="../../step2-06/exercise/dist/step2-06/exercise.js"></script><script type="text/javascript" src="../../step2-06/exercise/step2-06/exercise.js"></script></body>
|
||||
<script type="text/javascript" src="../../step2-06/exercise/step2-06/exercise.js"></script></body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="../step2-08/step2-08.js"></script></body>
|
||||
</html>
|
||||
@@ -1,9 +0,0 @@
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
export const actions = {
|
||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
||||
remove: (id: string) => ({ type: 'remove', id }),
|
||||
complete: (id: string) => ({ type: 'complete', id }),
|
||||
clear: () => ({ type: 'clear' }),
|
||||
setFilter: (filter: string) => ({ type: 'setFilter', filter })
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Customizer, mergeStyles, getTheme } from 'office-ui-fabric-react';
|
||||
import { TodoFooter } from './TodoFooter';
|
||||
import { TodoHeader } from './TodoHeader';
|
||||
import { TodoList } from './TodoList';
|
||||
import { Store } from '../store';
|
||||
import { FluentCustomizations } from '@uifabric/fluent-theme';
|
||||
|
||||
const className = mergeStyles({
|
||||
padding: 25,
|
||||
...getTheme().effects.elevation4
|
||||
});
|
||||
|
||||
export const TodoApp = () => {
|
||||
return (
|
||||
<Customizer {...FluentCustomizations}>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack style={{ width: 400 }} gap={25} className={className}>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Customizer>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@uifabric/experiments';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { DefaultButton } from 'office-ui-fabric-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
todos: Store['todos'];
|
||||
}
|
||||
|
||||
const TodoFooter = (props: TodoFooterProps) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
|
||||
return (
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text>
|
||||
{itemCount} item{itemCount > 1 ? 's' : ''} left
|
||||
</Text>
|
||||
<DefaultButton onClick={() => props.clear()}>Clear Completed</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
clear: () => dispatch(actions.clear())
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoFooter);
|
||||
|
||||
export { component as TodoFooter };
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@uifabric/experiments';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import { FilterTypes, Store } from '../store';
|
||||
import { actions } from '../actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
addTodo: (label: string) => void;
|
||||
setFilter: (filter: FilterTypes) => void;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
|
||||
interface TodoHeaderState {
|
||||
labelInput: string;
|
||||
}
|
||||
|
||||
class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
||||
constructor(props: TodoHeaderProps) {
|
||||
super(props);
|
||||
this.state = { labelInput: undefined };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Stack gap={10}>
|
||||
<Stack horizontal horizontalAlign="center">
|
||||
<Text variant="xxLarge">todos</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal gap={10}>
|
||||
<Stack.Item grow>
|
||||
<TextField placeholder="What needs to be done?" value={this.state.labelInput} onChange={this.onChange} />
|
||||
</Stack.Item>
|
||||
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
<Pivot onLinkClick={this.onFilter}>
|
||||
<PivotItem headerText="all" />
|
||||
<PivotItem headerText="active" />
|
||||
<PivotItem headerText="completed" />
|
||||
</Pivot>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: undefined });
|
||||
};
|
||||
|
||||
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||
this.setState({ labelInput: newValue });
|
||||
};
|
||||
|
||||
private onFilter = (item: PivotItem) => {
|
||||
this.props.setFilter(item.props.headerText as FilterTypes);
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
addTodo: (label: string) => dispatch(actions.addTodo(label)),
|
||||
setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter))
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoHeader);
|
||||
|
||||
export { component as TodoHeader };
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface TodoListProps {
|
||||
todos: Store['todos'];
|
||||
filter: FilterTypes;
|
||||
}
|
||||
|
||||
const TodoList = (props: TodoListProps) => {
|
||||
const { filter, todos } = props;
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={10}>
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoList);
|
||||
|
||||
export { component as TodoList };
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoListItemProps {
|
||||
id: string;
|
||||
todos: Store['todos'];
|
||||
remove: (id: string) => void;
|
||||
complete: (id: string) => void;
|
||||
}
|
||||
|
||||
class TodoListItem extends React.Component<TodoListItemProps, {}> {
|
||||
render() {
|
||||
const { todos, id, complete, remove } = this.props;
|
||||
const item = todos[id];
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
|
||||
<div>
|
||||
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => remove(id)} />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps({ todos }: Store) {
|
||||
return {
|
||||
todos
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
remove: (id: string) => dispatch(actions.remove(id)),
|
||||
complete: (id: string) => dispatch(actions.complete(id))
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoListItem);
|
||||
|
||||
export { component as TodoListItem };
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { reducer } from './reducers';
|
||||
import { createStore, compose } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { TodoApp } from './components/TodoApp';
|
||||
import { actions } from './actions';
|
||||
import { initializeIcons } from '@uifabric/icons';
|
||||
import { Store } from './store';
|
||||
|
||||
/* Goop for making the Redux dev tool to work */
|
||||
declare var window: any;
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
function createStoreWithDevTool(reducer, initialStore?: Store) {
|
||||
return createStore(reducer, initialStore, composeEnhancers());
|
||||
}
|
||||
|
||||
const store = createStoreWithDevTool(reducer);
|
||||
|
||||
store.dispatch(actions.addTodo('hello'));
|
||||
store.dispatch(actions.addTodo('world'));
|
||||
|
||||
initializeIcons();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<TodoApp />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Store } from '../store';
|
||||
import { addTodo, remove, complete, clear, setFilter } from './pureFunctions';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
||||
switch (action.type) {
|
||||
case 'addTodo':
|
||||
return addTodo(state, action.id, action.label);
|
||||
|
||||
case 'remove':
|
||||
return remove(state, action.id);
|
||||
|
||||
case 'clear':
|
||||
return clear(state);
|
||||
|
||||
case 'complete':
|
||||
return complete(state, action.id);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] {
|
||||
switch (action.type) {
|
||||
case 'setFilter':
|
||||
return setFilter(state, action.filter);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const reducer = combineReducers({
|
||||
todos: todoReducer,
|
||||
filter: filterReducer
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { addTodo, complete } from './pureFunctions';
|
||||
import { Store } from '../store';
|
||||
|
||||
describe('TodoApp reducers', () => {
|
||||
it('can add an item', () => {
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
const newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const keys = Object.keys(newState);
|
||||
|
||||
expect(newState).not.toBe(state);
|
||||
expect(keys.length).toBe(1);
|
||||
expect(newState[keys[0]].label).toBe('item1');
|
||||
expect(newState[keys[0]].completed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can complete an item', () => {
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
let newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const key = Object.keys(newState)[0];
|
||||
|
||||
newState = complete(newState, key);
|
||||
|
||||
expect(newState[key].completed).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Store, FilterTypes } from '../store';
|
||||
|
||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { label, completed: false } };
|
||||
}
|
||||
|
||||
export function remove(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
delete newTodos[id];
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function complete(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
newTodos[id].completed = !newTodos[id].completed;
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function clear(state: Store['todos']) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
Object.keys(state.todos).forEach(key => {
|
||||
if (state.todos[key].completed) {
|
||||
delete newTodos[key];
|
||||
}
|
||||
});
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function setFilter(state: Store['filter'], filter: FilterTypes) {
|
||||
return filter;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
|
||||
export interface TodoItem {
|
||||
label: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface Store {
|
||||
todos: {
|
||||
[id: string]: TodoItem;
|
||||
};
|
||||
|
||||
filter: FilterTypes;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="../step2-09/step2-09.js"></script></body>
|
||||
</html>
|
||||
@@ -1,43 +0,0 @@
|
||||
import uuid from 'uuid/v4';
|
||||
import { Store } from '../store';
|
||||
import * as service from '../service';
|
||||
|
||||
export const actions = {
|
||||
addTodo: (label: string) => ({ type: 'addTodo', id: uuid(), label }),
|
||||
remove: (id: string) => ({ type: 'remove', id }),
|
||||
complete: (id: string) => ({ type: 'complete', id }),
|
||||
clear: () => ({ type: 'clear' }),
|
||||
setFilter: (filter: string) => ({ type: 'setFilter', filter })
|
||||
};
|
||||
|
||||
export const actionsWithService = {
|
||||
addTodo: (label: string) => {
|
||||
return async (dispatch: any, getState: () => Store) => {
|
||||
const addAction = actions.addTodo(label);
|
||||
const id = addAction.id;
|
||||
dispatch(addAction);
|
||||
await service.add(id, getState().todos[id]);
|
||||
};
|
||||
},
|
||||
|
||||
remove: (id: string) => {
|
||||
return async (dispatch: any, getState: () => Store) => {
|
||||
dispatch(actions.remove(id));
|
||||
await service.remove(id);
|
||||
};
|
||||
},
|
||||
|
||||
complete: (id: string) => {
|
||||
return async (dispatch: any, getState: () => Store) => {
|
||||
dispatch(actions.complete(id));
|
||||
await service.edit(id, getState().todos[id]);
|
||||
};
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
return async (dispatch: any, getState: () => Store) => {
|
||||
dispatch(actions.clear());
|
||||
await service.editBulk(getState().todos);
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Customizer, mergeStyles, getTheme } from 'office-ui-fabric-react';
|
||||
import { TodoFooter } from './TodoFooter';
|
||||
import { TodoHeader } from './TodoHeader';
|
||||
import { TodoList } from './TodoList';
|
||||
import { Store } from '../store';
|
||||
import { FluentCustomizations } from '@uifabric/fluent-theme';
|
||||
|
||||
const className = mergeStyles({
|
||||
padding: 25,
|
||||
...getTheme().effects.elevation4
|
||||
});
|
||||
|
||||
export class TodoApp extends React.Component<any, Store> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<Customizer {...FluentCustomizations}>
|
||||
<Stack horizontalAlign="center">
|
||||
<Stack style={{ width: 400 }} gap={25} className={className}>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Customizer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@uifabric/experiments';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { DefaultButton } from 'office-ui-fabric-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { actionsWithService } from '../actions';
|
||||
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
todos: Store['todos'];
|
||||
}
|
||||
|
||||
const TodoFooter = (props: TodoFooterProps) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
|
||||
return (
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text>
|
||||
{itemCount} item{itemCount > 1 ? 's' : ''} left
|
||||
</Text>
|
||||
<DefaultButton onClick={() => props.clear()}>Clear Completed</DefaultButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
clear: () => dispatch(actionsWithService.clear())
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoFooter);
|
||||
|
||||
export { component as TodoFooter };
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@uifabric/experiments';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import { FilterTypes, Store } from '../store';
|
||||
import { actionsWithService, actions } from '../actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
addTodo: (label: string) => void;
|
||||
setFilter: (filter: FilterTypes) => void;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
|
||||
interface TodoHeaderState {
|
||||
labelInput: string;
|
||||
}
|
||||
|
||||
class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
||||
constructor(props: TodoHeaderProps) {
|
||||
super(props);
|
||||
this.state = { labelInput: undefined };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Stack gap={10}>
|
||||
<Stack horizontal horizontalAlign="center">
|
||||
<Text variant="xxLarge">todos</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal gap={10}>
|
||||
<Stack.Item grow>
|
||||
<TextField placeholder="What needs to be done?" value={this.state.labelInput} onChange={this.onChange} />
|
||||
</Stack.Item>
|
||||
<PrimaryButton onClick={this.onAdd}>Add</PrimaryButton>
|
||||
</Stack>
|
||||
|
||||
<Pivot onLinkClick={this.onFilter}>
|
||||
<PivotItem headerText="all" />
|
||||
<PivotItem headerText="active" />
|
||||
<PivotItem headerText="completed" />
|
||||
</Pivot>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
private onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: undefined });
|
||||
};
|
||||
|
||||
private onChange = (evt: React.FormEvent<HTMLInputElement>, newValue: string) => {
|
||||
this.setState({ labelInput: newValue });
|
||||
};
|
||||
|
||||
private onFilter = (item: PivotItem) => {
|
||||
this.props.setFilter(item.props.headerText as FilterTypes);
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
addTodo: (label: string) => dispatch(actionsWithService.addTodo(label)),
|
||||
setFilter: (filter: FilterTypes) => dispatch(actions.setFilter(filter))
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoHeader);
|
||||
|
||||
export { component as TodoHeader };
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface TodoListProps {
|
||||
todos: Store['todos'];
|
||||
filter: FilterTypes;
|
||||
}
|
||||
|
||||
const TodoList = (props: TodoListProps) => {
|
||||
const { filter, todos } = props;
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={10}>
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: Store) {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoList);
|
||||
|
||||
export { component as TodoList };
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import { actionsWithService } from '../actions';
|
||||
|
||||
interface TodoListItemProps {
|
||||
id: string;
|
||||
todos: Store['todos'];
|
||||
remove: (id: string) => void;
|
||||
complete: (id: string) => void;
|
||||
}
|
||||
|
||||
class TodoListItem extends React.Component<TodoListItemProps, {}> {
|
||||
render() {
|
||||
const { todos, id, complete, remove } = this.props;
|
||||
const item = todos[id];
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center" horizontalAlign="space-between">
|
||||
<Checkbox label={item.label} checked={item.completed} onChange={() => complete(id)} />
|
||||
<div>
|
||||
<IconButton iconProps={{ iconName: 'Cancel' }} onClick={() => remove(id)} />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps({ todos }: Store) {
|
||||
return {
|
||||
todos
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
remove: (id: string) => dispatch(actionsWithService.remove(id)),
|
||||
complete: (id: string) => dispatch(actionsWithService.complete(id))
|
||||
};
|
||||
}
|
||||
|
||||
const component = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TodoListItem);
|
||||
|
||||
export { component as TodoListItem };
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { reducer } from './reducers';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
import { TodoApp } from './components/TodoApp';
|
||||
import { initializeIcons } from '@uifabric/icons';
|
||||
import { Store, FilterTypes } from './store';
|
||||
import * as service from './service';
|
||||
|
||||
/* Goop for making the Redux dev tool to work */
|
||||
declare var window: any;
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
function createStoreWithDevTool(reducer, initialStore?: Store) {
|
||||
return createStore(reducer, initialStore, composeEnhancers(applyMiddleware(thunk)));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const preloadStore = {
|
||||
todos: await service.getAll(),
|
||||
filter: 'all' as FilterTypes
|
||||
};
|
||||
|
||||
const store = createStoreWithDevTool(reducer, preloadStore);
|
||||
|
||||
initializeIcons();
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<TodoApp />
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
})();
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Store } from '../store';
|
||||
import { addTodo, remove, complete, clear, setFilter } from './pureFunctions';
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
||||
switch (action.type) {
|
||||
case 'addTodo':
|
||||
return addTodo(state, action.id, action.label);
|
||||
|
||||
case 'remove':
|
||||
return remove(state, action.id);
|
||||
|
||||
case 'clear':
|
||||
return clear(state);
|
||||
|
||||
case 'complete':
|
||||
return complete(state, action.id);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function filterReducer(state: Store['filter'] = 'all', action: any): Store['filter'] {
|
||||
switch (action.type) {
|
||||
case 'setFilter':
|
||||
return setFilter(state, action.filter);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const reducer = combineReducers({
|
||||
todos: todoReducer,
|
||||
filter: filterReducer
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { addTodo, complete } from './pureFunctions';
|
||||
import { Store } from '../store';
|
||||
|
||||
describe('TodoApp reducers', () => {
|
||||
it('can add an item', () => {
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
const newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const keys = Object.keys(newState);
|
||||
|
||||
expect(newState).not.toBe(state);
|
||||
expect(keys.length).toBe(1);
|
||||
expect(newState[keys[0]].label).toBe('item1');
|
||||
expect(newState[keys[0]].completed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can complete an item', () => {
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
let newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const key = Object.keys(newState)[0];
|
||||
|
||||
newState = complete(newState, key);
|
||||
|
||||
expect(newState[key].completed).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Store, FilterTypes } from '../store';
|
||||
|
||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { label, completed: false } };
|
||||
}
|
||||
|
||||
export function edit(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { ...state[id], label } };
|
||||
}
|
||||
|
||||
export function remove(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
delete newTodos[id];
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function complete(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
newTodos[id].completed = !newTodos[id].completed;
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function clear(state: Store['todos']) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
Object.keys(state.todos).forEach(key => {
|
||||
if (state.todos[key].completed) {
|
||||
delete newTodos[key];
|
||||
}
|
||||
});
|
||||
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function setFilter(state: Store['filter'], filter: FilterTypes) {
|
||||
return filter;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { TodoItem, Store } from '../store';
|
||||
const HOST = 'http://localhost:3000';
|
||||
|
||||
export async function add(id: string, todo: TodoItem) {
|
||||
const response = await fetch(`${HOST}/todos/${id}`, {
|
||||
method: 'post',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(todo)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function edit(id: string, todo: TodoItem) {
|
||||
const response = await fetch(`${HOST}/todos/${id}`, {
|
||||
method: 'put',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(todo)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function remove(id: string) {
|
||||
const response = await fetch(`${HOST}/todos/${id}`, {
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getAll() {
|
||||
const response = await fetch(`${HOST}/todos`, {
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function editBulk(todos: Store['todos']) {
|
||||
const response = await fetch(`${HOST}/todos`, {
|
||||
method: 'post',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(todos)
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
|
||||
export interface TodoItem {
|
||||
label: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface Store {
|
||||
todos: {
|
||||
[id: string]: TodoItem;
|
||||
};
|
||||
|
||||
filter: FilterTypes;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user