Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FocusFlow | Modern To-Do</title> | |
| <!-- Importing Google Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Importing Phosphor Icons (Lightweight icon library) --> | |
| <script src="https://unpkg.com/@phosphor-icons/web"></script> | |
| <style> | |
| :root { | |
| /* Color Palette */ | |
| --primary: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --secondary: #ec4899; | |
| --bg-gradient-start: #e0c3fc; | |
| --bg-gradient-end: #8ec5fc; | |
| --glass-bg: rgba(255, 255, 255, 0.75); | |
| --glass-border: rgba(255, 255, 255, 0.5); | |
| --text-main: #1f2937; | |
| --text-muted: #6b7280; | |
| --danger: #ef4444; | |
| --success: #10b981; | |
| --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| --radius: 16px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| body { | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); | |
| background-size: 200% 200%; | |
| animation: gradientMove 15s ease infinite; | |
| padding: 20px; | |
| color: var(--text-main); | |
| } | |
| @keyframes gradientMove { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| /* Main Container */ | |
| .app-container { | |
| width: 100%; | |
| max-width: 500px; | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid var(--glass-border); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-lg); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s ease; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 24px 24px 16px; | |
| border-bottom: 1px solid rgba(0,0,0,0.05); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(to right, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .brand-link { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| font-weight: 500; | |
| padding: 4px 8px; | |
| background: rgba(255,255,255,0.5); | |
| border-radius: 6px; | |
| transition: all 0.2s; | |
| } | |
| .brand-link:hover { | |
| background: #fff; | |
| color: var(--primary); | |
| } | |
| /* Input Area */ | |
| .input-group { | |
| padding: 20px 24px; | |
| display: flex; | |
| gap: 12px; | |
| position: relative; | |
| } | |
| .input-wrapper { | |
| position: relative; | |
| flex: 1; | |
| } | |
| .task-input { | |
| width: 100%; | |
| padding: 12px 16px; | |
| padding-right: 40px; | |
| border: 2px solid transparent; | |
| background: rgba(255, 255, 255, 0.6); | |
| border-radius: 12px; | |
| font-size: 1rem; | |
| outline: none; | |
| transition: all 0.2s; | |
| box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); | |
| } | |
| .task-input:focus { | |
| background: #fff; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); | |
| } | |
| .add-btn { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| transition: all 0.2s; | |
| box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3); | |
| } | |
| .add-btn:hover { | |
| background: var(--primary-hover); | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 10px rgba(99, 102, 241, 0.4); | |
| } | |
| .add-btn:active { | |
| transform: translateY(0); | |
| } | |
| /* Filters */ | |
| .filters { | |
| display: flex; | |
| padding: 0 24px; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .filter-btn { | |
| background: none; | |
| border: none; | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| color: var(--text-muted); | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .filter-btn.active { | |
| background: rgba(99, 102, 241, 0.1); | |
| color: var(--primary); | |
| font-weight: 600; | |
| } | |
| .filter-btn:hover:not(.active) { | |
| background: rgba(0,0,0,0.03); | |
| color: var(--text-main); | |
| } | |
| /* Task List */ | |
| .task-list { | |
| list-style: none; | |
| padding: 0 24px 24px; | |
| max-height: 50vh; | |
| overflow-y: auto; | |
| /* Scrollbar styling */ | |
| scrollbar-width: thin; | |
| scrollbar-color: rgba(0,0,0,0.1) transparent; | |
| } | |
| .task-list::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .task-list::-webkit-scrollbar-thumb { | |
| background-color: rgba(0,0,0,0.1); | |
| border-radius: 20px; | |
| } | |
| .task-item { | |
| background: #fff; | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| box-shadow: var(--shadow-sm); | |
| animation: slideIn 0.3s ease-out forwards; | |
| transition: all 0.2s; | |
| border: 1px solid transparent; | |
| } | |
| .task-item:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| border-color: rgba(0,0,0,0.05); | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| @keyframes slideOut { | |
| to { opacity: 0; transform: translateX(20px); height: 0; margin-bottom: 0; padding: 0; } | |
| } | |
| .task-content { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| /* Custom Checkbox */ | |
| .checkbox-wrapper { | |
| position: relative; | |
| width: 22px; | |
| height: 22px; | |
| flex-shrink: 0; | |
| } | |
| .checkbox-wrapper input { | |
| opacity: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| position: absolute; | |
| z-index: 2; | |
| } | |
| .custom-checkbox { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| border: 2px solid #d1d5db; | |
| border-radius: 6px; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 14px; | |
| } | |
| .checkbox-wrapper input:checked + .custom-checkbox { | |
| background: var(--success); | |
| border-color: var(--success); | |
| } | |
| .task-text { | |
| font-size: 0.95rem; | |
| color: var(--text-main); | |
| transition: color 0.2s; | |
| word-break: break-word; | |
| cursor: text; | |
| } | |
| .task-item.completed .task-text { | |
| text-decoration: line-through; | |
| color: var(--text-muted); | |
| } | |
| /* Edit Mode */ | |
| .edit-input { | |
| width: 100%; | |
| font-size: 0.95rem; | |
| padding: 4px 8px; | |
| border: 1px solid var(--primary); | |
| border-radius: 4px; | |
| outline: none; | |
| } | |
| /* Actions */ | |
| .task-actions { | |
| display: flex; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .task-item:hover .task-actions { | |
| opacity: 1; | |
| } | |
| /* Mobile support: always show actions or make them bigger */ | |
| @media (max-width: 600px) { | |
| .task-actions { opacity: 1; } | |
| } | |
| .action-btn { | |
| background: none; | |
| border: none; | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .action-btn:hover { | |
| background: #f3f4f6; | |
| color: var(--text-main); | |
| } | |
| .delete-btn:hover { | |
| background: #fee2e2; | |
| color: var(--danger); | |
| } | |
| /* Footer Stats */ | |
| .app-footer { | |
| background: rgba(255,255,255,0.4); | |
| padding: 16px 24px; | |
| border-top: 1px solid rgba(0,0,0,0.05); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .clear-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| transition: color 0.2s; | |
| } | |
| .clear-btn:hover { | |
| color: var(--text-main); | |
| text-decoration: underline; | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: var(--text-muted); | |
| display: none; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 10px; | |
| opacity: 0.5; | |
| color: var(--primary); | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background: #333; | |
| color: #fff; | |
| padding: 10px 20px; | |
| border-radius: 30px; | |
| font-size: 0.9rem; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| opacity: 0; | |
| transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55); | |
| z-index: 1000; | |
| } | |
| .toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <header> | |
| <h1>FocusFlow</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <section class="input-group"> | |
| <div class="input-wrapper"> | |
| <input type="text" id="taskInput" class="task-input" placeholder="What needs to be done?" autocomplete="off"> | |
| </div> | |
| <button id="addBtn" class="add-btn" aria-label="Add Task"> | |
| <i class="ph ph-plus"></i> | |
| </button> | |
| </section> | |
| <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> | |
| <div class="empty-state" id="emptyState"> | |
| <i class="ph ph-check-circle"></i> | |
| <p>No tasks found. Time to relax or get productive!</p> | |
| </div> | |
| <ul class="task-list" id="taskList"> | |
| <!-- Tasks will be injected here via JS --> | |
| </ul> | |
| <footer class="app-footer"> | |
| <span id="itemsLeft">0 items left</span> | |
| <button class="clear-btn" id="clearCompleted">Clear Completed</button> | |
| </footer> | |
| </div> | |
| <!-- Toast Notification Element --> | |
| <div id="toast" class="toast">Action Successful</div> | |
| <script> | |
| // --- State Management --- | |
| let tasks = JSON.parse(localStorage.getItem('focusflow-tasks')) || []; | |
| let currentFilter = 'all'; | |
| // --- DOM Elements --- | |
| const taskInput = document.getElementById('taskInput'); | |
| const addBtn = document.getElementById('addBtn'); | |
| const taskList = document.getElementById('taskList'); | |
| const itemsLeft = document.getElementById('itemsLeft'); | |
| const clearCompletedBtn = document.getElementById('clearCompleted'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const toast = document.getElementById('toast'); | |
| // --- Functions --- | |
| function saveTasks() { | |
| localStorage.setItem('focusflow-tasks', JSON.stringify(tasks)); | |
| render(); | |
| } | |
| function showToast(message) { | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 2500); | |
| } | |
| function addTask() { | |
| const text = taskInput.value.trim(); | |
| if (text === '') { | |
| // Visual feedback for empty input | |
| taskInput.style.borderColor = 'var(--danger)'; | |
| setTimeout(() => taskInput.style.borderColor = 'transparent', 1000); | |
| return; | |
| } | |
| const newTask = { | |
| id: Date.now(), | |
| text: text, | |
| completed: false | |
| }; | |
| tasks.unshift(newTask); // Add to top | |
| taskInput.value = ''; | |
| taskInput.focus(); | |
| saveTasks(); | |
| showToast('Task added'); | |
| } | |
| function toggleTask(id) { | |
| tasks = tasks.map(task => | |
| task.id === id ? { ...task, completed: !task.completed } : task | |
| ); | |
| saveTasks(); | |
| } | |
| function deleteTask(id) { | |
| // Find element to animate out | |
| const element = document.querySelector(`[data-id="${id}"]`); | |
| if (element) { | |
| element.style.animation = 'slideOut 0.3s ease-out forwards'; | |
| setTimeout(() => { | |
| tasks = tasks.filter(task => task.id !== id); | |
| saveTasks(); | |
| showToast('Task deleted'); | |
| }, 300); // Wait for animation | |
| } | |
| } | |
| function enableEdit(id, textElement) { | |
| const currentText = textElement.textContent; | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'edit-input'; | |
| input.value = currentText; | |
| // Replace text with input | |
| textElement.replaceWith(input); | |
| input.focus(); | |
| const saveEdit = () => { | |
| const newText = input.value.trim(); | |
| if (newText) { | |
| tasks = tasks.map(t => t.id === id ? { ...t, text: newText } : t); | |
| saveTasks(); | |
| } else { | |
| render(); // Revert if empty | |
| } | |
| }; | |
| // Save on blur or Enter key | |
| input.addEventListener('blur', saveEdit); | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| input.blur(); | |
| } | |
| }); | |
| } | |
| function clearCompleted() { | |
| const completedCount = tasks.filter(t => t.completed).length; | |
| if (completedCount === 0) return; | |
| tasks = tasks.filter(task => !task.completed); | |
| saveTasks(); | |
| showToast(`${completedCount} tasks cleared`); | |
| } | |
| function render() { | |
| taskList.innerHTML = ''; | |
| // Filter logic | |
| const filteredTasks = tasks.filter(task => { | |
| if (currentFilter === 'active') return !task.completed; | |
| if (currentFilter === 'completed') return task.completed; | |
| return true; | |
| }); | |
| // Empty state handling | |
| if (filteredTasks.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. Good job!"; | |
| else emptyState.querySelector('p').textContent = "Your list is empty. Add a task!"; | |
| } else { | |
| emptyState.style.display = 'none'; | |
| } | |
| // Create HTML elements | |
| filteredTasks.forEach(task => { | |
| const li = document.createElement('li'); | |
| li.className = `task-item ${task.completed ? 'completed' : ''}`; | |
| li.setAttribute('data-id', task.id); | |
| li.innerHTML = ` | |
| <div class="task-content"> | |
| <div class="checkbox-wrapper"> | |
| <input type="checkbox" ${task.completed ? 'checked' : ''}> | |
| <div class="custom-checkbox"> | |
| <i class="ph ph-check" style="display: ${task.completed ? 'block' : 'none'}"></i> | |
| </div> | |
| </div> | |
| <span class="task-text">${escapeHtml(task.text)}</span> | |
| </div> | |
| <div class="task-actions"> | |
| <button class="action-btn edit-btn" title="Edit"> | |
| <i class="ph ph-pencil-simple"></i> | |
| </button> | |
| <button class="action-btn delete-btn" title="Delete"> | |
| <i class="ph ph-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Event Listeners for this item | |
| const checkbox = li.querySelector('input[type="checkbox"]'); | |
| checkbox.addEventListener('change', () => toggleTask(task.id)); | |
| const deleteBtn = li.querySelector('.delete-btn'); | |
| deleteBtn.addEventListener('click', () => deleteTask(task.id)); | |
| const editBtn = li.querySelector('.edit-btn'); | |
| const textSpan = li.querySelector('.task-text'); | |
| editBtn.addEventListener('click', () => enableEdit(task.id, textSpan)); | |
| // Allow double click on text to edit | |
| textSpan.addEventListener('dblclick', () => enableEdit(task.id, textSpan)); | |
| taskList.appendChild(li); | |
| }); | |
| // Update stats | |
| const activeCount = tasks.filter(t => !t.completed).length; | |
| itemsLeft.textContent = `${activeCount} item${activeCount !== 1 ? 's' : ''} left`; | |
| } | |
| // Helper to prevent XSS | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // --- Event Listeners --- | |
| addBtn.addEventListener('click', addTask); | |
| taskInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') addTask(); | |
| }); | |
| clearCompletedBtn.addEventListener('click', clearCompleted); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| // Update UI classes | |
| filterBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Update state | |
| currentFilter = btn.dataset.filter; | |
| render(); | |
| }); | |
| }); | |
| // --- Initial Render --- | |
| render(); | |
| </script> | |
| </body> | |
| </html> |