Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

day1の課題「簡易Todoアプリ」実装の一例 #1

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 28 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Expand Up @@ -10,13 +10,15 @@
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/uuid": "^8.3.4",
"@vitejs/plugin-react": "^2.0.0",
"typescript": "^4.6.4",
"vite": "^3.0.0"
}
}
}
35 changes: 6 additions & 29 deletions src/App.tsx
@@ -1,34 +1,11 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
import { TodoPage } from './features/todos/pages/TodoPage';

function App() {
const [count, setCount] = useState(0)

return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
)
<>
<TodoPage />
</>
);
}

export default App
export default App;
48 changes: 48 additions & 0 deletions src/features/todos/components/TodoForm.tsx
@@ -0,0 +1,48 @@
import { useState, FC } from 'react';
import { createTodo } from '../crud/create';
import type { Todo, TodoInput } from '../types';

type Props = {
onSubmit: (todo: Todo) => void;
};

export const TodoForm: FC<Props> = ({ onSubmit }) => {
const [input, setInput] = useState<TodoInput>({
title: '',
});

const onSubmitHandler: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
if (!input.title) {
alert('タイトルを入力してください');
return;
}

const todo = createTodo(input);
onSubmit(todo);
setInput({
title: '',
});
};

const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = (
event
) => {
setInput({
title: event.target.value,
});
};

return (
<form method="post" onSubmit={onSubmitHandler}>
<label htmlFor="inputTitle">タイトル : </label>
<input
id="inputTitle"
type="text"
value={input.title}
onChange={onChangeHandler}
/>
<input type="submit" value="作成" />
</form>
);
};
44 changes: 44 additions & 0 deletions src/features/todos/components/TodoList.tsx
@@ -0,0 +1,44 @@
import type { FC } from 'react';
import type { Todo, TodoUpdateInput } from '../types';

type TodoListProps = {
todos: Todo[];
onChangeCompleted: (updateInput: TodoUpdateInput) => void;
};

export const TodoList: FC<TodoListProps> = ({ todos, onChangeCompleted }) => {
return (
<table border={1}>
<thead>
<tr>
<th>id</th>
<th>タイトル</th>
<th>進捗</th>
</tr>
</thead>
<tbody>
{todos.map((todo) => {
const { id, completed, title } = todo;
return (
<tr key={id}>
<td>{id}</td>
<td>{title}</td>
<td>
<input
type="checkbox"
checked={completed}
onChange={(event) => {
onChangeCompleted({
...todo,
completed: event.target.checked,
});
}}
/>
</td>
</tr>
);
})}
</tbody>
</table>
);
};
11 changes: 11 additions & 0 deletions src/features/todos/crud/create.ts
@@ -0,0 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import type { Todo, TodoInput } from '../types';

export const createTodo = (input: TodoInput): Todo => {
return {
// https://github.com/uuidjs/uuid#uuidv4options-buffer-offset
id: uuidv4(),
title: input.title,
completed: false,
};
};
35 changes: 35 additions & 0 deletions src/features/todos/pages/TodoPage.tsx
@@ -0,0 +1,35 @@
import { FC, useState } from 'react';
import { TodoForm } from '../components/TodoForm';
import { TodoList } from '../components/TodoList';
import { Todo, TodoUpdateInput } from '../types';

export const TodoPage: FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);

const onSubmit = (todo: Todo) => {
setTodos([...todos, todo]);
};

const onChangeTodoCompleted = (updateTodoInput: TodoUpdateInput) => {
const updatedTodos = todos.map((todo) => {
if (todo.id !== updateTodoInput.id) return todo;
if (updateTodoInput.completed === undefined) return todo;

return {
...todo,
completed: updateTodoInput.completed,
};
});

setTodos(updatedTodos);
};

return (
<div>
<h1>簡易Todoアプリ</h1>
<TodoForm onSubmit={onSubmit} />
<hr />
<TodoList todos={todos} onChangeCompleted={onChangeTodoCompleted} />
</div>
);
};
13 changes: 13 additions & 0 deletions src/features/todos/types.ts
@@ -0,0 +1,13 @@
export type Todo = {
id: string;
title: string;
completed: boolean;
};

// Pickの参考記事
// https://typescriptbook.jp/reference/type-reuse/utility-types/pick
export type TodoInput = Pick<Todo, 'title'>;

// Partialの参考記事
// https://typescriptbook.jp/reference/type-reuse/utility-types/partial
export type TodoUpdateInput = Pick<Todo, 'id'> & Partial<Todo>;
9 changes: 4 additions & 5 deletions src/main.tsx
@@ -1,10 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
);