mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
adding a step 9 to call services
This commit is contained in:
78
package-lock.json
generated
78
package-lock.json
generated
@@ -2170,6 +2170,11 @@
|
||||
"integrity": "sha512-wv7IRqCGsL7WGKB8gPvrl+++HlFM9kxAM6jL1EXNPNTshEJYilMkbfS2SnuHha77uosp/YVK0wAp2jmlBzn1tg==",
|
||||
"dev": true
|
||||
},
|
||||
"curriable": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/curriable/-/curriable-1.2.5.tgz",
|
||||
"integrity": "sha512-hQwrkCn8DNiCw5CG8OS0td2wfpCDtxo1dmrVYxCDWUBHQPkpAvN9RqBVbmC64oSQaBqPQD2SOCXcTWH1zXe2mA=="
|
||||
},
|
||||
"cyclist": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz",
|
||||
@@ -3122,6 +3127,11 @@
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||
"dev": true
|
||||
},
|
||||
"fast-equals": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-1.6.2.tgz",
|
||||
"integrity": "sha512-6FiKwHkrHIqdvo9I92yzCR/zuWE6iy5ldcOszStPbmo7Zzj3OoVeng++GE//JqO4i6JQ4vH/BVAfCmUCki+C3g=="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
@@ -4518,6 +4528,14 @@
|
||||
"postcss": "^7.0.5"
|
||||
}
|
||||
},
|
||||
"identitate": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/identitate/-/identitate-1.0.1.tgz",
|
||||
"integrity": "sha512-xnDJ0JYhiZjBDuJRKbHoVzj5yP9FhATxLyUYswQyPdnJrwzGVBqS6DOmvKJi1lk7P+4dkL+hhUhuOZIcOUtG5A==",
|
||||
"requires": {
|
||||
"pathington": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
|
||||
@@ -6376,8 +6394,7 @@
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||
"dev": true
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"json3": {
|
||||
"version": "3.3.2",
|
||||
@@ -7626,6 +7643,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathington": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/pathington/-/pathington-1.1.7.tgz",
|
||||
"integrity": "sha512-JxzhUzagDfNIOm4qqwQqP3rWeo7rNNOfIahy4n+3GTEdwXLqw5cJHUR0soSopQtNEv763lzxb6eA2xBllpR8zw=="
|
||||
},
|
||||
"pbkdf2": {
|
||||
"version": "3.0.17",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
|
||||
@@ -8168,6 +8190,33 @@
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"redux-devtools-extension": {
|
||||
"version": "2.13.8",
|
||||
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
|
||||
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg=="
|
||||
},
|
||||
"redux-immutable-state-invariant": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz",
|
||||
"integrity": "sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==",
|
||||
"requires": {
|
||||
"invariant": "^2.1.0",
|
||||
"json-stringify-safe": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"redux-starter-kit": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/redux-starter-kit/-/redux-starter-kit-0.4.3.tgz",
|
||||
"integrity": "sha512-T3HXtwEGyFLCy63QlZEM95rDzTQ5BKbBhhTINNVFD1yWgHBKtgN3tXw4/sX9+Ve1Y8mAtXbAhep/E7I0YVvZPg==",
|
||||
"requires": {
|
||||
"immer": "^1.10.5",
|
||||
"redux": "^4.0.0",
|
||||
"redux-devtools-extension": "^2.13.7",
|
||||
"redux-immutable-state-invariant": "^2.1.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"selectorator": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||
@@ -8386,6 +8435,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
|
||||
@@ -8533,6 +8587,17 @@
|
||||
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=",
|
||||
"dev": true
|
||||
},
|
||||
"selectorator": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/selectorator/-/selectorator-4.0.3.tgz",
|
||||
"integrity": "sha512-A8+paRhzTab4Qm/38RAVnCgEZFbpn5xIWLyTCDqvyU3Obhmo94RS6UK1H00bVH7+U609sOhqbFJha09POsWURA==",
|
||||
"requires": {
|
||||
"fast-equals": "^1.2.1",
|
||||
"identitate": "^1.0.0",
|
||||
"reselect": "^4.0.0",
|
||||
"unchanged": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"selfsigned": {
|
||||
"version": "1.10.4",
|
||||
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.4.tgz",
|
||||
@@ -9754,6 +9819,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"unchanged": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unchanged/-/unchanged-2.1.0.tgz",
|
||||
"integrity": "sha512-CYVU0Mz35N821Pjxf+iG6JMPx/Yb6wStUq8uuhR52/+AU3vMiueOTrJ6u1vBKYSke7a75FAi2/jCdQkXcHaNGQ==",
|
||||
"requires": {
|
||||
"curriable": "^1.2.4",
|
||||
"pathington": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"undefsafe": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"react-dom": "^16.7.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"redux-starter-kit": "^0.4.3",
|
||||
"redux": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export const addTodo = (label: string) => ({ type: 'addTodo', label });
|
||||
export const remove = (id: string) => ({ type: 'remove', id });
|
||||
export const complete = (id: string) => ({ type: 'complete', id });
|
||||
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' })
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { reducer } from './reducers';
|
||||
import { createStore, compose } from 'redux';
|
||||
import { addTodo } from './actions';
|
||||
import { actions } from './actions';
|
||||
|
||||
/* Goop for making the Redux dev tool to work */
|
||||
declare var window: any;
|
||||
@@ -13,7 +13,7 @@ const store = createStoreWithDevTool(reducer, {});
|
||||
|
||||
console.log(store.getState());
|
||||
|
||||
store.dispatch(addTodo('hello'));
|
||||
store.dispatch(addTodo('world'));
|
||||
store.dispatch(actions.addTodo('hello'));
|
||||
store.dispatch(actions.addTodo('world'));
|
||||
|
||||
console.log(store.getState());
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import { Store } from '../store';
|
||||
import { addTodo, remove, complete } from './pureFunctions';
|
||||
import { addTodo, remove, complete, clear } from './pureFunctions';
|
||||
|
||||
export function reducer(state: Store, action: any): Store {
|
||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
||||
switch (action.type) {
|
||||
case 'addTodo':
|
||||
return addTodo(state, action.label);
|
||||
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;
|
||||
}
|
||||
|
||||
export function reducer(state: Store, action: any): Store {
|
||||
return {
|
||||
todos: todoReducer(state.todos, action),
|
||||
filter: 'all'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import { addTodo } from './pureFunctions';
|
||||
import { addTodo, complete } from './pureFunctions';
|
||||
import { Store } from '../store';
|
||||
|
||||
describe('TodoApp reducers', () => {
|
||||
it('can add an item', () => {
|
||||
const state = <Store>{
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
};
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
const newState = addTodo(state, 'item1');
|
||||
const newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const keys = Object.keys(newState.todos);
|
||||
const keys = Object.keys(newState);
|
||||
|
||||
expect(newState).not.toBe(state);
|
||||
expect(keys.length).toBe(1);
|
||||
expect(newState.todos[keys[0]].label).toBe('item1');
|
||||
expect(newState.todos[keys[0]].completed).toBeFalsy();
|
||||
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,34 +1,36 @@
|
||||
import { Store } from '../store';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export function addTodo(state: Store, label: string): Store {
|
||||
const { todos } = state;
|
||||
const id = index++;
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
};
|
||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { label, completed: false } };
|
||||
}
|
||||
|
||||
export function remove(state: Store, id: string) {
|
||||
const newTodos = { ...state.todos };
|
||||
export function remove(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
delete newTodos[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos
|
||||
};
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function complete(state: Store, id: string) {
|
||||
const newTodos = { ...state.todos };
|
||||
export function complete(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
newTodos[id].completed = !newTodos[id].completed;
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos
|
||||
};
|
||||
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,4 +1,8 @@
|
||||
export const addTodo = (label: string) => ({ type: 'addTodo', label });
|
||||
export const remove = (id: string) => ({ type: 'remove', id });
|
||||
export const complete = (id: string) => ({ type: 'complete', id });
|
||||
export const clear = () => ({ type: 'clear' });
|
||||
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' })
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Stack } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { DefaultButton } from 'office-ui-fabric-react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 * as actions from '../actions';
|
||||
import { TextField, PrimaryButton } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
addTodo: (label: string) => void;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
|
||||
interface TodoListProps {
|
||||
todos: Store['todos'];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||
import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoListItemProps {
|
||||
id: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { reducer } from './reducers';
|
||||
import { createStore, compose } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { TodoApp } from './components/TodoApp';
|
||||
import { addTodo } from './actions';
|
||||
import { actions } from './actions';
|
||||
import { initializeIcons } from '@uifabric/icons';
|
||||
|
||||
/* Goop for making the Redux dev tool to work */
|
||||
@@ -16,8 +16,8 @@ function createStoreWithDevTool(reducer, initialStore) {
|
||||
|
||||
const store = createStoreWithDevTool(reducer, {});
|
||||
|
||||
store.dispatch(addTodo('hello'));
|
||||
store.dispatch(addTodo('world'));
|
||||
store.dispatch(actions.addTodo('hello'));
|
||||
store.dispatch(actions.addTodo('world'));
|
||||
|
||||
initializeIcons();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Store } from '../store';
|
||||
import { addTodo, remove, complete, clear } from './pureFunctions';
|
||||
|
||||
export function reducer(state: Store, action: any): Store {
|
||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
||||
switch (action.type) {
|
||||
case 'addTodo':
|
||||
return addTodo(state, action.label);
|
||||
return addTodo(state, action.id, action.label);
|
||||
|
||||
case 'remove':
|
||||
return remove(state, action.id);
|
||||
@@ -18,3 +18,10 @@ export function reducer(state: Store, action: any): Store {
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function reducer(state: Store, action: any): Store {
|
||||
return {
|
||||
todos: todoReducer(state.todos, action),
|
||||
filter: 'all'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,33 +3,27 @@ import { Store } from '../store';
|
||||
|
||||
describe('TodoApp reducers', () => {
|
||||
it('can add an item', () => {
|
||||
const state = <Store>{
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
};
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
const newState = addTodo(state, 'item1');
|
||||
const newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const keys = Object.keys(newState.todos);
|
||||
const keys = Object.keys(newState);
|
||||
|
||||
expect(newState).not.toBe(state);
|
||||
expect(keys.length).toBe(1);
|
||||
expect(newState.todos[keys[0]].label).toBe('item1');
|
||||
expect(newState.todos[keys[0]].completed).toBeFalsy();
|
||||
expect(newState[keys[0]].label).toBe('item1');
|
||||
expect(newState[keys[0]].completed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can complete an item', () => {
|
||||
const state = <Store>{
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
};
|
||||
const state = <Store['todos']>{};
|
||||
|
||||
let newState = addTodo(state, 'item1');
|
||||
let newState = addTodo(state, '0', 'item1');
|
||||
|
||||
const key = Object.keys(newState.todos)[0];
|
||||
const key = Object.keys(newState)[0];
|
||||
|
||||
newState = complete(newState, key);
|
||||
|
||||
expect(newState.todos[key].completed).toBeTruthy();
|
||||
expect(newState[key].completed).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,49 +1,36 @@
|
||||
import { Store } from '../store';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export function addTodo(state: Store, label: string): Store {
|
||||
const { todos } = state;
|
||||
const id = index++;
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
};
|
||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { label, completed: false } };
|
||||
}
|
||||
|
||||
export function remove(state: Store, id: string) {
|
||||
const newTodos = { ...state.todos };
|
||||
export function remove(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
delete newTodos[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos
|
||||
};
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function complete(state: Store, id: string) {
|
||||
const newTodos = { ...state.todos };
|
||||
export function complete(state: Store['todos'], id: string) {
|
||||
const newTodos = { ...state };
|
||||
newTodos[id].completed = !newTodos[id].completed;
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos
|
||||
};
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function clear(state: Store) {
|
||||
const newTodos = {};
|
||||
export function clear(state: Store['todos']) {
|
||||
const newTodos = { ...state };
|
||||
|
||||
Object.keys(state.todos).forEach(key => {
|
||||
if (!state.todos[key].completed) {
|
||||
newTodos[key] = state.todos[key];
|
||||
if (state.todos[key].completed) {
|
||||
delete newTodos[key];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos
|
||||
};
|
||||
return newTodos;
|
||||
}
|
||||
|
||||
export function setFilter(state: Store['filter'], filter: FilterTypes) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const addTodo = (label: string) => ({ type: 'addTodo', label });
|
||||
export const remove = (id: string) => ({ type: 'remove', id });
|
||||
export const complete = (id: string) => ({ type: 'complete', id });
|
||||
export const clear = () => ({ type: 'clear' });
|
||||
export const setFilter = (filter: string) => ({ type: 'setFilter', filter });
|
||||
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 })
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Stack } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { DefaultButton } from 'office-ui-fabric-react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 * as actions from '../actions';
|
||||
import { actions } from '../actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Stack } from 'office-ui-fabric-react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { Store, FilterTypes } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
|
||||
interface TodoListProps {
|
||||
todos: Store['todos'];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react';
|
||||
import { Stack, Checkbox, IconButton } from 'office-ui-fabric-react';
|
||||
import { Store } from '../store';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import { actions } from '../actions';
|
||||
|
||||
interface TodoListItemProps {
|
||||
id: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { reducer } from './reducers';
|
||||
import { createStore, compose } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { TodoApp } from './components/TodoApp';
|
||||
import { addTodo } from './actions';
|
||||
import { actions } from './actions';
|
||||
import { initializeIcons } from '@uifabric/icons';
|
||||
import { Store } from './store';
|
||||
|
||||
@@ -17,8 +17,8 @@ function createStoreWithDevTool(reducer, initialStore?: Store) {
|
||||
|
||||
const store = createStoreWithDevTool(reducer);
|
||||
|
||||
store.dispatch(addTodo('hello'));
|
||||
store.dispatch(addTodo('world'));
|
||||
store.dispatch(actions.addTodo('hello'));
|
||||
store.dispatch(actions.addTodo('world'));
|
||||
|
||||
initializeIcons();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { combineReducers } from 'redux';
|
||||
function todoReducer(state: Store['todos'] = {}, action: any): Store['todos'] {
|
||||
switch (action.type) {
|
||||
case 'addTodo':
|
||||
return addTodo(state, action.label);
|
||||
return addTodo(state, action.id, action.label);
|
||||
|
||||
case 'remove':
|
||||
return remove(state, action.id);
|
||||
|
||||
29
step2-08/src/reducers/pureFunctions.spec.ts
Normal file
29
step2-08/src/reducers/pureFunctions.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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,9 +1,6 @@
|
||||
import { Store, FilterTypes } from '../store';
|
||||
|
||||
let index = 0;
|
||||
|
||||
export function addTodo(state: Store['todos'], label: string): Store['todos'] {
|
||||
const id = index++;
|
||||
export function addTodo(state: Store['todos'], id: string, label: string): Store['todos'] {
|
||||
return { ...state, [id]: { label, completed: false } };
|
||||
}
|
||||
|
||||
|
||||
3
step2-09/README.md
Normal file
3
step2-09/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Step 2.9
|
||||
|
||||
actions with service calls
|
||||
6
step2-09/index.html
Normal file
6
step2-09/index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
43
step2-09/src/actions/index.ts
Normal file
43
step2-09/src/actions/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
};
|
||||
36
step2-09/src/components/TodoApp.tsx
Normal file
36
step2-09/src/components/TodoApp.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
42
step2-09/src/components/TodoFooter.tsx
Normal file
42
step2-09/src/components/TodoFooter.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
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 };
|
||||
78
step2-09/src/components/TodoHeader.tsx
Normal file
78
step2-09/src/components/TodoHeader.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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 };
|
||||
40
step2-09/src/components/TodoList.tsx
Normal file
40
step2-09/src/components/TodoList.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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 };
|
||||
48
step2-09/src/components/TodoListItem.tsx
Normal file
48
step2-09/src/components/TodoListItem.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
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 };
|
||||
30
step2-09/src/index.tsx
Normal file
30
step2-09/src/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
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')
|
||||
);
|
||||
35
step2-09/src/reducers/index.ts
Normal file
35
step2-09/src/reducers/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
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
|
||||
});
|
||||
29
step2-09/src/reducers/pureFunctions.spec.ts
Normal file
29
step2-09/src/reducers/pureFunctions.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
40
step2-09/src/reducers/pureFunctions.ts
Normal file
40
step2-09/src/reducers/pureFunctions.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
}
|
||||
48
step2-09/src/service/index.ts
Normal file
48
step2-09/src/service/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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();
|
||||
}
|
||||
14
step2-09/src/store/index.ts
Normal file
14
step2-09/src/store/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
|
||||
export interface TodoItem {
|
||||
label: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface Store {
|
||||
todos: {
|
||||
[id: string]: TodoItem;
|
||||
};
|
||||
|
||||
filter: FilterTypes;
|
||||
}
|
||||
Reference in New Issue
Block a user