diff --git a/public/app.js b/public/app.js
index c1a6890..2174e8c 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,24 +1,95 @@
import { ToastManager } from './managers/toast.js';
+// 语言配置
+const translations = {
+ en: {
+ pageTitle: 'DumbDo - Stupidly Simple Todo List',
+ headerTitle: 'DumbDo',
+ placeholderTitle: 'Title',
+ placeholderDesc: 'Details (optional)',
+ addButton: 'Add',
+ saveButton: 'Save',
+ cancelButton: 'Cancel',
+ confirmDelete: 'Are you sure you want to delete this task?',
+ clearCompletedConfirm: (count) => `Are you sure you want to delete ${count} completed task${count === 1 ? '' : 's'}?`,
+ taskAdded: 'Task added',
+ taskUpdated: 'Task updated',
+ taskDeleted: 'Task deleted',
+ taskCompleted: 'Task completed! 🎉',
+ taskUncompleted: 'Task uncompleted',
+ listRenamed: 'List renamed',
+ listAdded: 'New list added',
+ listDeleted: 'List deleted',
+ clearedCompleted: (count) => `Cleared ${count} completed task${count === 1 ? '' : 's'}`,
+ noCompletedToClear: 'No completed tasks to clear',
+ failedToLoad: 'Failed to load todos',
+ failedToSave: 'Failed to save todos',
+ completed: 'Completed',
+ newListName: 'Enter new list name:',
+ deleteListConfirm: (name) => `Are you sure you want to delete "${name}" and all its tasks?`,
+ cannotDeleteList: 'Cannot delete this list',
+ selectList: 'Select todo list',
+ renameList: 'Rename current list',
+ addNewList: 'Add new list',
+ toggleTheme: 'Toggle theme',
+ toggleLanguage: 'Toggle language'
+ },
+ zh: {
+ pageTitle: 'DumbDo - 简单的待办列表',
+ headerTitle: 'DumbDo',
+ placeholderTitle: '标题',
+ placeholderDesc: '详情(可选)',
+ addButton: '添加',
+ saveButton: '保存',
+ cancelButton: '取消',
+ confirmDelete: '确定要删除这个任务吗?',
+ clearCompletedConfirm: (count) => `确定要删除 ${count} 个已完成的任务吗?`,
+ taskAdded: '任务已添加',
+ taskUpdated: '任务已更新',
+ taskDeleted: '任务已删除',
+ taskCompleted: '任务已完成!🎉',
+ taskUncompleted: '任务已取消完成',
+ listRenamed: '列表已重命名',
+ listAdded: '新列表已添加',
+ listDeleted: '列表已删除',
+ clearedCompleted: (count) => `已清除 ${count} 个已完成的任务`,
+ noCompletedToClear: '没有已完成的任务需要清除',
+ failedToLoad: '加载待办失败',
+ failedToSave: '保存待办失败',
+ completed: '已完成',
+ newListName: '输入新列表名称:',
+ deleteListConfirm: (name) => `确定要删除"${name}"及其所有任务吗?`,
+ cannotDeleteList: '无法删除此列表',
+ selectList: '选择待办列表',
+ renameList: '重命名当前列表',
+ addNewList: '添加新列表',
+ toggleTheme: '切换主题',
+ toggleLanguage: '切换语言'
+ }
+};
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const todoForm = document.getElementById('todoForm');
- const todoInput = document.getElementById('todoInput');
+ const todoTitleInput = document.getElementById('todoTitleInput');
+ const todoDescInput = document.getElementById('todoDescInput');
const todoList = document.getElementById('todoList');
const themeToggle = document.getElementById('themeToggle');
+ const langToggle = document.getElementById('langToggle');
+ const langText = document.getElementById('langText');
const moonIcon = themeToggle.querySelector('.moon');
const sunIcon = themeToggle.querySelector('.sun');
+ const submitBtn = document.getElementById('submitBtn');
+ const cancelEditBtn = document.getElementById('cancelEditBtn');
const toastContainer = document.getElementById('toast-container');
const toastManager = new ToastManager(toastContainer);
- const pinModal = document.getElementById('pinModal');
- const pinInputs = [...document.querySelectorAll('.pin-input')];
- const pinError = document.getElementById('pinError');
const clearCompletedBtn = document.getElementById('clearCompleted');
const listSelector = document.getElementById('listSelector');
const renameListBtn = document.getElementById('renameList');
const deleteListBtn = document.getElementById('deleteList');
const addListBtn = document.getElementById('addList');
+ const headerTitle = document.getElementById('header-title');
+ const pageTitle = document.getElementById('page-title');
const listControls = document.getElementById('listControls');
@@ -67,18 +138,82 @@ document.addEventListener('DOMContentLoaded', () => {
// State
let todos = {};
let currentList = 'List 1';
+ let editingTodo = null;
+ let currentLang = localStorage.getItem('language') || 'en';
+
+ // Language Management
+ function updateLanguage() {
+ const t = translations[currentLang];
+ langText.textContent = currentLang === 'en' ? 'EN' : '中';
+
+ todoTitleInput.placeholder = t.placeholderTitle;
+ todoDescInput.placeholder = t.placeholderDesc;
+ submitBtn.textContent = editingTodo ? t.saveButton : t.addButton;
+ cancelEditBtn.textContent = t.cancelButton;
+
+ listSelector.setAttribute('aria-label', t.selectList);
+ renameListBtn.setAttribute('aria-label', t.renameList);
+ addListBtn.setAttribute('aria-label', t.addNewList);
+ themeToggle.setAttribute('aria-label', t.toggleTheme);
+ langToggle.setAttribute('aria-label', t.toggleLanguage);
+
+ renderTodos();
+ }
+
+ function toggleLanguage() {
+ currentLang = currentLang === 'en' ? 'zh' : 'en';
+ localStorage.setItem('language', currentLang);
+ document.documentElement.setAttribute('data-lang', currentLang);
+ updateLanguage();
+ }
+
+ // Auto-resize textarea
+ function autoResize(textarea) {
+ textarea.style.height = 'auto';
+ textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px';
+ }
+
+ // Initialize auto-resize for textareas
+ function setupAutoResize() {
+ const textareas = document.querySelectorAll('.auto-resize');
+ textareas.forEach(textarea => {
+ textarea.addEventListener('input', () => autoResize(textarea));
+ autoResize(textarea);
+ });
+ }
+
+ // Generate unique ID for todos
+ function generateId() {
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
+ }
+
+ // Convert old data format to new format
+ function convertDataFormat(oldData) {
+ const newData = {};
+ Object.entries(oldData).forEach(([listName, listTodos]) => {
+ newData[listName] = listTodos.map(todo => {
+ if (todo.id && todo.title !== undefined) {
+ return todo;
+ }
+ return {
+ id: generateId(),
+ title: todo.text,
+ description: '',
+ completed: todo.completed
+ };
+ });
+ });
+ return newData;
+ }
// List Management
function initializeLists(data) {
if (!data || Object.keys(data).length === 0) {
- // Only create List 1 when there are no lists at all
todos = { 'List 1': [] };
currentList = 'List 1';
} else {
- // Convert only numeric keys, preserve custom names
const convertedData = {};
Object.entries(data).forEach(([key, value]) => {
- // Only convert numeric keys
if (/^\d+$/.test(key)) {
const newKey = `List ${Object.keys(convertedData).length + 1}`;
convertedData[newKey] = value;
@@ -87,8 +222,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- todos = convertedData;
- currentList = Object.keys(convertedData)[0];
+ todos = convertDataFormat(convertedData);
+ currentList = Object.keys(todos)[0];
}
updateListSelector();
@@ -96,22 +231,20 @@ document.addEventListener('DOMContentLoaded', () => {
}
function updateListSelector() {
- // Sort the list keys to ensure List 1 comes first
+ const t = translations[currentLang];
const sortedKeys = Object.keys(todos).sort((a, b) => {
if (a === 'List 1') return -1;
if (b === 'List 1') return 1;
return a.localeCompare(b);
});
- // Update the native select
listSelector.innerHTML = sortedKeys.map(listId =>
``
).join('');
- // Create a custom select
const customSelect = document.createElement('div');
customSelect.className = 'custom-select';
- customSelect.style.display = 'none'; // Explicitly set initial state
+ customSelect.style.display = 'none';
sortedKeys.forEach(listId => {
const item = document.createElement('div');
@@ -149,7 +282,6 @@ document.addEventListener('DOMContentLoaded', () => {
customSelect.appendChild(item);
});
- // Replace the existing custom select if any
const existingCustomSelect = selectorContainer.querySelector('.custom-select');
if (existingCustomSelect) {
const wasVisible = existingCustomSelect.style.display === 'block';
@@ -163,11 +295,13 @@ document.addEventListener('DOMContentLoaded', () => {
function switchList(listId) {
currentList = listId;
- listSelector.value = listId; // Update the native select value
+ listSelector.value = listId;
+ clearEditMode();
renderTodos();
}
function addNewList() {
+ const t = translations[currentLang];
const listCount = Object.keys(todos).length + 1;
const newListId = `List ${listCount}`;
todos[newListId] = [];
@@ -175,80 +309,64 @@ document.addEventListener('DOMContentLoaded', () => {
updateListSelector();
renderTodos();
saveTodos();
- toastManager.show('New list added');
+ toastManager.show(t.listAdded);
}
async function renameCurrentList() {
- const newName = prompt('Enter new list name:', currentList);
+ const t = translations[currentLang];
+ const newName = prompt(t.newListName, currentList);
if (newName && newName.trim() && newName !== currentList && !todos[newName]) {
const oldName = currentList;
- const oldTodos = { ...todos }; // Keep a full backup
+ const oldTodos = { ...todos };
try {
- // Update the data structure
todos[newName] = todos[currentList];
delete todos[currentList];
currentList = newName;
- // Update UI
updateListSelector();
- // Save changes
await saveTodos();
- toastManager.show('List renamed');
+ toastManager.show(t.listRenamed);
} catch (error) {
- // Revert all changes on failure
todos = oldTodos;
currentList = oldName;
updateListSelector();
- toastManager.show('Failed to save list name change', 'error', false, 5000);
+ toastManager.show(t.failedToSave, 'error', false, 5000);
}
}
}
async function deleteList(listId) {
- // Don't allow deleting the last list or List 1
+ const t = translations[currentLang];
if (Object.keys(todos).length <= 1 || listId === 'List 1') {
- toastManager.show('Cannot delete this list', 'error');
+ toastManager.show(t.cannotDeleteList, 'error');
return;
}
- if (confirm(`Are you sure you want to delete "${listId}" and all its tasks?`)) {
+ if (confirm(t.deleteListConfirm(listId))) {
const oldTodos = { ...todos };
try {
- // Remove the list
delete todos[listId];
- // If we're deleting the current list, switch to another one
if (listId === currentList) {
currentList = Object.keys(todos)[0];
}
- // Update UI
updateListSelector();
renderTodos();
- // Save changes
await saveTodos();
- toastManager.show('List deleted');
+ toastManager.show(t.listDeleted);
} catch (error) {
- // Revert changes on failure
todos = oldTodos;
updateListSelector();
renderTodos();
- toastManager.show('Failed to delete list', 'error', false, 5000);
+ toastManager.show(t.failedToSave, 'error', false, 5000);
}
}
}
- // Event Listeners for List Management
- listSelector.addEventListener('change', (e) => {
- switchList(e.target.value);
- });
-
- renameListBtn.addEventListener('click', renameCurrentList);
- addListBtn.addEventListener('click', addNewList);
-
// Enhanced fetch with auth headers
async function fetchWithAuth(url, options = {}) {
return fetch(url, options);
@@ -261,7 +379,12 @@ document.addEventListener('DOMContentLoaded', () => {
sunIcon.style.display = isDark ? 'block' : 'none';
}
- // Initialize theme icons
+ // Load theme from localStorage on page load
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme) {
+ document.documentElement.setAttribute('data-theme', savedTheme);
+ }
+
updateThemeIcons();
themeToggle.addEventListener('click', () => {
@@ -272,6 +395,54 @@ document.addEventListener('DOMContentLoaded', () => {
updateThemeIcons();
});
+ // Language Toggle
+ langToggle.addEventListener('click', toggleLanguage);
+
+ // Edit Mode
+ function setEditMode(todo) {
+ const t = translations[currentLang];
+ editingTodo = todo;
+ todoTitleInput.value = todo.title;
+ todoDescInput.value = todo.description;
+ submitBtn.textContent = t.saveButton;
+ cancelEditBtn.style.display = 'inline-block';
+ autoResize(todoTitleInput);
+ autoResize(todoDescInput);
+ todoTitleInput.focus();
+
+ todoList.querySelectorAll('.todo-item').forEach(item => {
+ if (item.dataset.id === todo.id) {
+ item.classList.add('editing');
+ } else {
+ item.classList.remove('editing');
+ }
+ });
+ }
+
+ function clearEditMode() {
+ const t = translations[currentLang];
+ editingTodo = null;
+ todoTitleInput.value = '';
+ todoDescInput.value = '';
+ submitBtn.textContent = t.addButton;
+ cancelEditBtn.style.display = 'none';
+ autoResize(todoTitleInput);
+ autoResize(todoDescInput);
+
+ todoList.querySelectorAll('.todo-item').forEach(item => {
+ item.classList.remove('editing');
+ });
+ }
+
+ cancelEditBtn.addEventListener('click', clearEditMode);
+
+ // Truncate text for preview
+ function truncateText(text, maxLength = 50) {
+ if (!text) return '';
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + '...';
+ }
+
// Todo Management
async function loadTodos() {
try {
@@ -279,9 +450,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (!response.ok) throw new Error('Failed to load todos');
const data = await response.json();
initializeLists(data);
- initializeDropdown(); // Initialize dropdown after data is loaded
+ initializeDropdown();
} catch (error) {
- toastManager.show('Failed to load todos', 'error', true);
+ const t = translations[currentLang];
+ toastManager.show(t.failedToLoad, 'error', true);
console.error(error);
}
}
@@ -298,108 +470,98 @@ document.addEventListener('DOMContentLoaded', () => {
if (!response.ok) throw new Error('Failed to save todos');
return true;
} catch (error) {
- toastManager.show('Failed to save todos', 'error');
+ const t = translations[currentLang];
+ toastManager.show(t.failedToSave, 'error');
console.error(error);
- throw error; // Re-throw to handle in calling function
+ throw error;
}
}
function createTodoElement(todo) {
+ const t = translations[currentLang];
const li = document.createElement('li');
- li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
+ li.className = `todo-item ${todo.completed ? 'completed' : ''} ${editingTodo && editingTodo.id === todo.id ? 'editing' : ''}`;
+ li.dataset.id = todo.id;
- // Add drag attributes only for non-completed items
if (!todo.completed) {
li.draggable = true;
- li.setAttribute('data-todo-id', todo.text); // Using text as a simple identifier
+ li.setAttribute('data-todo-id', todo.id);
}
+ const titlePreview = truncateText(todo.title, 40);
+ const descPreview = truncateText(todo.description, 60);
+
li.innerHTML = `
- ${linkifyText(todo.text)}
+
+ ${linkifyText(titlePreview)}
+ ${descPreview ? `${linkifyText(descPreview)}` : ''}
+
`;
const checkbox = li.querySelector('input');
const checkboxWrapper = li.querySelector('.checkbox-wrapper');
- const todoText = li.querySelector('.todo-text');
+ const todoContent = li.querySelector('.todo-content');
+ const deleteBtn = li.querySelector('.delete-btn');
- // Add click handler to the wrapper
checkboxWrapper.addEventListener('click', (e) => {
- // Only trigger if clicking the wrapper (not the checkbox directly)
+ e.stopPropagation();
if (e.target === checkboxWrapper) {
checkbox.checked = !checkbox.checked;
todo.completed = checkbox.checked;
renderTodos();
saveTodos();
- toastManager.show(todo.completed ? 'Task completed! 🎉' : 'Task uncompleted');
+ toastManager.show(todo.completed ? t.taskCompleted : t.taskUncompleted);
}
});
- checkbox.addEventListener('change', () => {
+ checkbox.addEventListener('change', (e) => {
+ e.stopPropagation();
todo.completed = checkbox.checked;
renderTodos();
saveTodos();
- toastManager.show(todo.completed ? 'Task completed! 🎉' : 'Task uncompleted');
+ toastManager.show(todo.completed ? t.taskCompleted : t.taskUncompleted);
});
- // Make text editable on click
- todoText.addEventListener('click', (e) => {
- // Don't trigger edit if clicking a link
+ // Click to edit - click on todo content to edit
+ todoContent.addEventListener('click', (e) => {
+ e.stopPropagation();
if (e.target.tagName === 'A') return;
-
- const input = document.createElement('input');
- input.type = 'text';
- input.value = todo.text;
- input.className = 'edit-input';
-
- const originalText = todoText.innerHTML;
- todoText.replaceWith(input);
- input.focus();
-
- function saveEdit() {
- const newText = input.value.trim();
- if (newText && newText !== todo.text) {
- todo.text = newText;
- renderTodos();
- saveTodos();
- toastManager.show('Task updated');
- } else {
- input.replaceWith(todoText);
- todoText.innerHTML = originalText;
- }
- }
-
- input.addEventListener('blur', saveEdit);
- input.addEventListener('keydown', (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- saveEdit();
- } else if (e.key === 'Escape') {
- input.replaceWith(todoText);
- todoText.innerHTML = originalText;
- }
- });
+ setEditMode(todo);
});
- const deleteBtn = li.querySelector('.delete-btn');
- deleteBtn.addEventListener('click', () => {
- if (confirm(`Are you sure you want to delete "${todo.text}"?`)) {
+ // Also bind click to li as fallback
+ li.addEventListener('click', (e) => {
+ if (e.target.tagName === 'A') return;
+ if (e.target.tagName === 'INPUT') return;
+ if (e.target.tagName === 'BUTTON') return;
+ if (checkboxWrapper.contains(e.target)) return;
+ if (deleteBtn.contains(e.target)) return;
+ setEditMode(todo);
+ });
+
+ deleteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (confirm(t.confirmDelete)) {
li.remove();
- todos[currentList] = todos[currentList].filter(t => t !== todo);
+ todos[currentList] = todos[currentList].filter(t => t.id !== todo.id);
+ if (editingTodo && editingTodo.id === todo.id) {
+ clearEditMode();
+ }
+ renderTodos();
saveTodos();
- toastManager.show('Task deleted', 'error');
+ toastManager.show(t.taskDeleted, 'error');
}
});
- // Add drag and drop event listeners for non-completed items
+ // Drag and drop
if (!todo.completed) {
li.addEventListener('dragstart', (e) => {
- e.dataTransfer.setData('text/plain', todo.text);
+ e.dataTransfer.setData('text/plain', todo.id);
li.classList.add('dragging');
- // Set a custom drag image (optional)
const dragImage = li.cloneNode(true);
dragImage.style.position = 'absolute';
dragImage.style.top = '-1000px';
@@ -431,11 +593,10 @@ document.addEventListener('DOMContentLoaded', () => {
li.parentNode.insertBefore(draggingItem, li.nextSibling);
}
- // Update the todos array to match the new order
const activeTodos = todos[currentList].filter(t => !t.completed);
const completedTodos = todos[currentList].filter(t => t.completed);
const newOrder = [...document.querySelectorAll('.todo-item:not(.completed)')].map(item => {
- return activeTodos.find(t => t.text === item.getAttribute('data-todo-id'));
+ return activeTodos.find(t => t.id === item.getAttribute('data-todo-id'));
});
todos[currentList] = [...newOrder, ...completedTodos];
saveTodos();
@@ -447,25 +608,21 @@ document.addEventListener('DOMContentLoaded', () => {
return li;
}
- // Helper function to convert URLs in text to clickable links
function linkifyText(text) {
- // Updated regex that doesn't include trailing punctuation in the URL
const urlRegex = /(https?:\/\/[^\s)]+)([)\s]|$)/g;
return text.replace(urlRegex, (match, url, endChar) => {
- // Return the URL as a link plus any trailing character
return `${url}${endChar}`;
});
}
function renderTodos() {
+ const t = translations[currentLang];
todoList.innerHTML = '';
const currentTodos = todos[currentList] || [];
- // Separate todos into active and completed
const activeTodos = currentTodos.filter(todo => !todo.completed);
const completedTodos = currentTodos.filter(todo => todo.completed);
- // Create a container for active todos
const activeTodosContainer = document.createElement('div');
activeTodosContainer.className = 'active-todos';
activeTodosContainer.addEventListener('dragover', (e) => {
@@ -480,20 +637,17 @@ document.addEventListener('DOMContentLoaded', () => {
});
todoList.appendChild(activeTodosContainer);
- // Render active todos
activeTodos.forEach(todo => {
activeTodosContainer.appendChild(createTodoElement(todo));
});
- // Add divider if there are both active and completed todos
if (activeTodos.length > 0 && completedTodos.length > 0) {
const divider = document.createElement('li');
divider.className = 'todo-divider';
- divider.textContent = 'Completed';
+ divider.textContent = t.completed;
todoList.appendChild(divider);
}
- // Render completed todos
completedTodos.forEach(todo => {
todoList.appendChild(createTodoElement(todo));
});
@@ -502,37 +656,74 @@ document.addEventListener('DOMContentLoaded', () => {
// Event Listeners
todoForm.addEventListener('submit', (e) => {
e.preventDefault();
- const text = todoInput.value.trim();
+ const t = translations[currentLang];
+ const title = todoTitleInput.value.trim();
+ const description = todoDescInput.value.trim();
- if (text) {
- const todo = { text, completed: false };
- todos[currentList].push(todo);
- renderTodos();
- saveTodos();
- todoInput.value = '';
- toastManager.show('Task added');
+ if (title) {
+ if (editingTodo) {
+ const todo = todos[currentList].find(t => t.id === editingTodo.id);
+ if (todo) {
+ todo.title = title;
+ todo.description = description;
+ renderTodos();
+ saveTodos();
+ toastManager.show(t.taskUpdated);
+ }
+ clearEditMode();
+ } else {
+ const todo = {
+ id: generateId(),
+ title,
+ description,
+ completed: false
+ };
+ todos[currentList].push(todo);
+ renderTodos();
+ saveTodos();
+ todoTitleInput.value = '';
+ todoDescInput.value = '';
+ autoResize(todoTitleInput);
+ autoResize(todoDescInput);
+ toastManager.show(t.taskAdded);
+ }
}
});
+ listSelector.addEventListener('change', (e) => {
+ switchList(e.target.value);
+ });
+
+ renameListBtn.addEventListener('click', renameCurrentList);
+ addListBtn.addEventListener('click', addNewList);
+
// Clear completed tasks
clearCompletedBtn.addEventListener('click', () => {
+ const t = translations[currentLang];
const currentTodos = todos[currentList];
const completedCount = currentTodos.filter(todo => todo.completed).length;
if (completedCount === 0) {
- toastManager.show('No completed tasks to clear');
+ toastManager.show(t.noCompletedToClear);
return;
}
- if (confirm(`Are you sure you want to delete ${completedCount} completed task${completedCount === 1 ? '' : 's'}?`)) {
+ if (confirm(t.clearCompletedConfirm(completedCount))) {
todos[currentList] = currentTodos.filter(todo => !todo.completed);
renderTodos();
saveTodos();
- toastManager.show(`Cleared ${completedCount} completed task${completedCount === 1 ? '' : 's'}`);
+ toastManager.show(t.clearedCompleted(completedCount));
}
});
const initialize = async () => {
+ // Initialize language
+ document.documentElement.setAttribute('data-lang', currentLang);
+ updateLanguage();
+
+ // Setup auto-resize
+ setupAutoResize();
+
fetch(`api/config`)
.then(resp => resp.json())
.then(config => {
@@ -540,8 +731,8 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error(config.error);
}
- document.getElementById('page-title').textContent = config.siteTitle;
- document.getElementById('header-title').textContent = config.siteTitle;
+ pageTitle.textContent = config.siteTitle;
+ headerTitle.textContent = config.siteTitle;
if (listControls) listControls.style.display = config.singleList ? 'none' : '';
loadTodos();
@@ -549,9 +740,8 @@ document.addEventListener('DOMContentLoaded', () => {
.catch(err => {
console.error('Error loading site config:', err);
toastManager.show(err, 'error', true);
- })
+ });
- // Register PWA Service Worker
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service-worker.js")
.then((reg) => console.log("Service Worker registered:", reg.scope))
diff --git a/public/assets/styles.css b/public/assets/styles.css
index 99b2b46..256f72f 100644
--- a/public/assets/styles.css
+++ b/public/assets/styles.css
@@ -37,10 +37,9 @@ body {
}
.app {
- max-width: 600px;
+ max-width: 1400px;
margin: 0 auto;
padding: 2rem 1rem;
- text-align: center;
}
header {
@@ -48,14 +47,19 @@ header {
text-align: center;
margin-bottom: 2rem;
display: flex;
- flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
gap: 1rem;
+ flex-wrap: wrap;
}
h1 {
margin: 0;
font-size: 2rem;
color: var(--text);
+ flex: 1;
+ text-align: left;
+ min-width: 150px;
}
button {
@@ -72,10 +76,36 @@ button:hover {
background: var(--primary-hover);
}
+.header-controls {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+#langToggle {
+ background: transparent;
+ font-size: 0.875rem;
+ padding: 0.5rem;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 2px solid var(--text);
+ border-radius: 50%;
+ font-weight: 600;
+ transition: all var(--transition);
+}
+
+#langToggle:hover {
+ background: rgba(255,255,255,0.1);
+}
+
+#langToggle #langText {
+ color: var(--text);
+}
+
#themeToggle {
- position: absolute;
- top: 0;
- right: 0;
background: transparent;
font-size: 1.5rem;
padding: 0.5rem;
@@ -127,27 +157,120 @@ button:hover {
background: rgba(255,68,68,0.1);
}
+/* Main Container - Responsive Two Column Layout */
+.main-container {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 2rem;
+}
+
+@media (min-width: 1024px) {
+ .main-container {
+ grid-template-columns: 380px 1fr;
+ gap: 2rem;
+ }
+}
+
+@media (min-width: 1200px) {
+ .main-container {
+ grid-template-columns: 420px 1fr;
+ }
+}
+
+/* Left Panel (Function Area) */
+.left-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
.todo-form {
display: flex;
- gap: 0.5rem;
- margin-bottom: 2rem;
- justify-content: center;
+ flex-direction: column;
+ gap: 1rem;
+ background: var(--container);
+ padding: 1.5rem;
+ border-radius: 12px;
+ box-shadow: var(--shadow);
}
-input {
- flex: 1;
- max-width: 400px;
+.input-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+/* Auto-resize Textarea */
+.auto-resize {
+ width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
- background: var(--container);
+ background: var(--background);
color: var(--text);
- transition: border-color var(--transition);
+ font-size: 1rem;
+ font-family: inherit;
+ resize: none;
+ overflow: hidden;
+ min-height: 40px;
+ max-height: 300px;
+ transition: border-color var(--transition), box-shadow var(--transition);
+ line-height: 1.5;
}
-input:focus {
+.auto-resize:focus {
outline: none;
border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
+}
+
+.auto-resize::placeholder {
+ color: var(--border);
+ opacity: 0.8;
+}
+
+#todoTitleInput {
+ font-weight: 600;
+ font-size: 1.1rem;
+}
+
+#todoDescInput {
+ font-size: 0.95rem;
+ opacity: 0.9;
+}
+
+.todo-form .button-group {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+}
+
+.todo-form button[type="submit"],
+#cancelEditBtn {
+ padding: 0.75rem 1.5rem;
+ font-size: 1rem;
+ border-radius: 8px;
+ min-width: 80px;
+}
+
+#cancelEditBtn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text);
+}
+
+#cancelEditBtn:hover {
+ background: rgba(0,0,0,0.05);
+ border-color: var(--text);
+}
+
+[data-theme="dark"] #cancelEditBtn:hover {
+ background: rgba(255,255,255,0.1);
+}
+
+/* Right Panel (Todo List) */
+.right-panel {
+ display: flex;
+ flex-direction: column;
}
.todo-list {
@@ -155,24 +278,32 @@ input:focus {
display: flex;
flex-direction: column;
gap: 0.5rem;
- align-items: center;
width: 100%;
}
.todo-item {
display: grid;
grid-template-columns: 30px 1fr auto;
- align-items: center;
+ align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: var(--container);
border-radius: 8px;
box-shadow: var(--shadow);
- transition: transform var(--transition), box-shadow var(--transition);
+ transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
width: 100%;
- max-width: 500px;
cursor: default;
position: relative;
+ border: 2px solid transparent;
+}
+
+.todo-item:hover {
+ transform: translateY(-2px);
+}
+
+.todo-item.editing {
+ border-color: var(--primary);
+ background: rgba(33, 150, 243, 0.05);
}
.todo-item:not(.completed) {
@@ -186,13 +317,16 @@ input:focus {
transform: scale(1.02) translateY(-2px);
}
-.todo-item:not(.completed):hover {
- transform: translateY(-2px);
+.todo-item.completed .todo-content {
+ opacity: 0.7;
}
-.todo-item.completed span {
+.todo-item.completed .todo-title {
+ text-decoration: line-through;
+}
+
+.todo-item.completed .todo-description {
text-decoration: line-through;
- opacity: 0.7;
}
.checkbox-wrapper {
@@ -200,11 +334,11 @@ input:focus {
width: 100%;
height: 100%;
display: flex;
- align-items: center;
+ align-items: flex-start;
justify-content: center;
cursor: pointer;
- padding: 0.5rem;
- margin: -0.5rem;
+ padding: 0.25rem;
+ margin-top: 0.25rem;
}
.todo-item input[type="checkbox"] {
@@ -217,36 +351,45 @@ input:focus {
z-index: 1;
}
-.todo-item span {
- overflow-wrap: break-word;
- word-break: break-word;
+.todo-content {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
min-width: 0;
- text-align: left;
- justify-self: start;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: background-color var(--transition);
}
-.todo-item span:hover {
- background-color: rgba(0, 0, 0, 0.05);
+.todo-content:hover {
+ background-color: rgba(0, 0, 0, 0.03);
}
-[data-theme="dark"] .todo-item span:hover {
- background-color: rgba(255, 255, 255, 0.05);
+[data-theme="dark"] .todo-content:hover {
+ background-color: rgba(255, 255, 255, 0.03);
}
-.todo-item .edit-input {
- width: 100%;
- padding: 0.25rem;
- margin: 0;
- border: 1px solid var(--primary);
- border-radius: 4px;
- background: var(--container);
+.todo-title {
+ font-weight: 500;
+ font-size: 1rem;
color: var(--text);
- font-size: inherit;
- font-family: inherit;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: left;
+ line-height: 1.4;
+}
+
+.todo-description {
+ font-size: 0.875rem;
+ color: var(--text);
+ opacity: 0.7;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: left;
+ line-height: 1.4;
}
.todo-item a {
@@ -272,6 +415,8 @@ input:focus {
padding: 0.25rem 0.5rem;
border: 1px solid #ff4444;
justify-self: end;
+ align-self: flex-start;
+ margin-top: 0.25rem;
}
.delete-btn:hover {
@@ -332,7 +477,6 @@ input:focus {
.todo-divider {
width: 100%;
- max-width: 500px;
text-align: left;
padding: 1rem 0;
margin: 1rem 0;
@@ -342,30 +486,6 @@ input:focus {
border-bottom: 1px solid var(--border);
}
-@media (max-width: 480px) {
- .app {
- padding: 1rem;
- }
-
- h1 {
- font-size: 1.5rem;
- }
-
- .todo-form {
- flex-direction: column;
- align-items: center;
- }
-
- .button-group {
- width: 100%;
- max-width: 400px;
- }
-
- .button-group button[type="submit"] {
- flex: 1;
- }
-}
-
/* PIN Modal Styles */
.modal {
position: fixed;
@@ -465,7 +585,7 @@ input:focus {
gap: 0.5rem;
align-items: center;
justify-content: flex-start;
- width: 100%;
+ flex: 1;
padding: 0 1rem;
}
@@ -624,24 +744,6 @@ input:focus {
display: none;
}
-@media (max-width: 600px) {
- .list-controls {
- position: static;
- margin-bottom: 1rem;
- justify-content: center;
- }
-
- header {
- flex-direction: column;
- gap: 1rem;
- }
-
- #themeToggle {
- position: static;
- margin-left: auto;
- }
-}
-
/* Login Page Styles */
.login-page {
min-height: 100vh;
@@ -681,20 +783,6 @@ input:focus {
margin-bottom: 0.5rem;
}
-@media (max-width: 480px) {
- .login-container {
- padding: 1rem;
- }
-
- .login-box {
- padding: 1.5rem;
- }
-
- .login-page h1 {
- font-size: 2rem;
- }
-}
-
/* PIN Status Styles */
.pin-status {
margin-top: 1rem;
@@ -745,7 +833,6 @@ input:focus {
flex-direction: column;
gap: 0.5rem;
width: 100%;
- align-items: center;
}
/* Add a visual indicator for drop target */
@@ -770,10 +857,164 @@ input:focus {
opacity: 1;
}
-/* Add touch-friendly tap target on mobile */
-@media (max-width: 768px) {
+/* Responsive Design */
+@media (max-width: 1023px) {
+ .app {
+ padding: 1rem;
+ }
+
+ header {
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+ }
+
+ h1 {
+ order: 1;
+ text-align: left;
+ font-size: 1.5rem;
+ flex: 1;
+ min-width: auto;
+ }
+
+ .list-controls {
+ order: 3;
+ width: 100%;
+ justify-content: flex-start;
+ padding: 0;
+ }
+
+ .header-controls {
+ order: 2;
+ }
+
+ .main-container {
+ gap: 1.5rem;
+ }
+
+ .todo-form {
+ padding: 1rem;
+ }
+
+ .todo-form .button-group {
+ justify-content: stretch;
+ }
+
+ .todo-form button[type="submit"],
+ #cancelEditBtn {
+ flex: 1;
+ min-width: auto;
+ }
+
+ .todo-item {
+ padding: 0.75rem;
+ gap: 0.5rem;
+ }
+
+ .todo-title {
+ font-size: 0.95rem;
+ }
+
+ .todo-description {
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 600px) {
+ .list-controls {
+ position: static;
+ margin-bottom: 0.5rem;
+ justify-content: flex-start;
+ }
+
+ header {
+ flex-direction: row;
+ gap: 0.5rem;
+ align-items: center;
+ }
+
+ #themeToggle,
+ #langToggle {
+ position: static;
+ margin-left: 0;
+ }
+
+ h1 {
+ font-size: 1.25rem;
+ }
+
+ .todo-form {
+ flex-direction: column;
+ }
+
+ .button-group {
+ width: 100%;
+ }
+
+ .button-group button[type="submit"] {
+ flex: 1;
+ }
+
+ /* Add touch-friendly tap target on mobile */
.checkbox-wrapper {
padding: 0.75rem;
margin: -0.75rem;
}
-}
\ No newline at end of file
+}
+
+@media (max-width: 480px) {
+ .login-container {
+ padding: 1rem;
+ }
+
+ .login-box {
+ padding: 1.5rem;
+ }
+
+ .login-page h1 {
+ font-size: 2rem;
+ }
+}
+
+/* Two column specific styles */
+@media (min-width: 1024px) {
+ .left-panel {
+ position: sticky;
+ top: 2rem;
+ align-self: flex-start;
+ }
+
+ .todo-form {
+ padding: 1.5rem;
+ }
+
+ .right-panel {
+ min-height: 60vh;
+ }
+
+ .todo-list {
+ max-height: 75vh;
+ overflow-y: auto;
+ padding-right: 0.5rem;
+ }
+
+ /* Custom scrollbar for todo list */
+ .todo-list::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .todo-list::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .todo-list::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
+ }
+
+ .todo-list::-webkit-scrollbar-thumb:hover {
+ background: var(--text);
+ opacity: 0.5;
+ }
+}
diff --git a/public/index.html b/public/index.html
index 7b6cfa0..7c110f4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -40,50 +40,71 @@
-
+
-
-
-
-
+
+
+
+
-
+