Windsurf × Todoアプリ開発レポート

Windsurf

AIって本当にコード書けるの?
そんな疑問を胸に、今回は話題のAI「Windsurf」にTodoリストアプリを作ってもらいました。HTML・CSS・JavaScriptで構成されたシンプルなアプリですが、意外と使える機能が盛りだくさん。この記事では、その成果物とプロンプト、コードの中身までまるっとご紹介します!


シンプルだけど侮れないTodoアプリ

今回のTodoリストアプリは、以下の3つのファイルで構成されています。

  • index.html(画面の構造)
  • styles.css(デザイン)
  • script.js(動きやデータの処理)

主な機能は以下の通りです。

  • タスクの追加:テキストを入力して「Add」を押すだけ!
  • 完了マーク:チェックボックスでタスクの完了状態を切り替え
  • 削除:不要なタスクはゴミ箱ボタンで削除
  • フィルタリング:すべて・未完了・完了済みの切り替えが可能

しかも、タスクの状態はローカルストレージに保存されるので、ページを再読み込みしてもデータが消えません。地味にうれしいポイントです。


プロンプト:実際に投げたのはこちら!

AIにお願いしたプロンプトはとてもシンプルです。

Create a simple Todo List app using HTML, CSS, and JavaScript.
Include functionality to add, delete, and mark tasks as completed.

直訳すると「HTML・CSS・JSで、追加・削除・完了マークができるTodoアプリ作って!」って感じですね。これだけで、AIが3ファイルをまるっと生成してくれました。驚きです。


コードの中身紹介:それぞれのファイルのポイント

index.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 App</title>
    <link rel="stylesheet" href="styles.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
    <div class="container">
        <h1>Todo List</h1>
        <div class="todo-input">
            <input type="text" id="task-input" placeholder="Add a new task...">
            <button id="add-button">Add</button>
        </div>
        <div class="filters">
            <button class="filter-btn active" data-filter="all">All</button>
            <button class="filter-btn" data-filter="active">Active</button>
            <button class="filter-btn" data-filter="completed">Completed</button>
        </div>
        <ul id="todo-list">
            <!-- Tasks will be added here dynamically -->
        </ul>
        <div class="todo-stats">
            <span id="tasks-counter">0 tasks left</span>
            <button id="clear-completed">Clear Completed</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

HTMLでは、入力フォーム、フィルター、リスト、統計表示などを配置しています。

<input type="text" id="task-input" placeholder="Add a new task...">
<button id="add-button">Add</button>

ポイント💡
👉 入力欄とボタンは一列に並ぶ設計。初心者にも扱いやすい見た目です。

styles.css:シンプルかつ実用的なデザイン

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Arial', sans-serif;
}

body {
    background-color: #f5f5f5;
    display: flex;
    justify-content: center;
    padding-top: 50px;
    min-height: 100vh;
}

.container {
    width: 100%;
    max-width: 500px;
    background-color: white;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    padding: 20px;
}

h1 {
    text-align: center;
    color: #333;
    margin-bottom: 20px;
}

.todo-input {
    display: flex;
    margin-bottom: 20px;
}

#task-input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px 0 0 4px;
    font-size: 16px;
}

#add-button {
    padding: 10px 15px;
    background-color: #4caf50;
    color: white;
    border: none;
    border-radius: 0 4px 4px 0;
    cursor: pointer;
    font-size: 16px;
}

#add-button:hover {
    background-color: #45a049;
}

.filters {
    display: flex;
    justify-content: center;
    margin-bottom: 15px;
    gap: 10px;
}

.filter-btn {
    padding: 8px 12px;
    background-color: transparent;
    border: 1px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
}

.filter-btn.active {
    background-color: #4caf50;
    color: white;
    border-color: #4caf50;
}

#todo-list {
    list-style-type: none;
    margin-bottom: 20px;
}

.todo-item {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid #eee;
}

.todo-item.completed .todo-text {
    text-decoration: line-through;
    color: #888;
}

.todo-checkbox {
    margin-right: 10px;
    width: 20px;
    height: 20px;
    cursor: pointer;
}

.todo-text {
    flex: 1;
    font-size: 16px;
}

.delete-btn {
    color: #e74c3c;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 18px;
}

.todo-stats {
    display: flex;
    justify-content: space-between;
    color: #777;
    font-size: 14px;
}

#clear-completed {
    background: none;
    border: none;
    color: #777;
    cursor: pointer;
    font-size: 14px;
}

#clear-completed:hover {
    text-decoration: underline;
}

グリーンを基調にしたカラーリングで、目にもやさしい印象です。ホバー効果もあり、操作感がしっかりしています。

.container {
    max-width: 500px;
    background-color: white;
    border-radius: 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

script.js:アプリの心臓部

document.addEventListener('DOMContentLoaded', () => {
    // DOM Elements
    const taskInput = document.getElementById('task-input');
    const addButton = document.getElementById('add-button');
    const todoList = document.getElementById('todo-list');
    const tasksCounter = document.getElementById('tasks-counter');
    const clearCompletedBtn = document.getElementById('clear-completed');
    const filterButtons = document.querySelectorAll('.filter-btn');
    
    // App State
    let todos = JSON.parse(localStorage.getItem('todos')) || [];
    let currentFilter = 'all';
    
    // Initialize app
    renderTodos();
    updateTasksCounter();
    
    // Event Listeners
    addButton.addEventListener('click', addTodo);
    taskInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            addTodo();
        }
    });
    
    clearCompletedBtn.addEventListener('click', clearCompleted);
    
    filterButtons.forEach(btn => {
        btn.addEventListener('click', () => {
            filterButtons.forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            currentFilter = btn.getAttribute('data-filter');
            renderTodos();
        });
    });
    
    // Functions
    function addTodo() {
        const taskText = taskInput.value.trim();
        
        if (taskText === '') return;
        
        const newTodo = {
            id: Date.now(),
            text: taskText,
            completed: false
        };
        
        todos.push(newTodo);
        saveTodos();
        renderTodos();
        updateTasksCounter();
        
        taskInput.value = '';
        taskInput.focus();
    }
    
    function deleteTodo(id) {
        todos = todos.filter(todo => todo.id !== id);
        saveTodos();
        renderTodos();
        updateTasksCounter();
    }
    
    function toggleTodo(id) {
        todos = todos.map(todo => {
            if (todo.id === id) {
                return { ...todo, completed: !todo.completed };
            }
            return todo;
        });
        
        saveTodos();
        renderTodos();
        updateTasksCounter();
    }
    
    function clearCompleted() {
        todos = todos.filter(todo => !todo.completed);
        saveTodos();
        renderTodos();
        updateTasksCounter();
    }
    
    function saveTodos() {
        localStorage.setItem('todos', JSON.stringify(todos));
    }
    
    function renderTodos() {
        todoList.innerHTML = '';
        
        let filteredTodos = todos;
        
        if (currentFilter === 'active') {
            filteredTodos = todos.filter(todo => !todo.completed);
        } else if (currentFilter === 'completed') {
            filteredTodos = todos.filter(todo => todo.completed);
        }
        
        filteredTodos.forEach(todo => {
            const todoItem = document.createElement('li');
            todoItem.classList.add('todo-item');
            if (todo.completed) {
                todoItem.classList.add('completed');
            }
            
            todoItem.innerHTML = `
                <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
                <span class="todo-text">${todo.text}</span>
                <button class="delete-btn"><i class="fas fa-trash"></i></button>
            `;
            
            const checkbox = todoItem.querySelector('.todo-checkbox');
            const deleteBtn = todoItem.querySelector('.delete-btn');
            
            checkbox.addEventListener('change', () => toggleTodo(todo.id));
            deleteBtn.addEventListener('click', () => deleteTodo(todo.id));
            
            todoList.appendChild(todoItem);
        });
    }
    
    function updateTasksCounter() {
        const activeTasks = todos.filter(todo => !todo.completed).length;
        tasksCounter.textContent = `${activeTasks} task${activeTasks !== 1 ? 's' : ''} left`;
    }
});

この一行で、ローカルストレージから過去のデータを読み込みつつ、なければ空の配列でスタートする仕組みが作られています。

let todos = JSON.parse(localStorage.getItem('todos')) || [];

また、タスクの追加・削除・完了切り替えなどの関数も以下のように整理されています。

function addTodo() { ... }
function deleteTodo(id) { ... }
function toggleTodo(id) { ... }

関数がしっかり分割されていて読みやすく、拡張もしやすい構成です。

フィルター機能の実装も秀逸で、「すべて」「未完了」「完了済み」の切り替えがボタン操作でできるのが便利です。


まとめ:AIと一緒にモノづくりしてみよう!

今回のTodoアプリ、思った以上にしっかり使えるものでした。何より、ほんの一言のプロンプトでここまで作ってくれるというのがすごいですよね。

WindsurfのようなAIは、初心者の「最初の一歩」を後押ししてくれる相棒になってくれます。「自分で全部作れないとダメ」じゃなくて、「まずはAIに手伝ってもらおう」くらいの気持ちでOKです。

次はメモアプリや習慣トラッカーも試してみたいなぁと思っています。

ぜひ、あなたもWindsurfと一緒に、ちょっとしたアプリづくりから始めてみてください!

タイトルとURLをコピーしました