Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Modern Todo App</title> | |
| <!-- Import Remix Icon for high-quality icons --> | |
| <link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet"> | |
| <style> | |
| /* --- CSS Variables & Reset --- */ | |
| :root { | |
| /* Light Theme */ | |
| --primary: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --bg-body: #f3f4f6; | |
| --bg-card: #ffffff; | |
| --text-main: #1f2937; | |
| --text-muted: #6b7280; | |
| --border: #e5e7eb; | |
| --danger: #ef4444; | |
| --success: #10b981; | |
| --shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| --radius: 16px; | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| [data-theme="dark"] { | |
| /* Dark Theme */ | |
| --primary: #818cf8; | |
| --primary-hover: #6366f1; | |
| --bg-body: #111827; | |
| --bg-card: #1f2937; | |
| --text-main: #f9fafb; | |
| --text-muted: #9ca3af; | |
| --border: #374151; | |
| --shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 2rem 1rem; | |
| transition: background-color 0.3s ease, color 0.3s ease; | |
| } | |
| /* --- Header & Branding --- */ | |
| header { | |
| width: 100%; | |
| max-width: 600px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 2rem; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| color: var(--primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| background: rgba(99, 102, 241, 0.1); | |
| padding: 0.4rem 0.8rem; | |
| border-radius: 20px; | |
| transition: var(--transition); | |
| font-weight: 500; | |
| } | |
| .anycoder-link:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| color: var(--primary); | |
| } | |
| .theme-toggle { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--text-main); | |
| font-size: 1.25rem; | |
| padding: 0.5rem; | |
| border-radius: 50%; | |
| transition: var(--transition); | |
| } | |
| .theme-toggle:hover { | |
| background-color: rgba(0,0,0,0.05); | |
| } | |
| /* --- Main Container --- */ | |
| .app-container { | |
| width: 100%; | |
| max-width: 600px; | |
| background: var(--bg-card); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* --- Progress Section --- */ | |
| .progress-section { | |
| padding: 1.5rem 2rem 1rem; | |
| } | |
| .progress-labels { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 0.5rem; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| } | |
| .progress-bar-bg { | |
| width: 100%; | |
| height: 8px; | |
| background-color: var(--border); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .progress-bar-fill { | |
| height: 100%; | |
| background-color: var(--primary); | |
| width: 0%; | |
| transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* --- Input Area --- */ | |
| .input-area { | |
| padding: 1rem 2rem; | |
| display: flex; | |
| gap: 0.75rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| flex-grow: 1; | |
| } | |
| .input-wrapper i { | |
| position: absolute; | |
| left: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| } | |
| input[type="text"] { | |
| width: 100%; | |
| padding: 0.8rem 1rem 0.8rem 2.5rem; | |
| border: 2px solid var(--border); | |
| border-radius: 12px; | |
| background: var(--bg-body); | |
| color: var(--text-main); | |
| font-size: 1rem; | |
| outline: none; | |
| transition: var(--transition); | |
| } | |
| input[type="text"]:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); | |
| } | |
| .add-btn { | |
| background-color: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 0 1.5rem; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .add-btn:hover { | |
| background-color: var(--primary-hover); | |
| transform: translateY(-1px); | |
| } | |
| .add-btn:active { | |
| transform: translateY(1px); | |
| } | |
| /* --- Filters --- */ | |
| .filters { | |
| display: flex; | |
| padding: 1rem 2rem; | |
| gap: 1rem; | |
| border-bottom: 1px solid var(--border); | |
| overflow-x: auto; | |
| } | |
| .filter-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| cursor: pointer; | |
| padding-bottom: 0.25rem; | |
| position: relative; | |
| transition: color 0.3s; | |
| } | |
| .filter-btn.active { | |
| color: var(--primary); | |
| } | |
| .filter-btn.active::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -4px; | |
| left: 0; | |
| width: 100%; | |
| height: 2px; | |
| background-color: var(--primary); | |
| border-radius: 2px; | |
| } | |
| /* --- Task List --- */ | |
| .todo-list { | |
| list-style: none; | |
| padding: 0; | |
| max-height: 50vh; | |
| overflow-y: auto; | |
| } | |
| .todo-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 1rem 2rem; | |
| border-bottom: 1px solid var(--border); | |
| transition: var(--transition); | |
| animation: slideIn 0.3s ease forwards; | |
| } | |
| .todo-item:last-child { | |
| border-bottom: none; | |
| } | |
| .todo-item:hover { | |
| background-color: rgba(0,0,0,0.02); | |
| } | |
| .todo-item.slide-out { | |
| animation: slideOut 0.3s ease forwards; | |
| } | |
| /* Custom Checkbox */ | |
| .checkbox-wrapper { | |
| position: relative; | |
| width: 22px; | |
| height: 22px; | |
| margin-right: 1rem; | |
| cursor: pointer; | |
| } | |
| .checkbox-wrapper input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .checkmark { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 22px; | |
| width: 22px; | |
| background-color: var(--bg-body); | |
| border: 2px solid var(--border); | |
| border-radius: 6px; | |
| transition: var(--transition); | |
| } | |
| .checkbox-wrapper:hover .checkmark { | |
| border-color: var(--primary); | |
| } | |
| .checkbox-wrapper input:checked ~ .checkmark { | |
| background-color: var(--primary); | |
| border-color: var(--primary); | |
| } | |
| .checkmark:after { | |
| content: ""; | |
| position: absolute; | |
| display: none; | |
| left: 7px; | |
| top: 3px; | |
| width: 5px; | |
| height: 10px; | |
| border: solid white; | |
| border-width: 0 2px 2px 0; | |
| transform: rotate(45deg); | |
| } | |
| .checkbox-wrapper input:checked ~ .checkmark:after { | |
| display: block; | |
| } | |
| /* Task Content */ | |
| .task-content { | |
| flex-grow: 1; | |
| font-size: 1rem; | |
| color: var(--text-main); | |
| transition: var(--transition); | |
| word-break: break-all; | |
| } | |
| .todo-item.completed .task-content { | |
| text-decoration: line-through; | |
| color: var(--text-muted); | |
| opacity: 0.7; | |
| } | |
| .edit-input { | |
| width: 100%; | |
| padding: 0.5rem; | |
| font-size: 1rem; | |
| border: 1px solid var(--primary); | |
| border-radius: 6px; | |
| outline: none; | |
| background: var(--bg-body); | |
| color: var(--text-main); | |
| } | |
| /* Actions */ | |
| .actions { | |
| display: flex; | |
| gap: 0.5rem; | |
| opacity: 0; /* Hidden by default for cleaner look */ | |
| transition: opacity 0.2s; | |
| } | |
| .todo-item:hover .actions { | |
| opacity: 1; | |
| } | |
| @media (max-width: 600px) { | |
| .actions { opacity: 1; } /* Always show actions on touch */ | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--text-muted); | |
| padding: 0.4rem; | |
| border-radius: 6px; | |
| transition: var(--transition); | |
| } | |
| .action-btn:hover { | |
| background-color: rgba(0,0,0,0.05); | |
| color: var(--text-main); | |
| } | |
| .action-btn.delete:hover { | |
| background-color: rgba(239, 68, 68, 0.1); | |
| color: var(--danger); | |
| } | |
| /* --- Empty State --- */ | |
| .empty-state { | |
| padding: 3rem 1rem; | |
| text-align: center; | |
| color: var(--text-muted); | |
| display: none; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| display: block; | |
| opacity: 0.5; | |
| } | |
| /* --- Footer Stats --- */ | |
| .app-footer { | |
| padding: 1rem 2rem; | |
| background-color: var(--bg-body); | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| } | |
| .clear-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: var(--transition); | |
| } | |
| .clear-btn:hover { | |
| color: var(--danger); | |
| text-decoration: underline; | |
| } | |
| /* --- Toast Notification --- */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| z-index: 1000; | |
| } | |
| .toast { | |
| background-color: #333; | |
| color: white; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 50px; | |
| font-size: 0.9rem; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| animation: fadeIn 0.3s ease, fadeOut 0.3s ease 2.7s forwards; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| /* --- Animations --- */ | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideOut { | |
| from { opacity: 1; transform: translateX(0); } | |
| to { opacity: 0; transform: translateX(50px); } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes fadeOut { | |
| from { opacity: 1; } | |
| to { opacity: 0; } | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="ri-checkbox-circle-fill"></i> TaskMaster | |
| </div> | |
| <div style="display: flex; gap: 1rem; align-items: center;"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| <button class="theme-toggle" id="themeToggle" aria-label="Toggle Dark Mode"> | |
| <i class="ri-moon-line"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <main class="app-container"> | |
| <!-- Progress Section --> | |
| <section class="progress-section"> | |
| <div class="progress-labels"> | |
| <span id="progressText">0/0 Completed</span> | |
| <span id="progressPercent">0%</span> | |
| </div> | |
| <div class="progress-bar-bg"> | |
| <div class="progress-bar-fill" id="progressBar"></div> | |
| </div> | |
| </section> | |
| <!-- Input Area --> | |
| <section class="input-area"> | |
| <div class="input-wrapper"> | |
| <i class="ri-add-line"></i> | |
| <input type="text" id="todoInput" placeholder="Add a new task..." autocomplete="off"> | |
| </div> | |
| <button class="add-btn" id="addBtn"> | |
| <i class="ri-add-line"></i> <span>Add</span> | |
| </button> | |
| </section> | |
| <!-- Filters --> | |
| <nav 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> | |
| </nav> | |
| <!-- List --> | |
| <ul class="todo-list" id="todoList"> | |
| <!-- Items injected via JS --> | |
| </ul> | |
| <!-- Empty State --> | |
| <div class="empty-state" id="emptyState"> | |
| <i class="ri-clipboard-line"></i> | |
| <p>No tasks found. Add one to get started!</p> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="app-footer"> | |
| <span id="itemsLeft">0 items left</span> | |
| <button class="clear-btn" id="clearCompletedBtn">Clear Completed</button> | |
| </footer> | |
| </main> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <script> | |
| // --- State Management --- | |
| let todos = JSON.parse(localStorage.getItem('todos')) || []; | |
| let currentFilter = 'all'; | |
| // --- DOM Elements --- | |
| const todoInput = document.getElementById('todoInput'); | |
| const addBtn = document.getElementById('addBtn'); | |
| const todoList = document.getElementById('todoList'); | |
| const itemsLeft = document.getElementById('itemsLeft'); | |
| const clearCompletedBtn = document.getElementById('clearCompletedBtn'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressText = document.getElementById('progressText'); | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const themeToggle = document.getElementById('themeToggle'); | |
| const toastContainer = document.getElementById('toastContainer'); | |
| // --- Initialization --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| render(); | |
| loadTheme(); | |
| }); | |
| // --- Event Listeners --- | |
| addBtn.addEventListener('click', addTodo); | |
| todoInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') addTodo(); | |
| }); | |
| clearCompletedBtn.addEventListener('click', () => { | |
| const completedCount = todos.filter(t => t.completed).length; | |
| if (completedCount === 0) { | |
| showToast('No completed tasks to clear', 'info'); | |
| return; | |
| } | |
| todos = todos.filter(todo => !todo.completed); | |
| saveAndRender(); | |
| showToast(`${completedCount} tasks cleared`, 'success'); | |
| }); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| // Update UI classes | |
| filterBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Update state | |
| currentFilter = btn.getAttribute('data-filter'); | |
| render(); | |
| }); | |
| }); | |
| themeToggle.addEventListener('click', () => { | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const newTheme = isDark ? 'light' : 'dark'; | |
| document.documentElement.setAttribute('data-theme', newTheme); | |
| themeToggle.innerHTML = isDark ? '<i class="ri-moon-line"></i>' : '<i class="ri-sun-line"></i>'; | |
| localStorage.setItem('theme', newTheme); | |
| }); | |
| // --- Core Functions --- | |
| function addTodo() { | |
| const text = todoInput.value.trim(); | |
| if (text === '') { | |
| showToast('Please enter a task', 'warning'); | |
| return; | |
| } | |
| const newTodo = { | |
| id: Date.now(), | |
| text: text, | |
| completed: false, | |
| createdAt: new Date().toISOString() | |
| }; | |
| todos.unshift(newTodo); // Add to top | |
| todoInput.value = ''; | |
| saveAndRender(); | |
| showToast('Task added successfully', 'success'); | |
| } | |
| function toggleTodo(id) { | |
| todos = todos.map(todo => { | |
| if (todo.id === id) { | |
| return { ...todo, completed: !todo.completed }; | |
| } | |
| return todo; | |
| }); | |
| saveAndRender(); | |
| } | |
| function deleteTodo(id) { | |
| // Find element to animate out | |
| const itemElement = document.querySelector(`[data-id="${id}"]`); | |
| if (itemElement) { | |
| itemElement.classList.add('slide-out'); | |
| itemElement.addEventListener('animationend', () => { | |
| todos = todos.filter(todo => todo.id !== id); | |
| saveAndRender(); | |
| showToast('Task deleted', 'info'); | |
| }); | |
| } else { | |
| todos = todos.filter(todo => todo.id !== id); | |
| saveAndRender(); | |
| } | |
| } | |
| function editTodo(id) { | |
| const todo = todos.find(t => t.id === id); | |
| if (!todo) return; | |
| const li = document.querySelector(`[data-id="${id}"]`); | |
| const span = li.querySelector('.task-content'); | |
| // Create input | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.value = todo.text; | |
| input.className = 'edit-input'; | |
| // Replace span with input | |
| span.replaceWith(input); | |
| input.focus(); | |
| // Save on blur or enter | |
| const saveEdit = () => { | |
| const newText = input.value.trim(); | |
| if (newText) { | |
| todos = todos.map(t => t.id === id ? { ...t, text: newText } : t); | |
| saveAndRender(); | |
| } else { | |
| render(); // Revert if empty | |
| } | |
| }; | |
| input.addEventListener('blur', saveEdit); | |
| input.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| input.blur(); | |
| } | |
| }); | |
| } | |
| function saveAndRender() { | |
| localStorage.setItem('todos', JSON.stringify(todos)); | |
| render(); | |
| } | |
| function render() { | |
| // 1. Filter List | |
| let filteredTodos = todos; | |
| if (currentFilter === 'active') { | |
| filteredTodos = todos.filter(t => !t.completed); | |
| } else if (currentFilter === 'completed') { | |
| filteredTodos = todos.filter(t => t.completed); | |
| } | |
| // 2. Clear DOM | |
| todoList.innerHTML = ''; | |
| // 3. Check Empty State | |
| if (filteredTodos.length === 0) { | |
| emptyState.style.display = 'block'; | |
| } else { | |
| emptyState.style.display = 'none'; | |
| } | |
| // 4. Build DOM | |
| filteredTodos.forEach(todo => { | |
| const li = document.createElement('li'); | |
| li.className = `todo-item ${todo.completed ? 'completed' : ''}`; | |
| li.setAttribute('data-id', todo.id); | |
| li.innerHTML = ` | |
| <label class="checkbox-wrapper"> | |
| <input type="checkbox" ${todo.completed ? 'checked' : ''}> | |
| <span class="checkmark"></span> | |
| </label> | |
| <span class="task-content">${escapeHtml(todo.text)}</span> | |
| <div class="actions"> | |
| <button class="action-btn edit" aria-label="Edit"> | |
| <i class="ri-pencil-line"></i> | |
| </button> | |
| <button class="action-btn delete" aria-label="Delete"> | |
| <i class="ri-delete-bin-line"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Attach specific listeners to elements inside | |
| const checkbox = li.querySelector('input[type="checkbox"]'); | |
| checkbox.addEventListener('change', () => toggleTodo(todo.id)); | |
| const deleteBtn = li.querySelector('.delete'); | |
| deleteBtn.addEventListener('click', () => deleteTodo(todo.id)); | |
| const editBtn = li.querySelector('.edit'); | |
| editBtn.addEventListener('click', () => editTodo(todo.id)); | |
| todoList.appendChild(li); | |
| }); | |
| updateStats(); | |
| } | |
| function updateStats() { | |
| const activeCount = todos.filter(t => !t.completed).length; | |
| const totalCount = todos.length; | |
| const completedCount = totalCount - activeCount; | |
| const percentage = totalCount === 0 ? 0 : Math.round((completedCount / totalCount) * 100); | |
| // Footer Text | |
| itemsLeft.textContent = `${activeCount} item${activeCount !== 1 ? 's' : ''} left`; | |
| // Progress Bar | |
| progressBar.style.width = `${percentage}%`; | |
| progressText.textContent = `${completedCount}/${totalCount} Completed`; | |
| progressPercent.textContent = `${percentage}%`; | |
| } | |
| // --- Helper Functions --- | |
| function loadTheme() { | |
| const savedTheme = localStorage.getItem('theme') || 'light'; | |
| document.documentElement.setAttribute('data-theme', savedTheme); | |
| themeToggle.innerHTML = savedTheme === 'dark' ? '<i class="ri-sun-line"></i>' : '<i class="ri-moon-line"></i>'; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| let icon = 'ri-information-line'; | |
| if (type === 'success') icon = 'ri-checkbox-circle-line'; | |
| if (type === 'warning') icon = 'ri-error-warning-line'; | |
| toast.innerHTML = `<i class="${icon}"></i> ${message}`; | |
| toastContainer.appendChild(toast); | |
| // Remove after animation (3s total: 0.3 in + 2.4 wait + 0.3 out) | |
| setTimeout(() => { | |
| toast.remove(); | |
| }, 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> |