見出し画像

ToDoリストで「しないこと」も管理:Claude 3.5とWEBアプリ改修

前回に引き続き、ToDoリストを管理するWEBアプリに関する内容です。(マイブームになりました。)

追加で5時間くらいかけて機能追加をしたので、改めて記事にさせていただきます。例によって、ソースコードは全量公開しているので(というか、WEBアプリなので全量見れますが)、良識の範囲内でご利用いただければ幸いです。

↑これだけで動きます。

↑自ドメイン格納版。

ちなみにイメージとしては、期日管理しなくてよいタスクを、おおよその時間でざっと一旦入れておく想定です。その中で超過するものがあれば、「しないこと」に振り分けても良いと考えています。


「しないこと」を管理する

通常、ToDoリスト自体は「しないこと」を管理するものではありません。しかし、今回作成したものは、通常のタスクに加えて、「しないこと」も記録できるようになっています。

これは実験的な試みで、もともとChatGPT-4oの指摘で、マイナス時間の入力を許容しているバグ報告がありました。これを、仕様として昇格させたものです。内部的には、時間をマイナス入力すると、「しないことリスト」として認識しています。

以下、補足的に「しないこと」を記録するメリットを掲載します。

優先順位の明確化
何をしないかを明示することで、本当に重要なタスクに集中できます。やるべきこととやらないことを明確にすることで、リソースを効率的に配分できるようになります。
ストレス軽減
やることだけでなく、やらないことを明示することで、タスクの過負荷やプレッシャーを減らすことができます。これは、心理的な負担を軽減し、より健全な働き方を促進します。
時間管理の向上
不要なタスクや時間を浪費する活動を避けることで、より効率的な時間管理が可能になります。これは、仕事だけでなく、プライベートな時間にも適用でき、全体的な生活の質を向上させます。
目標の達成
やらないことをリストに書くことで、目標に対するブレを防ぐことができます。無駄なタスクや寄り道を避けることで、目標に一貫して向かうことができます。
自己認識の向上
やらないことをリストアップする過程で、自分の優先事項や価値観を再確認することができます。これにより、自己認識が深まり、自分にとって何が本当に重要かを再評価できます。
決断力の向上
何をしないかを明示することで、決断を下す際の基準が明確になります。これは、迷いやすい状況での迅速な判断を助け、効果的な行動を促進します。

ChatGPT-4oより、しないことリストのメリット

「しない(やらない)ことリスト」自体は書籍やブログでも解説されているので、いくつか参考程度に以下に共有します。

※Amazonリンクはアソシエイトリンクです。

修正ポイントなど

全体の進捗バーのデザイン、機能を変更

ステータスを表示するエリアに「しないこと」を追加し、各タスクの進捗率も加味して計算するようにしました。完了した分は緑色、進行中は黄色、赤色は「しないこと」を指しています。

この仕様について、特にClaude 3.5とやりとりが多くなりました。
どうあるべきか、というのを考えるのが難しいですね。

優先度フィルター、絞り込み機能を追加

優先度も登録できるので、絞り込みできるフィルターを追加しました。フィルタリングした際のタスクに合わせて各ステータスが再計算されるようにしましたが、全体の進捗は変えないようにしました。

Claude 3.5からは、以下のようにも言われて、悩んでこの仕様にしました。

(統計情報と、全体の進捗状況の実装の提示)
ただし、この実装には一つ注意点があります。フィルタリングすると、全体の統計情報がフィルターされたタスクのみを反映するようになります。これが望ましい動作かどうかは、アプリケーションの目的や要件によって異なります。

Claude 3.5の意見

ただ黙って実装するだけではなく、こう意見してきて、例としてパターンで示してくれるのは怖いくらいに凄いです。

フィルターを使用した際に、現在、優先度「低」のタスクのみ…が表示される仕様は、
Claude 3.5のアイデア(アイデアというか、こちらの指示が不足していると先読みで実装してきますね)

その他

・各タスクの進捗を一番右までスライドさせると、そのまま完了タスクとする仕様にしました。また、チェックボタンを押した際に、「未完了→完了→しない」と3段階で遷移するようにしました。

・バグとして、マイナス値で「しないタスク」を判定していたため、0hで登録した際に正常に動作しない問題がありました。必須入力と仕様を改め、する・しないボタンを設置しました。「しない」を選択すると内部的にはマイナスで登録しますが、見た目上は表示されません。

ちなみにこのバグ(仕様)はまだ悪さをしていて、「しないこと」として1h等で登録した後に、0hとすると「完了済タスク」として判定され、さらにこのタスクだけチェックボックスをいじれなくなります。(再び時間を0以外にすれば大丈夫です。)

・スマホ向けにもさらに最適化を進めました。ただし、解像度が低い端末ではレイアウトが崩れるため、入力エリアを二段にしたり、未完了タスク等の文言から「タスク」を削って崩れないようにしています。(ブラウザの拡大率の調整で、たいてい解消します。)

左:スマホ向け
右:PC向け

まとめ

どこまでやるかという話でもありますが、しばらくは思いついたことで、そう時間がかからないことならば遊び半分で改修していこうと思います。しかし、自分がイメージしているものが、これまで(ChatGPT-4など)よりもかなり正確に実装できるのは楽しいです。いろいろ作りたくなりますね。
ちなみに、このコードをChatGPT-4oに連携して機能追加を依頼しましたが、動かないものが生成されたり、いまいちでした。指示の仕方も悪いとは思いますが、コーディング等の性能は、ChatGPT-4oより高い気がします。最も、ベースはClaude 3.5が作っているので、フェアな条件ではないです。

コード

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ゆるToDo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap');
        body {
            font-family: 'Orbitron', sans-serif;
            background-color: #0a0a0a;
            color: #00ff00;
        }
        .cyber-glow {
            text-shadow: 0 0 5px #00ff00, 0 0 10px #00ff00;
        }
        .cyber-input {
            background-color: rgba(0, 255, 0, 0.1);
            border: 1px solid #00ff00;
            color: #00ff00;
        }
        .cyber-button {
            background-color: rgba(0, 255, 0, 0.2);
            border: 1px solid #00ff00;
            color: #00ff00;
            transition: all 0.3s ease;
        }
        .cyber-button:hover {
            background-color: rgba(0, 255, 0, 0.4);
            box-shadow: 0 0 10px #00ff00;
        }
        .cyber-button:not(:disabled):hover {
            background-color: rgba(0, 255, 0, 0.4);
            box-shadow: 0 0 10px #00ff00;
        }
        .cyber-button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            box-shadow: none;
        }
        .cyber-button:focus {
            outline: none;
            box-shadow: 0 0 0 2px #00ff00, 0 0 10px #00ff00;
        }
        .cyber-list-item {
            background-color: rgba(0, 255, 0, 0.05);
            border: 1px solid rgba(0, 255, 0, 0.2);
        }
        .cyber-stats {
            background-color: rgba(0, 255, 0, 0.1);
            border: 1px solid #00ff00;
            box-shadow: 0 0 5px #00ff00;
        }
        .progress-bar {
            -webkit-appearance: none;
            appearance: none;
            width: 100%;
            height: 10px;
            background: rgba(0, 255, 0, 0.1);
            outline: none;
            transition: 0.2s;
            border-radius: 5px;
            position: relative;
        }
        .progress-bar::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: var(--progress);
            height: 100%;
            background-color: #00ff00;
            border-radius: 5px 0 0 5px;
            z-index: 1;
        }
        .progress-bar::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 20px;
            height: 20px;
            background: #00ff00;
            cursor: pointer;
            border-radius: 50%;
            box-shadow: 0 0 5px #00ff00;
            position: relative;
            z-index: 2;
        }
        .progress-bar::-moz-range-thumb {
            width: 20px;
            height: 20px;
            background: #00ff00;
            cursor: pointer;
            border-radius: 50%;
            box-shadow: 0 0 5px #00ff00;
            position: relative;
            z-index: 2;
        }
        .progress-bar:disabled {
            opacity: 0.5;
        }
        .progress-bar:disabled::-webkit-slider-thumb {
            display: none;
        }
        .progress-bar:disabled::-moz-range-thumb {
            display: none;
        }
        .progress-bar::-webkit-slider-runnable-track {
            background: transparent;
        }
        .progress-bar::-moz-range-track {
        background: transparent;
    }

        .progress-bar::-moz-range-progress {
            background-color: #00ff00;
            border-radius: 5px;
        }

        .task-text {
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
            text-overflow: ellipsis;
            max-height: 3em;
            line-height: 1.5em;
        }
        .cyber-checkbox {
            appearance: none;
            -webkit-appearance: none;
            width: 1.5em;
            height: 1.5em;
            border: 2px solid rgba(0, 255, 0, 0.3);
            border-radius: 50%;
            outline: none;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .cyber-checkbox:checked {
            background-color: #00ff00;
            box-shadow: 0 0 5px #00ff00, 0 0 10px #00ff00;
        }

        .cyber-checkbox:hover {
            border-color: #00ff00;
            box-shadow: 0 0 5px #00ff00;
        }
        @media (max-width: 640px) {
            body {
                font-size: 14px;
            }
            .task-text {
                max-width: none; 
                white-space: normal; 
            }
        }
        .cyber-subtitle {
            color: #00ffff;
            text-shadow: 0 0 5px #00ffff;
            letter-spacing: 2px;
        }

        .cyber-bracket {
            color: #ff00ff;
            text-shadow: 0 0 5px #ff00ff;
            margin: 0 5px;
        }

        @keyframes pulse {
            0% { opacity: 0.5; }
            50% { opacity: 1; }
            100% { opacity: 0.5; }
        }

        .cyber-subtitle {
            animation: pulse 2s infinite;
        }
    </style>
</head>
<body class="min-h-screen text-base sm:text-lg">
    <div id="root"></div>
    <script type="text/babel">
        const { useState, useEffect, useRef } = React;

        // CyberAlert コンポーネント
        const CyberAlert = ({ message, isVisible, onClose }) => {
            if (!isVisible) return null;

            return (
                <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
                    <div className="bg-gray-900 border-2 border-green-500 p-6 rounded-lg shadow-lg max-w-sm w-full mx-4">
                        <p className="text-green-500 mb-4">{message}</p>
                        <div className="flex justify-end">
                        <button
                          onClick={onClose}
                            className="bg-green-500 hover:bg-green-600 text-black px-4 py-2 rounded transition duration-300 ease-in-out transform hover:scale-105"
                     >
                         OK
                        </button>
                        </div>
                    </div>
                </div>
                );
            };

        // Inputコンポーネント
        const Input = ({ className, value, onChange, allowZero = false, ...props }) => {
            const handleFocus = (e) => {
                if (!allowZero && e.target.value === '0') {
                    e.target.value = '';
                }
            };

            const handleBlur = (e) => {
                if (!allowZero && e.target.value === '') {
                    e.target.value = '0';
                    onChange(e);
                }
            };

            return (
                <input
                    className={`cyber-input rounded px-2 py-1 ${className}`}
                    value={value}
                    onChange={onChange}
                    onFocus={handleFocus}
                    onBlur={handleBlur}
                    {...props}
                />
            );
        };

        const NewTaskInput = ({ className, value, onChange, ...props }) => (
            <input
                className={`cyber-input rounded px-2 py-1 ${className}`}
                value={value}
                onChange={onChange}
                {...props}
            />
        );

        const Button = ({ className, children, disabled, ...props }) => (
            <button 
                className={`cyber-button px-3 py-1 rounded ${className} ${
                    disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-green-600 hover:shadow-lg'
                }`}
                disabled={disabled}
                {...props}
            >
                {children}
            </button>
        );

        const Select = ({ value, onValueChange, children }) => (
            <select value={value} onChange={(e) => onValueChange(e.target.value)} className="cyber-input rounded px-2 py-1">
                {children}
            </select>
        );

        const ProgressBar = ({ value, onChange, disabled }) => (
            <div className="flex items-center space-x-2">
                <input
                    type="range"
                    min="0"
                    max="100"
                    value={value}
                    onChange={onChange}
                    disabled={disabled}
                    className="progress-bar"
                    style={{ "--progress": `${value}%` }}
                />
                <span className="text-sm">{value}%</span>
            </div>
        );
        // TodoAppコンポーネント
        const TodoApp = () => {
            const [todos, setTodos] = useState([]);
            const [newTodo, setNewTodo] = useState('');
            const [newEstimate, setNewEstimate] = useState('');
            const [newPriority, setNewPriority] = useState('中');
            const fileInputRef = useRef(null);
            const [newIsNegative, setNewIsNegative] = useState(false);
            const [alertMessage, setAlertMessage] = useState('');
            const [isAlertVisible, setIsAlertVisible] = useState(false);
            const [priorityFilter, setPriorityFilter] = useState('all');

            useEffect(() => {
                const storedTodos = localStorage.getItem('todos');
                if (storedTodos) {
                    setTodos(JSON.parse(storedTodos));
                }
            }, []);

            useEffect(() => {
                localStorage.setItem('todos', JSON.stringify(todos));
            }, [todos]);

            // addTodo 関数
            const addTodo = () => {
                const estimateValue = parseFloat(newEstimate);
                if (newTodo.trim() !== '' && estimateValue && estimateValue !== 0) {
                    const finalEstimate = newIsNegative ? -Math.abs(estimateValue) : estimateValue;
                    setTodos([...todos, { 
                        id: Date.now(), 
                        text: newTodo, 
                        completed: newIsNegative,
                        estimate: finalEstimate,
                        priority: newPriority,
                        progress: newIsNegative ? 100 : 0
                    }]);
                    setNewTodo('');
                    setNewEstimate('');
                    setNewPriority('中');
                    setNewIsNegative(false);
                } else {
                    setAlertMessage('タスク名と0以外の時間を入力してください。');
                    setIsAlertVisible(true);
                }
            };

            const toggleTodo = (id) => {
                setTodos(todos.map(todo => {
                    if (todo.id === id) {
                        if (!todo.completed && todo.estimate >= 0) {
                            // 未完了 → 完了済み
                            return { ...todo, completed: true, progress: 100 };
                        } else if (todo.completed && todo.estimate >= 0) {
                            // 完了済み → しないタスク
                            return { ...todo, completed: true, estimate: -Math.abs(todo.estimate), progress: 100 };
                        } else {
                            // しないタスク → 未完了
                            return { ...todo, completed: false, estimate: Math.abs(todo.estimate), progress: 0 };
                        }
                    }
                    return todo;
                }));
            };

            const deleteTodo = (id) => {
                setTodos(todos.filter(todo => todo.id !== id));
            };

            const updateEstimate = (id, newEstimate) => {
                setTodos(todos.map(todo =>
                    todo.id === id ? { ...todo, estimate: newEstimate === '' ? 0 : parseFloat(newEstimate) } : todo
                ));
            };

            const updatePriority = (id, newPriority) => {
                setTodos(todos.map(todo =>
                    todo.id === id ? { ...todo, priority: newPriority } : todo
                ));
            };

            const updateProgress = (id, newProgress) => {
                setTodos(todos.map(todo =>
                    todo.id === id && todo.estimate >= 0 ? { ...todo, progress: newProgress, completed: newProgress === 100 } : todo
                ));
            };

            const priorityColors = {
                '高': 'text-red-500',
                '中': 'text-yellow-500',
                '低': 'text-blue-500'
            };

            const getRemainingTasksCount = () => {
                return filteredTodos.filter(todo => !todo.completed && todo.estimate >= 0).length;
            };

            const getCompletedTasksCount = () => {
                return filteredTodos.filter(todo => todo.completed && todo.estimate >= 0).length;
            };

            const getNotDoingTasksCount = () => {
                return filteredTodos.filter(todo => todo.estimate < 0).length;
            };
            /*優先度フィルターに応じてタスク数を切り替える実装前
            const getRemainingTasksCount = () => {
                return todos.filter(todo => !todo.completed && todo.estimate >= 0).length;
            };

            const getCompletedTasksCount = () => {
                return todos.filter(todo => todo.completed && todo.estimate >= 0).length;
            };

            const getNotDoingTasksCount = () => {
                return todos.filter(todo => todo.estimate < 0).length;
            };*/

            const calculateRemainingTime = () => {
                return todos
                    .filter(todo => !todo.completed && todo.estimate >= 0)
                    .reduce((total, todo) => total + (todo.estimate || 0), 0)
                    .toFixed(1);
            };

            const calculateCompletedTime = () => {
                return todos
                    .filter(todo => todo.completed && todo.estimate >= 0)
                    .reduce((total, todo) => total + todo.estimate, 0)
                    .toFixed(1);
            };

            const calculateNotDoingTime = () => {
                return Math.abs(todos
                    .filter(todo => todo.estimate < 0)
                    .reduce((total, todo) => total + todo.estimate, 0)
                    .toFixed(1));
            };

            const exportTodos = () => {
                const dataStr = JSON.stringify(todos);
                const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
                const exportFileDefaultName = 'todos.json';

                const linkElement = document.createElement('a');
                linkElement.setAttribute('href', dataUri);
                linkElement.setAttribute('download', exportFileDefaultName);
                linkElement.click();
            };

            const importTodos = (event) => {
                const file = event.target.files[0];
                if (file) {
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        try {
                            const importedTodos = JSON.parse(e.target.result);
                            setTodos(importedTodos);
                        } catch (error) {
                            alert('無効なJSONファイルです。');
                        }
                    };
                    reader.readAsText(file);
                }
            };

            const calculateOverallProgress = () => {
                if (todos.length === 0) return { done: 0, inProgress: 0, notDoing: 0 };
                
                const totalTasks = todos.length;
                const notDoingTasks = todos.filter(todo => todo.estimate < 0);
                const completedTasks = todos.filter(todo => todo.completed && todo.estimate >= 0);
                const inProgressTasks = todos.filter(todo => !todo.completed && todo.estimate >= 0);
                
                const notDoingPercentage = (notDoingTasks.length / totalTasks) * 100;
                const completedPercentage = (completedTasks.length / totalTasks) * 100;
                
                const inProgressPercentage = inProgressTasks.reduce((sum, todo) => {
                    return sum + (todo.progress / totalTasks);
                }, 0);
                
                return {
                    done: Math.round(completedPercentage),
                    inProgress: Math.round(inProgressPercentage),
                    notDoing: Math.round(notDoingPercentage)
                };
            };

            const handleKeyPress = (event, action) => {
                if (event.key === 'Enter') {
                    action();
                }
            };

            const isAddButtonDisabled = () => {
                return !newTodo.trim() || !newEstimate || parseFloat(newEstimate) <= 0;
            };

            const filteredTodos = priorityFilter === 'all'
            ? todos
            : todos.filter(todo => todo.priority === priorityFilter);

            return (
                <div className="max-w-full sm:max-w-4xl mx-auto mt-4 sm:mt-10 p-2 sm:p-6 bg-gray-900 rounded-lg shadow-lg border border-green-500">
                    <div className="text-center mb-6">
                        <h1 className="text-2xl sm:text-3xl font-bold cyber-glow">ゆるToDo</h1>
                        <p className="text-xs sm:text-sm mt-2 cyber-subtitle">
                            <span className="cyber-bracket">[</span>
                            すること・しないことも管理
                            <span className="cyber-bracket">]</span>
                        </p>
                    </div>
                    <div className="flex flex-wrap mb-4 space-y-2 sm:space-y-0 sm:space-x-2">
                        <NewTaskInput
                            type="text"
                            value={newTodo}
                            onChange={(e) => setNewTodo(e.target.value)}
                            placeholder="新しいタスクを入力..."
                            className="flex-grow min-w-0 text-sm sm:text-base"
                        />
                        <div className="flex w-full sm:w-auto space-x-2">
                            <NewTaskInput
                                type="number"
                                value={newEstimate}
                                onChange={(e) => setNewEstimate(e.target.value)}
                                placeholder="時間(必須)"
                                className="w-20 sm:w-16 text-sm sm:text-base"
                                min="0.1"
                                step="0.1"
                                required
                            />
                            <Select 
                                value={newPriority} 
                                onValueChange={setNewPriority}
                                className="w-20 sm:w-16 text-sm sm:text-base"
                            >
                                <option value="高">高</option>
                                <option value="中">中</option>
                                <option value="低">低</option>
                            </Select>
                            <Button 
                                onClick={() => setNewIsNegative(!newIsNegative)} 
                                onKeyPress={(e) => handleKeyPress(e, () => setNewIsNegative(!newIsNegative))}
                                className={`w-1/3 sm:w-auto ${newIsNegative ? 'bg-red-500' : ''}`}
                                tabIndex="0"
                            >
                                {newIsNegative ? 'しない' : 'する'}
                            </Button>

                            <Button 
                                onClick={addTodo} 
                                className="whitespace-nowrap text-sm sm:text-base"
                                disabled={isAddButtonDisabled()}
                            >
                                + 追加
                            </Button>
                        </div>
                    </div>
                    <div className="cyber-stats mb-4 p-3 rounded">
                        {priorityFilter !== 'all' && (
                        <p className="text-sm sm:text-base mb-2">
                            現在、優先度「{priorityFilter}」のタスクのみ表示しています。
                        </p>
                        )}
                        <div className="grid grid-cols-3 gap-4 text-xs sm:text-sm">
                            <div className="bg-gray-800 p-3 rounded">
                                <h3 className="text-base font-bold mb-2 text-green-400">
                                    <span className="hidden sm:inline">未完了タスク</span>
                                    <span className="sm:hidden">未完了</span>
                                </h3>
                                <div className="flex items-center mb-1">
                                    <span className="mr-2">📋</span>
                                    <span className="font-bold text-lg">{getRemainingTasksCount()}</span>
                                </div>
                                <div className="flex items-center">
                                    <span className="mr-2">⏱️</span>
                                    <span className="font-bold text-lg">{calculateRemainingTime()} h</span>
                                </div>
                            </div>
                            <div className="bg-gray-800 p-3 rounded">
                                <h3 className="text-base font-bold mb-2 text-blue-400">
                                    <span className="hidden sm:inline">完了済タスク</span>
                                    <span className="sm:hidden">完了済</span>
                                </h3>
                                <div className="flex items-center mb-1">
                                    <span className="mr-2">✅</span>
                                    <span className="font-bold text-lg">{getCompletedTasksCount()}</span>
                                </div>
                                <div className="flex items-center">
                                    <span className="mr-2">⏱️</span>
                                    <span className="font-bold text-lg">{calculateCompletedTime()} h</span>
                                </div>
                            </div>
                            <div className="bg-gray-800 p-3 rounded">
                                <h3 className="text-base font-bold mb-2 text-red-400">
                                    <span className="hidden sm:inline">しないタスク</span>
                                    <span className="sm:hidden">しない</span>
                                </h3>
                                <div className="flex items-center mb-1">
                                    <span className="mr-2">🚫</span>
                                    <span className="font-bold text-lg">{getNotDoingTasksCount()}</span>
                                </div>
                                <div className="flex items-center">
                                    <span className="mr-2">⏱️</span>
                                    <span className="font-bold text-lg">{calculateNotDoingTime()} h</span>
                                </div>
                            </div>
                        </div>
                        <div className="mt-3">
                            <div className="text-sm mb-1">
                                全体の進捗: {calculateOverallProgress().done + calculateOverallProgress().inProgress}% 完了 / {calculateOverallProgress().notDoing}% しない
                            </div>
                            <div className="w-full bg-gray-700 rounded-full h-2.5 relative overflow-hidden">
                                <div 
                                    className="absolute top-0 left-0 bg-green-600 h-full" 
                                    style={{width: `${calculateOverallProgress().done}%`}}
                                ></div>
                                <div 
                                    className="absolute top-0 left-0 bg-yellow-400 h-full" 
                                    style={{width: `${calculateOverallProgress().inProgress}%`, left: `${calculateOverallProgress().done}%`}}
                                ></div>
                                <div 
                                    className="absolute top-0 right-0 bg-red-600 h-full" 
                                    style={{width: `${calculateOverallProgress().notDoing}%`}}
                                ></div>
                            </div>
                        </div>
                    </div>
                    <div className="mb-4">
                        <label htmlFor="priority-filter" className="mr-2 text-sm sm:text-base">優先度フィルター:</label>
                        <select
                            id="priority-filter"
                            value={priorityFilter}
                            onChange={(e) => setPriorityFilter(e.target.value)}
                            className="cyber-input rounded px-2 py-1 text-sm sm:text-base"
                        >
                            <option value="all">全て</option>
                            <option value="高">高</option>
                            <option value="中">中</option>
                            <option value="低">低</option>
                        </select>
                        </div>
                    <ul className="space-y-2 mb-4">
                        {filteredTodos.map(todo => (
                            <li key={todo.id} className={`cyber-list-item p-2 sm:p-3 rounded text-xs sm:text-sm ${
                            todo.estimate < 0 ? 'bg-red-900' : todo.completed ? 'bg-green-900' : ''
                            }`}>
                                <div className="flex items-center mb-2">
                                    <button
                                        onClick={() => toggleTodo(todo.id)}
                                        className={`mr-2 flex-shrink-0 w-6 h-6 rounded-full border-2 ${
                                            todo.estimate < 0 ? 'border-red-500 bg-red-500' :
                                            todo.completed ? 'border-green-500 bg-green-500' : 'border-yellow-500'
                                        }`}
                                    >
                                        {todo.estimate < 0 ? '✗' : todo.completed ? '✓' : ''}
                                    </button>
                                    <span 
                                        className={`task-text flex-grow mr-2 ${todo.completed || todo.estimate < 0 ? 'line-through' : ''} ${
                                            todo.estimate < 0 ? 'text-red-500' : todo.completed ? 'text-green-500' : ''
                                        }`}
                                        title={todo.text}
                                    >
                                        {todo.text}
                                    </span>
                                    <div className="flex items-center space-x-2 ml-auto">
                                        <div className="flex items-center">
                                            <span className="mr-1">⏱️</span>
                                            <Input
                                                type="number"
                                                value={Math.abs(todo.estimate)}
                                                onChange={(e) => updateEstimate(todo.id, todo.estimate < 0 ? -Math.abs(e.target.value) : Math.abs(e.target.value))}
                                                className="w-16 sm:w-16 text-sm"
                                                min="0"
                                                step="0.1"
                                            />
                                            <span className="ml-1 text-sm">h</span>
                                        </div>
                                        <Select 
                                            value={todo.priority} 
                                            onValueChange={(value) => updatePriority(todo.id, value)}
                                            className="w-16 sm:w-14 text-sm"
                                        >
                                            <option value="高">高</option>
                                            <option value="中">中</option>
                                            <option value="低">低</option>
                                        </Select>
                                        <span className={`flex-shrink-0 ${priorityColors[todo.priority]}`}>⚑</span>
                                        <Button onClick={() => deleteTodo(todo.id)} className="text-red-500 hover:text-red-700 flex-shrink-0 text-sm">
                                            ✗
                                        </Button>
                                    </div>
                                </div>
                                <ProgressBar 
                                    value={todo.progress} 
                                    onChange={(e) => updateProgress(todo.id, parseInt(e.target.value))} 
                                    disabled={todo.completed || todo.estimate < 0}
                                />
                            </li>
                        ))}
                    </ul>
                    <div className="flex justify-between mb-4">
                        <Button onClick={exportTodos} className="mr-2">
                            エクスポート
                        </Button>
                        <div>
                            <input
                                type="file"
                                ref={fileInputRef}
                                style={{ display: 'none' }}
                                onChange={importTodos}
                                accept=".json"
                            />
                            <Button onClick={() => fileInputRef.current.click()}>
                                インポート
                            </Button>
                        </div>
                    </div>
                    <div className="text-center text-xs sm:text-sm text-gray-500 mb-2">
                        &copy; 2024 ゆるToDo All Rights Reserved.
                        <br />
                        制作: たぬ | コーディング協力: Claude 3.5
                        <br />
                        <span className="text-xs">Version 1.3.1 "Cyber ToDo" (2024-06-24 0:52)</span>
                    </div>
                    <div className="text-center text-xs text-gray-400 mt-4">
                        <p>This application uses React, ReactDOM, Babel, and Tailwind CSS, which are licensed under the MIT License.</p>
                        <p>
                            <a href="https://opensource.org/licenses/MIT" className="underline hover:text-gray-300" target="_blank" rel="noopener noreferrer">
                                MIT License
                            </a>
                        </p>
                    </div>
                    <div className="text-xs text-gray-400 mt-2">
                        <p>• データはローカルストレージに保存されます</p>
                        <p>• 本アプリの使用は自己責任でお願いします</p>
                    </div>
                    
                    <CyberAlert
                        message={alertMessage}
                        isVisible={isAlertVisible}
                        onClose={() => setIsAlertVisible(false)}
                    />

                </div>
            );
        };

ReactDOM.render(<TodoApp />, document.getElementById('root'));
</script>
</body>
</html>

この記事が気に入ったらサポートをしてみませんか?