thenewapptest / index.html
akhaliq's picture
akhaliq HF Staff
Upload index.html with huggingface_hub
3febd9e verified
Raw
History Blame Contribute Delete
25.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Todo App</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
:root {
--bg: #ffffff;
--bg-soft: #f7f7f8;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--primary: #2563eb;
--primary-contrast: #ffffff;
--danger: #dc2626;
--shadow: 0 2px 6px rgba(0,0,0,0.06), 0 1px 1px rgba(0,0,0,0.04);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b0d10;
--bg-soft: #141820;
--text: #e5e7eb;
--muted: #9aa4b2;
--border: #1f2937;
--primary: #60a5fa;
--primary-contrast: #0b0d10;
--danger: #f87171;
--shadow: 0 2px 8px rgba(0,0,0,0.35), 0 1px 1px rgba(0,0,0,0.25);
}
}
html[data-theme="light"] {
--bg: #ffffff;
--bg-soft: #f7f7f8;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--primary: #2563eb;
--primary-contrast: #ffffff;
--danger: #dc2626;
--shadow: 0 2px 6px rgba(0,0,0,0.06), 0 1px 1px rgba(0,0,0,0.04);
}
html[data-theme="dark"] {
--bg: #0b0d10;
--bg-soft: #141820;
--text: #e5e7eb;
--muted: #9aa4b2;
--border: #1f2937;
--primary: #60a5fa;
--primary-contrast: #0b0d10;
--danger: #f87171;
--shadow: 0 2px 8px rgba(0,0,0,0.35), 0 1px 1px rgba(0,0,0,0.25);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
background: linear-gradient(180deg, var(--bg), var(--bg-soft));
color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 820px;
margin: 40px auto;
padding: 0 16px;
}
header.app-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.title {
display: flex;
align-items: center;
gap: 10px;
}
.title h1 {
margin: 0;
font-weight: 800;
letter-spacing: -0.02em;
font-size: 28px;
}
.title .badge {
font-size: 12px;
color: var(--muted);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 999px;
background: rgba(127,127,127,0.05);
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
button, .btn {
appearance: none;
border: 1px solid var(--border);
background: #fff0;
color: var(--text);
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.15s ease, transform 0.04s ease;
font-size: 14px;
line-height: 1;
}
button:hover, .btn:hover { background: rgba(127,127,127,0.06); }
button:active, .btn:active { transform: translateY(1px); }
.btn-primary {
background: var(--primary);
color: var(--primary-contrast);
border-color: transparent;
}
.btn-primary:hover { filter: brightness(0.95); }
.btn-danger {
border-color: color-mix(in oklch, var(--danger), var(--border) 60%);
color: var(--danger);
}
.btn-ghost {
border-color: transparent;
background: transparent;
}
.icon-btn {
padding: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
}
.card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow);
}
/* Add form */
#addForm {
display: flex;
gap: 8px;
padding: 12px;
margin-bottom: 14px;
}
#newTodo {
flex: 1 1 auto;
min-width: 160px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg-soft);
color: var(--text);
outline: none;
transition: border 0.15s ease, background 0.15s ease;
}
#newTodo::placeholder { color: var(--muted); }
#newTodo:focus {
border-color: var(--primary);
background: color-mix(in oklch, var(--bg-soft), var(--primary) 6%);
}
/* Toolbar */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
margin-bottom: 10px;
}
.filters {
display: inline-flex;
gap: 6px;
background: var(--bg-soft);
border: 1px solid var(--border);
padding: 4px;
border-radius: 10px;
}
.filters button {
border: 0;
background: transparent;
padding: 8px 10px;
border-radius: 8px;
color: var(--muted);
font-weight: 600;
}
.filters button.active {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.stats {
color: var(--muted);
font-size: 13px;
}
/* List */
#todoList {
list-style: none;
padding: 8px;
margin: 0;
display: grid;
gap: 8px;
}
.todo-item {
display: grid;
grid-template-columns: auto 1fr auto auto auto;
align-items: center;
gap: 10px;
padding: 10px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg);
transition: background 0.2s ease, border 0.2s ease, box-shadow 0.2s ease;
}
.todo-item.drag-over {
border-style: dashed;
border-color: color-mix(in oklch, var(--primary), var(--border) 50%);
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary), transparent 85%);
}
.todo-item.completed {
opacity: 0.75;
background: color-mix(in oklch, var(--bg), #888 3%);
}
.todo-item .grab {
cursor: grab;
user-select: none;
color: var(--muted);
font-size: 16px;
width: 24px;
text-align: center;
}
.todo-item.dragging { opacity: 0.6; }
.checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
}
.checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--primary);
}
.text {
min-width: 0;
word-break: break-word;
white-space: pre-wrap;
}
.text.completed {
text-decoration: line-through;
color: var(--muted);
}
.todo-item .edit-input {
grid-column: 2 / span 1;
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg-soft);
color: var(--text);
outline: none;
}
.todo-item .edit-input:focus {
border-color: var(--primary);
}
.todo-actions {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* Footer */
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-top: 1px dashed var(--border);
color: var(--muted);
font-size: 12px;
}
.sr-only {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0,0,0,0);
white-space: nowrap; border: 0;
}
/* Responsive */
@media (max-width: 520px) {
header.app-header {
flex-direction: column;
align-items: flex-start;
}
.todo-item {
grid-template-columns: auto 1fr auto;
grid-auto-flow: row;
}
.todo-item .grab,
.todo-item .todo-actions { justify-self: end; }
.todo-item .text { grid-column: 2 / span 1; }
}
</style>
</head>
<body>
<div class="container">
<header class="app-header">
<div class="title">
<h1>Todo</h1>
<span class="badge">Offline • Private</span>
</div>
<div class="actions">
<button id="themeToggle" class="icon-btn" aria-label="Toggle theme" title="Toggle theme">🌓</button>
<button id="exportBtn" class="btn" title="Export tasks as JSON">Export</button>
<button id="importBtn" class="btn" title="Import tasks from JSON">Import</button>
<button id="clearCompletedBtn" class="btn btn-danger" title="Delete all completed tasks">Clear completed</button>
</div>
</header>
<div class="card">
<form id="addForm" autocomplete="off">
<label class="sr-only" for="newTodo">Add a task</label>
<input id="newTodo" type="text" placeholder="What needs to be done?" aria-label="Add a task" required />
<button type="submit" class="btn btn-primary">Add</button>
</form>
<div class="toolbar">
<div class="filters" role="group" aria-label="Filter tasks">
<button class="active" data-filter="all" aria-pressed="true">All</button>
<button data-filter="active" aria-pressed="false">Active</button>
<button data-filter="completed" aria-pressed="false">Completed</button>
</div>
<div class="stats">
<span id="itemsLeft">0 items left</span>
</div>
</div>
<ul id="todoList" role="list" aria-live="polite" aria-label="Todo list"></ul>
<div class="footer">
<div>Tip: Drag the ⠿ handle to reorder. Double-click a task to edit.</div>
<div id="lastSaved">Saved</div>
</div>
</div>
</div>
<input type="file" id="importFile" accept="application/json" class="sr-only" />
<script>
(function () {
'use strict';
// State and persistence
const STORAGE_KEY = 'todoApp.v1';
let tasks = [];
let filter = 'all';
let draggingId = null;
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// Elements
const listEl = $('#todoList');
const addForm = $('#addForm');
const inputEl = $('#newTodo');
const itemsLeftEl = $('#itemsLeft');
const filtersEl = $('.filters');
const exportBtn = $('#exportBtn');
const importBtn = $('#importBtn');
const importFileEl = $('#importFile');
const clearCompletedBtn = $('#clearCompletedBtn');
const themeToggle = $('#themeToggle');
const lastSavedEl = $('#lastSaved');
// Theme handling
function applyTheme(theme) {
if (theme === 'light' || theme === 'dark') {
document.documentElement.setAttribute('data-theme', theme);
} else {
document.documentElement.removeAttribute('data-theme');
}
}
function getPreferredTheme() {
const saved = localStorage.getItem('todoApp.theme');
if (saved === 'light' || saved === 'dark') return saved;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
// If current is explicit (light/dark), switch; else toggle against system
const next = current === 'light' ? 'dark' : current === 'dark' ? (sysDark ? null : 'light') : (sysDark ? 'light' : 'dark');
if (next) {
localStorage.setItem('todoApp.theme', next);
} else {
localStorage.removeItem('todoApp.theme');
}
applyTheme(next || getPreferredTheme());
}
// Utilities
const uid = () => (crypto?.randomUUID ? crypto.randomUUID() : `id_${Date.now()}_${Math.random().toString(16).slice(2)}`);
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
lastSavedEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
}
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) tasks = JSON.parse(raw);
if (!Array.isArray(tasks)) tasks = [];
} catch (e) {
console.warn('Failed to parse saved tasks:', e);
tasks = [];
}
}
// Rendering
function render() {
listEl.innerHTML = '';
const frag = document.createDocumentFragment();
for (const t of tasks) {
const li = document.createElement('li');
li.className = 'todo-item' + (t.completed ? ' completed' : '');
li.dataset.id = t.id;
li.draggable = true;
// Drag handle
const handle = document.createElement('div');
handle.className = 'grab';
handle.title = 'Drag to reorder';
handle.setAttribute('aria-hidden', 'true');
handle.textContent = '⠿';
// Checkbox
const checkboxWrap = document.createElement('label');
checkboxWrap.className = 'checkbox';
checkboxWrap.title = 'Mark as ' + (t.completed ? 'incomplete' : 'complete');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = t.completed;
checkbox.setAttribute('aria-label', 'Completed');
checkboxWrap.appendChild(checkbox);
// Text
const span = document.createElement('span');
span.className = 'text' + (t.completed ? ' completed' : '');
span.textContent = t.text;
// Edit input (hidden until editing)
const editInput = document.createElement('input');
editInput.type = 'text';
editInput.className = 'edit-input sr-only';
editInput.value = t.text;
// Actions
const actions = document.createElement('div');
actions.className = 'todo-actions';
const editBtn = document.createElement('button');
editBtn.className = 'icon-btn';
editBtn.title = 'Edit';
editBtn.setAttribute('aria-label', 'Edit task');
editBtn.textContent = '✏️';
const delBtn = document.createElement('button');
delBtn.className = 'icon-btn';
delBtn.title = 'Delete';
delBtn.setAttribute('aria-label', 'Delete task');
delBtn.textContent = '🗑️';
actions.append(editBtn, delBtn);
li.append(handle, checkboxWrap, span, editInput, actions);
frag.appendChild(li);
}
listEl.appendChild(frag);
updateStats();
}
function updateStats() {
const left = tasks.filter(t => !t.completed).length;
itemsLeftEl.textContent = `${left} item${left !== 1 ? 's' : ''} left`;
}
// Actions
function addTask(text) {
const trimmed = text.trim();
if (!trimmed) return;
const newTask = { id: uid(), text: trimmed, completed: false, createdAt: Date.now() };
tasks.push(newTask);
save();
render();
inputEl.value = '';
inputEl.focus();
}
function toggleTask(id, completed) {
const t = tasks.find(t => t.id === id);
if (!t) return;
t.completed = !!completed;
save();
render();
}
function deleteTask(id) {
const i = tasks.findIndex(t => t.id === id);
if (i !== -1) {
tasks.splice(i, 1);
save();
render();
}
}
function startEdit(id) {
const li = listEl.querySelector(`[data-id="${CSS.escape(id)}"]`);
if (!li) return;
li.classList.add('editing');
const input = $('.edit-input', li);
const text = $('.text', li);
text.classList.add('sr-only');
input.classList.remove('sr-only');
input.value = text.textContent;
input.setSelectionRange(0, input.value.length);
input.focus();
}
function commitEdit(id, newText) {
const t = tasks.find(t => t.id === id);
if (!t) return;
const trimmed = newText.trim();
if (!trimmed) {
// If emptied, delete
deleteTask(id);
return;
}
if (t.text !== trimmed) {
t.text = trimmed;
save();
}
render();
}
function cancelEdit(id) {
render(); // restores view
}
function clearCompleted() {
const before = tasks.length;
tasks = tasks.filter(t => !t.completed);
if (tasks.length !== before) {
save();
render();
}
}
function setFilter(newFilter) {
filter = newFilter;
// Update buttons UI
$$('.filters button').forEach(btn => {
const active = btn.dataset.filter === filter;
btn.classList.toggle('active', active);
btn.setAttribute('aria-pressed', String(active));
});
applyFilterStyles();
}
function applyFilterStyles() {
$$('.todo-item', listEl).forEach(li => {
const t = tasks.find(tt => tt.id === li.dataset.id);
if (!t) return;
const show =
filter === 'all' ||
(filter === 'active' && !t.completed) ||
(filter === 'completed' && t.completed);
li.style.display = show ? '' : 'none';
});
}
function exportTasks() {
const data = {
version: 1,
exportedAt: new Date().toISOString(),
tasks
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `todos-${new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function importTasksFromFile(file) {
const reader = new FileReader();
reader.onload = () => {
try {
const obj = JSON.parse(reader.result);
if (!obj || typeof obj !== 'object' || !Array.isArray(obj.tasks)) {
alert('Invalid file format.');
return;
}
// Sanitize tasks
const imported = obj.tasks.map(t => ({
id: String(t.id || uid()),
text: String(t.text || '').trim(),
completed: Boolean(t.completed),
createdAt: Number.isFinite(t.createdAt) ? t.createdAt : Date.now()
})).filter(t => t.text);
if (!imported.length) {
alert('No valid tasks found in file.');
return;
}
tasks = imported;
save();
render();
} catch (e) {
console.error(e);
alert('Failed to import: ' + e.message);
}
};
reader.readAsText(file);
}
// Drag & Drop reordering
function getDragAfterElement(container, y) {
const els = [...container.querySelectorAll('.todo-item:not(.dragging)')];
return els.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
}
function moveTask(dragId, hoverId) {
if (!dragId || !hoverId || dragId === hoverId) return;
const fromIndex = tasks.findIndex(t => t.id === dragId);
const toIndex = tasks.findIndex(t => t.id === hoverId);
if (fromIndex === -1 || toIndex === -1) return;
const [moved] = tasks.splice(fromIndex, 1);
tasks.splice(toIndex, 0, moved);
save();
render();
// Keep filter applied after rerender
applyFilterStyles();
}
// Events
addForm.addEventListener('submit', (e) => {
e.preventDefault();
addTask(inputEl.value);
});
listEl.addEventListener('change', (e) => {
const checkbox = e.target.closest('input[type="checkbox"]');
if (!checkbox) return;
const li = e.target.closest('.todo-item');
if (!li) return;
toggleTask(li.dataset.id, checkbox.checked);
});
listEl.addEventListener('click', (e) => {
const delBtn = e.target.closest('button[title="Delete"]');
if (delBtn) {
const li = delBtn.closest('.todo-item');
if (li) deleteTask(li.dataset.id);
return;
}
const editBtn = e.target.closest('button[title="Edit"]');
if (editBtn) {
const li = editBtn.closest('.todo-item');
if (li) startEdit(li.dataset.id);
}
});
listEl.addEventListener('dblclick', (e) => {
const span = e.target.closest('.text');
if (!span) return;
const li = span.closest('.todo-item');
if (!li) return;
startEdit(li.dataset.id);
});
listEl.addEventListener('keydown', (e) => {
const input = e.target.closest('.edit-input');
if (!input) return;
const li = input.closest('.todo-item');
if (!li) return;
if (e.key === 'Enter') {
e.preventDefault();
commitEdit(li.dataset.id, input.value);
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit(li.dataset.id);
}
});
listEl.addEventListener('focusout', (e) => {
const input = e.target.closest('.edit-input');
if (!input) return;
const li = input.closest('.todo-item');
if (!li) return;
// Commit on blur if still in DOM and not hidden
if (input === document.activeElement) return;
commitEdit(li.dataset.id, input.value);
});
filtersEl.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-filter]');
if (!btn) return;
setFilter(btn.dataset.filter);
});
exportBtn.addEventListener('click', exportTasks);
importBtn.addEventListener('click', () => importFileEl.click());
importFileEl.addEventListener('change', (e) => {
const file = e.target.files?.[0];
if (file) importTasksFromFile(file);
importFileEl.value = '';
});
clearCompletedBtn.addEventListener('click', () => {
const completedCount = tasks.filter(t => t.completed).length;
if (!completedCount) {
alert('No completed tasks to clear.');
return;
}
if (confirm(`Delete ${completedCount} completed task${completedCount === 1 ? '' : 's'}?`)) {
clearCompleted();
}
});
themeToggle.addEventListener('click', toggleTheme);
// Drag events on the list container
listEl.addEventListener('dragstart', (e) => {
const li = e.target.closest('.todo-item');
if (!li) return;
draggingId = li.dataset.id;
li.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggingId);
});
listEl.addEventListener('dragover', (e) => {
e.preventDefault();
const after = getDragAfterElement(listEl, e.clientY);
$$('.todo-item', listEl).forEach(li => li.classList.remove('drag-over'));
if (after) {
after.classList.add('drag-over');
} else {
// optional visual
}
e.dataTransfer.dropEffect = 'move';
});
listEl.addEventListener('dragleave', (e) => {
const li = e.target.closest('.todo-item');
if (li) li.classList.remove('drag-over');
});
listEl.addEventListener('drop', (e) => {
e.preventDefault();
$$('.todo-item', listEl).forEach(li => li.classList.remove('drag-over', 'dragging'));
const after = getDragAfterElement(listEl, e.clientY);
const dragId = draggingId;
draggingId = null;
if (!dragId) return;
const hoverId = after?.dataset?.id;
if (hoverId) {
moveTask(dragId, hoverId);
}
});
listEl.addEventListener('dragend', () => {
$$('.todo-item', listEl).forEach(li => li.classList.remove('drag-over', 'dragging'));
draggingId = null;
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + Enter to add
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
addTask(inputEl.value);
}
// Ctrl/Cmd + L to focus search
if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'l')) {
e.preventDefault();
inputEl.focus();
inputEl.select();
}
});
// Init
function init() {
load();
applyTheme(getPreferredTheme());
render();
setFilter('all');
}
init();
})();
</script>
</body>
</html>