diff --git a/public/style.css b/public/css/style.css similarity index 100% rename from public/style.css rename to public/css/style.css diff --git a/public/index.html b/public/index.html index aea40dd..4e99742 100644 --- a/public/index.html +++ b/public/index.html @@ -1,16 +1,12 @@ - + Fredriks todos - - - - - - + +
@@ -69,6 +65,10 @@
- + + + + + diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..28d1062 --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,25 @@ +export function checkSession() { + return fetch('/check-session') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }); +} + +export function login(username, password) { + return fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + btoa(username + ':' + password) + } + }); +} + +export function logout() { + return fetch('/logout', { + method: 'POST' + }); +} \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..4f7bab4 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,171 @@ +import { checkSession, login, logout } from './auth.js'; +import { saveTask } from './tasks.js'; +import { saveTags, loadTags } from './tags.js'; + +document.addEventListener('DOMContentLoaded', function() { + const loginForm = document.getElementById('loginForm'); + const loginContainer = document.getElementById('loginContainer'); + const appContainer = document.getElementById('appContainer'); + const loginMessage = document.getElementById('loginMessage'); + const logoutButton = document.getElementById('logoutButton'); + const taskForm = document.getElementById('taskForm'); + const sidenav = document.querySelector('.sidenav'); + + if (!loginForm || !loginContainer || !appContainer || !loginMessage || !logoutButton || !taskForm || !sidenav) { + console.error('One or more elements are missing in the DOM'); + return; + } + + // Initialize Materialize components + M.Sidenav.init(sidenav); + + // Calculate tomorrow's date + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(today.getDate() + 1); + + // Initialize datepicker with tomorrow as the default date + M.Datepicker.init(document.querySelectorAll('.datepicker'), { + format: 'yyyy-mm-dd', + defaultDate: tomorrow, + setDefaultDate: true, + firstDay: 1 + }); + + // Initialize timepicker + M.Timepicker.init(document.querySelectorAll('.timepicker'), { + twelveHour: false // Use 24-hour format + }); + + // Check if user is already logged in + checkSession() + .then(data => { + if (data.loggedIn) { + loginContainer.style.display = 'none'; + appContainer.style.display = 'block'; + loadTags().then(tags => { + localStorage.setItem('tags', JSON.stringify(tags)); + const autocompleteData = {}; + tags.forEach(tag => { + autocompleteData[tag] = null; // Materialize autocomplete requires a key-value pair + }); + + const tagsInput = document.getElementById('tags'); + M.Autocomplete.init(tagsInput, { + data: autocompleteData, + onAutocomplete: function(selectedTag) { + const currentTags = tagsInput.value.split(',').map(tag => tag.trim()).filter(tag => tag); + if (!currentTags.includes(selectedTag)) { + currentTags.push(selectedTag); + tagsInput.value = currentTags.join(', '); + } + } + }); + }); + } else { + loginContainer.style.display = 'block'; + appContainer.style.display = 'none'; + } + }) + .catch(error => { + console.error('Error checking session:', error); + loginMessage.textContent = 'Error checking session. Please try again later.'; + }); + + loginForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + login(username, password) + .then(response => { + if (response.ok) { + sessionStorage.setItem('loggedIn', 'true'); + loginContainer.style.display = 'none'; + appContainer.style.display = 'block'; + loadTags().then(tags => { + localStorage.setItem('tags', JSON.stringify(tags)); + const autocompleteData = {}; + tags.forEach(tag => { + autocompleteData[tag] = null; // Materialize autocomplete requires a key-value pair + }); + + const tagsInput = document.getElementById('tags'); + M.Autocomplete.init(tagsInput, { + data: autocompleteData, + onAutocomplete: function(selectedTag) { + const currentTags = tagsInput.value.split(',').map(tag => tag.trim()).filter(tag => tag); + if (!currentTags.includes(selectedTag)) { + currentTags.push(selectedTag); + tagsInput.value = currentTags.join(', '); + } + } + }); + }); + } else { + loginMessage.textContent = 'Invalid username or password'; + } + }) + .catch(error => { + loginMessage.textContent = 'Error logging in'; + }); + }); + + logoutButton.addEventListener('click', function() { + logout() + .then(response => { + if (response.ok) { + sessionStorage.removeItem('loggedIn'); + loginContainer.style.display = 'block'; + appContainer.style.display = 'none'; + } + }) + .catch(error => { + console.error('Error logging out:', error); + }); + }); + + taskForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + // Get form values + const subject = document.getElementById('subject').value; + const description = document.getElementById('description').value; + const scheduled = document.getElementById('scheduled').value; + const time = document.getElementById('time').value; + const tagsInput = document.getElementById('tags').value; + const tags = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag).join(':'); + + // Combine scheduled date and time if time is provided + const scheduledDateTime = time ? `${scheduled}T${time}:00` : scheduled; + + // Structure data for Org mode + const taskData = { + subject: tags ? `${subject} :${tags}:` : subject, + description, + scheduled: scheduledDateTime + }; + + // Save tags to server + saveTags(tags.split(',').map(tag => tag.trim()).filter(tag => tag)) + .then(() => { + loadTags(); // Force refresh tags after saving + }); + + // Save task to server or IndexedDB if offline + try { + const data = await saveTask(taskData); + document.getElementById('responseMessage').textContent = data.message; + taskForm.reset(); // Reset the form after saving the task + } catch (error) { + if (error.status === 401) { + sessionStorage.removeItem('loggedIn'); + loginContainer.style.display = 'block'; + appContainer.style.display = 'none'; + } else { + document.getElementById('responseMessage').textContent = "Error saving task!"; + } + } + }); +}); \ No newline at end of file diff --git a/public/js/tags.js b/public/js/tags.js new file mode 100644 index 0000000..8803b89 --- /dev/null +++ b/public/js/tags.js @@ -0,0 +1,14 @@ +export function saveTags(tags) { + return fetch('/save-tags', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ tags }) + }); +} + +export function loadTags() { + return fetch('/get-tags') + .then(response => response.json()); +} \ No newline at end of file diff --git a/public/js/tasks.js b/public/js/tasks.js new file mode 100644 index 0000000..141ad00 --- /dev/null +++ b/public/js/tasks.js @@ -0,0 +1,30 @@ +import { idb } from './utils.js'; + +export async function saveTask(taskData) { + if (navigator.onLine) { + try { + const response = await fetch('/add-task', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(taskData) + }); + return await response.json(); + } catch (error) { + throw error; + } + } else { + try { + const db = await idb.openDB('org-todo-pwa', 1, { + upgrade(db) { + db.createObjectStore('tasks', { keyPath: 'id', autoIncrement: true }); + } + }); + await db.add('tasks', taskData); + return { message: "Task saved offline!" }; + } catch (error) { + throw error; + } + } +} \ No newline at end of file diff --git a/public/js/utils.js b/public/js/utils.js new file mode 100644 index 0000000..d7763e0 --- /dev/null +++ b/public/js/utils.js @@ -0,0 +1,16 @@ +export const idb = { + openDB(name, version, { upgrade }) { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name, version); + request.onupgradeneeded = (event) => { + upgrade(request.result, event.oldVersion, event.newVersion, request.transaction); + }; + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); + } +}; \ No newline at end of file