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:
Micah Godbolt
2022-01-13 09:22:50 -08:00
committed by GitHub
parent 4998c158d2
commit 7cea32428e
60 changed files with 923 additions and 929 deletions

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};