ai_box / index.html
javed-78's picture
Upload 3 files
25ead9d verified
Raw
History Blame Contribute Delete
14.6 kB
<!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>
/* ── Reset & Base ── */
*, *::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;
}
/* ── Layout ── */
.shell {
max-width: 860px;
margin: 0 auto;
padding: 0 20px 60px;
}
/* ── Header ── */
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 Bar ── */
.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 Task Form ── */
.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 ── */
.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 ── */
#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; }
/* Priority bar */
.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); }
/* Checkbox */
.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 */
.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); }
/* Delete btn */
.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 state ── */
.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 ── */
#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 -->
<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>
<!-- Stats -->
<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>
<!-- Add Form -->
<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>
<!-- Filters -->
<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>
<!-- Task List -->
<div id="task-list">
<div class="empty"><div class="icon">⏳</div><p>Loading tasks…</p></div>
</div>
</div>
<!-- Toast -->
<div id="toast"></div>
<script>
let currentFilter = 'all';
/* ── API helpers ── */
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();
}
/* ── Load & render ── */
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;
}
/* ── Actions ── */
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');
}
/* ── Filters ── */
function setFilter(filter, btn) {
currentFilter = filter;
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
loadTasks();
}
/* ── Enter key ── */
document.getElementById('inp-title').addEventListener('keydown', e => {
if (e.key === 'Enter') addTask();
});
/* ── Toast ── */
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);
}
/* ── Utils ── */
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
/* ── Init ── */
loadTasks();
</script>
</body>
</html>