mirror of
https://github.com/microsoft/frontend-bootcamp.git
synced 2026-01-26 14:56:42 +08:00
Rewrite of Day 1 to use modern React (#294)
* update to hooks * more class to function * cleanup * finish ts final * update html lesson * add lessons page * clean up * move getters into context * adding type * fix bug * step 5 cleanup * init final pass * text tweak * fix ternaries * readme cleanup * fixed root readme
This commit is contained in:
@@ -2,67 +2,94 @@ import React from 'react';
|
||||
import { TodoFooter } from './components/TodoFooter';
|
||||
import { TodoHeader } from './components/TodoHeader';
|
||||
import { TodoList } from './components/TodoList';
|
||||
import { Todos, FilterTypes } from './TodoApp.types';
|
||||
import { Todo, Todos, FilterTypes, AppContextProps } from './TodoApp.types';
|
||||
|
||||
let index = 0;
|
||||
export const AppContext = React.createContext<AppContextProps>(undefined);
|
||||
|
||||
export class TodoApp extends React.Component<{}, { todos: Todos; filter: FilterTypes }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
todos: {},
|
||||
filter: 'all'
|
||||
const defaultTodos: Todos = [
|
||||
{
|
||||
id: '04',
|
||||
label: 'Todo 4',
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
label: 'Todo 3',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
label: 'Todo 2',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
label: 'Todo 1',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export const TodoApp = () => {
|
||||
const [filter, setFilter] = React.useState<FilterTypes>('all');
|
||||
const [todos, setTodos] = React.useState<Todos>(defaultTodos);
|
||||
|
||||
// TODO Convert to useReducer
|
||||
const addTodo = (label: string): void => {
|
||||
const getId = () => Date.now().toString();
|
||||
const newTodo: Todo = {
|
||||
id: getId(),
|
||||
label: label,
|
||||
status: 'active',
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, todos } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<TodoHeader addTodo={this._addTodo} setFilter={this._setFilter} filter={filter} />
|
||||
<TodoList complete={this._complete} todos={todos} filter={filter} />
|
||||
<TodoFooter clear={this._clear} todos={todos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _addTodo = label => {
|
||||
const { todos } = this.state;
|
||||
const id = index++;
|
||||
|
||||
this.setState({
|
||||
todos: { ...todos, [id]: { label, completed: false } }
|
||||
});
|
||||
setTodos([...todos, newTodo]);
|
||||
};
|
||||
|
||||
private _complete = id => {
|
||||
const { todos } = this.state;
|
||||
const todo = todos[id];
|
||||
const newTodos = { ...todos, [id]: { ...todo, completed: !todo.completed } };
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
};
|
||||
|
||||
private _clear = () => {
|
||||
const { todos } = this.state;
|
||||
const newTodos = {};
|
||||
|
||||
Object.keys(this.state.todos).forEach(id => {
|
||||
if (!todos[id].completed) {
|
||||
newTodos[id] = todos[id];
|
||||
const toggleCompleted = (id: string) => {
|
||||
const newTodos = todos.map((todo): Todo => {
|
||||
if (todo.id === id) {
|
||||
return { ...todo, status: todo.status === 'active' ? 'completed' : 'active' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
todos: newTodos
|
||||
});
|
||||
setTodos(newTodos);
|
||||
};
|
||||
|
||||
private _setFilter = filter => {
|
||||
this.setState({
|
||||
filter: filter
|
||||
const clearCompleted = () => {
|
||||
const updatedTodos = todos.map((todo): Todo => {
|
||||
if (todo.status === 'completed') {
|
||||
return { ...todo, status: 'cleared' };
|
||||
} else {
|
||||
return todo;
|
||||
}
|
||||
});
|
||||
setTodos(updatedTodos);
|
||||
};
|
||||
}
|
||||
|
||||
const changeFilter = (filter: FilterTypes) => {
|
||||
setFilter(filter);
|
||||
};
|
||||
|
||||
const getFilter = () => {
|
||||
return filter;
|
||||
}
|
||||
|
||||
const getTodos = () => {
|
||||
return todos;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
addTodo,
|
||||
toggleCompleted,
|
||||
clearCompleted,
|
||||
changeFilter,
|
||||
getFilter,
|
||||
getTodos
|
||||
}}>
|
||||
<TodoHeader />
|
||||
<TodoList />
|
||||
<TodoFooter />
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
export type FilterTypes = 'all' | 'active' | 'completed';
|
||||
export type TodoType = 'active' | 'completed' | 'cleared';
|
||||
|
||||
export interface TodoItem {
|
||||
export interface Todo {
|
||||
id: string;
|
||||
label: string;
|
||||
completed: boolean;
|
||||
status: TodoType;
|
||||
}
|
||||
|
||||
export interface Todos {
|
||||
[id: string]: TodoItem;
|
||||
export type Todos = Todo[];
|
||||
|
||||
|
||||
export interface AppContextProps {
|
||||
addTodo: (label: string) => void;
|
||||
toggleCompleted: (id: string) => void;
|
||||
clearCompleted: () => void;
|
||||
changeFilter: (filter: FilterTypes) => void;
|
||||
getFilter: () => FilterTypes;
|
||||
getTodos: () => Todos;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Todos } from '../TodoApp.types';
|
||||
interface TodoFooterProps {
|
||||
clear: () => void;
|
||||
todos: Todos;
|
||||
}
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
export const TodoFooter = (props: TodoFooterProps) => {
|
||||
const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
|
||||
const _onClick = () => {
|
||||
props.clear();
|
||||
export const TodoFooter = () => {
|
||||
const { clearCompleted, getTodos } = React.useContext(AppContext);
|
||||
|
||||
const itemCount = getTodos().filter((todo) => todo.status === 'active').length;
|
||||
const handleClick = () => {
|
||||
clearCompleted();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -16,7 +14,7 @@ export const TodoFooter = (props: TodoFooterProps) => {
|
||||
<span>
|
||||
{itemCount} item{itemCount === 1 ? '' : 's'} left
|
||||
</span>
|
||||
<button onClick={_onClick} className="submit">
|
||||
<button onClick={handleClick} className="submit">
|
||||
Clear Completed
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
@@ -1,58 +1,39 @@
|
||||
import React from 'react';
|
||||
import React, { ChangeEventHandler, MouseEventHandler, useState, useContext } from 'react';
|
||||
import { FilterTypes } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoHeaderProps {
|
||||
addTodo: (label: string) => void;
|
||||
setFilter: (filter: FilterTypes) => void;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
export const TodoHeader = () => {
|
||||
const { changeFilter, addTodo, getFilter } = useContext(AppContext);
|
||||
const [inputText, setInputText] = useState<string>('');
|
||||
|
||||
interface TodoHeaderState {
|
||||
labelInput: string;
|
||||
}
|
||||
|
||||
export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { labelInput: '' };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filter, setFilter } = this.props;
|
||||
return (
|
||||
<header>
|
||||
<h1>todos <small>(1.7 final)</small></h1>
|
||||
<div className="addTodo">
|
||||
<input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
|
||||
<button onClick={this._onAdd} className="submit">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button onClick={this._onFilter} className={filter === 'all' ? 'selected' : ''}>
|
||||
all
|
||||
</button>
|
||||
<button onClick={this._onFilter} className={filter === 'active' ? 'selected' : ''}>
|
||||
active
|
||||
</button>
|
||||
<button onClick={this._onFilter} className={filter === 'completed' ? 'selected' : ''}>
|
||||
completed
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
_onFilter = evt => {
|
||||
this.props.setFilter(evt.target.innerText);
|
||||
const onInput: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
_onChange = evt => {
|
||||
this.setState({ labelInput: evt.target.value });
|
||||
const onSubmit = () => {
|
||||
if (inputText.length > 0) addTodo(inputText);
|
||||
setInputText('');
|
||||
};
|
||||
|
||||
_onAdd = () => {
|
||||
this.props.addTodo(this.state.labelInput);
|
||||
this.setState({ labelInput: '' });
|
||||
const onFilter: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
changeFilter(e.currentTarget.textContent as FilterTypes)
|
||||
};
|
||||
}
|
||||
return (
|
||||
<header>
|
||||
<h1>
|
||||
todos <small>(1.7 final)</small>
|
||||
</h1>
|
||||
<div className="addTodo">
|
||||
<input value={inputText} onChange={onInput} className="textfield" placeholder="add todo" />
|
||||
<button onClick={onSubmit} className="submit">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<nav className="filter">
|
||||
<button onClick={onFilter} className={getFilter() === 'all' ? 'selected' : ''}> all</button>
|
||||
<button onClick={onFilter} className={getFilter() === 'active' ? 'selected' : ''}>active</button>
|
||||
<button onClick={onFilter} className={getFilter() === 'completed' ? 'selected' : ''}>completed</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import React from 'react';
|
||||
import { TodoListItem } from './TodoListItem';
|
||||
import { FilterTypes, Todos } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoListProps {
|
||||
complete: (id: string) => void;
|
||||
todos: Todos;
|
||||
filter: FilterTypes;
|
||||
}
|
||||
export const TodoList = () => {
|
||||
const { getFilter, getTodos } = React.useContext(AppContext);
|
||||
|
||||
export class TodoList extends React.Component<TodoListProps, any> {
|
||||
render() {
|
||||
const { filter, todos, complete } = this.props;
|
||||
const filteredTodos = getTodos().filter((todo) => {
|
||||
if (todo.status === 'cleared') return false;
|
||||
return getFilter() === 'all' ||
|
||||
(getFilter() === 'completed' && todo.status === 'completed') ||
|
||||
(getFilter() === 'active' && todo.status === 'active');
|
||||
});
|
||||
|
||||
const filteredTodos = Object.keys(todos).filter(id => {
|
||||
return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map(id => (
|
||||
<TodoListItem key={id} id={id} complete={complete} {...todos[id]} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ul className="todos">
|
||||
{filteredTodos.map((todo) => (
|
||||
<TodoListItem key={todo.id} {...todo} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import React from 'react';
|
||||
import { TodoItem } from '../TodoApp.types';
|
||||
import { Todo } from '../TodoApp.types';
|
||||
import { AppContext } from '../TodoApp';
|
||||
|
||||
interface TodoListItemProps extends TodoItem {
|
||||
id: string;
|
||||
complete: (id: string) => void;
|
||||
}
|
||||
export const TodoListItem = (props: Todo) => {
|
||||
const { label, status, id } = props;
|
||||
const { toggleCompleted } = React.useContext(AppContext);
|
||||
|
||||
export class TodoListItem extends React.Component<TodoListItemProps, any> {
|
||||
render() {
|
||||
const { label, completed, complete, id } = this.props;
|
||||
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={completed} onChange={() => complete(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li className="todo">
|
||||
<label>
|
||||
<input type="checkbox" checked={status === 'completed'} onChange={() => toggleCompleted(id)} /> {label}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user