diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/JS_Template.iml b/.idea/JS_Template.iml new file mode 100644 index 0000000..3ad4ebd --- /dev/null +++ b/.idea/JS_Template.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..3565144 --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..454992c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8811d7b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 09c6dc0..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Simple HTML Page - - - - - - - \ No newline at end of file diff --git a/src/analytics.html b/src/analytics.html new file mode 100644 index 0000000..05a755d --- /dev/null +++ b/src/analytics.html @@ -0,0 +1,64 @@ + + + + + + Task Analytics + + + + + + + + +
+ +
+ +

📊 Task Analytics

+ + + + + + +
+ +
+
+ + +
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/analytics.js b/src/analytics.js new file mode 100644 index 0000000..e39be57 --- /dev/null +++ b/src/analytics.js @@ -0,0 +1,156 @@ +/** + * Ініціалізація статистичної панелі та pivot таблиці після завантаження DOM + */ +document.addEventListener('DOMContentLoaded', () => { + // Завантажуємо завдання з localStorage з fallback на порожній масив + const tasks = JSON.parse(localStorage.getItem('tasks')) || []; + + // Розраховуємо ключові метрики для швидкого огляду продуктивності користувача + + const totalTasks = tasks.length; // Загальна кількість завдань + + // filter() створює новий масив з елементів що відповідають умові + // Більш читабельно ніж циклі for або reduce для простого підрахунку + const completedTasks = tasks.filter(t => t.completed).length; + + // Обчислюємо кількість незавершених завдань через віднімання + const pendingTasks = totalTasks - completedTasks; + + // Розраховуємо відсоток завершення з округленням до цілого числа + // Перевіряємо на ділення на нуль для уникнення NaN + const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; + + // Підраховуємо завдання з високим пріоритетом для виділення критичних задач + const highPriorityTasks = tasks.filter(t => t.priority === 'high').length; + + // ===== ВІДОБРАЖЕННЯ СТАТИСТИЧНИХ КАРТОК ===== + // Використовуємо innerHTML для швидкого створення статичного контенту + // Template literals забезпечують чисту інтерполяцию змінних + document.getElementById('stats-bar').innerHTML = ` +
+
${totalTasks}
+
Total Tasks
+
+
+
${completedTasks}
+
Completed
+
+
+
${pendingTasks}
+
Pending
+
+
+
${completionRate}%
+
Progress
+
+
+
${highPriorityTasks}
+
High Priority
+
+ `; + + // Трансформуємо сирі дані завдань у структуру придатну для аналітики + // map() створює новий масив з трансформованими об'єктами + const reportData = tasks.map(task => { + // Створюємо об'єкт Date для роботи з датою завдання + const dateObj = new Date(task.date); + + // Витягуємо компоненти дати для різних рівнів групування + const year = dateObj.getFullYear(); + const month = dateObj.getMonth() + 1; // +1 оскільки getMonth() повертає 0-11 + const day = dateObj.getDate(); + + // Отримуємо назви днів тижня та місяців для кращої читабельності + const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' }); + const monthName = dateObj.toLocaleDateString('en-US', { month: 'long' }); + + // Мапінг пріоритетів для відображення з великої літери + const priorityMap = { 'high': 'High', 'medium': 'Medium', 'low': 'Low' }; + + // Повертаємо структуровані дані для pivot таблиці + // Кожне поле стане доступним для групування та аналізу + return { + 'Month': `${monthName} ${year}`, // Комбінована мітка для місячного групування + 'Day': `${day} (${dayName})`, // День з назвою дня тижня для контексту + 'Task': task.title, // Назва завдання для деталізації + 'Priority': priorityMap[task.priority], // Пріоритет з правильним форматуванням + 'Status': task.completed ? 'Completed' : 'Pending', // Статус у зрозумілому форматі + 'Count': 1 // Лічильник для агрегації (кожне завдання = 1 одиниця) + }; + }); + + // WebDataRocks - стороння бібліотека для створення інтерактивних pivot таблиць + const pivot = new WebDataRocks({ + container: '#pivot-container', // CSS селектор контейнера + toolbar: true, // Показуємо панель інструментів для користувача + height: 500, // Фіксована висота для консистентного вигляду + + report: { + // Джерело даних - наш трансформований масив + dataSource: { data: reportData }, + + // Конфігурація структури pivot таблиці + slice: { + // Рядки - ієрархічне групування даних + // collapsed: false/true контролює початковий стан розгортання + rows: [ + { uniqueName: 'Month', collapsed: false }, // Місяці розгорнуті за замовчанням + { uniqueName: 'Day', collapsed: true }, // Дні згорнуті для компактності + { uniqueName: 'Task', collapsed: true }, // Індивідуальні завдання згорнуті + { uniqueName: 'Priority', collapsed: true } // Пріоритети згорнуті + ], + + // Стовпці - горизонтальне групування + columns: [ + { uniqueName: 'Status' } // Розділення на Completed/Pending + ], + + // Метрики - що рахуємо та як відображаємо + measures: [ + { + uniqueName: 'Count', + aggregation: 'sum', // Сумуємо значення Count (кількість завдань) + format: 'count' // Використовуємо кастомний формат для відображення + } + ] + }, + + // Налаштування форматування чисел + formats: [ + { + name: 'count', + decimalPlaces: 0, // Цілі числа без десяткових знаків + thousandsSeparator: '', // Без розділювача тисяч для простоти + currencySymbol: '' // Без валютних символів + } + ], + + // Додаткові опції відображення + options: { + grid: { + type: 'classic', // Класичний вигляд таблиці + showTotals: 'off', // Вимикаємо проміжні підсумки для чистоти + showGrandTotals: 'off', // Вимикаємо загальні підсумки + dragging: true, // Дозволяємо перетягування полів для реорганізації + showEmptyData: false // Приховуємо порожні комірки + } + }, + + // Локалізація текстових міток + localization: { + grid: { + blankMember: '(Empty)', // Мітка для порожніх значень + total: 'Total', // Мітка для підсумків + grandTotal: 'Grand Total' // Мітка для загальних підсумків + }, + toolbar: { + connect: 'Connect', // Кнопка підключення до джерел даних + clear: 'Clear', // Кнопка очищення + format: 'Format', // Кнопка форматування + export: 'Export', // Кнопка експорту + settings: 'Settings' // Кнопка налаштувань + } + } + } + }); +}); \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..f89ce2f --- /dev/null +++ b/src/index.html @@ -0,0 +1,133 @@ + + + + + + To-Do List + + + + +
+ +
+ +

To-Do List

+ +
+ + + + + + 📊 Analytics +
+ +
+ +
+ + +
+ +
+ + + + +
+
+
+ +
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index e69de29..357a1da 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,523 @@ +// Глобальний масив для зберігання всіх завдань в пам'яті +// Використовується як основне джерело даних для всього додатку +let tasks = []; + +/** + * Відкриває модальне вікно для створення нового або редагування існуючого завдання + * @param {string} mode - режим роботи ('add' для нового завдання, 'edit' для редагування) + * @param {number|null} taskId - ID завдання для редагування (null для нового завдання) + */ +function openPopup(mode = 'add', taskId = null) { + // Отримуємо посилання на DOM елементи + const popupElement = document.getElementById('popup'); + const form = document.getElementById('task-form'); + const titleInput = document.getElementById('task-name-input'); + const dateInput = document.getElementById('task-date-input'); + const popupTitle = document.querySelector('.popup-title'); + const quickTaskInput = document.getElementById('quick-task-input'); + + // Логіка для режиму редагування + if (mode === 'edit' && taskId !== null) { + // Змінюємо заголовок для зрозумілості користувача + popupTitle.textContent = 'Edit Task'; + + // Знаходимо завдання за ID використовуючи find() для читабельності коду + const task = tasks.find(t => t.id === taskId); + if (task) { + // Заповнюємо форму даними існуючого завдання + titleInput.value = task.title; + dateInput.value = task.date; + + // Встановлюємо правильний радіо-бутон для пріоритету + // Використовуємо template literal для динамічного селектора + document.querySelector(`input[name="priority"][value="${task.priority}"]`).checked = true; + + // Зберігаємо ID завдання в dataset форми для подальшого використання + form.dataset.taskId = taskId; + } + } else { + // Логіка для режиму створення нового завдання + popupTitle.textContent = 'Add Task'; + form.reset(); // Очищаємо всі поля форми + + // Якщо є текст у полі швидкого додавання, переносимо його в основну форму + if (quickTaskInput.value.trim()) { + titleInput.value = quickTaskInput.value; + quickTaskInput.value = ''; // Очищаємо поле після перенесення + } + + // Видаляємо ID завдання з dataset, оскільки створюємо нове + delete form.dataset.taskId; + } + + // showModal() - сучасний API для роботи з модальними вікнами + // Автоматично керує фокусом та блокує взаємодію з фоном + popupElement.showModal(); +} + +/** + * Закриває модальне вікно та очищає форму + * Окрема функція для повторного використання та кращої організації коду + */ +function closePopup() { + const popupElement = document.getElementById('popup'); + popupElement.close(); // Закриваємо модальне вікно + document.getElementById('task-form').reset(); // Очищаємо форму для запобігання залишкових даних +} + +/** + * Завантажує завдання з localStorage або ініціалізує початковими даними + * Виконується один раз при завантаженні сторінки + */ +function loadTasks() { + const savedTasks = localStorage.getItem('tasks'); + + if (savedTasks) { + // JSON.parse перетворює рядок назад в JavaScript об'єкт/масив + tasks = JSON.parse(savedTasks); + } else { + // Початкові дані для демонстрації функціональності + tasks = [ + { + "id": 1, + "title": "Complete the project", + "priority": "medium", + "date": "2024-05-01", + "completed": false + }, + { + "id": 2, + "title": "Read the book", + "priority": "low", + "date": "2024-04-25", + "completed": true + }, + { + "id": 3, + "title": "Go to the gym", + "priority": "high", + "date": "2024-04-23", + "completed": false + }, + { + "id": 4, + "title": "Make a doctor's appointment", + "priority": "medium", + "date": "2024-04-20", + "completed": false + } + ]; + + // Зберігаємо початкові дані в localStorage для майбутніх сесій + localStorage.setItem('tasks', JSON.stringify(tasks)); + } + + // Відображаємо завдання після завантаження + renderTasks(); +} + +/** + * Обробляє збереження нового завдання або оновлення існуючого + * @param {Event} event - об'єкт події submit форми + */ +function saveTask(event) { + event.preventDefault(); + + // Збираємо дані з форми + const title = document.getElementById('task-name-input').value; + const date = document.getElementById('task-date-input').value; + // querySelector для радіо-кнопок - знаходить вибраний елемент + const priority = document.querySelector('input[name="priority"]:checked').value; + const form = document.getElementById('task-form'); + + // Перевіряємо чи це редагування існуючого завдання + if (form.dataset.taskId) { + // Режим редагування + const taskId = parseInt(form.dataset.taskId); + const taskIndex = tasks.findIndex(t => t.id === taskId); + + if (taskIndex !== -1) { + // Оновлюємо існуюче завдання, зберігаючи статус completed + tasks[taskIndex] = { + id: taskId, + title, + priority, + date, + completed: tasks[taskIndex].completed // Зберігаємо поточний статус + }; + } + } else { + // Режим створення нового завдання + const newTask = { + // Генеруємо унікальний ID: знаходимо максимальний існуючий ID та додаємо 1 + // Якщо завдань немає, починаємо з 1 + id: tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1, + title, + priority, + date, + completed: false // Нові завдання завжди не виконані + }; + tasks.push(newTask); + } + + // Зберігаємо оновлені дані в localStorage + // JSON.stringify перетворює JavaScript об'єкт в рядок для зберігання + localStorage.setItem('tasks', JSON.stringify(tasks)); + + // Оновлюємо відображення та закриваємо модальне вікно + renderTasks(); + closePopup(); +} + +/** + * Перемикає статус завершення завдання між completed та не completed + * @param {number} taskId - ID завдання для зміни статусу + */ +function toggleTaskCompletion(taskId) { + const taskIndex = tasks.findIndex(t => t.id === taskId); + + if (taskIndex !== -1) { + // Використовуємо логічний NOT для перемикання булевого значення + tasks[taskIndex].completed = !tasks[taskIndex].completed; + + // Зберігаємо зміни в localStorage + localStorage.setItem('tasks', JSON.stringify(tasks)); + } + + // Оновлюємо відображення для миттєвого відображення змін + renderTasks(); +} + +/** + * Видаляє завдання з масиву та localStorage + * @param {number} taskId - ID завдання для видалення + */ +function deleteTask(taskId) { + // filter створює новий масив без завдання з вказаним ID + tasks = tasks.filter(t => t.id !== taskId); + + // Зберігаємо оновлений масив + localStorage.setItem('tasks', JSON.stringify(tasks)); + + // Оновлюємо відображення + renderTasks(); +} + +/** + * Відкриває модальне вікно для налаштування кастомного фільтру за датою + */ +function openCustomFilterPopup() { + const popupElement = document.getElementById('custom-filter-popup'); + const form = document.getElementById('custom-filter-form'); + const filterTypeSelect = document.getElementById('filter-date-type'); + const dateInput = document.getElementById('custom-filter-date-input'); + + // Завантажуємо збережені налаштування або встановлюємо значення за замовчуванням + // Це забезпечує консистентність між сесіями + const savedFilterType = localStorage.getItem('currentFilterType') || 'today'; + const savedDate = localStorage.getItem('currentFilterDate') || new Date().toISOString().split('T')[0]; + + // Заповнюємо форму збереженими значеннями + filterTypeSelect.value = savedFilterType; + dateInput.value = savedDate; + + // Оновлюємо видимість полів відповідно до вибраного типу фільтру + updateFilterInputVisibility(); + + popupElement.showModal(); +} + +/** + * Закриває модальне вікно кастомного фільтру + */ +function closeCustomFilterPopup() { + const popupElement = document.getElementById('custom-filter-popup'); + popupElement.close(); +} + +/** + * Оновлює видимість та значення поля для введення дати залежно від типу фільтру + */ +function updateFilterInputVisibility() { + const filterType = document.getElementById('filter-date-type').value; + const dateInputGroup = document.getElementById('date-input-group'); + const dateInput = document.getElementById('custom-filter-date-input'); + + // Показуємо поле дати для всіх типів фільтрів + // В поточній реалізації дата потрібна для всіх режимів + dateInputGroup.style.display = 'block'; + + // Встановлюємо сьогоднішню дату за замовчуванням якщо поле порожнє + if (!dateInput.value) { + const today = new Date().toISOString().split('T')[0]; + dateInput.value = today; + } +} + +/** + * Wrapper функція для оновлення фільтру при зміні його типу + * Потрібна для event listener'а + */ +function updateFilter() { + updateFilterInputVisibility(); +} + +/** + * Обробляє форму кастомного фільтру та зберігає налаштування + * @param {Event} event - об'єкт події submit форми + */ +function handleCustomFilterForm(event) { + event.preventDefault(); + + // Отримуємо значення з форми + const filterType = document.getElementById('filter-date-type').value; + const filterDate = document.getElementById('custom-filter-date-input').value; + + // Зберігаємо налаштування фільтру в localStorage для збереження між сесіями + localStorage.setItem('currentFilterType', filterType); + localStorage.setItem('currentFilterDate', filterDate); + localStorage.setItem('isCustomFilterActive', 'true'); // Прапорець активності фільтру + + // Закриваємо попап та оновлюємо інтерфейс + closeCustomFilterPopup(); + renderTasks(); // Застосовуємо фільтр до завдань + updateFilterButtonText(); // Оновлюємо текст кнопки фільтру +} + +/** + * Оновлює текст кнопки фільтру та видимість кнопки очищення + */ +function updateFilterButtonText() { + const filterBtn = document.querySelector('.filter-btn'); + const clearFilterBtn = document.querySelector('.clear-filter-btn'); + const isActive = localStorage.getItem('isCustomFilterActive') === 'true'; + + if (isActive) { + // Формуємо описовий текст для активного фільтру + const filterType = localStorage.getItem('currentFilterType'); + const filterDate = localStorage.getItem('currentFilterDate'); + + let buttonText = 'Filter: '; + if (filterType === 'today') { + buttonText += `Today (${formatDate(filterDate)})`; + } else if (filterType === 'overdue') { + buttonText += `Overdue (before ${formatDate(filterDate)})`; + } else if (filterType === 'upcoming') { + buttonText += `Upcoming (after ${formatDate(filterDate)})`; + } + + filterBtn.textContent = buttonText; + filterBtn.classList.add('active-filter'); // Додаємо клас для стилізації + clearFilterBtn.style.display = 'inline-block'; // Показуємо кнопку очищення + } else { + // Скидаємо до початкового стану + filterBtn.textContent = 'Filter by Date'; + filterBtn.classList.remove('active-filter'); + clearFilterBtn.style.display = 'none'; + } +} + +/** + * Форматує дату для відображення у зручному для користувача форматі + * @param {string} dateString - дата в форматі YYYY-MM-DD + * @returns {string} - відформатована дата + * + * Використовуємо toLocaleDateString для локалізації відображення дати + */ +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); +} + +/** + * Очищає активний фільтр та повертає відображення всіх завдань + */ +function clearFilter() { + // Видаляємо всі ключі пов'язані з фільтрацією + localStorage.removeItem('isCustomFilterActive'); + localStorage.removeItem('currentFilterType'); + localStorage.removeItem('currentFilterDate'); + + // Оновлюємо інтерфейс + renderTasks(); + updateFilterButtonText(); +} + +/** + * Встановлює критерій сортування та оновлює відображення + * @param {string} sortBy - критерій сортування ('date', 'priority', 'name') + * + * Зберігаємо вибір користувача для збереження між сесіями + */ +function sortTasks(sortBy) { + localStorage.setItem('sortBy', sortBy); + const sortSelect = document.getElementById('sort-select'); + + // Оновлюємо текст опцій в select для кращого UX + // Показуємо поточний вибір з префіксом "Sort by:" + Array.from(sortSelect.options).forEach(option => { + if (option.value === sortBy) { + option.textContent = `Sort by: ${option.value.charAt(0).toUpperCase() + option.value.slice(1)}`; + } else { + option.textContent = option.value.charAt(0).toUpperCase() + option.value.slice(1); + } + }); + + sortSelect.value = sortBy; + renderTasks(); // Застосовуємо сортування +} + +/** + * Основна функція відображення завдань у DOM + * Застосовує фільтрацію, сортування та створює HTML елементи + */ +function renderTasks() { + const tasksContainer = document.getElementById('tasks-container'); + tasksContainer.innerHTML = ''; // Очищаємо контейнер перед новим рендерингом + + // Отримуємо поточні налаштування сортування та фільтрації + const sortBy = localStorage.getItem('sortBy') || 'date'; + const isCustomFilterActive = localStorage.getItem('isCustomFilterActive') === 'true'; + + // Створюємо копію масиву для уникнення мутації оригінального + let filteredTasks = [...tasks]; + + // Застосовуємо фільтр якщо він активний + if (isCustomFilterActive) { + const filterType = localStorage.getItem('currentFilterType'); + const filterDate = localStorage.getItem('currentFilterDate'); + + if (filterType === 'today') { + // Точна відповідність дати + filteredTasks = tasks.filter(task => task.date === filterDate); + } else if (filterType === 'overdue') { + // Завдання з датою раніше вказаної + filteredTasks = tasks.filter(task => task.date < filterDate); + } else if (filterType === 'upcoming') { + // Завдання з датою пізніше вказаної + filteredTasks = tasks.filter(task => task.date > filterDate); + } + } + + // Налаштування сортування + const priorityOrder = { high: 1, medium: 2, low: 3 }; // Числове представлення для сортування + + filteredTasks.sort((a, b) => { + // Пріоритет сортування: спочатку не виконані завдання, потім виконані + // Це забезпечує кращий UX - активні завдання завжди зверху + if (a.completed !== b.completed) { + return a.completed ? 1 : -1; + } + + // Вторинне сортування за вибраним критерієм + if (sortBy === 'date') { + // Сортування за датою - більш ранні дати спочатку + return new Date(a.date) - new Date(b.date); + } else if (sortBy === 'priority') { + // Сортування за пріоритетом - високий пріоритет спочатку + return priorityOrder[a.priority] - priorityOrder[b.priority]; + } else if (sortBy === 'name') { + // Алфавітне сортування за назвою + return a.title.localeCompare(b.title); + } + return 0; + }); + + // Створюємо HTML елементи для кожного завдання + filteredTasks.forEach(task => { + const taskItem = document.createElement('div'); + taskItem.className = 'task-item'; + + // Використовуємо template literals для зручного створення HTML + // Умовні класи для візуального відображення статусу + taskItem.innerHTML = ` +
+
+
${task.title}
+
${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}
+
+
+
${new Date(task.date).toLocaleDateString('en-GB', { month: 'long', day: 'numeric', year: 'numeric' })}
+
+ + +
+
+ `; + tasksContainer.appendChild(taskItem); + }); + + // Показуємо повідомлення якщо немає завдань для відображення + if (filteredTasks.length === 0) { + const noTasksMessage = document.createElement('div'); + noTasksMessage.className = 'no-tasks-message'; + + // Inline стилі для швидкого центрування (можна винести в CSS) + noTasksMessage.style.cssText = ` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + padding: 20px; + `; + + // Умовно показуємо кнопку очищення фільтру якщо він активний + noTasksMessage.innerHTML = ` +

No tasks to display

+ ${isCustomFilterActive ? '' : ''} + `; + tasksContainer.appendChild(noTasksMessage); + } +} + +/** + * Ініціалізація додатку після повного завантаження DOM + * + * DOMContentLoaded гарантує що всі HTML елементи доступні для маніпуляцій + */ +document.addEventListener('DOMContentLoaded', () => { + // Завантажуємо завдання та відображаємо їх + loadTasks(); + + // Відновлюємо збережені налаштування сортування + const sortBy = localStorage.getItem('sortBy') || 'date'; + sortTasks(sortBy); + + // Оновлюємо текст кнопки фільтру відповідно до збережених налаштувань + updateFilterButtonText(); + + // Реєструємо event listeners для різних елементів інтерфейсу + + // Обробка форми додавання/редагування завдань + document.getElementById('task-form').addEventListener('submit', saveTask); + + // Швидке додавання завдання через Enter у полі швидкого введення + document.getElementById('quick-task-input').addEventListener('keypress', (e) => { + if (e.key === 'Enter' && e.target.value.trim()) { + openPopup(); // Відкриваємо попап з попередньо заповненою назвою + } + }); + + // Зміна критерію сортування + document.getElementById('sort-select').addEventListener('change', (e) => { + sortTasks(e.target.value); + }); + + // Обробка форми кастомного фільтру + document.getElementById('custom-filter-form').addEventListener('submit', handleCustomFilterForm); + + // Оновлення інтерфейсу при зміні типу фільтру + document.getElementById('filter-date-type').addEventListener('change', updateFilter); + + // Обробник для кнопки очищення фільтру (якщо вона існує) + const clearFilterBtn = document.querySelector('.clear-filter-btn'); + if (clearFilterBtn) { + clearFilterBtn.addEventListener('click', clearFilter); + } +}); \ No newline at end of file diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..57053de --- /dev/null +++ b/src/style.css @@ -0,0 +1,675 @@ +/* ГЛОБАЛЬНІ СТИЛІ */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background-color: #f5f5f7; + color: #1d1d1f; + line-height: 1.4; +} + +/* ОСНОВНИЙ КОНТЕЙНЕР */ +.container { + max-width: 800px; + margin: 0 auto; + padding: 15px 12px; + display: flex; + flex-direction: column; + height: 100vh; +} + +/* ===== ФІКСОВАНИЙ ЗАГОЛОВОК ===== */ +.fixed-header { + flex-shrink: 0; +} + +h1 { + text-align: center; + font-size: 2.2rem; + font-weight: 700; + margin-bottom: 20px; + color: #1d1d1f; +} + +.add-task-section { + display: flex; + gap: 12px; + margin-bottom: 15px; +} + +.task-input { + flex: 1; + padding: 12px 16px; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 16px; + background: white; + outline: none; + transition: border-color 0.2s ease; +} + +.task-input:focus { + border-color: #007aff; +} + +.task-input::placeholder { + color: #86868b; +} + +.add-btn { + padding: 12px 20px; + background: white; + color: #1d1d1f; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.add-btn:hover { + border-color: #007aff; + background: #f9f9fb; +} + +/* Кнопка Analytics */ +.analytics-btn { + padding: 12px 20px; + background: white; + color: #1d1d1f; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.analytics-btn:hover { + border-color: #007aff; + background: #f9f9fb; +} + +.controls { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + gap: 20px; +} + +.control-group { + display: flex; + align-items: center; +} + +.control-group label { + display: none; +} + +.select { + padding: 10px 14px; + border: 2px solid #e5e5e7; + border-radius: 8px; + background: white; + font-size: 14px; + cursor: pointer; + min-width: 150px; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 12px center; + background-repeat: no-repeat; + background-size: 16px; + padding-right: 40px; +} + +.select:focus { + outline: none; + border-color: #007aff; +} + +.filter-btn { + padding: 10px 14px; + background: white; + color: #1d1d1f; + border: 2px solid #e5e5e7; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-btn:hover { + border-color: #007aff; + background: #f9f9fb; +} + +.tasks-container { + background: white; + border-radius: 16px; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + flex-grow: 1; + max-height: calc(100vh - 200px); + -webkit-overflow-scrolling: touch; +} + +.tasks-container::-webkit-scrollbar { + width: 8px; + background: transparent; +} + +.tasks-container::-webkit-scrollbar-track { + background: transparent; + margin: 16px 0; +} + +.tasks-container::-webkit-scrollbar-thumb { + background: #86868b; + border-radius: 4px; + border: 2px solid white; +} + +.tasks-container::-webkit-scrollbar-thumb:hover { + background: #6b7280; +} + +.task-item { + display: flex; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f2f2f7; + transition: background-color 0.2s ease; + min-height: 60px; +} + +.task-item:last-child { + border-bottom: none; +} + +.task-item:hover { + background-color: #f9f9fb; +} + +.task-checkbox { + width: 20px; + height: 20px; + border: 2px solid #d1d1d6; + border-radius: 50%; + margin-right: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.task-checkbox.completed { + background: white; + border-color: #34c759; +} + +.task-checkbox.completed::after { + content: '✓'; + color: #34c759; + font-size: 12px; + font-weight: bold; +} + +.task-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + align-items: flex-start; +} + +.task-title { + font-size: 16px; + font-weight: 500; + color: #1d1d1f; + line-height: 1.3; +} + +.task-title.completed { + text-decoration: line-through; + color: #86868b; +} + +.task-priority { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.priority-high { + background: #ff3b30; + color: white; +} + +.priority-medium { + background: #ff9500; + color: white; +} + +.priority-low { + background: #e5e5e7; + color: #86868b; +} + +.task-meta { + display: flex; + align-items: center; + gap: 12px; +} + +.task-date { + color: #86868b; + font-size: 12px; +} + +.task-actions { + display: flex; + gap: 6px; +} + +.action-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.action-btn:hover { + background: #f2f2f7; +} + +.edit-btn::after { + content: '✏️'; + font-size: 12px; +} + +.delete-btn::after { + content: '🗑️'; + font-size: 12px; +} + +/* ===== POPUP ===== */ +.popup-overlay { + background: none; + border: none; + padding: 0; + max-width: none; + width: 100%; + height: 100%; +} + +.popup { + background: white; + border-radius: 20px; + padding: 32px; + width: 90%; + max-width: 500px; + margin: auto; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; +} + +.popup-title { + font-size: 24px; + font-weight: 700; + color: #1d1d1f; +} + +.close-btn { + width: 32px; + height: 32px; + border: none; + background: transparent; + cursor: pointer; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: #86868b; + transition: background-color 0.2s ease; +} + +.close-btn:hover { + background: #f2f2f7; +} + +.form-group { + margin-bottom: 24px; +} + +.form-label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #1d1d1f; + font-size: 16px; +} + +.form-input { + width: 100%; + padding: 16px; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 16px; + background: white; + outline: none; + transition: border-color 0.2s ease; +} + +.form-input:focus { + border-color: #007aff; +} + +.form-input::placeholder { + color: #86868b; +} + +.priority-options { + display: inline-flex; + gap: 16px; + margin-top: 8px; +} + +.priority-option { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.priority-radio { + width: 20px; + height: 20px; + border: 2px solid #d1d1d6; + border-radius: 50%; + position: relative; + transition: all 0.2s ease; +} + +.priority-radio.selected { + border-color: #007aff; + background: #007aff; +} + +.priority-radio.selected::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + background: white; + border-radius: 50%; +} + +.priority-label { + font-size: 16px; + color: #1d1d1f; +} + +.popup-actions { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.btn { + padding: 12px 24px; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary { + background: #007aff; + color: white; +} + +.btn-primary:hover { + background: #0056b3; +} + +.btn-secondary { + background: #f2f2f7; + color: #1d1d1f; +} + +.btn-secondary:hover { + background: #e5e5e7; +} + +.navigation { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 20px; + gap: 12px; +} + +.stats-back-btn { + padding: 12px 20px; + background: white; + color: #1d1d1f; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.stats-back-btn:hover { + border-color: #007aff; + background: #f9f9fb; +} + +.stats-bar { + background: white; + border-radius: 16px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 16px; +} + +.stat-card { + text-align: center; + padding: 12px; + border-radius: 12px; + background: #f9f9fb; +} + +.stat-number { + font-size: 1.8rem; + font-weight: 700; + color: #007aff; +} + +.stat-label { + font-size: 12px; + color: #86868b; + margin-top: 4px; +} + +.view-controls { + background: white; + border-radius: 16px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.view-controls h3 { + font-size: 18px; + font-weight: 600; + color: #1d1d1f; + margin-bottom: 16px; + text-align: center; +} + +.view-buttons { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; +} + +.stats-view-btn { + padding: 12px 16px; + background: white; + color: #1d1d1f; + border: 2px solid #e5e5e7; + border-radius: 12px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-align: center; + position: relative; +} + +.stats-view-btn:hover { + border-color: #007aff; + background: #f9f9fb; +} + +.stats-view-btn.active { + background: #007aff; + color: white; + border-color: #007aff; +} + +.view-description { + font-size: 10px; + color: #86868b; + margin-top: 4px; + line-height: 1.2; +} + +.stats-view-btn.active .view-description { + color: rgba(255, 255, 255, 0.8); +} + +.pivot-wrapper { + background: white; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + flex-grow: 1; + min-height: 500px; +} + +#pivot-container { + width: 100%; + height: 100%; + min-height: 500px; + +} + +.wdr-toolbar-wrapper { + background: #f9f9fb !important; + border-bottom: 1px solid #e5e5e7 !important; + border-radius: 16px 16px 0 0 !important; +} + +.wdr-grid-container { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; +} + +.wdr-grid-header { + background: #f9f9fb !important; + border-color: #e5e5e7 !important; +} + +.wdr-grid-cell { + border-color: #f2f2f7 !important; +} + +.wdr-grid-total { + background: #f2f2f7 !important; + font-weight: 600 !important; +} + +/* ===== АДАПТИВНІ СТИЛІ ===== */ +@media (max-width: 768px) { + h1 { + font-size: 2rem; + margin-bottom: 15px; + } + + .controls { + gap: 12px; + } + + .popup { + margin: 20px; + padding: 24px; + } + + .priority-options { + flex-direction: column; + gap: 12px; + } + + .navigation { + flex-direction: column; + align-items: stretch; + } + + .view-buttons { + grid-template-columns: 1fr; + } + + .view-controls { + padding: 16px; + } + + .container { + padding: 15px 8px; + } + + .stats-bar { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/style/style.css b/style/style.css deleted file mode 100644 index e69de29..0000000