タスクが自動で消える！タイマー付きToDoリストを作ってみた


## はじめに
"タイマー付きのToDoリスト"を作ってみました。

"従来のToDoリスト"では一度設定したタスクは消去しない限り残るのに対し、"タイマー付きのToDoリスト"では、タイムオーバーになったタスクは自動的に消えることが大きな特徴です。

## 基本仕様
- タスクそれぞれにタイマーを設定する（デフォルトは1h） 
- タイムオーバーになると実施の有無にかかわらずタスクが自動的に消える
- ファイル保存、一括削除ができる（タスク保存したい場合はファイル保存を行う）
- タスクごとにチェック、削除ができる
- local storageにデータを保存している

## 想定される使用場面
- 難しいタスクを、細分化したタスクにして処理する
- 時々刻々発生する課題を新しいタスクとして記録する
- １日の業務手順を、タイマー設定により計画・実行する

## スクリプト
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo List</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

    <style>
        input[type="number"] {
            width: 60px;
            margin-left: 10px;
        }
        .timer {
            font-weight: bold;
            color: red;
        }
    </style>
</head>
<body>
    <div id="app">
        <h1>Todo List</h1>
        <textarea v-model="newTodos" placeholder="Enter multiple todos, each on a new line"></textarea>
        <button @click="addTodos">Add Todos</button>
        <button @click="saveToFile">Save to File</button>
        <button @click="resetTodos">Reset</button>
        <input type="number" v-model.number="defaultTime" placeholder="時間（時間単位）" min="0" />
        <ul>
            <li v-for="(todo, index) in todos" :key="index">
                <input type="checkbox" v-model="todo.done" />
                <span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">{{ todo.text }}</span>&nbsp;
                <span class="timer" v-if="todo.remainingTime >= 0">{{ formatRemainingTime(todo.remainingTime) }}</span>
                <button @click="removeTodo(index)">Remove</button>
            </li>
        </ul>
    </div>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    newTodos: '',
                    todos: [],
                    defaultTime: 1 // 一定時間を保持する変数
                };
            },
            mounted() {
                const savedTodos = localStorage.getItem('todos');
                if (savedTodos) {
                    this.todos = JSON.parse(savedTodos);
                    this.todos.forEach(todo => {
                        todo.remainingTime = this.calculateRemainingTime(todo.deleteAt);
                    });
                }
                this.startGlobalTimer(); 
            },
            watch: {
                todos: {
                    deep: true,
                    handler(newTodos) {
                        localStorage.setItem('todos', JSON.stringify(newTodos));
                    }
                }
            },
            methods: {
                addTodos() {
                    const todosArray = this.newTodos.trim().split('\n').filter(todo => todo.trim() !== '');
                    todosArray.forEach(todoText => {
                        const deleteAt = Date.now() + this.defaultTime * 3600000;
                        this.todos.push({ 
                            text: todoText, 
                            done: false, 
                            deleteAt: deleteAt,
                            remainingTime: this.calculateRemainingTime(deleteAt)
                        });
                    });
                    this.newTodos = '';
                },
                removeTodo(index) {
                    this.todos.splice(index, 1);
                },
                saveToFile() {
                    const todosText = this.todos.map(todo => `${todo.text}${todo.done ? ' (done)' : ''}`).join('\n');
                    const blob = new Blob([todosText], { type: 'text/plain' });
                    const url = URL.createObjectURL(blob);
                    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
                    const filename = `todos-${timestamp}.txt`;
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                },
                resetTodos() {
                    this.todos = [];
                },
                startGlobalTimer() {
                    setInterval(() => {
                        const now = Date.now();
                        this.todos = this.todos.filter(todo => {
                            todo.remainingTime = this.calculateRemainingTime(todo.deleteAt);
                            return todo.remainingTime > 0;
                        });
                    }, 1000);
                },
                calculateRemainingTime(deleteAt) {
                    const remainingTime = deleteAt - Date.now();
                    return remainingTime > 0 ? remainingTime : 0;
                },
                formatRemainingTime(remainingTime) {
                    const hours = Math.floor(remainingTime / 3600000);
                    const minutes = Math.floor((remainingTime % 3600000) / 60000);
                    const seconds = Math.floor((remainingTime % 60000) / 1000);
                    return `${hours}h ${minutes}m ${seconds}s `;
                }
            }
        });
        app.mount('#app');
    </script>
</body>
</html>
```


## 技術的特徴
以下がアプリの主な技術的特徴です。

1. **Vue.jsを使用したアプリケーション構成**
   - Vueの`data`と`methods`を活用して、タスクデータの管理やユーザー操作の処理を実装
   - Vueの`mounted`ライフサイクルメソッドを利用し、ページの読み込み時にローカルストレージからタスクを読み込む処理が行われる

2. **LocalStorageへの自動保存**
   - `watch`プロパティを使用し、`todos`の変更を監視してリアルタイムでlocalStorageに保存する

3. **タイマー機能**
   - `defaultTime`変数でタスクのデフォルトのタイムアウト時間（1時間）を設定
   - `addTodos`メソッドでタスク追加時に「削除予定時刻」を設定し、`remainingTime`を計算することで、残り時間が表示される
   - `startGlobalTimer`メソッドで1秒ごとに全タスクをチェックし、`remainingTime`が0以下になったタスクはリストから自動削除される

4. **タスクの残り時間を動的に表示**
   - `calculateRemainingTime`メソッドで各タスクの残り時間をミリ秒単位で計算し、`formatRemainingTime`メソッドで「時:分:秒」にフォーマットする


https://vercel-todotimer.vercel.app/