Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Taskflow - Modern Todo App</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <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=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; | |
| --bg-secondary: #12121a; | |
| --fg: #f0f0f5; | |
| --muted: #6b6b80; | |
| --accent: #00d4aa; | |
| --accent-glow: rgba(0, 212, 170, 0.3); | |
| --card: rgba(20, 20, 30, 0.8); | |
| --border: rgba(255, 255, 255, 0.08); | |
| --danger: #ff4757; | |
| --danger-glow: rgba(255, 71, 87, 0.3); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg); | |
| color: var(--fg); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| .font-display { | |
| font-family: 'Space Grotesk', sans-serif; | |
| } | |
| /* Animated background */ | |
| .bg-mesh { | |
| position: fixed; | |
| inset: 0; | |
| z-index: -1; | |
| overflow: hidden; | |
| } | |
| .bg-mesh::before { | |
| content: ''; | |
| position: absolute; | |
| width: 150%; | |
| height: 150%; | |
| top: -25%; | |
| left: -25%; | |
| background: | |
| radial-gradient(ellipse 600px 600px at 20% 20%, rgba(0, 212, 170, 0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse 500px 500px at 80% 80%, rgba(100, 50, 200, 0.06) 0%, transparent 50%), | |
| radial-gradient(ellipse 400px 400px at 60% 30%, rgba(0, 150, 255, 0.05) 0%, transparent 50%); | |
| animation: meshMove 20s ease-in-out infinite; | |
| } | |
| @keyframes meshMove { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 33% { transform: translate(2%, 2%) rotate(1deg); } | |
| 66% { transform: translate(-1%, 1%) rotate(-1deg); } | |
| } | |
| /* Grid pattern overlay */ | |
| .grid-pattern { | |
| position: fixed; | |
| inset: 0; | |
| z-index: -1; | |
| background-image: | |
| linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); | |
| background-size: 60px 60px; | |
| mask-image: radial-gradient(ellipse 80% 80% at 50% 50%, black 20%, transparent 70%); | |
| } | |
| /* Glass card effect */ | |
| .glass-card { | |
| background: var(--card); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| } | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.15); | |
| } | |
| /* Input styling */ | |
| .todo-input { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| padding: 16px 20px; | |
| font-size: 16px; | |
| color: var(--fg); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| width: 100%; | |
| } | |
| .todo-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 4px var(--accent-glow); | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .todo-input::placeholder { | |
| color: var(--muted); | |
| } | |
| /* Button styling */ | |
| .btn-primary { | |
| background: var(--accent); | |
| color: var(--bg); | |
| font-weight: 600; | |
| padding: 16px 28px; | |
| border-radius: 14px; | |
| border: none; | |
| cursor: pointer; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| font-size: 15px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| white-space: nowrap; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px var(--accent-glow); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0); | |
| } | |
| /* Filter buttons */ | |
| .filter-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--muted); | |
| padding: 10px 18px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.25s ease; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .filter-btn:hover { | |
| border-color: rgba(255, 255, 255, 0.15); | |
| color: var(--fg); | |
| } | |
| .filter-btn.active { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| color: var(--bg); | |
| } | |
| /* Todo item */ | |
| .todo-item { | |
| background: rgba(255, 255, 255, 0.02); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| padding: 18px 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .todo-item:hover { | |
| background: rgba(255, 255, 255, 0.04); | |
| border-color: rgba(255, 255, 255, 0.12); | |
| transform: translateX(4px); | |
| } | |
| .todo-item.completed { | |
| opacity: 0.5; | |
| } | |
| .todo-item.completed .todo-text { | |
| text-decoration: line-through; | |
| color: var(--muted); | |
| } | |
| .todo-item.removing { | |
| animation: slideOut 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes slideOut { | |
| to { | |
| opacity: 0; | |
| transform: translateX(30px); | |
| } | |
| } | |
| /* Custom checkbox */ | |
| .custom-checkbox { | |
| width: 24px; | |
| height: 24px; | |
| border: 2px solid var(--border); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.25s ease; | |
| flex-shrink: 0; | |
| } | |
| .custom-checkbox:hover { | |
| border-color: var(--accent); | |
| } | |
| .custom-checkbox.checked { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| .custom-checkbox svg { | |
| opacity: 0; | |
| transform: scale(0.5); | |
| transition: all 0.2s ease; | |
| } | |
| .custom-checkbox.checked svg { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| /* Delete button */ | |
| .delete-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--muted); | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 8px; | |
| transition: all 0.25s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| } | |
| .todo-item:hover .delete-btn { | |
| opacity: 1; | |
| } | |
| .delete-btn:hover { | |
| background: var(--danger-glow); | |
| color: var(--danger); | |
| } | |
| /* Progress bar */ | |
| .progress-bar { | |
| height: 6px; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), #00ffcc); | |
| border-radius: 3px; | |
| transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Empty state */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: var(--muted); | |
| } | |
| .empty-state svg { | |
| margin-bottom: 20px; | |
| opacity: 0.5; | |
| } | |
| /* Stats card */ | |
| .stat-card { | |
| background: rgba(255, 255, 255, 0.03); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 16px 20px; | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--accent); | |
| line-height: 1; | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| color: var(--muted); | |
| margin-top: 4px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| /* Header link */ | |
| .header-link { | |
| color: var(--muted); | |
| text-decoration: none; | |
| font-size: 12px; | |
| transition: color 0.2s ease; | |
| } | |
| .header-link:hover { | |
| color: var(--accent); | |
| } | |
| /* Entrance animations */ | |
| .fade-up { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| animation: fadeUp 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards; | |
| } | |
| .fade-up:nth-child(1) { animation-delay: 0.1s; } | |
| .fade-up:nth-child(2) { animation-delay: 0.2s; } | |
| .fade-up:nth-child(3) { animation-delay: 0.3s; } | |
| @keyframes fadeUp { | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Reduced motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| /* Focus visible */ | |
| :focus-visible { | |
| outline: 2px solid var(--accent); | |
| outline-offset: 2px; | |
| } | |
| button:focus:not(:focus-visible) { | |
| outline: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-mesh"></div> | |
| <div class="grid-pattern"></div> | |
| <div class="min-h-screen py-8 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-2xl mx-auto"> | |
| <!-- Header --> | |
| <header class="text-center mb-10 fade-up"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <span></span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener" class="header-link"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| <h1 class="font-display text-4xl sm:text-5xl font-bold tracking-tight mb-3"> | |
| Task<span style="color: var(--accent)">flow</span> | |
| </h1> | |
| <p class="text-base" style="color: var(--muted)">Organize your day, one task at a time</p> | |
| </header> | |
| <!-- Main Card --> | |
| <main class="glass-card p-6 sm:p-8 fade-up" style="animation-delay: 0.15s;"> | |
| <!-- Input Section --> | |
| <form id="todo-form" class="flex flex-col sm:flex-row gap-3 mb-8"> | |
| <input | |
| type="text" | |
| id="todo-input" | |
| class="todo-input flex-1" | |
| placeholder="What needs to be done?" | |
| aria-label="New todo input" | |
| autocomplete="off" | |
| > | |
| <button type="submit" class="btn-primary justify-center"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"> | |
| <line x1="12" y1="5" x2="12" y2="19"></line> | |
| <line x1="5" y1="12" x2="19" y2="12"></line> | |
| </svg> | |
| Add Task | |
| </button> | |
| </form> | |
| <!-- Progress Section --> | |
| <div class="mb-6"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="text-sm font-medium" style="color: var(--muted)">Progress</span> | |
| <span id="progress-text" class="text-sm font-semibold" style="color: var(--accent)">0%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div id="progress-fill" class="progress-fill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="grid grid-cols-3 gap-3 mb-6"> | |
| <div class="stat-card"> | |
| <div id="stat-total" class="stat-value">0</div> | |
| <div class="stat-label">Total</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div id="stat-active" class="stat-value">0</div> | |
| <div class="stat-label">Active</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div id="stat-completed" class="stat-value">0</div> | |
| <div class="stat-label">Done</div> | |
| </div> | |
| </div> | |
| <!-- Filters --> | |
| <div class="flex flex-wrap gap-2 mb-6"> | |
| <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> | |
| <button id="clear-completed" class="filter-btn ml-auto" style="color: var(--danger); border-color: rgba(255, 71, 87, 0.3);"> | |
| Clear Done | |
| </button> | |
| </div> | |
| <!-- Todo List --> | |
| <div id="todo-list" class="space-y-3" role="list" aria-label="Todo list"> | |
| <!-- Todos will be rendered here --> | |
| </div> | |
| <!-- Empty State --> | |
| <div id="empty-state" class="empty-state"> | |
| <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M9 11l3 3L22 4"></path> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path> | |
| </svg> | |
| <p class="font-display text-lg font-medium mb-1">No tasks yet</p> | |
| <p class="text-sm">Add your first task to get started</p> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="text-center mt-8 fade-up" style="animation-delay: 0.25s;"> | |
| <p class="text-xs" style="color: var(--muted)"> | |
| Press Enter to add task. Click to complete. Data saved locally. | |
| </p> | |
| </footer> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize state | |
| let todos = JSON.parse(localStorage.getItem('taskflow-todos')) || []; | |
| let currentFilter = 'all'; | |
| // DOM Elements | |
| const todoForm = document.getElementById('todo-form'); | |
| const todoInput = document.getElementById('todo-input'); | |
| const todoList = document.getElementById('todo-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const filterBtns = document.querySelectorAll('.filter-btn[data-filter]'); | |
| const clearCompletedBtn = document.getElementById('clear-completed'); | |
| const progressFill = document.getElementById('progress-fill'); | |
| const progressText = document.getElementById('progress-text'); | |
| const statTotal = document.getElementById('stat-total'); | |
| const statActive = document.getElementById('stat-active'); | |
| const statCompleted = document.getElementById('stat-completed'); | |
| // Generate unique ID | |
| function generateId() { | |
| return Date.now().toString(36) + Math.random().toString(36).substr(2); | |
| } | |
| // Save to localStorage | |
| function saveTodos() { | |
| localStorage.setItem('taskflow-todos', JSON.stringify(todos)); | |
| } | |
| // Update stats | |
| function updateStats() { | |
| const total = todos.length; | |
| const completed = todos.filter(t => t.completed).length; | |
| const active = total - completed; | |
| const progress = total > 0 ? Math.round((completed / total) * 100) : 0; | |
| statTotal.textContent = total; | |
| statActive.textContent = active; | |
| statCompleted.textContent = completed; | |
| progressFill.style.width = `${progress}%`; | |
| progressText.textContent = `${progress}%`; | |
| } | |
| // Create todo element | |
| function createTodoElement(todo) { | |
| const item = document.createElement('div'); | |
| item.className = `todo-item ${todo.completed ? 'completed' : ''}`; | |
| item.dataset.id = todo.id; | |
| item.setAttribute('role', 'listitem'); | |
| item.innerHTML = ` | |
| <div class="custom-checkbox ${todo.completed ? 'checked' : ''}" role="checkbox" aria-checked="${todo.completed}" tabindex="0" aria-label="Mark as ${todo.completed ? 'incomplete' : 'complete'}"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#0a0a0f" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 6 9 17 4 12"></polyline> | |
| </svg> | |
| </div> | |
| <span class="todo-text flex-1">${escapeHtml(todo.text)}</span> | |
| <button class="delete-btn" aria-label="Delete task"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="3 6 5 6 21 6"></polyline> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> | |
| </svg> | |
| </button> | |
| `; | |
| // Toggle complete | |
| const checkbox = item.querySelector('.custom-checkbox'); | |
| checkbox.addEventListener('click', () => toggleTodo(todo.id)); | |
| checkbox.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| toggleTodo(todo.id); | |
| } | |
| }); | |
| // Delete | |
| const deleteBtn = item.querySelector('.delete-btn'); | |
| deleteBtn.addEventListener('click', () => deleteTodo(todo.id)); | |
| return item; | |
| } | |
| // Escape HTML | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Render todos | |
| function renderTodos() { | |
| const filteredTodos = todos.filter(todo => { | |
| if (currentFilter === 'active') return !todo.completed; | |
| if (currentFilter === 'completed') return todo.completed; | |
| return true; | |
| }); | |
| todoList.innerHTML = ''; | |
| if (filteredTodos.length === 0) { | |
| emptyState.style.display = 'block'; | |
| todoList.style.display = 'none'; | |
| } else { | |
| emptyState.style.display = 'none'; | |
| todoList.style.display = 'flex'; | |
| todoList.style.flexDirection = 'column'; | |
| filteredTodos.forEach(todo => { | |
| todoList.appendChild(createTodoElement(todo)); | |
| }); | |
| } | |
| updateStats(); | |
| } | |
| // Add todo | |
| function addTodo(text) { | |
| const trimmedText = text.trim(); | |
| if (!trimmedText) return; | |
| const todo = { | |
| id: generateId(), | |
| text: trimmedText, | |
| completed: false, | |
| createdAt: Date.now() | |
| }; | |
| todos.unshift(todo); | |
| saveTodos(); | |
| renderTodos(); | |
| todoInput.value = ''; | |
| } | |
| // Toggle todo | |
| function toggleTodo(id) { | |
| todos = todos.map(todo => | |
| todo.id === id ? { ...todo, completed: !todo.completed } : todo | |
| ); | |
| saveTodos(); | |
| renderTodos(); | |
| } | |
| // Delete todo | |
| function deleteTodo(id) { | |
| const item = todoList.querySelector(`[data-id="${id}"]`); | |
| if (item) { | |
| item.classList.add('removing'); | |
| setTimeout(() => { | |
| todos = todos.filter(todo => todo.id !== id); | |
| saveTodos(); | |
| renderTodos(); | |
| }, 300); | |
| } | |
| } | |
| // Clear completed | |
| function clearCompleted() { | |
| const completedItems = todoList.querySelectorAll('.todo-item.completed'); | |
| completedItems.forEach(item => item.classList.add('removing')); | |
| setTimeout(() => { | |
| todos = todos.filter(todo => !todo.completed); | |
| saveTodos(); | |
| renderTodos(); | |
| }, 300); | |
| } | |
| // Event Listeners | |
| todoForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| addTodo(todoInput.value); | |
| }); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| filterBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| currentFilter = btn.dataset.filter; | |
| renderTodos(); | |
| }); | |
| }); | |
| clearCompletedBtn.addEventListener('click', clearCompleted); | |
| // Initial render | |
| renderTodos(); | |
| </script> | |
| </body> | |
| </html> |