| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>TaskMaster</title> |
| <style> |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --bg: #0f1117; |
| --surface: #181c27; |
| --card: #1e2333; |
| --border: #2a3045; |
| --accent: #7c6af7; |
| --accent2: #a78bfa; |
| --text: #e2e8f0; |
| --muted: #64748b; |
| --high: #f87171; |
| --medium: #fbbf24; |
| --low: #34d399; |
| --done-bg: #141820; |
| --done-text: #3f4a5a; |
| --radius: 12px; |
| --font: 'Inter', system-ui, sans-serif; |
| } |
| |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| body { |
| font-family: var(--font); |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| line-height: 1.6; |
| } |
| |
| |
| .shell { |
| max-width: 860px; |
| margin: 0 auto; |
| padding: 0 20px 60px; |
| } |
| |
| |
| header { |
| padding: 40px 0 30px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| flex-wrap: wrap; |
| gap: 16px; |
| } |
| .logo { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| .logo-icon { |
| width: 40px; height: 40px; |
| background: linear-gradient(135deg, var(--accent), #c084fc); |
| border-radius: 10px; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 20px; |
| } |
| .logo h1 { |
| font-size: 1.5rem; |
| font-weight: 700; |
| letter-spacing: -0.03em; |
| background: linear-gradient(to right, var(--accent2), #c084fc); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| |
| .stats { |
| display: flex; |
| gap: 12px; |
| flex-wrap: wrap; |
| margin-bottom: 28px; |
| } |
| .stat { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 14px 20px; |
| flex: 1; |
| min-width: 100px; |
| text-align: center; |
| } |
| .stat-num { |
| font-size: 1.75rem; |
| font-weight: 700; |
| line-height: 1; |
| margin-bottom: 4px; |
| } |
| .stat-label { |
| font-size: 0.72rem; |
| text-transform: uppercase; |
| letter-spacing: 0.08em; |
| color: var(--muted); |
| } |
| .stat-num.purple { color: var(--accent2); } |
| .stat-num.green { color: var(--low); } |
| .stat-num.red { color: var(--high); } |
| |
| |
| .add-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 22px; |
| margin-bottom: 24px; |
| } |
| .add-card h2 { |
| font-size: 0.85rem; |
| text-transform: uppercase; |
| letter-spacing: 0.08em; |
| color: var(--muted); |
| margin-bottom: 16px; |
| } |
| .form-row { |
| display: flex; |
| gap: 10px; |
| flex-wrap: wrap; |
| } |
| input, select { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| color: var(--text); |
| font-family: var(--font); |
| font-size: 0.9rem; |
| padding: 10px 14px; |
| outline: none; |
| transition: border-color 0.15s; |
| } |
| input:focus, select:focus { |
| border-color: var(--accent); |
| } |
| input[type="text"] { flex: 1; min-width: 200px; } |
| select { min-width: 120px; } |
| input[type="date"] { min-width: 150px; color-scheme: dark; } |
| |
| .btn { |
| border: none; |
| border-radius: 8px; |
| cursor: pointer; |
| font-family: var(--font); |
| font-size: 0.9rem; |
| font-weight: 600; |
| padding: 10px 22px; |
| transition: opacity 0.15s, transform 0.1s; |
| } |
| .btn:active { transform: scale(0.97); } |
| .btn-primary { |
| background: linear-gradient(135deg, var(--accent), #a855f7); |
| color: #fff; |
| } |
| .btn-primary:hover { opacity: 0.88; } |
| |
| |
| .filters { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| margin-bottom: 16px; |
| } |
| .chip { |
| border: 1px solid var(--border); |
| border-radius: 20px; |
| background: var(--surface); |
| color: var(--muted); |
| cursor: pointer; |
| font-family: var(--font); |
| font-size: 0.8rem; |
| padding: 5px 14px; |
| transition: all 0.15s; |
| } |
| .chip.active { |
| background: var(--accent); |
| border-color: var(--accent); |
| color: #fff; |
| } |
| |
| |
| #task-list { display: flex; flex-direction: column; gap: 10px; } |
| |
| .task-card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 16px 18px; |
| display: flex; |
| align-items: flex-start; |
| gap: 14px; |
| transition: opacity 0.2s, border-color 0.2s; |
| animation: slideIn 0.2s ease; |
| } |
| @keyframes slideIn { |
| from { opacity: 0; transform: translateY(-6px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .task-card.done-card { |
| background: var(--done-bg); |
| opacity: 0.6; |
| } |
| .task-card:hover { border-color: #3a4060; } |
| |
| |
| .task-card { border-left: 3px solid transparent; } |
| .task-card[data-priority="high"] { border-left-color: var(--high); } |
| .task-card[data-priority="medium"] { border-left-color: var(--medium); } |
| .task-card[data-priority="low"] { border-left-color: var(--low); } |
| |
| |
| .check-btn { |
| width: 22px; height: 22px; |
| border: 2px solid var(--border); |
| border-radius: 50%; |
| background: transparent; |
| cursor: pointer; |
| flex-shrink: 0; |
| margin-top: 2px; |
| transition: all 0.15s; |
| display: flex; align-items: center; justify-content: center; |
| } |
| .check-btn:hover { border-color: var(--low); } |
| .check-btn.checked { |
| background: var(--low); |
| border-color: var(--low); |
| } |
| .check-btn.checked::after { content: 'β'; color: #0f1117; font-size: 13px; font-weight: 700; } |
| |
| |
| .task-body { flex: 1; min-width: 0; } |
| .task-title { |
| font-size: 0.95rem; |
| font-weight: 500; |
| color: var(--text); |
| margin-bottom: 6px; |
| word-break: break-word; |
| } |
| .task-card.done-card .task-title { |
| text-decoration: line-through; |
| color: var(--done-text); |
| } |
| .task-meta { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| .badge { |
| border-radius: 4px; |
| font-size: 0.7rem; |
| font-weight: 600; |
| padding: 2px 8px; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| .badge-high { background: rgba(248,113,113,0.15); color: var(--high); } |
| .badge-medium { background: rgba(251,191,36,0.15); color: var(--medium); } |
| .badge-low { background: rgba(52,211,153,0.15); color: var(--low); } |
| .badge-cat { background: rgba(124,106,247,0.15); color: var(--accent2); } |
| .badge-date { background: rgba(100,116,139,0.1); color: var(--muted); } |
| |
| |
| .del-btn { |
| background: transparent; |
| border: none; |
| color: var(--muted); |
| cursor: pointer; |
| font-size: 18px; |
| padding: 2px 6px; |
| border-radius: 6px; |
| transition: color 0.15s, background 0.15s; |
| flex-shrink: 0; |
| } |
| .del-btn:hover { color: var(--high); background: rgba(248,113,113,0.1); } |
| |
| |
| .empty { |
| text-align: center; |
| padding: 60px 20px; |
| color: var(--muted); |
| } |
| .empty .icon { font-size: 3rem; margin-bottom: 12px; } |
| .empty p { font-size: 0.9rem; } |
| |
| |
| #toast { |
| position: fixed; |
| bottom: 30px; right: 30px; |
| background: #1e293b; |
| border: 1px solid var(--border); |
| border-radius: 10px; |
| color: var(--text); |
| font-size: 0.88rem; |
| padding: 12px 20px; |
| opacity: 0; |
| transform: translateY(10px); |
| transition: all 0.25s; |
| z-index: 999; |
| pointer-events: none; |
| } |
| #toast.show { opacity: 1; transform: translateY(0); } |
| |
| @media (max-width: 540px) { |
| .form-row { flex-direction: column; } |
| .form-row input, .form-row select, .form-row .btn { width: 100%; } |
| .stats .stat-num { font-size: 1.4rem; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="shell"> |
|
|
| |
| <header> |
| <div class="logo"> |
| <div class="logo-icon">β¦</div> |
| <h1>TaskMaster</h1> |
| </div> |
| <span style="color:var(--muted);font-size:0.82rem;">Your personal task engine</span> |
| </header> |
|
|
| |
| <div class="stats" id="stats"> |
| <div class="stat"><div class="stat-num purple" id="s-total">β</div><div class="stat-label">Total</div></div> |
| <div class="stat"><div class="stat-num green" id="s-done">β</div><div class="stat-label">Done</div></div> |
| <div class="stat"><div class="stat-num purple" id="s-pending">β</div><div class="stat-label">Pending</div></div> |
| <div class="stat"><div class="stat-num red" id="s-high">β</div><div class="stat-label">Urgent</div></div> |
| </div> |
|
|
| |
| <div class="add-card"> |
| <h2>Add New Task</h2> |
| <div class="form-row"> |
| <input type="text" id="inp-title" placeholder="What needs to be done?" /> |
| <select id="inp-priority"> |
| <option value="high">π΄ High</option> |
| <option value="medium" selected>π‘ Medium</option> |
| <option value="low">π’ Low</option> |
| </select> |
| <select id="inp-category"> |
| <option value="work">πΌ Work</option> |
| <option value="personal">π Personal</option> |
| <option value="learning">π Learning</option> |
| <option value="general" selected>π General</option> |
| </select> |
| <input type="date" id="inp-due" /> |
| <button class="btn btn-primary" onclick="addTask()">Add Task</button> |
| </div> |
| </div> |
|
|
| |
| <div class="filters"> |
| <button class="chip active" onclick="setFilter('all', this)">All</button> |
| <button class="chip" onclick="setFilter('pending', this)">Pending</button> |
| <button class="chip" onclick="setFilter('done', this)">Done</button> |
| <button class="chip" onclick="setFilter('high', this)">π΄ High</button> |
| <button class="chip" onclick="setFilter('work', this)">πΌ Work</button> |
| <button class="chip" onclick="setFilter('personal', this)">π Personal</button> |
| <button class="chip" onclick="setFilter('learning', this)">π Learning</button> |
| </div> |
|
|
| |
| <div id="task-list"> |
| <div class="empty"><div class="icon">β³</div><p>Loading tasksβ¦</p></div> |
| </div> |
| </div> |
|
|
| |
| <div id="toast"></div> |
|
|
| <script> |
| let currentFilter = 'all'; |
| |
| |
| async function api(method, path, body) { |
| const opts = { |
| method, |
| headers: { 'Content-Type': 'application/json' }, |
| }; |
| if (body) opts.body = JSON.stringify(body); |
| const r = await fetch(path, opts); |
| return r.json(); |
| } |
| |
| |
| async function loadTasks() { |
| let url = '/api/tasks'; |
| const params = []; |
| if (currentFilter === 'pending') params.push('done=false'); |
| if (currentFilter === 'done') params.push('done=true'); |
| if (currentFilter === 'high') params.push('priority=high'); |
| if (['work','personal','learning'].includes(currentFilter)) |
| params.push(`category=${currentFilter}`); |
| if (params.length) url += '?' + params.join('&'); |
| |
| const data = await api('GET', url); |
| renderTasks(data.tasks); |
| loadStats(); |
| } |
| |
| function renderTasks(tasks) { |
| const el = document.getElementById('task-list'); |
| if (!tasks.length) { |
| el.innerHTML = `<div class="empty"><div class="icon">π</div><p>No tasks here. Add one above!</p></div>`; |
| return; |
| } |
| el.innerHTML = tasks.map(t => ` |
| <div class="task-card ${t.done ? 'done-card' : ''}" data-priority="${t.priority}" id="tc-${t.id}"> |
| <button class="check-btn ${t.done ? 'checked' : ''}" onclick="toggleDone('${t.id}', ${!t.done})" title="Toggle done"></button> |
| <div class="task-body"> |
| <div class="task-title">${escHtml(t.title)}</div> |
| <div class="task-meta"> |
| <span class="badge badge-${t.priority}">${t.priority}</span> |
| <span class="badge badge-cat">${t.category}</span> |
| ${t.due_date ? `<span class="badge badge-date">π
${t.due_date}</span>` : ''} |
| <span style="color:var(--muted);font-size:0.72rem;">${t.created_at}</span> |
| </div> |
| </div> |
| <button class="del-btn" onclick="deleteTask('${t.id}')" title="Delete">β</button> |
| </div> |
| `).join(''); |
| } |
| |
| async function loadStats() { |
| const s = await api('GET', '/api/stats'); |
| document.getElementById('s-total').textContent = s.total; |
| document.getElementById('s-done').textContent = s.done; |
| document.getElementById('s-pending').textContent = s.pending; |
| document.getElementById('s-high').textContent = s.high_priority; |
| } |
| |
| |
| async function addTask() { |
| const title = document.getElementById('inp-title').value.trim(); |
| const priority = document.getElementById('inp-priority').value; |
| const category = document.getElementById('inp-category').value; |
| const due_date = document.getElementById('inp-due').value || null; |
| |
| if (!title) { toast('Please enter a task title.'); return; } |
| |
| await api('POST', '/api/tasks', { title, priority, category, due_date }); |
| document.getElementById('inp-title').value = ''; |
| document.getElementById('inp-due').value = ''; |
| loadTasks(); |
| toast('Task added β¦'); |
| } |
| |
| async function toggleDone(id, done) { |
| await api('PATCH', `/api/tasks/${id}`, { done }); |
| loadTasks(); |
| toast(done ? 'Task completed π' : 'Marked as pending'); |
| } |
| |
| async function deleteTask(id) { |
| const card = document.getElementById(`tc-${id}`); |
| if (card) card.style.opacity = '0.3'; |
| await api('DELETE', `/api/tasks/${id}`); |
| loadTasks(); |
| toast('Task removed'); |
| } |
| |
| |
| function setFilter(filter, btn) { |
| currentFilter = filter; |
| document.querySelectorAll('.chip').forEach(c => c.classList.remove('active')); |
| btn.classList.add('active'); |
| loadTasks(); |
| } |
| |
| |
| document.getElementById('inp-title').addEventListener('keydown', e => { |
| if (e.key === 'Enter') addTask(); |
| }); |
| |
| |
| let toastTimer; |
| function toast(msg) { |
| const el = document.getElementById('toast'); |
| el.textContent = msg; |
| el.classList.add('show'); |
| clearTimeout(toastTimer); |
| toastTimer = setTimeout(() => el.classList.remove('show'), 2200); |
| } |
| |
| |
| function escHtml(s) { |
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| } |
| |
| |
| loadTasks(); |
| </script> |
| </body> |
| </html> |
|
|