anycoder-d4f892d0 / index.html
akhaliq's picture
akhaliq HF Staff
Upload folder using huggingface_hub
93725e8 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FocusFlow | Modern Todo App</title>
<!-- Importing Google Fonts -->
<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=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Importing Phosphor Icons (Lightweight icon library) -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<style>
:root {
/* Light Theme Variables */
--bg-body: #f3f4f6;
--bg-card: #ffffff;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--accent-color: #6366f1;
--accent-hover: #4f46e5;
--danger-color: #ef4444;
--success-color: #10b981;
--border-color: #e5e7eb;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--radius-md: 0.75rem;
--radius-lg: 1rem;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] {
/* Dark Theme Variables */
--bg-body: #111827;
--bg-card: #1f2937;
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--text-muted: #6b7280;
--accent-color: #818cf8;
--accent-hover: #6366f1;
--border-color: #374151;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
line-height: 1.5;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 2rem 1rem;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Container */
.app-container {
width: 100%;
max-width: 600px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
}
.header-title h1 {
font-size: 1.875rem;
font-weight: 700;
letter-spacing: -0.025em;
background: linear-gradient(135deg, var(--accent-color), #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-title p {
color: var(--text-secondary);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.header-actions {
display: flex;
gap: 0.5rem;
}
.icon-btn {
background: var(--bg-card);
border: 1px solid var(--border-color);
color: var(--text-secondary);
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
font-size: 1.25rem;
}
.icon-btn:hover {
color: var(--accent-color);
border-color: var(--accent-color);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
/* Input Section */
.input-card {
background: var(--bg-card);
padding: 0.5rem;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
display: flex;
gap: 0.5rem;
border: 1px solid var(--border-color);
}
.input-wrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
.input-wrapper i {
position: absolute;
left: 1rem;
color: var(--text-muted);
font-size: 1.25rem;
}
#todo-input {
width: 100%;
padding: 1rem 1rem 1rem 3rem;
border: none;
background: transparent;
font-size: 1rem;
color: var(--text-primary);
outline: none;
}
#todo-input::placeholder {
color: var(--text-muted);
}
#add-btn {
background: var(--accent-color);
color: white;
border: none;
padding: 0 1.5rem;
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
}
#add-btn:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
#add-btn:active {
transform: translateY(0);
}
/* Filters */
.filters {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: none; /* Firefox */
}
.filters::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.filter-btn {
background: transparent;
border: 1px solid transparent;
color: var(--text-secondary);
padding: 0.5rem 1rem;
border-radius: 2rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.filter-btn:hover {
background: rgba(99, 102, 241, 0.1);
color: var(--accent-color);
}
.filter-btn.active {
background: var(--accent-color);
color: white;
box-shadow: var(--shadow-sm);
}
/* Todo List */
.todo-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 200px;
}
.todo-item {
background: var(--bg-card);
padding: 1rem;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 1rem;
transition: var(--transition);
animation: slideIn 0.3s ease-out;
position: relative;
cursor: grab;
}
.todo-item:active {
cursor: grabbing;
}
.todo-item.dragging {
opacity: 0.5;
border: 2px dashed var(--accent-color);
background: transparent;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.todo-checkbox {
appearance: none;
width: 1.5rem;
height: 1.5rem;
border: 2px solid var(--border-color);
border-radius: 50%;
cursor: pointer;
position: relative;
flex-shrink: 0;
transition: var(--transition);
}
.todo-checkbox:checked {
background-color: var(--success-color);
border-color: var(--success-color);
}
.todo-checkbox:checked::after {
content: '✔';
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.875rem;
}
.todo-content {
flex: 1;
font-size: 1rem;
color: var(--text-primary);
transition: var(--transition);
word-break: break-word;
}
.todo-item.completed .todo-content {
text-decoration: line-through;
color: var(--text-muted);
}
.todo-actions {
display: flex;
gap: 0.5rem;
opacity: 0;
transition: opacity 0.2s;
}
.todo-item:hover .todo-actions {
opacity: 1;
}
/* Mobile specific: always show actions */
@media (max-width: 600px) {
.todo-actions {
opacity: 1;
}
}
.action-btn {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.25rem;
border-radius: 0.25rem;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
}
.action-btn:hover {
background: rgba(0,0,0,0.05);
color: var(--text-primary);
}
.delete-btn:hover {
color: var(--danger-color);
background: rgba(239, 68, 68, 0.1);
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-muted);
text-align: center;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
color: var(--border-color);
}
/* Stats Footer */
.stats-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
color: var(--text-secondary);
font-size: 0.875rem;
background: var(--bg-card);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
}
.clear-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.875rem;
transition: var(--transition);
}
.clear-btn:hover {
color: var(--danger-color);
text-decoration: underline;
}
/* Toast Notification */
.toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--text-primary);
color: var(--bg-card);
padding: 0.75rem 1.5rem;
border-radius: 2rem;
box-shadow: var(--shadow-lg);
font-size: 0.875rem;
opacity: 0;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
z-index: 1000;
display: flex;
align-items: center;
gap: 0.5rem;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Credit Link */
.credit-link {
text-align: center;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 1rem;
text-decoration: none;
transition: color 0.2s;
}
.credit-link:hover {
color: var(--accent-color);
}
.credit-link span {
font-weight: 600;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header>
<div class="header-title">
<h1>FocusFlow</h1>
<p id="date-display">Loading date...</p>
</div>
<div class="header-actions">
<button class="icon-btn" id="theme-toggle" aria-label="Toggle Theme">
<i class="ph ph-moon"></i>
</button>
</div>
</header>
<!-- Input Area -->
<div class="input-card">
<div class="input-wrapper">
<i class="ph ph-plus"></i>
<input type="text" id="todo-input" placeholder="What needs to be done?" autocomplete="off">
</div>
<button id="add-btn">Add</button>
</div>
<!-- Filters -->
<div class="filters">
<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>
</div>
<!-- Todo List -->
<ul class="todo-list" id="todo-list">
<!-- Items will be injected here via JS -->
</ul>
<!-- Empty State (Hidden by default) -->
<div class="empty-state" id="empty-state" style="display: none;">
<i class="ph ph-clipboard-text"></i>
<p>No tasks found. Enjoy your day!</p>
</div>
<!-- Footer Stats -->
<div class="stats-footer">
<span id="items-left">0 items left</span>
<button class="clear-btn" id="clear-completed">Clear Completed</button>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="credit-link">
Built with <span>anycoder</span>
</a>
</div>
<!-- Toast Notification -->
<div class="toast" id="toast">
<i class="ph ph-check-circle"></i>
<span id="toast-message">Action successful</span>
</div>
<script>
// --- State Management ---
let todos = JSON.parse(localStorage.getItem('todos')) || [];
let currentFilter = 'all';
// --- DOM Elements ---
const todoInput = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const todoList = document.getElementById('todo-list');
const emptyState = document.getElementById('empty-state');
const itemsLeftLabel = document.getElementById('items-left');
const clearCompletedBtn = document.getElementById('clear-completed');
const filterBtns = document.querySelectorAll('.filter-btn');
const themeToggle = document.getElementById('theme-toggle');
const dateDisplay = document.getElementById('date-display');
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
// --- Initialization ---
function init() {
renderTodos();
updateDate();
applyTheme();
}
// --- Date Display ---
function updateDate() {
const options = { weekday: 'long', month: 'long', day: 'numeric' };
const today = new Date();
dateDisplay.textContent = today.toLocaleDateString('en-US', options);
}
// --- Theme Handling ---
function applyTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
}
function updateThemeIcon(theme) {
const icon = themeToggle.querySelector('i');
if (theme === 'dark') {
icon.className = 'ph ph-sun';
} else {
icon.className = 'ph ph-moon';
}
}
// --- Core Functions ---
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
renderTodos();
}
function addTodo() {
const text = todoInput.value.trim();
if (text === '') {
showToast('Please enter a task', 'error');
return;
}
const newTodo = {
id: Date.now(),
text: text,
completed: false
};
todos.unshift(newTodo); // Add to top
todoInput.value = '';
saveTodos();
showToast('Task added successfully');
}
function toggleTodo(id) {
todos = todos.map(todo => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
saveTodos();
}
function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
saveTodos();
showToast('Task deleted');
}
function clearCompleted() {
const completedCount = todos.filter(t => t.completed).length;
if (completedCount === 0) return;
todos = todos.filter(todo => !todo.completed);
saveTodos();
showToast('Completed tasks cleared');
}
// --- Rendering ---
function renderTodos() {
todoList.innerHTML = '';
let filteredTodos = todos;
if (currentFilter === 'active') {
filteredTodos = todos.filter(t => !t.completed);
} else if (currentFilter === 'completed') {
filteredTodos = todos.filter(t => t.completed);
}
// Update Empty State
if (filteredTodos.length === 0) {
emptyState.style.display = 'flex';
todoList.style.display = 'none';
} else {
emptyState.style.display = 'none';
todoList.style.display = 'flex';
}
// Update Stats
const activeCount = todos.filter(t => !t.completed).length;
itemsLeftLabel.textContent = `${activeCount} item${activeCount !== 1 ? 's' : ''} left`;
// Create Elements
filteredTodos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.draggable = true;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}>
<span class="todo-content">${escapeHtml(todo.text)}</span>
<div class="todo-actions">
<button class="action-btn delete-btn" aria-label="Delete">
<i class="ph ph-trash"></i>
</button>
</div>
`;
// Event Listeners for Item
const checkbox = li.querySelector('.todo-checkbox');
checkbox.addEventListener('change', () => toggleTodo(todo.id));
const deleteBtn = li.querySelector('.delete-btn');
deleteBtn.addEventListener('click', () => deleteTodo(todo.id));
// Drag Events
li.addEventListener('dragstart', handleDragStart);
li.addEventListener('dragend', handleDragEnd);
todoList.appendChild(li);
});
}
// --- Drag and Drop Logic ---
let draggedItem = null;
function handleDragStart(e) {
draggedItem = this;
setTimeout(() => this.classList.add('dragging'), 0);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
draggedItem = null;
// Reorder array based on DOM order
const newOrderIds = Array.from(todoList.children).map(li => parseInt(li.dataset.id));
// Sort todos array based on new ID order
// Note: This simple sort only works perfectly if 'all' filter is active.
// For simplicity in this demo, we only allow reordering in 'all' mode visually,
// but to keep state consistent, we map the full list.
if(currentFilter === 'all') {
const idTodoMap = new Map(todos.map(t => [t.id, t]));
todos = newOrderIds.map(id => idTodoMap.get(id)).filter(t => t !== undefined);
// Filter undefined just in case, though map should cover all
saveTodos(); // Save new order
}
}
todoList.addEventListener('dragover', (e) => {
e.preventDefault();
const afterElement = getDragAfterElement(todoList, e.clientY);
const draggable = document.querySelector('.dragging');
if (afterElement == null) {
todoList.appendChild(draggable);
} else {
todoList.insertBefore(draggable, afterElement);
}
});
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.todo-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
// --- Utilities ---
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showToast(message, type = 'success') {
toastMessage.textContent = message;
const icon = toast.querySelector('i');
if (type === 'error') {
icon.className = 'ph ph-warning-circle';
toast.style.backgroundColor = 'var(--danger-color)';
} else {
icon.className = 'ph ph-check-circle';
toast.style.backgroundColor = 'var(--text-primary)';
}
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// --- Event Listeners ---
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTodo();
});
clearCompletedBtn.addEventListener('click', clearCompleted);
themeToggle.addEventListener('click', toggleTheme);
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Update UI classes
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update Logic
currentFilter = btn.dataset.filter;
renderTodos();
});
});
// Run Init
init();
</script>
</body>
</html>