diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e56834..8fb65a2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,34 +1,36 @@ 'use client'; -import { useState, useEffect } from 'react'; // useEffectを追加 +import { useState, useEffect } from 'react'; +import TodoList from '@/components/TodoList'; +import FilterButtons from '@/components/FilterButtons'; // Todo型の定義 type Todo = { - id: number; // ユニークな識別子。今回はDate.now()で一意性をある程度確保。 - text: string; // TODOの内容。ユーザーが入力する文字列。 - completed: boolean; // 完了したかどうかのフラグ。チェックボックスに対応。 + id: number; + text: string; + completed: boolean; }; -type FilterType = 'all' | 'active' | 'completed'; // フィルターの種類。全て、未完了、完了の3種類。 +type FilterType = 'all' | 'active' | 'completed'; export default function Home() { // Local Storage のキー const STORAGE_KEY = 'todo-app-data'; // Todoリスト本体の状態管理 - const [todos, setTodos] = useState([]); // Todo配列を保持 + const [todos, setTodos] = useState([]); // 入力欄の状態管理 - const [inputValue, setInputValue] = useState(''); // 入力中のテキスト + const [inputValue, setInputValue] = useState(''); // 削除確認ダイアログの表示状態 - const [showConfirm, setShowConfirm] = useState(false); // ダイアログ表示フラグ + const [showConfirm, setShowConfirm] = useState(false); // 削除対象のTodoのID - const [targetId, setTargetId] = useState(null); // 削除対象ID + const [targetId, setTargetId] = useState(null); // 空欄入力時の警告ダイアログ表示状態 - const [showInputAlert, setShowInputAlert] = useState(false); // 空欄警告フラグ + const [showInputAlert, setShowInputAlert] = useState(false); // 編集機能のための状態管理 const [editingId, setEditingId] = useState(null); @@ -43,105 +45,96 @@ export default function Home() { const activeCount = todos.filter(todo => !todo.completed).length; const completedCount = todos.filter(todo => todo.completed).length; + const startEdit = (todo: Todo) => { + setEditingId(todo.id); + setEditText(todo.text); + }; - // フィルターを変更する関数 - const getFilteredTodos = () => { // フィルターに応じてTodoリストを返す関数 - switch (filter) { // フィルターの種類に応じて処理を分岐 - case 'active': // 未完了のTodoのみを返す - return todos.filter(todo => !todo.completed); - case 'completed': // 完了したTodoのみを返す - return todos.filter(todo => todo.completed); - default: // 'all'の場合は全てのTodoを返す - return todos; // 'all'の場合は全てのTodoを返す - } - }; - - const startEdit = (todo: Todo) => { // 編集開始関数 - setEditingId(todo.id); // 編集対象のIDをセット - setEditText(todo.text); // 編集用のテキストをセット - }; - - const saveEdit = () => { // 編集内容を保存する関数 - if (editText.trim() === '') { // 編集内容が空欄だったら警告ダイアログ表示 - alert('TODOが空です'); // 簡易的なアラート表示 - return; // 空欄の場合は保存しない + const saveEdit = () => { + if (editText.trim() === '') { + alert('TODOが空です'); + return; } - const updatedTodos = todos.map(todo => { // Todoリストを更新 - if (todo.id === editingId) { // 編集対象のTodoを見つけたら - return { ...todo, text: editText }; // 編集内容で更新 + const updatedTodos = todos.map(todo => { + if (todo.id === editingId) { + return { ...todo, text: editText }; } - return todo; // 他のTodoはそのまま返す + return todo; }); - setTodos(updatedTodos); // 更新されたTodoリストをセット - setEditingId(null); // 編集モードを終了 - setEditText(''); // 編集用テキストをクリア + setTodos(updatedTodos); + setEditingId(null); + setEditText(''); }; // 新しいTODOを追加する関数 const addTodo = () => { - // 入力が空欄だったら警告ダイアログ表示 - if (inputValue.trim() === '') { //trim()で前後の空白を除去 - setShowInputAlert(true); // 警告ダイアログを表示 + if (inputValue.trim() === '') { + setShowInputAlert(true); return; } - // 新しいTodoオブジェクトを作成 - const newTodo: Todo = { // 新規Todoオブジェクト - id: Date.now(), // 現在時刻のミリ秒をIDとして使用することで、ほかのTODOと重複しないようにする - text: inputValue, // 入力値 - completed: false // 初期状態を未完了に + + const newTodo: Todo = { + id: Date.now(), + text: inputValue, + completed: false }; - setTodos([...todos, newTodo]); // Todoリストに追加 - setInputValue(''); // 入力欄をクリア + setTodos([...todos, newTodo]); + setInputValue(''); }; - const cancelEdit = () => { // 編集キャンセル関数 - setEditingId(null); // 編集モードを終了 - setEditText(''); // 編集用テキストをクリア + const cancelEdit = () => { + setEditingId(null); + setEditText(''); }; // フォーム送信時の処理 - const handleSubmit = (e: React.FormEvent) => { // フォーム送信イベント - e.preventDefault(); // ページリロード防止 - addTodo(); // Todo追加処理 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + addTodo(); }; // Todoの完了状態をトグルする関数 - const toggleTodo = (id: number) => { // Todoの完了状態を切り替える - const updatedTodos = todos.map(todo => { // 現在のTodoリストをマッピング - if (todo.id === id) { // 対象のTodoを見つけたら - return { ...todo, completed: !todo.completed }; // そのtodoの完了状態を反転 + const toggleTodo = (id: number) => { + const updatedTodos = todos.map(todo => { + if (todo.id === id) { + return { ...todo, completed: !todo.completed }; } - return todo; // 対象外はそのまま返す + return todo; }); - setTodos(updatedTodos); // 状態更新 + setTodos(updatedTodos); }; // 削除ボタンが押されたときの処理(確認ダイアログ表示) - const handleDeleteClick = (id: number) => { // 削除ボタン押下時の処理 - setShowConfirm(true); // 削除確認ダイアログ表示 - setTargetId(id); // 削除対象IDをセット + const handleDeleteClick = (id: number) => { + setShowConfirm(true); + setTargetId(id); }; // ダイアログで「はい」が押されたときの本削除処理 - const confirmDelete = () => { // 削除確認ダイアログで「はい」が押されたときの処理 - if (targetId !== null) { // 削除対象IDがセットされているか確認 - setTodos(todos.filter(todo => todo.id !== targetId)); // 該当ID以外を残す - setShowConfirm(false); // ダイアログを閉じる - setTargetId(null); // 削除対象IDをリセット + const confirmDelete = () => { + if (targetId !== null) { + setTodos(todos.filter(todo => todo.id !== targetId)); + setShowConfirm(false); + setTargetId(null); } }; // ダイアログで「いいえ」が押されたときの処理 - const cancelDelete = () => { // 削除確認ダイアログで「いいえ」が押されたときの処理 - setShowConfirm(false); // ダイアログを閉じる - setTargetId(null); // 削除対象IDをリセット + const cancelDelete = () => { + setShowConfirm(false); + setTargetId(null); }; // 空欄入力時ダイアログを閉じる関数 - const closeInputAlert = () => { // - setShowInputAlert(false); // 警告ダイアログを閉じる + const closeInputAlert = () => { + setShowInputAlert(false); + }; + + // 完了済みTODOをすべて削除 + const clearCompleted = () => { + setTodos(todos.filter(todo => !todo.completed)); }; // アプリ起動時にデータを読み込み @@ -155,12 +148,12 @@ export default function Home() { console.error('データの読み込みに失敗しました:', error); } } - }, []); // 空の依存配列で初回のみ実行 + }, []); // TODOが変更されるたびに保存 useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); - }, [todos]); // todosが変更されるたびに実行 + }, [todos]); // データのエクスポート機能 const exportData = () => { @@ -191,10 +184,9 @@ export default function Home() { } }; - // JSX: 実際に表示されるUI定義 return ( -
{/* 全体の背景や余白 */} -
{/* カード風の白背景 */} +
+
{/* ヘッダー部分にエクスポート・インポート機能を追加 */}

TODOアプリ

@@ -218,11 +210,11 @@ export default function Home() {
{/* 入力フォーム */} -
{/* フォーム全体のスタイル */} + setInputValue(e.target.value)} // 入力値変更時に状態更新 + value={inputValue} + onChange={(e) => setInputValue(e.target.value)} placeholder="TODOを入力してEnterキー" className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" /> @@ -233,136 +225,38 @@ export default function Home() { 追加
-
{/* フィルターと削除ボタンのコンテナ */} -
{/* フィルター用のボタン */} - - - -
- - {/* 完了済みTODOをすべて削除 */} - {completedCount > 0 && ( - - )} -
-
- {getFilteredTodos().length === 0 ? ( -

- {filter === 'active' && 'すべて完了しました!'} - {filter === 'completed' && '完了したTODOがありません'} - {filter === 'all' && 'TODOがありません'} -

- ) : ( - getFilteredTodos().map((todo) => ( -
- toggleTodo(todo.id)} - className="mr-3 h-5 w-5 cursor-pointer" - /> - {editingId === todo.id ? ( - // 編集モード - setEditText(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') saveEdit(); - if (e.key === 'Escape') cancelEdit(); - }} - className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - autoFocus - /> - ) : ( - // 通常モード - startEdit(todo)} - className={`flex-1 cursor-pointer p-2 rounded hover:bg-gray-200 transition-colors ${ - todo.completed ? 'line-through text-gray-500' : 'text-gray-800' - }`} - > - {todo.text} - - )} - {editingId === todo.id ? ( - // 編集モードのボタン -
- - -
- ) : ( - // 通常モードのボタン -
- - -
- )} -
- )) - )} -
+ {/* フィルターボタン */} + + + {/* TODOリスト */} + {/* 確認ダイアログ */} {showConfirm && (

本当に削除しますか?

- {/* 削除確定 */} - {/* 削除キャンセル */} + +
)} @@ -373,7 +267,7 @@ export default function Home() {

TODOを入力してください

+ + +
+ + {completedCount > 0 && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/TodoItem.tsx b/src/components/TodoItem.tsx new file mode 100644 index 0000000..10244ee --- /dev/null +++ b/src/components/TodoItem.tsx @@ -0,0 +1,99 @@ +import React from 'react'; + +type Todo = { + id: number; + text: string; + completed: boolean; +}; + +type TodoItemProps = { + todo: Todo; + isEditing: boolean; + editText: string; + onToggle: () => void; + onStartEdit: () => void; + onSaveEdit: () => void; + onCancelEdit: () => void; + onDelete: () => void; + onEditTextChange: (text: string) => void; +}; + +export default function TodoItem({ + todo, + isEditing, + editText, + onToggle, + onStartEdit, + onSaveEdit, + onCancelEdit, + onDelete, + onEditTextChange, +}: TodoItemProps) { + return ( +
+ + {isEditing ? ( + // 編集モード + onEditTextChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') onSaveEdit(); + if (e.key === 'Escape') onCancelEdit(); + }} + className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" + autoFocus + /> + ) : ( + // 通常モード + + {todo.text} + + )} + {isEditing ? ( + // 編集モードのボタン +
+ + +
+ ) : ( + // 通常モードのボタン +
+ + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/TodoList.tsx b/src/components/TodoList.tsx new file mode 100644 index 0000000..a93cb4b --- /dev/null +++ b/src/components/TodoList.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import TodoItem from './TodoItem'; + +type Todo = { + id: number; + text: string; + completed: boolean; +}; + +type FilterType = 'all' | 'active' | 'completed'; + +type TodoListProps = { + todos: Todo[]; + filter: FilterType; + editingId: number | null; + editText: string; + onToggleTodo: (id: number) => void; + onStartEdit: (todo: Todo) => void; + onSaveEdit: () => void; + onCancelEdit: () => void; + onDeleteClick: (id: number) => void; + onEditTextChange: (text: string) => void; +}; + +export default function TodoList({ + todos, + filter, + editingId, + editText, + onToggleTodo, + onStartEdit, + onSaveEdit, + onCancelEdit, + onDeleteClick, + onEditTextChange, +}: TodoListProps) { + const getFilteredTodos = () => { + switch (filter) { + case 'active': + return todos.filter(todo => !todo.completed); + case 'completed': + return todos.filter(todo => todo.completed); + default: + return todos; + } + }; + + return ( +
+ {getFilteredTodos().length === 0 ? ( +

+ {filter === 'active' && 'すべて完了しました!'} + {filter === 'completed' && '完了したTODOがありません'} + {filter === 'all' && 'TODOがありません'} +

+ ) : ( + getFilteredTodos().map((todo) => ( + onToggleTodo(todo.id)} + onStartEdit={() => onStartEdit(todo)} + onSaveEdit={onSaveEdit} + onCancelEdit={onCancelEdit} + onDelete={() => onDeleteClick(todo.id)} + onEditTextChange={onEditTextChange} + /> + )) + )} +
+ ); +} \ No newline at end of file