Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Modern To-Do App</title> | |
| <!-- Importing Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Importing FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| /* Color Palette */ | |
| --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --secondary-gradient: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); | |
| --bg-color: #f3f4f6; | |
| --glass-bg: rgba(255, 255, 255, 0.85); | |
| --glass-border: rgba(255, 255, 255, 0.5); | |
| --text-main: #1f2937; | |
| --text-secondary: #6b7280; | |
| --accent-color: #764ba2; | |
| --danger-color: #ef4444; | |
| --success-color: #10b981; | |
| --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| body { | |
| background: var(--bg-color); | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| background-image: | |
| radial-gradient(circle at 10% 20%, rgba(118, 75, 162, 0.1) 0%, transparent 20%), | |
| radial-gradient(circle at 90% 80%, rgba(102, 126, 234, 0.1) 0%, transparent 20%); | |
| } | |
| /* App Container */ | |
| .app-container { | |
| width: 100%; | |
| max-width: 500px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 24px; | |
| box-shadow: var(--shadow-lg); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 30px 30px 20px 30px; | |
| background: var(--primary-gradient); | |
| color: white; | |
| position: relative; | |
| } | |
| .header-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 10px; | |
| } | |
| h1 { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .date-display { | |
| font-size: 0.9rem; | |
| opacity: 0.9; | |
| font-weight: 500; | |
| } | |
| .credits { | |
| font-size: 0.75rem; | |
| opacity: 0.7; | |
| text-align: right; | |
| } | |
| .credits a { | |
| color: white; | |
| text-decoration: none; | |
| border-bottom: 1px dotted rgba(255,255,255,0.7); | |
| } | |
| /* Input Area */ | |
| .input-area { | |
| padding: 25px 30px; | |
| display: flex; | |
| gap: 12px; | |
| border-bottom: 1px solid rgba(0,0,0,0.05); | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| flex-grow: 1; | |
| } | |
| #todo-input { | |
| width: 100%; | |
| padding: 14px 16px 14px 45px; | |
| border: 2px solid transparent; | |
| background: #f9fafb; | |
| border-radius: 12px; | |
| font-size: 1rem; | |
| color: var(--text-main); | |
| transition: all 0.2s ease; | |
| outline: none; | |
| } | |
| #todo-input:focus { | |
| background: white; | |
| border-color: var(--accent-color); | |
| box-shadow: 0 0 0 4px rgba(118, 75, 162, 0.1); | |
| } | |
| .input-icon { | |
| position: absolute; | |
| left: 16px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-secondary); | |
| pointer-events: none; | |
| } | |
| #add-btn { | |
| background: var(--primary-gradient); | |
| color: white; | |
| border: none; | |
| width: 50px; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| font-size: 1.2rem; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| #add-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3); | |
| } | |
| #add-btn:active { | |
| transform: scale(0.95); | |
| } | |
| /* Filters */ | |
| .filters { | |
| display: flex; | |
| padding: 15px 30px 0; | |
| gap: 10px; | |
| } | |
| .filter-btn { | |
| background: none; | |
| border: none; | |
| padding: 8px 16px; | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| border-radius: 20px; | |
| transition: all 0.2s; | |
| } | |
| .filter-btn.active { | |
| background: rgba(118, 75, 162, 0.1); | |
| color: var(--accent-color); | |
| } | |
| .filter-btn:hover:not(.active) { | |
| background: rgba(0,0,0,0.05); | |
| } | |
| /* Todo List */ | |
| .todo-list { | |
| list-style: none; | |
| padding: 20px 30px; | |
| overflow-y: auto; | |
| max-height: 400px; | |
| min-height: 200px; | |
| } | |
| .todo-item { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| box-shadow: var(--shadow-sm); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| border-left: 4px solid transparent; | |
| cursor: grab; | |
| position: relative; | |
| animation: slideIn 0.3s ease forwards; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideOut { | |
| to { opacity: 0; transform: translateX(20px); } | |
| } | |
| .todo-item:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| .todo-item.completed { | |
| opacity: 0.7; | |
| background: #f9fafb; | |
| } | |
| .todo-item.completed .todo-text { | |
| text-decoration: line-through; | |
| color: var(--text-secondary); | |
| } | |
| .todo-item.priority-high { | |
| border-left-color: var(--danger-color); | |
| } | |
| .todo-item.priority-medium { | |
| border-left-color: #f59e0b; | |
| } | |
| .todo-item.priority-low { | |
| border-left-color: var(--success-color); | |
| } | |
| /* Custom Checkbox */ | |
| .custom-checkbox { | |
| width: 22px; | |
| height: 22px; | |
| border: 2px solid #d1d5db; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| flex-shrink: 0; | |
| } | |
| .todo-item.completed .custom-checkbox { | |
| background: var(--success-color); | |
| border-color: var(--success-color); | |
| } | |
| .custom-checkbox i { | |
| color: white; | |
| font-size: 0.8rem; | |
| opacity: 0; | |
| transform: scale(0.5); | |
| transition: all 0.2s; | |
| } | |
| .todo-item.completed .custom-checkbox i { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| .todo-text { | |
| flex-grow: 1; | |
| font-size: 1rem; | |
| color: var(--text-main); | |
| word-break: break-word; | |
| } | |
| .actions { | |
| display: flex; | |
| gap: 8px; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .todo-item:hover .actions { | |
| opacity: 1; | |
| } | |
| /* Mobile specific: always show actions or use a swipe pattern logic, | |
| but for simplicity in this demo, we keep hover or ensure they are touchable */ | |
| @media (max-width: 600px) { | |
| .actions { opacity: 1; } | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 6px; | |
| color: var(--text-secondary); | |
| transition: background 0.2s, color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .action-btn:hover { | |
| background: #f3f4f6; | |
| color: var(--text-main); | |
| } | |
| .delete-btn:hover { | |
| background: #fee2e2; | |
| color: var(--danger-color); | |
| } | |
| /* Priority Selector inside Todo Item (Optional UI expansion) */ | |
| .priority-selector { | |
| position: absolute; | |
| right: 0; | |
| top: 100%; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: var(--shadow-lg); | |
| display: none; | |
| flex-direction: column; | |
| z-index: 10; | |
| overflow: hidden; | |
| border: 1px solid #e5e7eb; | |
| } | |
| .priority-selector.show { | |
| display: flex; | |
| } | |
| .p-option { | |
| padding: 8px 16px; | |
| font-size: 0.85rem; | |
| cursor: pointer; | |
| transition: background 0.1s; | |
| } | |
| .p-option:hover { background: #f3f4f6; } | |
| /* Empty State */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--text-secondary); | |
| display: none; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 16px; | |
| color: #d1d5db; | |
| } | |
| /* Footer Stats */ | |
| .footer-stats { | |
| padding: 15px 30px; | |
| border-top: 1px solid rgba(0,0,0,0.05); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| background: rgba(255,255,255,0.5); | |
| } | |
| .clear-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: color 0.2s; | |
| } | |
| .clear-btn:hover { | |
| color: var(--danger-color); | |
| text-decoration: underline; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background: #1f2937; | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 30px; | |
| font-size: 0.9rem; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| z-index: 100; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <header> | |
| <div class="header-top"> | |
| <div> | |
| <h1>My Tasks</h1> | |
| <div class="date-display" id="date-display">Loading date...</div> | |
| </div> | |
| <div class="credits"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="input-area"> | |
| <div class="input-wrapper"> | |
| <i class="fas fa-plus input-icon"></i> | |
| <input type="text" id="todo-input" placeholder="Add a new task..." autocomplete="off"> | |
| </div> | |
| <button id="add-btn"><i class="fas fa-arrow-up"></i></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 class="todo-list" id="todo-list"> | |
| <!-- Todo items will be injected here --> | |
| </ul> | |
| <div class="empty-state" id="empty-state"> | |
| <i class="fas fa-clipboard-list"></i> | |
| <p>No tasks found. Enjoy your day!</p> | |
| </div> | |
| <div class="footer-stats"> | |
| <span id="items-left">0 items left</span> | |
| <button class="clear-btn" id="clear-completed">Clear Completed</button> | |
| </div> | |
| <div class="toast" id="toast"> | |
| <i class="fas fa-check-circle"></i> | |
| <span id="toast-message">Action successful</span> | |
| </div> | |
| </div> | |
| <script> | |
| // --- State Management --- | |
| let todos = JSON.parse(localStorage.getItem('myTodos')) || []; | |
| let currentFilter = 'all'; | |
| // --- DOM Elements --- | |
| const todoInput = document.getElementById('todo-input'); | |
| const addBtn = document.getElementById('add-btn'); | |
| const todoList = document.getElementById('todo-list'); | |
| const itemsLeft = document.getElementById('items-left'); | |
| const clearBtn = document.getElementById('clear-completed'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const dateDisplay = document.getElementById('date-display'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const toast = document.getElementById('toast'); | |
| const toastMsg = document.getElementById('toast-message'); | |
| // --- Initialization --- | |
| document.addEventListener('DOMContentLoaded', () => { | |
| renderTodos(); | |
| updateDate(); | |
| }); | |
| // --- Event Listeners --- | |
| addBtn.addEventListener('click', addTodo); | |
| todoInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') addTodo(); | |
| }); | |
| clearBtn.addEventListener('click', () => { | |
| const completedCount = todos.filter(t => t.completed).length; | |
| if (completedCount === 0) return; | |
| todos = todos.filter(todo => !todo.completed); | |
| saveAndRender(); | |
| showToast(`${completedCount} tasks cleared`); | |
| }); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelector('.filter-btn.active').classList.remove('active'); | |
| btn.classList.add('active'); | |
| currentFilter = btn.dataset.filter; | |
| renderTodos(); | |
| }); | |
| }); | |
| // --- Core Functions --- | |
| function addTodo() { | |
| const text = todoInput.value.trim(); | |
| if (text === '') { | |
| showToast('Please enter a task', true); | |
| return; | |
| } | |
| const newTodo = { | |
| id: Date.now(), | |
| text: text, | |
| completed: false, | |
| priority: 'medium' // default priority | |
| }; | |
| todos.unshift(newTodo); // Add to top | |
| todoInput.value = ''; | |
| todoInput.focus(); | |
| saveAndRender(); | |
| showToast('Task added successfully'); | |
| } | |
| function toggleTodo(id) { | |
| todos = todos.map(todo => { | |
| if (todo.id === id) { | |
| return { ...todo, completed: !todo.completed }; | |
| } | |
| return todo; | |
| }); | |
| saveAndRender(); | |
| } | |
| function deleteTodo(id) { | |
| const item = document.querySelector(`li[data-id="${id}"]`); | |
| if (item) { | |
| item.style.animation = 'slideOut 0.3s ease forwards'; | |
| setTimeout(() => { | |
| todos = todos.filter(todo => todo.id !== id); | |
| saveAndRender(); | |
| showToast('Task deleted'); | |
| }, 300); | |
| } | |
| } | |
| function cyclePriority(id) { | |
| const priorities = ['low', 'medium', 'high']; | |
| todos = todos.map(todo => { | |
| if (todo.id === id) { | |
| const currentIndex = priorities.indexOf(todo.priority); | |
| const nextIndex = (currentIndex + 1) % priorities.length; | |
| return { ...todo, priority: priorities[nextIndex] }; | |
| } | |
| return todo; | |
| }); | |
| saveAndRender(); | |
| } | |
| function saveAndRender() { | |
| localStorage.setItem('myTodos', JSON.stringify(todos)); | |
| renderTodos(); | |
| } | |
| function renderTodos() { | |
| todoList.innerHTML = ''; | |
| let filteredTodos = todos; | |
| if (currentFilter === 'active') { | |
| filteredTodos = todos.filter(t => !t.completed); | |
| } else if (currentFilter === 'completed') { | |
| filteredTodos = todos.filter(t => t.completed); | |
| } | |
| // Update items left count | |
| const activeCount = todos.filter(t => !t.completed).length; | |
| itemsLeft.textContent = `${activeCount} ${activeCount === 1 ? 'item' : 'items'} left`; | |
| // Show/Hide empty state | |
| if (filteredTodos.length === 0) { | |
| emptyState.style.display = 'block'; | |
| if(currentFilter === 'completed') emptyState.querySelector('p').textContent = "No completed tasks yet."; | |
| else if(currentFilter === 'active') emptyState.querySelector('p').textContent = "No active tasks. You're all caught up!"; | |
| else emptyState.querySelector('p').textContent = "No tasks found. Enjoy your day!"; | |
| } else { | |
| emptyState.style.display = 'none'; | |
| } | |
| filteredTodos.forEach(todo => { | |
| const li = document.createElement('li'); | |
| li.className = `todo-item ${todo.completed ? 'completed' : ''} priority-${todo.priority}`; | |
| li.dataset.id = todo.id; | |
| // Priority Icon Color Logic | |
| let priorityIcon = 'fa-circle'; | |
| let priorityColor = 'color: #f59e0b'; // default medium (orange) | |
| if(todo.priority === 'high') priorityColor = 'color: #ef4444'; // red | |
| if(todo.priority === 'low') priorityColor = 'color: #10b981'; // green | |
| li.innerHTML = ` | |
| <div class="custom-checkbox" onclick="toggleTodo(${todo.id})"> | |
| <i class="fas fa-check"></i> | |
| </div> | |
| <span class="todo-text">${escapeHtml(todo.text)}</span> | |
| <div class="actions"> | |
| <button class="action-btn" onclick="cyclePriority(${todo.id})" title="Change Priority"> | |
| <i class="fas fa-flag" style="${priorityColor}"></i> | |
| </button> | |
| <button class="action-btn delete-btn" onclick="deleteTodo(${todo.id})" title="Delete Task"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| todoList.appendChild(li); | |
| }); | |
| } | |
| // --- Helper Functions --- | |
| function updateDate() { | |
| const options = { weekday: 'long', month: 'long', day: 'numeric' }; | |
| const today = new Date(); | |
| dateDisplay.textContent = today.toLocaleDateString('en-US', options); | |
| } | |
| function showToast(message, isError = false) { | |
| toastMsg.textContent = message; | |
| const icon = toast.querySelector('i'); | |
| if (isError) { | |
| icon.className = 'fas fa-exclamation-circle'; | |
| toast.style.background = '#ef4444'; | |
| } else { | |
| icon.className = 'fas fa-check-circle'; | |
| toast.style.background = '#1f2937'; | |
| } | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // --- Drag and Drop Logic (Basic) --- | |
| let dragStartIndex; | |
| function dragStart() { | |
| dragStartIndex = +this.closest('li').getAttribute('data-index'); | |
| } | |
| function dragDrop() { | |
| const dragEndIndex = +this.closest('li').getAttribute('data-index'); | |
| swapItems(dragStartIndex, dragEndIndex); | |
| this.classList.remove('over'); | |
| } | |
| function swapItems(fromIndex, toIndex) { | |
| const itemOne = todos[fromIndex]; | |
| const itemTwo = todos[toIndex]; | |
| todos[fromIndex] = itemTwo; | |
| todos[toIndex] = itemOne; | |
| saveAndRender(); | |
| } | |
| /* Note: Implementing full native HTML5 Drag and Drop for this list structure requires | |
| more extensive event handling. For this version, we focus on the core CRUD functionality | |
| and keeping the UI responsive. */ | |
| </script> | |
| </body> | |
| </html> |