Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TASKFLOW</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@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #0a0a0b; | |
| --bg-elevated: #131315; | |
| --fg: #e8e8e8; | |
| --muted: #6b6b6b; | |
| --accent: #00ff88; | |
| --accent-dim: rgba(0, 255, 136, 0.15); | |
| --danger: #ff4757; | |
| --border: #2a2a2d; | |
| --card: #18181b; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: var(--bg); | |
| color: var(--fg); | |
| min-height: 100vh; | |
| margin: 0; | |
| overflow-x: hidden; | |
| } | |
| /* Background Pattern */ | |
| .bg-pattern { | |
| position: fixed; | |
| inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0, 255, 136, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 255, 136, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .bg-glow { | |
| position: fixed; | |
| top: -200px; | |
| right: -200px; | |
| width: 600px; | |
| height: 600px; | |
| background: radial-gradient(circle, rgba(0, 255, 136, 0.08) 0%, transparent 70%); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .bg-glow-2 { | |
| position: fixed; | |
| bottom: -300px; | |
| left: -200px; | |
| width: 500px; | |
| height: 500px; | |
| background: radial-gradient(circle, rgba(0, 255, 136, 0.05) 0%, transparent 70%); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* Typography */ | |
| .font-mono { | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--muted); | |
| } | |
| /* Input Styling */ | |
| .todo-input { | |
| background: var(--bg); | |
| border: 2px solid var(--border); | |
| color: var(--fg); | |
| transition: all 0.2s ease; | |
| } | |
| .todo-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 4px var(--accent-dim); | |
| } | |
| .todo-input::placeholder { | |
| color: var(--muted); | |
| } | |
| /* Button Styling */ | |
| .btn-primary { | |
| background: var(--accent); | |
| color: var(--bg); | |
| font-weight: 700; | |
| transition: all 0.2s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn-primary::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| transform: translateX(-100%); | |
| transition: transform 0.5s ease; | |
| } | |
| .btn-primary:hover::before { | |
| transform: translateX(100%); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0); | |
| } | |
| /* Filter Buttons */ | |
| .filter-btn { | |
| background: transparent; | |
| color: var(--muted); | |
| border: 1px solid transparent; | |
| transition: all 0.2s ease; | |
| } | |
| .filter-btn:hover { | |
| color: var(--fg); | |
| background: var(--bg-elevated); | |
| } | |
| .filter-btn.active { | |
| color: var(--accent); | |
| border-color: var(--accent); | |
| background: var(--accent-dim); | |
| } | |
| /* Todo Item */ | |
| .todo-item { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s ease; | |
| animation: slideIn 0.4s ease forwards; | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| .todo-item:hover { | |
| border-color: rgba(0, 255, 136, 0.3); | |
| 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 ease forwards; | |
| } | |
| @keyframes slideIn { | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes slideOut { | |
| to { | |
| opacity: 0; | |
| transform: translateX(100px); | |
| } | |
| } | |
| /* Checkbox */ | |
| .checkbox-container { | |
| position: relative; | |
| width: 24px; | |
| height: 24px; | |
| flex-shrink: 0; | |
| } | |
| .checkbox-input { | |
| position: absolute; | |
| opacity: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| z-index: 2; | |
| } | |
| .checkbox-custom { | |
| position: absolute; | |
| inset: 0; | |
| border: 2px solid var(--border); | |
| border-radius: 6px; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .checkbox-input:hover + .checkbox-custom { | |
| border-color: var(--accent); | |
| } | |
| .checkbox-input:checked + .checkbox-custom { | |
| background: var(--accent); | |
| border-color: var(--accent); | |
| } | |
| .checkbox-custom svg { | |
| opacity: 0; | |
| transform: scale(0); | |
| transition: all 0.2s ease; | |
| } | |
| .checkbox-input:checked + .checkbox-custom svg { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| .checkbox-input:focus-visible + .checkbox-custom { | |
| box-shadow: 0 0 0 3px var(--accent-dim); | |
| } | |
| /* Delete Button */ | |
| .delete-btn { | |
| opacity: 0; | |
| transition: all 0.2s ease; | |
| color: var(--muted); | |
| } | |
| .todo-item:hover .delete-btn { | |
| opacity: 1; | |
| } | |
| .delete-btn:hover { | |
| color: var(--danger); | |
| transform: scale(1.1); | |
| } | |
| /* Progress Bar */ | |
| .progress-bar { | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent), #00ccff); | |
| transition: width 0.5s ease; | |
| border-radius: 2px; | |
| } | |
| /* Stats Card */ | |
| .stat-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| transition: all 0.2s ease; | |
| } | |
| .stat-card:hover { | |
| border-color: var(--accent); | |
| } | |
| /* Header Animation */ | |
| .header-title { | |
| animation: glitch 3s infinite; | |
| } | |
| @keyframes glitch { | |
| 0%, 90%, 100% { | |
| text-shadow: none; | |
| } | |
| 92% { | |
| text-shadow: -2px 0 var(--accent), 2px 0 #ff4757; | |
| } | |
| 94% { | |
| text-shadow: 2px 0 var(--accent), -2px 0 #ff4757; | |
| } | |
| 96% { | |
| text-shadow: none; | |
| } | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { | |
| transform: translateY(0); | |
| } | |
| 50% { | |
| transform: translateY(-10px); | |
| } | |
| } | |
| /* Stagger Animation */ | |
| .stagger-1 { animation-delay: 0.1s; } | |
| .stagger-2 { animation-delay: 0.2s; } | |
| .stagger-3 { animation-delay: 0.3s; } | |
| .stagger-4 { animation-delay: 0.4s; } | |
| /* Reduced Motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| *, *::before, *::after { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| /* Priority Indicators */ | |
| .priority-high { border-left: 3px solid var(--danger); } | |
| .priority-medium { border-left: 3px solid #ffa502; } | |
| .priority-low { border-left: 3px solid var(--accent); } | |
| /* Priority Select */ | |
| .priority-select { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| color: var(--fg); | |
| transition: all 0.2s ease; | |
| } | |
| .priority-select:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-pattern"></div> | |
| <div class="bg-glow"></div> | |
| <div class="bg-glow-2"></div> | |
| <div class="relative z-10 min-h-screen px-4 py-8 md:py-12"> | |
| <div class="max-w-2xl mx-auto"> | |
| <!-- Header --> | |
| <header class="mb-10 md:mb-12"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" | |
| class="text-xs font-mono text-[var(--muted)] hover:text-[var(--accent)] transition-colors"> | |
| Built with anycoder | |
| </a> | |
| <span class="font-mono text-xs text-[var(--muted)]" id="datetime"></span> | |
| </div> | |
| <h1 class="header-title text-4xl md:text-5xl font-bold tracking-tight"> | |
| TASK<span class="text-[var(--accent)]">FLOW</span> | |
| </h1> | |
| <p class="text-[var(--muted)] mt-2 font-mono text-sm">// your productivity terminal</p> | |
| </header> | |
| <!-- Stats --> | |
| <div class="grid grid-cols-3 gap-3 mb-8" id="stats"> | |
| <div class="stat-card rounded-lg p-4"> | |
| <div class="text-2xl md:text-3xl font-bold text-[var(--accent)]" id="totalCount">0</div> | |
| <div class="text-xs text-[var(--muted)] font-mono mt-1">TOTAL</div> | |
| </div> | |
| <div class="stat-card rounded-lg p-4"> | |
| <div class="text-2xl md:text-3xl font-bold" id="activeCount">0</div> | |
| <div class="text-xs text-[var(--muted)] font-mono mt-1">ACTIVE</div> | |
| </div> | |
| <div class="stat-card rounded-lg p-4"> | |
| <div class="text-2xl md:text-3xl font-bold text-[var(--accent)]" id="completedCount">0</div> | |
| <div class="text-xs text-[var(--muted)] font-mono mt-1">DONE</div> | |
| </div> | |
| </div> | |
| <!-- Progress Bar --> | |
| <div class="mb-8"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="text-xs font-mono text-[var(--muted)]">PROGRESS</span> | |
| <span class="text-xs font-mono text-[var(--accent)]" id="progressPercent">0%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Input Section --> | |
| <div class="mb-8"> | |
| <form id="todoForm" class="flex flex-col sm:flex-row gap-3"> | |
| <input | |
| type="text" | |
| id="todoInput" | |
| class="todo-input flex-1 px-4 py-3 rounded-lg text-base" | |
| placeholder="What needs to be done?" | |
| autocomplete="off" | |
| aria-label="New todo input" | |
| > | |
| <select id="prioritySelect" class="priority-select px-4 py-3 rounded-lg font-mono text-sm" aria-label="Priority level"> | |
| <option value="low">LOW</option> | |
| <option value="medium" selected>MED</option> | |
| <option value="high">HIGH</option> | |
| </select> | |
| <button | |
| type="submit" | |
| class="btn-primary px-6 py-3 rounded-lg font-mono text-sm whitespace-nowrap" | |
| aria-label="Add todo" | |
| > | |
| ADD TASK | |
| </button> | |
| </form> | |
| </div> | |
| <!-- Filters --> | |
| <div class="flex gap-2 mb-6 overflow-x-auto pb-2" role="tablist" aria-label="Filter todos"> | |
| <button class="filter-btn active px-4 py-2 rounded-lg font-mono text-xs" data-filter="all" role="tab" aria-selected="true"> | |
| ALL | |
| </button> | |
| <button class="filter-btn px-4 py-2 rounded-lg font-mono text-xs" data-filter="active" role="tab" aria-selected="false"> | |
| ACTIVE | |
| </button> | |
| <button class="filter-btn px-4 py-2 rounded-lg font-mono text-xs" data-filter="completed" role="tab" aria-selected="false"> | |
| COMPLETED | |
| </button> | |
| </div> | |
| <!-- Todo List --> | |
| <div id="todoList" class="space-y-3" role="list" aria-label="Todo list"> | |
| <!-- Todos will be rendered here --> | |
| </div> | |
| <!-- Empty State --> | |
| <div id="emptyState" class="hidden text-center py-16"> | |
| <div class="empty-state inline-block mb-4"> | |
| <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" class="text-[var(--muted)]"> | |
| <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/> | |
| </svg> | |
| </div> | |
| <p class="text-[var(--muted)] font-mono text-sm">NO TASKS YET</p> | |
| <p class="text-[var(--muted)] font-mono text-xs mt-1 opacity-50">// add your first task above</p> | |
| </div> | |
| <!-- Clear Completed --> | |
| <div class="mt-8 flex justify-between items-center" id="clearSection"> | |
| <span class="text-xs font-mono text-[var(--muted)]" id="itemsLeft">0 items left</span> | |
| <button | |
| id="clearCompleted" | |
| class="text-xs font-mono text-[var(--danger)] hover:underline hidden" | |
| aria-label="Clear completed tasks" | |
| > | |
| CLEAR COMPLETED | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize todos array first | |
| let todos = []; | |
| let currentFilter = 'all'; | |
| // DOM Elements - get after initialization | |
| const todoForm = document.getElementById('todoForm'); | |
| const todoInput = document.getElementById('todoInput'); | |
| const prioritySelect = document.getElementById('prioritySelect'); | |
| const todoList = document.getElementById('todoList'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const clearCompletedBtn = document.getElementById('clearCompleted'); | |
| const totalCountEl = document.getElementById('totalCount'); | |
| const activeCountEl = document.getElementById('activeCount'); | |
| const completedCountEl = document.getElementById('completedCount'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const itemsLeft = document.getElementById('itemsLeft'); | |
| const datetimeEl = document.getElementById('datetime'); | |
| // Load from localStorage | |
| function loadTodos() { | |
| const stored = localStorage.getItem('taskflow-todos'); | |
| if (stored) { | |
| try { | |
| todos = JSON.parse(stored); | |
| } catch (e) { | |
| todos = []; | |
| } | |
| } | |
| } | |
| // Save to localStorage | |
| function saveTodos() { | |
| localStorage.setItem('taskflow-todos', JSON.stringify(todos)); | |
| } | |
| // Generate unique ID | |
| function generateId() { | |
| return Date.now().toString(36) + Math.random().toString(36).substr(2); | |
| } | |
| // Update Stats | |
| function updateStats() { | |
| const total = todos.length; | |
| const completed = todos.filter(t => t.completed).length; | |
| const active = total - completed; | |
| const percent = total > 0 ? Math.round((completed / total) * 100) : 0; | |
| totalCountEl.textContent = total; | |
| activeCountEl.textContent = active; | |
| completedCountEl.textContent = completed; | |
| progressFill.style.width = percent + '%'; | |
| progressPercent.textContent = percent + '%'; | |
| itemsLeft.textContent = active + ' item' + (active !== 1 ? 's' : '') + ' left'; | |
| // Show/hide clear completed button | |
| if (completed > 0) { | |
| clearCompletedBtn.classList.remove('hidden'); | |
| } else { | |
| clearCompletedBtn.classList.add('hidden'); | |
| } | |
| } | |
| // Render Todos | |
| function renderTodos() { | |
| const filtered = todos.filter(todo => { | |
| if (currentFilter === 'active') return !todo.completed; | |
| if (currentFilter === 'completed') return todo.completed; | |
| return true; | |
| }); | |
| if (filtered.length === 0) { | |
| todoList.innerHTML = ''; | |
| emptyState.classList.remove('hidden'); | |
| } else { | |
| emptyState.classList.add('hidden'); | |
| todoList.innerHTML = filtered.map((todo, index) => ` | |
| <div | |
| class="todo-item rounded-lg p-4 flex items-center gap-4 priority-${todo.priority} ${todo.completed ? 'completed' : ''} stagger-${(index % 4) + 1}" | |
| data-id="${todo.id}" | |
| role="listitem" | |
| > | |
| <label class="checkbox-container"> | |
| <input | |
| type="checkbox" | |
| class="checkbox-input" | |
| ${todo.completed ? 'checked' : ''} | |
| onchange="toggleTodo('${todo.id}')" | |
| aria-label="${todo.completed ? 'Mark as incomplete' : 'Mark as complete'}" | |
| > | |
| <div class="checkbox-custom"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#0a0a0b" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="20 6 9 17 4 12"></polyline> | |
| </svg> | |
| </div> | |
| </label> | |
| <span class="todo-text flex-1 text-base">${escapeHtml(todo.text)}</span> | |
| <span class="font-mono text-xs text-[var(--muted)] hidden sm:block">${todo.priority.toUpperCase()}</span> | |
| <button | |
| class="delete-btn p-2 rounded-lg hover:bg-[var(--bg-elevated)] transition-all" | |
| onclick="deleteTodo('${todo.id}')" | |
| 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> | |
| </div> | |
| `).join(''); | |
| } | |
| updateStats(); | |
| } | |
| // Escape HTML to prevent XSS | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Add Todo | |
| function addTodo(text, priority) { | |
| if (!text.trim()) return; | |
| todos.unshift({ | |
| id: generateId(), | |
| text: text.trim(), | |
| completed: false, | |
| priority: priority, | |
| createdAt: new Date().toISOString() | |
| }); | |
| saveTodos(); | |
| renderTodos(); | |
| } | |
| // Toggle Todo | |
| function toggleTodo(id) { | |
| const todo = todos.find(t => t.id === id); | |
| if (todo) { | |
| todo.completed = !todo.completed; | |
| saveTodos(); | |
| renderTodos(); | |
| } | |
| } | |
| // Delete Todo | |
| function deleteTodo(id) { | |
| const item = document.querySelector(`[data-id="${id}"]`); | |
| if (item) { | |
| item.classList.add('removing'); | |
| setTimeout(() => { | |
| todos = todos.filter(t => t.id !== id); | |
| saveTodos(); | |
| renderTodos(); | |
| }, 300); | |
| } | |
| } | |
| // Clear Completed | |
| function clearCompleted() { | |
| todos = todos.filter(t => !t.completed); | |
| saveTodos(); | |
| renderTodos(); | |
| } | |
| // Update DateTime | |
| function updateDateTime() { | |
| const now = new Date(); | |
| const options = { | |
| weekday: 'short', | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }; | |
| datetimeEl.textContent = now.toLocaleDateString('en-US', options).toUpperCase(); | |
| } | |
| // Event Listeners | |
| todoForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| addTodo(todoInput.value, prioritySelect.value); | |
| todoInput.value = ''; | |
| todoInput.focus(); | |
| }); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| filterBtns.forEach(b => { | |
| b.classList.remove('active'); | |
| b.setAttribute('aria-selected', 'false'); | |
| }); | |
| btn.classList.add('active'); | |
| btn.setAttribute('aria-selected', 'true'); | |
| currentFilter = btn.dataset.filter; | |
| renderTodos(); | |
| }); | |
| }); | |
| clearCompletedBtn.addEventListener('click', clearCompleted); | |
| // Keyboard Navigation | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) { | |
| e.preventDefault(); | |
| todoInput.focus(); | |
| } | |
| }); | |
| // Initialize | |
| loadTodos(); | |
| renderTodos(); | |
| updateDateTime(); | |
| setInterval(updateDateTime, 60000); | |
| </script> | |
| </body> | |
| </html> |