forked from semi-23e/nextjs-todo-tutorial
282 lines
8.4 KiB
TypeScript
282 lines
8.4 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import TodoList from '@/components/TodoList';
|
||
import FilterButtons from '@/components/FilterButtons';
|
||
|
||
// Todo型の定義
|
||
type Todo = {
|
||
id: number;
|
||
text: string;
|
||
completed: boolean;
|
||
};
|
||
|
||
type FilterType = 'all' | 'active' | 'completed';
|
||
|
||
export default function Home() {
|
||
// Local Storage のキー
|
||
const STORAGE_KEY = 'todo-app-data';
|
||
|
||
// Todoリスト本体の状態管理
|
||
const [todos, setTodos] = useState<Todo[]>([]);
|
||
|
||
// 入力欄の状態管理
|
||
const [inputValue, setInputValue] = useState<string>('');
|
||
|
||
// 削除確認ダイアログの表示状態
|
||
const [showConfirm, setShowConfirm] = useState(false);
|
||
|
||
// 削除対象のTodoのID
|
||
const [targetId, setTargetId] = useState<number | null>(null);
|
||
|
||
// 空欄入力時の警告ダイアログ表示状態
|
||
const [showInputAlert, setShowInputAlert] = useState(false);
|
||
|
||
// 編集機能のための状態管理
|
||
const [editingId, setEditingId] = useState<number | null>(null);
|
||
|
||
// 編集中のテキスト
|
||
const [editText, setEditText] = useState('');
|
||
|
||
// フィルター状態の管理(全て、未完了、完了)
|
||
const [filter, setFilter] = useState<FilterType>('all');
|
||
|
||
// フィルターに応じたTodoの数をカウント
|
||
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 saveEdit = () => {
|
||
if (editText.trim() === '') {
|
||
alert('TODOが空です');
|
||
return;
|
||
}
|
||
|
||
const updatedTodos = todos.map(todo => {
|
||
if (todo.id === editingId) {
|
||
return { ...todo, text: editText };
|
||
}
|
||
return todo;
|
||
});
|
||
|
||
setTodos(updatedTodos);
|
||
setEditingId(null);
|
||
setEditText('');
|
||
};
|
||
|
||
// 新しいTODOを追加する関数
|
||
const addTodo = () => {
|
||
if (inputValue.trim() === '') {
|
||
setShowInputAlert(true);
|
||
return;
|
||
}
|
||
|
||
const newTodo: Todo = {
|
||
id: Date.now(),
|
||
text: inputValue,
|
||
completed: false
|
||
};
|
||
setTodos([...todos, newTodo]);
|
||
setInputValue('');
|
||
};
|
||
|
||
const cancelEdit = () => {
|
||
setEditingId(null);
|
||
setEditText('');
|
||
};
|
||
|
||
// フォーム送信時の処理
|
||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
addTodo();
|
||
};
|
||
|
||
// Todoの完了状態をトグルする関数
|
||
const toggleTodo = (id: number) => {
|
||
const updatedTodos = todos.map(todo => {
|
||
if (todo.id === id) {
|
||
return { ...todo, completed: !todo.completed };
|
||
}
|
||
return todo;
|
||
});
|
||
setTodos(updatedTodos);
|
||
};
|
||
|
||
// 削除ボタンが押されたときの処理(確認ダイアログ表示)
|
||
const handleDeleteClick = (id: number) => {
|
||
setShowConfirm(true);
|
||
setTargetId(id);
|
||
};
|
||
|
||
// ダイアログで「はい」が押されたときの本削除処理
|
||
const confirmDelete = () => {
|
||
if (targetId !== null) {
|
||
setTodos(todos.filter(todo => todo.id !== targetId));
|
||
setShowConfirm(false);
|
||
setTargetId(null);
|
||
}
|
||
};
|
||
|
||
// ダイアログで「いいえ」が押されたときの処理
|
||
const cancelDelete = () => {
|
||
setShowConfirm(false);
|
||
setTargetId(null);
|
||
};
|
||
|
||
// 空欄入力時ダイアログを閉じる関数
|
||
const closeInputAlert = () => {
|
||
setShowInputAlert(false);
|
||
};
|
||
|
||
// 完了済みTODOをすべて削除
|
||
const clearCompleted = () => {
|
||
setTodos(todos.filter(todo => !todo.completed));
|
||
};
|
||
|
||
// アプリ起動時にデータを読み込み
|
||
useEffect(() => {
|
||
const savedTodos = localStorage.getItem(STORAGE_KEY);
|
||
if (savedTodos) {
|
||
try {
|
||
const parsedTodos = JSON.parse(savedTodos);
|
||
setTodos(parsedTodos);
|
||
} catch (error) {
|
||
console.error('データの読み込みに失敗しました:', error);
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
// TODOが変更されるたびに保存
|
||
useEffect(() => {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
|
||
}, [todos]);
|
||
|
||
// データのエクスポート機能
|
||
const exportData = () => {
|
||
const dataStr = JSON.stringify(todos, null, 2);
|
||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||
const url = URL.createObjectURL(dataBlob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = 'todos.json';
|
||
link.click();
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
// データのインポート機能
|
||
const importData = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
try {
|
||
const importedTodos = JSON.parse(e.target?.result as string);
|
||
setTodos(importedTodos);
|
||
} catch (error) {
|
||
alert('ファイルの読み込みに失敗しました');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<main className="min-h-screen p-8 bg-gray-50">
|
||
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
||
{/* ヘッダー部分にエクスポート・インポート機能を追加 */}
|
||
<div className="flex justify-between items-center mb-6">
|
||
<h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={exportData}
|
||
className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors text-sm"
|
||
>
|
||
エクスポート
|
||
</button>
|
||
<label className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors cursor-pointer text-sm">
|
||
インポート
|
||
<input
|
||
type="file"
|
||
accept=".json"
|
||
onChange={importData}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 入力フォーム */}
|
||
<form onSubmit={handleSubmit} className="mb-4">
|
||
<input
|
||
type="text"
|
||
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"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors"
|
||
>
|
||
追加
|
||
</button>
|
||
</form>
|
||
|
||
{/* フィルターボタン */}
|
||
<FilterButtons
|
||
filter={filter}
|
||
totalCount={todos.length}
|
||
activeCount={activeCount}
|
||
completedCount={completedCount}
|
||
onFilterChange={setFilter}
|
||
onClearCompleted={clearCompleted}
|
||
/>
|
||
|
||
{/* TODOリスト */}
|
||
<TodoList
|
||
todos={todos}
|
||
filter={filter}
|
||
editingId={editingId}
|
||
editText={editText}
|
||
onToggleTodo={toggleTodo}
|
||
onStartEdit={startEdit}
|
||
onSaveEdit={saveEdit}
|
||
onCancelEdit={cancelEdit}
|
||
onDeleteClick={handleDeleteClick}
|
||
onEditTextChange={setEditText}
|
||
/>
|
||
|
||
{/* 確認ダイアログ */}
|
||
{showConfirm && (
|
||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||
<div className="bg-white p-6 rounded shadow">
|
||
<p className="mb-4">本当に削除しますか?</p>
|
||
<button onClick={confirmDelete} className="mr-2 px-4 py-2 bg-red-500 text-white rounded">はい</button>
|
||
<button onClick={cancelDelete} className="px-4 py-2 bg-gray-300 rounded">いいえ</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 空欄入力時のカスタムダイアログ */}
|
||
{showInputAlert && (
|
||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||
<div className="bg-white p-6 rounded shadow flex flex-col items-center">
|
||
<p className="mb-4">TODOを入力してください</p>
|
||
<button
|
||
onClick={closeInputAlert}
|
||
className="w-full px-4 py-2 bg-blue-500 text-white rounded text-center"
|
||
style={{ maxWidth: "240px" }}
|
||
>
|
||
閉じる
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
);
|
||
} |