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 Todo App</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 { | |
| /* Light Theme Variables */ | |
| --bg-body: #f3f4f6; | |
| --bg-card: #ffffff; | |
| --text-primary: #111827; | |
| --text-secondary: #6b7280; | |
| --text-muted: #9ca3af; | |
| --accent-color: #6366f1; | |
| --accent-hover: #4f46e5; | |
| --danger-color: #ef4444; | |
| --success-color: #10b981; | |
| --border-color: #e5e7eb; | |
| --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); | |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | |
| --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); | |
| --radius-md: 0.75rem; | |
| --radius-lg: 1rem; | |
| --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| [data-theme="dark"] { | |
| /* Dark Theme Variables */ | |
| --bg-body: #111827; | |
| --bg-card: #1f2937; | |
| --text-primary: #f9fafb; | |
| --text-secondary: #9ca3af; | |
| --text-muted: #6b7280; | |
| --accent-color: #818cf8; | |
| --accent-hover: #6366f1; | |
| --border-color: #374151; | |
| --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); | |
| --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4); | |
| --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-primary); | |
| line-height: 1.5; | |
| min-height: 100vh; | |
| display: flex; | |
| justify-content: center; | |
| padding: 2rem 1rem; | |
| transition: background-color 0.3s ease, color 0.3s ease; | |
| } | |
| /* Container */ | |
| .app-container { | |
| width: 100%; | |
| max-width: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| /* Header */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding-bottom: 0.5rem; | |
| } | |
| .header-title h1 { | |
| font-size: 1.875rem; | |
| font-weight: 700; | |
| letter-spacing: -0.025em; | |
| background: linear-gradient(135deg, var(--accent-color), #ec4899); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .header-title p { | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| margin-top: 0.25rem; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .icon-btn { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| color: var(--text-secondary); | |
| width: 2.5rem; | |
| height: 2.5rem; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| font-size: 1.25rem; | |
| } | |
| .icon-btn:hover { | |
| color: var(--accent-color); | |
| border-color: var(--accent-color); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| /* Input Section */ | |
| .input-card { | |
| background: var(--bg-card); | |
| padding: 0.5rem; | |
| border-radius: var(--radius-lg); | |
| box-shadow: var(--shadow-md); | |
| display: flex; | |
| gap: 0.5rem; | |
| border: 1px solid var(--border-color); | |
| } | |
| .input-wrapper { | |
| flex: 1; | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .input-wrapper i { | |
| position: absolute; | |
| left: 1rem; | |
| color: var(--text-muted); | |
| font-size: 1.25rem; | |
| } | |
| #todo-input { | |
| width: 100%; | |
| padding: 1rem 1rem 1rem 3rem; | |
| border: none; | |
| background: transparent; | |
| font-size: 1rem; | |
| color: var(--text-primary); | |
| outline: none; | |
| } | |
| #todo-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| #add-btn { | |
| background: var(--accent-color); | |
| color: white; | |
| border: none; | |
| padding: 0 1.5rem; | |
| border-radius: var(--radius-md); | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| #add-btn:hover { | |
| background: var(--accent-hover); | |
| transform: translateY(-1px); | |
| } | |
| #add-btn:active { | |
| transform: translateY(0); | |
| } | |
| /* Filters */ | |
| .filters { | |
| display: flex; | |
| gap: 0.5rem; | |
| overflow-x: auto; | |
| padding-bottom: 0.5rem; | |
| scrollbar-width: none; /* Firefox */ | |
| } | |
| .filters::-webkit-scrollbar { | |
| display: none; /* Chrome/Safari */ | |
| } | |
| .filter-btn { | |
| background: transparent; | |
| border: 1px solid transparent; | |
| color: var(--text-secondary); | |
| padding: 0.5rem 1rem; | |
| border-radius: 2rem; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| white-space: nowrap; | |
| } | |
| .filter-btn:hover { | |
| background: rgba(99, 102, 241, 0.1); | |
| color: var(--accent-color); | |
| } | |
| .filter-btn.active { | |
| background: var(--accent-color); | |
| color: white; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| /* Todo List */ | |
| .todo-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| min-height: 200px; | |
| } | |
| .todo-item { | |
| background: var(--bg-card); | |
| padding: 1rem; | |
| border-radius: var(--radius-md); | |
| box-shadow: var(--shadow-sm); | |
| border: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| transition: var(--transition); | |
| animation: slideIn 0.3s ease-out; | |
| position: relative; | |
| cursor: grab; | |
| } | |
| .todo-item:active { | |
| cursor: grabbing; | |
| } | |
| .todo-item.dragging { | |
| opacity: 0.5; | |
| border: 2px dashed var(--accent-color); | |
| background: transparent; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .todo-checkbox { | |
| appearance: none; | |
| width: 1.5rem; | |
| height: 1.5rem; | |
| border: 2px solid var(--border-color); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| position: relative; | |
| flex-shrink: 0; | |
| transition: var(--transition); | |
| } | |
| .todo-checkbox:checked { | |
| background-color: var(--success-color); | |
| border-color: var(--success-color); | |
| } | |
| .todo-checkbox:checked::after { | |
| content: '✔'; | |
| color: white; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 0.875rem; | |
| } | |
| .todo-content { | |
| flex: 1; | |
| font-size: 1rem; | |
| color: var(--text-primary); | |
| transition: var(--transition); | |
| word-break: break-word; | |
| } | |
| .todo-item.completed .todo-content { | |
| text-decoration: line-through; | |
| color: var(--text-muted); | |
| } | |
| .todo-actions { | |
| display: flex; | |
| gap: 0.5rem; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .todo-item:hover .todo-actions { | |
| opacity: 1; | |
| } | |
| /* Mobile specific: always show actions */ | |
| @media (max-width: 600px) { | |
| .todo-actions { | |
| opacity: 1; | |
| } | |
| } | |
| .action-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| padding: 0.25rem; | |
| border-radius: 0.25rem; | |
| transition: var(--transition); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .action-btn:hover { | |
| background: rgba(0,0,0,0.05); | |
| color: var(--text-primary); | |
| } | |
| .delete-btn:hover { | |
| color: var(--danger-color); | |
| background: rgba(239, 68, 68, 0.1); | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 3rem; | |
| color: var(--text-muted); | |
| text-align: center; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| color: var(--border-color); | |
| } | |
| /* Stats Footer */ | |
| .stats-footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1rem; | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| background: var(--bg-card); | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-color); | |
| } | |
| .clear-btn { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 0.875rem; | |
| transition: var(--transition); | |
| } | |
| .clear-btn:hover { | |
| color: var(--danger-color); | |
| text-decoration: underline; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background: var(--text-primary); | |
| color: var(--bg-card); | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 2rem; | |
| box-shadow: var(--shadow-lg); | |
| font-size: 0.875rem; | |
| opacity: 0; | |
| transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s; | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| opacity: 1; | |
| } | |
| /* Credit Link */ | |
| .credit-link { | |
| text-align: center; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| margin-top: 1rem; | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| } | |
| .credit-link:hover { | |
| color: var(--accent-color); | |
| } | |
| .credit-link span { | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header> | |
| <div class="header-title"> | |
| <h1>FocusFlow</h1> | |
| <p id="date-display">Loading date...</p> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="icon-btn" id="theme-toggle" aria-label="Toggle Theme"> | |
| <i class="ph ph-moon"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Input Area --> | |
| <div class="input-card"> | |
| <div class="input-wrapper"> | |
| <i class="ph ph-plus"></i> | |
| <input type="text" id="todo-input" placeholder="What needs to be done?" autocomplete="off"> | |
| </div> | |
| <button id="add-btn">Add</button> | |
| </div> | |
| <!-- Filters --> | |
| <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> | |
| <!-- Todo List --> | |
| <ul class="todo-list" id="todo-list"> | |
| <!-- Items will be injected here via JS --> | |
| </ul> | |
| <!-- Empty State (Hidden by default) --> | |
| <div class="empty-state" id="empty-state" style="display: none;"> | |
| <i class="ph ph-clipboard-text"></i> | |
| <p>No tasks found. Enjoy your day!</p> | |
| </div> | |
| <!-- Footer Stats --> | |
| <div class="stats-footer"> | |
| <span id="items-left">0 items left</span> | |
| <button class="clear-btn" id="clear-completed">Clear Completed</button> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="credit-link"> | |
| Built with <span>anycoder</span> | |
| </a> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div class="toast" id="toast"> | |
| <i class="ph ph-check-circle"></i> | |
| <span id="toast-message">Action successful</span> | |
| </div> | |
| <script> | |
| // --- State Management --- | |
| let todos = JSON.parse(localStorage.getItem('todos')) || []; | |
| let currentFilter = 'all'; | |
| // --- DOM Elements --- | |
| const todoInput = document.getElementById('todo-input'); | |
| const addBtn = document.getElementById('add-btn'); | |
| const todoList = document.getElementById('todo-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const itemsLeftLabel = document.getElementById('items-left'); | |
| const clearCompletedBtn = document.getElementById('clear-completed'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const dateDisplay = document.getElementById('date-display'); | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toast-message'); | |
| // --- Initialization --- | |
| function init() { | |
| renderTodos(); | |
| updateDate(); | |
| applyTheme(); | |
| } | |
| // --- Date Display --- | |
| function updateDate() { | |
| const options = { weekday: 'long', month: 'long', day: 'numeric' }; | |
| const today = new Date(); | |
| dateDisplay.textContent = today.toLocaleDateString('en-US', options); | |
| } | |
| // --- Theme Handling --- | |
| function applyTheme() { | |
| const savedTheme = localStorage.getItem('theme') || 'light'; | |
| document.documentElement.setAttribute('data-theme', savedTheme); | |
| updateThemeIcon(savedTheme); | |
| } | |
| function toggleTheme() { | |
| const currentTheme = document.documentElement.getAttribute('data-theme'); | |
| const newTheme = currentTheme === 'light' ? 'dark' : 'light'; | |
| document.documentElement.setAttribute('data-theme', newTheme); | |
| localStorage.setItem('theme', newTheme); | |
| updateThemeIcon(newTheme); | |
| } | |
| function updateThemeIcon(theme) { | |
| const icon = themeToggle.querySelector('i'); | |
| if (theme === 'dark') { | |
| icon.className = 'ph ph-sun'; | |
| } else { | |
| icon.className = 'ph ph-moon'; | |
| } | |
| } | |
| // --- Core Functions --- | |
| function saveTodos() { | |
| localStorage.setItem('todos', JSON.stringify(todos)); | |
| renderTodos(); | |
| } | |
| function addTodo() { | |
| const text = todoInput.value.trim(); | |
| if (text === '') { | |
| showToast('Please enter a task', 'error'); | |
| return; | |
| } | |
| const newTodo = { | |
| id: Date.now(), | |
| text: text, | |
| completed: false | |
| }; | |
| todos.unshift(newTodo); // Add to top | |
| todoInput.value = ''; | |
| saveTodos(); | |
| showToast('Task added successfully'); | |
| } | |
| function toggleTodo(id) { | |
| todos = todos.map(todo => { | |
| if (todo.id === id) { | |
| return { ...todo, completed: !todo.completed }; | |
| } | |
| return todo; | |
| }); | |
| saveTodos(); | |
| } | |
| function deleteTodo(id) { | |
| todos = todos.filter(todo => todo.id !== id); | |
| saveTodos(); | |
| showToast('Task deleted'); | |
| } | |
| function clearCompleted() { | |
| const completedCount = todos.filter(t => t.completed).length; | |
| if (completedCount === 0) return; | |
| todos = todos.filter(todo => !todo.completed); | |
| saveTodos(); | |
| showToast('Completed tasks cleared'); | |
| } | |
| // --- Rendering --- | |
| 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 Empty State | |
| if (filteredTodos.length === 0) { | |
| emptyState.style.display = 'flex'; | |
| todoList.style.display = 'none'; | |
| } else { | |
| emptyState.style.display = 'none'; | |
| todoList.style.display = 'flex'; | |
| } | |
| // Update Stats | |
| const activeCount = todos.filter(t => !t.completed).length; | |
| itemsLeftLabel.textContent = `${activeCount} item${activeCount !== 1 ? 's' : ''} left`; | |
| // Create Elements | |
| filteredTodos.forEach(todo => { | |
| const li = document.createElement('li'); | |
| li.className = `todo-item ${todo.completed ? 'completed' : ''}`; | |
| li.draggable = true; | |
| li.dataset.id = todo.id; | |
| li.innerHTML = ` | |
| <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}> | |
| <span class="todo-content">${escapeHtml(todo.text)}</span> | |
| <div class="todo-actions"> | |
| <button class="action-btn delete-btn" aria-label="Delete"> | |
| <i class="ph ph-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| // Event Listeners for Item | |
| const checkbox = li.querySelector('.todo-checkbox'); | |
| checkbox.addEventListener('change', () => toggleTodo(todo.id)); | |
| const deleteBtn = li.querySelector('.delete-btn'); | |
| deleteBtn.addEventListener('click', () => deleteTodo(todo.id)); | |
| // Drag Events | |
| li.addEventListener('dragstart', handleDragStart); | |
| li.addEventListener('dragend', handleDragEnd); | |
| todoList.appendChild(li); | |
| }); | |
| } | |
| // --- Drag and Drop Logic --- | |
| let draggedItem = null; | |
| function handleDragStart(e) { | |
| draggedItem = this; | |
| setTimeout(() => this.classList.add('dragging'), 0); | |
| } | |
| function handleDragEnd(e) { | |
| this.classList.remove('dragging'); | |
| draggedItem = null; | |
| // Reorder array based on DOM order | |
| const newOrderIds = Array.from(todoList.children).map(li => parseInt(li.dataset.id)); | |
| // Sort todos array based on new ID order | |
| // Note: This simple sort only works perfectly if 'all' filter is active. | |
| // For simplicity in this demo, we only allow reordering in 'all' mode visually, | |
| // but to keep state consistent, we map the full list. | |
| if(currentFilter === 'all') { | |
| const idTodoMap = new Map(todos.map(t => [t.id, t])); | |
| todos = newOrderIds.map(id => idTodoMap.get(id)).filter(t => t !== undefined); | |
| // Filter undefined just in case, though map should cover all | |
| saveTodos(); // Save new order | |
| } | |
| } | |
| todoList.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| const afterElement = getDragAfterElement(todoList, e.clientY); | |
| const draggable = document.querySelector('.dragging'); | |
| if (afterElement == null) { | |
| todoList.appendChild(draggable); | |
| } else { | |
| todoList.insertBefore(draggable, afterElement); | |
| } | |
| }); | |
| function getDragAfterElement(container, y) { | |
| const draggableElements = [...container.querySelectorAll('.todo-item:not(.dragging)')]; | |
| return draggableElements.reduce((closest, child) => { | |
| const box = child.getBoundingClientRect(); | |
| const offset = y - box.top - box.height / 2; | |
| if (offset < 0 && offset > closest.offset) { | |
| return { offset: offset, element: child }; | |
| } else { | |
| return closest; | |
| } | |
| }, { offset: Number.NEGATIVE_INFINITY }).element; | |
| } | |
| // --- Utilities --- | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function showToast(message, type = 'success') { | |
| toastMessage.textContent = message; | |
| const icon = toast.querySelector('i'); | |
| if (type === 'error') { | |
| icon.className = 'ph ph-warning-circle'; | |
| toast.style.backgroundColor = 'var(--danger-color)'; | |
| } else { | |
| icon.className = 'ph ph-check-circle'; | |
| toast.style.backgroundColor = 'var(--text-primary)'; | |
| } | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // --- Event Listeners --- | |
| addBtn.addEventListener('click', addTodo); | |
| todoInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') addTodo(); | |
| }); | |
| clearCompletedBtn.addEventListener('click', clearCompleted); | |
| themeToggle.addEventListener('click', toggleTheme); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| // Update UI classes | |
| filterBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Update Logic | |
| currentFilter = btn.dataset.filter; | |
| renderTodos(); | |
| }); | |
| }); | |
| // Run Init | |
| init(); | |
| </script> | |
| </body> | |
| </html> |