diff --git a/index.html b/index.html index 09c6dc0..9877b14 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,113 @@ - + - Simple HTML Page + To-Do List + - +
+

To-Do List

+
+ +
+ +
+
+
+ + +
+ + +
+
+ + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/report.html b/report.html new file mode 100644 index 0000000..9c02afc --- /dev/null +++ b/report.html @@ -0,0 +1,23 @@ + + + + + + Аналітика завдань + + + + +
+

Аналітика завдань

+ Назад до списку + +
+
+ + + + + + + \ No newline at end of file diff --git a/report.js b/report.js new file mode 100644 index 0000000..6fb67cf --- /dev/null +++ b/report.js @@ -0,0 +1,54 @@ +document.addEventListener('DOMContentLoaded', function() { + fetch('task.json') + .then(response => { + if (!response.ok) throw new Error('Помилка завантаження даних'); + return response.json(); + }) + .then(tasks => { + if (!tasks || tasks.length === 0) throw new Error('Немає даних для аналітики'); + + const report = { + dataSource: { + data: tasks.map(task => ({ + "Завдання": task.title, + "Дата": task.date, + "Пріоритет": task.priority, + "Статус": task.completed ? "Виконано" : "Не виконано" + })) + }, + slice: { + rows: [ + { uniqueName: "Статус" }, + { uniqueName: "Пріоритет" } + ], + columns: [ + { uniqueName: "Measures" } + ], + measures: [ + { uniqueName: "Завдання", aggregation: "count" } + ] + }, + options: { + grid: { + type: "flat", + showTotals: "off" + } + } + }; + + new WebDataRocks({ + container: "#wdr-component", + toolbar: true, + report: report + }); + }) + .catch(error => { + console.error(error); + document.getElementById('wdr-component').innerHTML = ` +
+

${error.message}

+

Перевірте вміст файлу task.json

+
+ `; + }); +}); \ No newline at end of file diff --git a/src/main.js b/src/main.js index e69de29..cf70255 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,435 @@ +// DOM elements +let App = () => { + const addTaskBtn = document.getElementById('addTaskBtn'); + const taskInput = document.getElementById('taskTitle'); + const taskList = document.getElementById('taskList'); + const taskModal = document.getElementById('taskModal'); + const taskForm = document.getElementById('taskForm'); + const closeModal = document.getElementById('closeModal'); + const cancelTaskBtn = document.getElementById('cancelTaskBtn'); + const openBtn = document.getElementById('openDateFilter'); + const popup = document.getElementById('dateFilterPopup'); + const applyBtn = document.getElementById('applyDateFilter'); + const modalTitle = document.getElementById('modalTitle'); + const saveTaskBtn = document.getElementById('saveTaskBtn'); + const sortOptions = document.getElementById('sortSelect'); + + let allTasks = []; + let originalTasks = []; + let editingTask = null; + let isFiltered = false; + + // Функція для завантаження завдань з JSON + const loadTasksFromJSON = async () => { + try { + const response = await fetch('task.json'); + if (!response.ok) throw new Error('Не вдалося завантажити завдання'); + const tasks = await response.json(); + + taskList.innerHTML = ''; + allTasks = []; + originalTasks = []; + + tasks.forEach(task => { + addTask(task.title, task.date, task.priority, task.completed); + }); + } catch (error) { + console.error('Помилка завантаження завдань:', error); + + initializeDefaultTasks(); + } + }; + + // Завантажуємо завдання при старті додатка + loadTasksFromJSON(); + + addTaskBtn.addEventListener('click', () => { + taskModal.style.display = 'flex'; + taskForm.reset(); + document.querySelector('input[name="priority"][value="medium"]').checked = true; + document.getElementById('taskTitle').focus(); + modalTitle.textContent = 'Додати нове завдання'; + saveTaskBtn.textContent = 'Додати завдання'; + editingTask = null; + }); + + closeModal.addEventListener('click', () => { + taskModal.style.display = 'none'; + }); + + cancelTaskBtn.addEventListener('click', () => { + taskModal.style.display = 'none'; + }); + + window.addEventListener('click', (e) => { + if (e.target === taskModal) { + taskModal.style.display = 'none'; + } + }); + + taskForm.addEventListener('submit', (e) => { + e.preventDefault(); + + const title = document.getElementById('taskTitle').value.trim(); + const date = document.getElementById('taskDate').value; + const priority = document.querySelector('input[name="priority"]:checked').value; + + if (!title) return; + + if (editingTask) { + updateTask(editingTask, title, date, priority); + editingTask = null; + } else { + addTask(title, date, priority); + } + taskModal.style.display = 'none'; + }); + + // Функція addTask + function addTask(title, date = '', priority = 'medium', completed = false) { + const priorityClasses = { + high: 'priority-high', + medium: 'priority-medium', + low: 'priority-low' + }; + + const priorityTexts = { + high: 'Високий', + medium: 'Середній', + low: 'Низький' + }; + + const taskItem = document.createElement('li'); + taskItem.className = 'task-item'; + + const formattedDate = date ? formatDate(date) : ''; + + taskItem.innerHTML = ` + +
+
${title}
+
+ ${priorityTexts[priority]} +
+
+ ${formattedDate ? `
${formattedDate}
` : ''} +
+ + +
+ `; + + if (completed) { + taskItem.classList.add('task-completed'); + } + + initTaskEventListeners(taskItem); + allTasks.push(taskItem); + originalTasks.push(taskItem); + insertTask(taskItem, completed); + sortOptions.dispatchEvent(new Event('change')); + } + + function updateTask(taskItem, newTitle, newDate, newPriority) { + const priorityClasses = { + high: 'priority-high', + medium: 'priority-medium', + low: 'priority-low' + }; + + const priorityTexts = { + high: 'Високий', + medium: 'Середній', + low: 'Низький' + }; + + taskItem.querySelector('.task-title').textContent = newTitle; + + const prioritySpan = taskItem.querySelector('.task-priority'); + prioritySpan.textContent = priorityTexts[newPriority]; + Object.values(priorityClasses).forEach(cls => prioritySpan.classList.remove(cls)); + prioritySpan.classList.add(priorityClasses[newPriority]); + + const taskDateElement = taskItem.querySelector('.task-date'); + const formattedDate = newDate ? formatDate(newDate) : ''; + + if (formattedDate) { + if (taskDateElement) { + taskDateElement.textContent = formattedDate; + } else { + const newDateDiv = document.createElement('div'); + newDateDiv.className = 'task-date'; + newDateDiv.textContent = formattedDate; + taskItem.querySelector('.task-actions').before(newDateDiv); + } + } else { + if (taskDateElement) { + taskDateElement.remove(); + } + } + sortOptions.dispatchEvent(new Event('change')); + } + + function formatDate(dateString) { + const date = new Date(dateString); + const months = [ + 'Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', + 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень' + ]; + const day = date.getDate(); + const month = months[date.getMonth()]; + const year = date.getFullYear(); + return `${month} ${day}, ${year}`; + } + + function parseFormattedDateToInputDate(formattedDateStr) { + if (!formattedDateStr) return ''; + const date = parseDate(formattedDateStr); + if (!date || isNaN(date.getTime())) return ''; + + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + function insertTask(taskItem, completed) { + if (taskItem.parentNode === taskList) { + taskItem.remove(); + } + + const tasksInDom = Array.from(taskList.children); + + if (completed) { + taskList.appendChild(taskItem); + } else { + const firstCompletedTask = tasksInDom.find(task => + task.classList.contains('task-completed') + ); + + if (firstCompletedTask) { + taskList.insertBefore(taskItem, firstCompletedTask); + } else { + taskList.appendChild(taskItem); + } + } + } + + function initTaskEventListeners(taskItem) { + const checkbox = taskItem.querySelector('.task-checkbox'); + checkbox.addEventListener('change', function() { + taskItem.classList.toggle('task-completed', this.checked); + taskItem.querySelector('.task-title').classList.toggle('completed', this.checked); + insertTask(taskItem, this.checked); + sortOptions.dispatchEvent(new Event('change')); + }); + + const deleteBtn = taskItem.querySelector('.delete-btn'); + deleteBtn.addEventListener('click', function() { + taskItem.remove(); + allTasks = allTasks.filter(t => t !== taskItem); + originalTasks = originalTasks.filter(t => t !== taskItem); + sortOptions.dispatchEvent(new Event('change')); + }); + + const editBtn = taskItem.querySelector('.edit-btn'); + editBtn.addEventListener('click', function() { + editingTask = taskItem; + const title = taskItem.querySelector('.task-title').textContent; + const prioritySpan = taskItem.querySelector('.task-priority'); + const priority = prioritySpan.classList.contains('priority-high') ? 'high' : + prioritySpan.classList.contains('priority-medium') ? 'medium' : 'low'; + const date = taskItem.querySelector('.task-date')?.textContent || ''; + + document.getElementById('taskTitle').value = title; + document.getElementById('taskDate').value = parseFormattedDateToInputDate(date); + document.querySelector(`input[name="priority"][value="${priority}"]`).checked = true; + + modalTitle.textContent = 'Редагувати завдання'; + saveTaskBtn.textContent = 'Зберегти зміни'; + taskModal.style.display = 'flex'; + }); + } + + document.querySelectorAll('.task-item').forEach(taskItem => { + initTaskEventListeners(taskItem); + }); + + sortOptions.addEventListener('change', (e) => { + const value = e.target.value; + switch (value) { + case 'date': + sortByDate(); + break; + case 'priority': + sortByPriority(); + break; + case 'name': + sortByName(); + break; + } + }); + + openBtn.addEventListener('click', () => { + popup.classList.toggle('hidden'); + }); + + document.addEventListener('click', (e) => { + if (!popup.contains(e.target) && e.target !== openBtn) { + popup.classList.add('hidden'); + } + }); + + applyBtn.addEventListener('click', () => { + const mode = document.getElementById('dateFilterMode').value; + const selectedDateInput = document.getElementById('dateInput').value; + + popup.classList.add('hidden'); + + if (!selectedDateInput) { + // Якщо дата не вибрана, показуємо всі завдання + resetFilter(); + return; + } + + const selectedFilterDate = new Date(selectedDateInput); + selectedFilterDate.setHours(0, 0, 0, 0); + + // Фільтруємо оригінальні завдання + const filteredTasks = originalTasks.filter(task => { + const dateEl = task.querySelector('.task-date'); + if (!dateEl) return false; + + const parsedTaskDate = parseDate(dateEl.textContent); + if (!parsedTaskDate) return false; + + parsedTaskDate.setHours(0, 0, 0, 0); + + switch (mode) { + case 'current': + return parsedTaskDate.getTime() === selectedFilterDate.getTime(); + case 'past': + return parsedTaskDate.getTime() < selectedFilterDate.getTime(); + case 'upcoming': + return parsedTaskDate.getTime() > selectedFilterDate.getTime(); + default: + return false; + } + }); + + // Оновлюємо поточний список завдань + allTasks = [...filteredTasks]; + isFiltered = true; + + // Відображаємо відфільтровані завдання + renderFilteredTasks(); + sortOptions.dispatchEvent(new Event('change')); + }); + + function resetFilter() { + allTasks = [...originalTasks]; + isFiltered = false; + renderFilteredTasks(); + sortOptions.dispatchEvent(new Event('change')); + } + + function renderFilteredTasks() { + taskList.innerHTML = ''; + + const completedTasks = allTasks.filter(task => task.classList.contains('task-completed')); + const incompleteTasks = allTasks.filter(task => !task.classList.contains('task-completed')); + + incompleteTasks.forEach(task => taskList.appendChild(task)); + completedTasks.forEach(task => taskList.appendChild(task)); + } + + function parseDate(dateStr) { + const months = { + 'Січень': 0, 'Лютий': 1, 'Березень': 2, 'Квітень': 3, 'Травень': 4, + 'Червень': 5, 'Липень': 6, 'Серпень': 7, 'Вересень': 8, 'Жовтень': 9, + 'Листопад': 10, 'Грудень': 11 + }; + + const [monthName, dayStr, yearStr] = dateStr.replace(',', '').split(' '); + const month = months[monthName]; + const day = parseInt(dayStr, 10); + const year = parseInt(yearStr, 10); + + if (month === undefined || isNaN(day) || isNaN(year)) { + return null; + } + + return new Date(year, month, day); + } + + function sortByDate() { + const incompleteTasks = allTasks.filter(task => !task.classList.contains('task-completed')); + const completedTasks = allTasks.filter(task => task.classList.contains('task-completed')); + + incompleteTasks.sort((a, b) => { + const dateA = a.querySelector('.task-date')?.textContent; + const dateB = b.querySelector('.task-date')?.textContent; + + if (!dateA && !dateB) return 0; + if (!dateA) return 1; + if (!dateB) return -1; + + const parsedDateA = parseDate(dateA); + const parsedDateB = parseDate(dateB); + + if (!parsedDateA && !parsedDateB) return 0; + if (!parsedDateA) return 1; + if (!parsedDateB) return -1; + + return parsedDateA - parsedDateB; + }); + + taskList.innerHTML = ''; + incompleteTasks.forEach(task => taskList.appendChild(task)); + completedTasks.forEach(task => taskList.appendChild(task)); + } + + function sortByPriority() { + const incompleteTasks = allTasks.filter(task => !task.classList.contains('task-completed')); + const completedTasks = allTasks.filter(task => task.classList.contains('task-completed')); + const priorityOrder = { high: 1, medium: 2, low: 3 }; + + incompleteTasks.sort((a, b) => { + const prioA = getPriorityValue(a); + const prioB = getPriorityValue(b); + return priorityOrder[prioA] - priorityOrder[prioB]; + }); + + taskList.innerHTML = ''; + incompleteTasks.forEach(task => taskList.appendChild(task)); + completedTasks.forEach(task => taskList.appendChild(task)); + } + + function getPriorityValue(taskItem) { + if (taskItem.querySelector('.priority-high')) return 'high'; + if (taskItem.querySelector('.priority-medium')) return 'medium'; + if (taskItem.querySelector('.priority-low')) return 'low'; + return 'medium'; + } + + function sortByName() { + const incompleteTasks = allTasks.filter(task => !task.classList.contains('task-completed')); + const completedTasks = allTasks.filter(task => task.classList.contains('task-completed')); + + incompleteTasks.sort((a, b) => { + const nameA = a.querySelector('.task-title').textContent.toLowerCase(); + const nameB = b.querySelector('.task-title').textContent.toLowerCase(); + return nameA.localeCompare(nameB); + }); + + taskList.innerHTML = ''; + incompleteTasks.forEach(task => taskList.appendChild(task)); + completedTasks.forEach(task => taskList.appendChild(task)); + } +}; + +window.onload = () => { + App(); +}; \ No newline at end of file diff --git a/style/style.css b/style/style.css index e69de29..68e2362 100644 --- a/style/style.css +++ b/style/style.css @@ -0,0 +1,442 @@ +* { + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background-color: #f5f5f5; + margin: 0; + padding: 20px; + color: #333; +} + +.container { + max-width: 500px; + margin: 0 auto; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 20px; +} + +h1 { + margin-top: 0; + color: #2c3e50; + text-align: center; +} + +.add-task { + margin-bottom: 24px; +} + +.btn-large { + width: 100%; + padding: 14px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.3s ease; + background: #3b82f6; + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; +} + +.btn-large:hover { + background: #2563eb; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.btn-large i { + font-size: 14px; +} + +.btn { + background: #3b82f6; + color: #fff; + border: none; + border-radius: 8px; + padding: 10px 18px; + font-size: 15px; + cursor: pointer; + transition: background 0.2s; +} + +.btn:hover { + background: #2563eb; +} + +.btn-add { + background-color: #2ecc71; +} + +.btn-add:hover { + background-color: #27ae60; +} + +.btn-secondary { + background: #eee; + color: #222; +} + +.controls { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.sort-filter { + display: flex; + width: 100%; + justify-content: space-between; + gap: 15px; + overflow: hidden; +} + +.sort-filter > div { + flex: 1; + min-width: 0; +} + +.select-box { + padding: 8px; + border-radius: 8px; + border: 1px solid #ddd; + background-color: white; + font-size: 15px; + width: 100%; + box-sizing: border-box; + +} + +.filter-button { + background-color: white; + border: 1px solid #ccc; + border-radius: 8px; + padding: 8px 12px; + font-weight: 500; + cursor: pointer; +} + +.date-popup { + position: absolute; + top: 160px; + right: 280px; + background: white; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + padding: 20px; + z-index: 1000; +} + +.popup-inner { + display: flex; + flex-direction: column; + gap: 10px; +} + +.popup-inner select, +.popup-inner input { + padding: 8px; + border-radius: 8px; + border: 1px solid #ccc; +} + +.popup-inner button { + background-color: #2563eb; + color: white; + border: none; + padding: 10px; + border-radius: 8px; + font-weight: bold; + cursor: pointer; +} + +.popup-inner button:hover { + background-color: #1d4ed8; +} + +.hidden { + display: none; +} + +.task-list { + list-style-type: none; + padding: 0; +} + +.task-item { + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + gap: 12px; +} + +.task-item:last-child { + border-bottom: none; +} + +.task-checkbox { + width: 20px; + height: 20px; + border: 2px solid #bbb; + border-radius: 50%; + display: inline-block; + position: relative; + cursor: pointer; + appearance: none; + margin-right: 8px; +} + +.task-checkbox:checked { + background: #3b82f6; + border-color: #3b82f6; +} + +.task-checkbox:checked::after { + content: "✓"; + color: white; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 12px; +} + +.task-content { + flex-grow: 1; +} + +.task-title { + font-weight: 500; + margin-bottom: 5px; +} + +.task-meta { + display: flex; + align-items: center; + gap: 10px; +} + +.task-priority { + padding: 2px 10px; + border-radius: 8px; + font-size: 13px; + color: #fff; +} + +.priority-high { + background: #ef4444; +} + +.priority-medium { + background: #f59e42; +} + +.priority-low { + background: #a3a3a3; +} + +.task-date { + color: #666; + font-size: 13px; +} + +.task-actions { + display: flex; + gap: 8px; + margin-left: auto; +} + +.action-btn { + background: none; + border: none; + color: #95a5a6; + cursor: pointer; + font-size: 14px; + transition: color 0.3s; +} + +.action-btn:hover { + color: #3498db; +} + +.delete-btn:hover { + color: #e74c3c; +} + +/* Модальне вікно */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: #fff; + border-radius: 14px; + padding: 20px; + width: 90%; + max-width: 400px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); + position: relative; + margin: 20px; +} + +.close { + position: absolute; + right: 18px; + top: 12px; + font-size: 24px; + color: #bbb; + cursor: pointer; +} + +.task-title.completed { + text-decoration: line-through; + opacity: 0.6; +} + +.modal-title { + margin-top: 0; + color: #2c3e50; +} + +.form-group { + margin-bottom: 15px; +} + +.form-label { + display: block; + margin-bottom: 6px; + font-size: 15px; + font-weight: 500; +} + +.form-input { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 15px; + box-sizing: border-box; +} +.priority-options { + display: flex; + gap: 10px; + margin: 10px 0; + overflow: hidden; +} + +.priority-radio { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.priority-radio input[type="radio"] { + accent-color: #3b82f6; +} + +.modal-actions { + display: flex; + gap: 10px; + margin-top: 18px; +} + +#datePicker { + padding: 8px; + border-radius: 8px; + border: 1px solid #ddd; + font-size: 15px; + width: 100%; + max-width: 150px; + box-sizing: border-box; +} + + +.back-link { + display: inline-block; + margin-bottom: 20px; + color: #4a6baf; + text-decoration: none; +} + +.back-link:hover { + text-decoration: underline; +} + +.error { + color: #d32f2f; + padding: 20px; + background: #ffebee; + border-radius: 4px; +} + +@media (max-width: 600px) { + .container { + max-width: 100%; + padding: 15px; + border-radius: 0; + } + + .sort-filter { + flex-direction: row; + justify-content: space-between; + overflow-x: auto; + padding-bottom: 10px; + } + + + .task-item { + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + display: flex; + gap: 12px; + max-width: 100%; + flex-wrap: nowrap; + align-items: flex-start; + } + + .task-meta { + width: auto; + margin-top: 5px; + flex-wrap: wrap; + } + + .task-actions { + margin-left: auto; + width: auto; + margin-top: 0; + } + + .select-box { + min-width: 100px; + } +} + +@media (max-width: 1024px) { + .date-popup { + right: 80px; + } +} + +@media (max-width: 768px) { + .date-popup { + right: 20px; + } +} \ No newline at end of file diff --git a/task.json b/task.json new file mode 100644 index 0000000..1b7a7ba --- /dev/null +++ b/task.json @@ -0,0 +1,26 @@ +[ + { + "title": "Завершити проект", + "date": "2024-05-01", + "priority": "medium", + "completed": false + }, + { + "title": "Піти в зал", + "date": "2024-04-01", + "priority": "high", + "completed": false + }, + { + "title": "Записатись до лікаря", + "date": "2024-04-01", + "priority": "medium", + "completed": false + }, + { + "title": "Прочитати книгу", + "date": "2024-04-01", + "priority": "low", + "completed": true + } +] \ No newline at end of file