Files
nextjs-todo-tutorial/src/app/page.tsx

282 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}