// public/script.js // Handles frontend logic for login, registration, and todo management // Version 3: Improved error handling message for JSON parse errors document.addEventListener('DOMContentLoaded', () => { const path = window.location.pathname; // --- Page-Specific Logic --- if (path === '/login' || path.endsWith('login.html')) { setupLoginForm(); } else if (path === '/register' || path.endsWith('register.html')) { setupRegisterForm(); } else if (path === '/' || path.endsWith('index.html')) { // Main page logic: Check login status and set up UI accordingly checkLoginStatusAndSetupPage(); } }); // --- Authentication Form Handlers (Unchanged from previous version) --- function setupLoginForm() { const loginForm = document.getElementById('login-form'); const errorMessage = document.getElementById('error-message'); if (loginForm) { loginForm.addEventListener('submit', async (event) => { event.preventDefault(); // Prevent default form submission errorMessage.textContent = ''; // Clear previous errors const username = loginForm.username.value; const password = loginForm.password.value; try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const result = await response.json(); if (response.ok) { // Login successful, redirect to the main todo page window.location.href = '/'; // Redirect to home page } else { // Display error message from server errorMessage.textContent = result.message || 'Login fehlgeschlagen.'; } } catch (error) { console.error('Login request failed:', error); errorMessage.textContent = 'Ein Netzwerkfehler ist aufgetreten. Bitte versuchen Sie es erneut.'; } }); } } function setupRegisterForm() { const registerForm = document.getElementById('register-form'); const errorMessage = document.getElementById('error-message'); const successMessage = document.getElementById('success-message'); if (registerForm) { registerForm.addEventListener('submit', async (event) => { event.preventDefault(); errorMessage.textContent = ''; successMessage.textContent = ''; const username = registerForm.username.value; const password = registerForm.password.value; const confirmPassword = registerForm['confirm-password'].value; if (password !== confirmPassword) { errorMessage.textContent = 'Passwörter stimmen nicht überein.'; return; } try { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const result = await response.json(); if (response.ok) { successMessage.textContent = result.message + ' Sie werden zum Login weitergeleitet...'; registerForm.reset(); // Clear the form // Redirect to login page after a short delay setTimeout(() => { window.location.href = '/login'; }, 2000); // 2 seconds delay } else { errorMessage.textContent = result.message || 'Registrierung fehlgeschlagen.'; } } catch (error) { console.error('Registration request failed:', error); errorMessage.textContent = 'Ein Netzwerkfehler ist aufgetreten. Bitte versuchen Sie es erneut.'; } }); } } // --- Main Page Logic (index.html) --- // Global references for main page elements let todoAppContent = null; let loginPrompt = null; let authButtonContainer = null; let todoList = null; let errorMessage = null; // Error message specifically for todo actions // Function to check login status and set up the main page UI async function checkLoginStatusAndSetupPage() { // Get references to the main containers todoAppContent = document.getElementById('todo-app-content'); loginPrompt = document.getElementById('login-prompt'); authButtonContainer = document.getElementById('auth-button-container'); todoList = document.getElementById('todo-list'); // Needed for clearing/rendering errorMessage = document.getElementById('error-message'); // Error display within todo-app-content if (!todoAppContent || !loginPrompt || !authButtonContainer || !todoList || !errorMessage) { console.error('Essential page elements not found!'); return; } try { const response = await fetch('/api/auth/status'); // Check if response is ok and content type is JSON before parsing if (response.ok && response.headers.get('Content-Type')?.includes('application/json')) { const status = await response.json(); if (status.loggedIn) { updateUIForLoggedIn(status.username); setupTodoListeners(); // Set up listeners only if logged in await fetchTodos(); // Fetch todos only if logged in } else { updateUIForLoggedOut(); } } else { // Handle non-JSON or error responses from /status endpoint console.error('Unexpected response from /api/auth/status:', response.status, response.statusText); updateUIForLoggedOut(); // Fallback to logged out state errorMessage.textContent = 'Fehler beim Prüfen des Login-Status (Serverantwort).'; } } catch (error) { console.error('Error checking login status:', error); // Show logged out state as a fallback in case of network error etc. updateUIForLoggedOut(); errorMessage.textContent = 'Fehler beim Prüfen des Login-Status (Netzwerk/Skript).'; } } // Update UI for Logged-In User function updateUIForLoggedIn(username) { if (!todoAppContent || !loginPrompt || !authButtonContainer) return; // Show Todo content, hide login prompt todoAppContent.style.display = 'block'; loginPrompt.style.display = 'none'; // Update Auth Button to "Abmelden" authButtonContainer.innerHTML = ''; // Clear previous button const logoutButton = document.createElement('button'); logoutButton.id = 'logout-button'; logoutButton.textContent = 'Abmelden'; logoutButton.classList.add('btn', 'btn-secondary'); logoutButton.addEventListener('click', handleLogout); // Add listener directly authButtonContainer.appendChild(logoutButton); // Optional: Display username (e.g., next to the button or in the header) const welcomeMessage = document.createElement('span'); welcomeMessage.textContent = `Hallo, ${username}! `; welcomeMessage.style.marginRight = '10px'; authButtonContainer.prepend(welcomeMessage); // Add before button } // Update UI for Logged-Out User function updateUIForLoggedOut() { if (!todoAppContent || !loginPrompt || !authButtonContainer || !todoList) return; // Hide Todo content, show login prompt todoAppContent.style.display = 'none'; loginPrompt.style.display = 'block'; // Update Auth Button to "Anmelden" authButtonContainer.innerHTML = ''; // Clear previous button const loginLink = document.createElement('a'); // Use a link for navigation loginLink.href = '/login'; loginLink.textContent = 'Anmelden'; loginLink.classList.add('btn'); // Style as button authButtonContainer.appendChild(loginLink); // Clear any existing todos from the list todoList.innerHTML = ''; // Clear any previous error messages related to todos if(errorMessage) errorMessage.textContent = ''; } // Set up event listeners for the todo form and list (only when logged in) function setupTodoListeners() { const addTodoForm = document.getElementById('add-todo-form'); const newTodoInput = document.getElementById('new-todo-input'); // todoList reference is already global // Add Todo Form Submission if (addTodoForm) { // Remove previous listener if any to prevent duplicates addTodoForm.onsubmit = null; addTodoForm.onsubmit = async (event) => { event.preventDefault(); const task = newTodoInput.value.trim(); if (task) { await addTodoItem(task); newTodoInput.value = ''; // Clear input field } }; } // Todo List Click Handler (for checkboxes and delete buttons) if (todoList) { // Remove previous listener if any todoList.onclick = null; todoList.onclick = async (event) => { const target = event.target; const todoItem = target.closest('.todo-item'); // Find the parent li element if (!todoItem) return; // Click was not inside a todo item const todoId = todoItem.dataset.id; // Handle Checkbox Click (Toggle Completion) if (target.type === 'checkbox') { const isCompleted = target.checked; await updateTodoStatus(todoId, isCompleted); } // Handle Delete Button Click if (target.classList.contains('btn-delete')) { await deleteTodoItem(todoId); } }; } } // Handle Logout Process async function handleLogout() { if(errorMessage) errorMessage.textContent = ''; // Clear errors try { const response = await fetch('/api/auth/logout', { method: 'POST' }); if (response.ok) { // Logout successful, update UI to logged-out state updateUIForLoggedOut(); } else { // Try to parse error response as JSON let result = { message: 'Logout fehlgeschlagen (unbekannter Fehler).' }; try { result = await response.json(); } catch (e) { console.error("Could not parse logout error response as JSON"); } if(errorMessage) errorMessage.textContent = result.message || 'Logout fehlgeschlagen.'; } } catch (error) { console.error('Logout failed:', error); if(errorMessage) errorMessage.textContent = 'Logout fehlgeschlagen. Netzwerkfehler.'; } } // --- API Call Functions (Modified Error Handling) --- // Fetch Todos from Server async function fetchTodos() { if(errorMessage) errorMessage.textContent = ''; try { const response = await fetch('/api/todos'); // GET request by default // Check for auth errors specifically (though status check should prevent this) if (response.status === 401 || response.status === 403) { console.warn('Auth error during fetchTodos - should have been caught by status check.'); updateUIForLoggedOut(); // Update UI to logged out state return; // Stop processing } if (!response.ok) { // Handle other potential errors (e.g., 500 server error) const errorData = await response.json().catch(() => ({ message: 'Unbekannter Serverfehler beim Laden der Todos' })); throw new Error(errorData.message || `HTTP error! status: ${response.status}`); } // Ensure response is JSON before parsing if (!response.headers.get('Content-Type')?.includes('application/json')) { throw new Error('Unerwartete Serverantwort (kein JSON) beim Laden der Todos.'); } const todos = await response.json(); renderTodoList(todos); } catch (error) { console.error('Error fetching todos:', error); if(errorMessage) errorMessage.textContent = `Fehler beim Laden der Todos: ${error.message}`; // Do NOT switch to logged-out UI here, as it might be a temporary server issue } } // Add a New Todo Item async function addTodoItem(task) { if(errorMessage) errorMessage.textContent = ''; let response; console.log('Adding todo item:', task); try { response = await fetch('/api/todos/newEntry', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' // Explicitly request JSON response }, credentials: 'same-origin', // Explicitly include cookies/credentials body: JSON.stringify({ task }), }); console.log('Response status:', response.status); // If we got redirected to the login page, the status might still be 200 // but the content type will be text/html instead of application/json const contentType = response.headers.get('Content-Type'); console.log('Response headers:', contentType); if (contentType && contentType.includes('text/html')) { // We got HTML instead of JSON - this means auth failed and we were redirected console.warn('Auth failed - received HTML instead of JSON'); updateUIForLoggedOut(); return; } if (response.status === 401 || response.status === 403) { console.warn('Auth error during addTodoItem.'); updateUIForLoggedOut(); return; } // Clone and log response body for debugging console.log('Response body:', await response.clone().text()); console.log('Response ok:', response.ok); if (!response.ok) { const result = await response.json().catch(() => ({ message: `Serverfehler (${response.status}) beim Hinzufügen.` })); throw new Error(result.message); } // We already checked content type above, so just parse JSON now const newTodo = await response.json(); addTodoToDOM(newTodo); } catch (error) { console.error('Error adding todo:', error); let displayError = `Fehler beim Hinzufügen: ${error.message}`; if (error instanceof SyntaxError) { displayError = 'Fehler beim Hinzufügen: Unerwartete Antwort vom Server.'; } if(errorMessage) errorMessage.textContent = displayError; } } // Update Todo Completion Status async function updateTodoStatus(id, isCompleted) { if(errorMessage) errorMessage.textContent = ''; let response; try { response = await fetch(`/api/todos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_completed: isCompleted }), }); if (response.status === 401 || response.status === 403) { console.warn('Auth error during updateTodoStatus.'); updateUIForLoggedOut(); return; } if (!response.ok) { const result = await response.json().catch(() => ({ message: `Serverfehler (${response.status}) beim Aktualisieren.` })); throw new Error(result.message); } if (!response.headers.get('Content-Type')?.includes('application/json')) { throw new Error('Unerwartete Serverantwort (kein JSON) nach dem Aktualisieren.'); } const updatedTodo = await response.json(); // Update the specific todo item in the DOM const todoElement = todoList.querySelector(`li[data-id="${id}"]`); if (todoElement) { todoElement.classList.toggle('completed', updatedTodo.is_completed); const checkbox = todoElement.querySelector('input[type="checkbox"]'); if(checkbox) checkbox.checked = updatedTodo.is_completed; } } catch (error) { console.error('Error updating todo status:', error); let displayError = `Fehler beim Aktualisieren: ${error.message}`; if (error instanceof SyntaxError) { displayError = 'Fehler beim Aktualisieren: Unerwartete Antwort vom Server.'; } if(errorMessage) errorMessage.textContent = displayError; // Optional: Revert checkbox state on error const todoElement = todoList.querySelector(`li[data-id="${id}"]`); if(todoElement) { const checkbox = todoElement.querySelector('input[type="checkbox"]'); if(checkbox) checkbox.checked = !isCompleted; // Revert state } } } // Delete a Todo Item async function deleteTodoItem(id) { if(errorMessage) errorMessage.textContent = ''; try { const response = await fetch(`/api/todos/${id}`, { method: 'DELETE', }); if (response.status === 401 || response.status === 403) { console.warn('Auth error during deleteTodoItem.'); updateUIForLoggedOut(); return; } // Allow 204 No Content (which might not have a JSON body) if (!response.ok && response.status !== 204) { const result = await response.json().catch(() => ({ message: `Serverfehler (${response.status}) beim Löschen.` })); throw new Error(result.message); } // Remove the todo item from the DOM const todoElement = todoList.querySelector(`li[data-id="${id}"]`); if (todoElement) { todoElement.remove(); // Check if list is now empty and show message if needed if (todoList.children.length === 0) { renderTodoList([]); // Will add the "empty" message } } } catch (error) { console.error('Error deleting todo:', error); let displayError = `Fehler beim Löschen: ${error.message}`; if (error instanceof SyntaxError) { // Should not happen for DELETE usually, but good practice displayError = 'Fehler beim Löschen: Unerwartete Antwort vom Server.'; } if(errorMessage) errorMessage.textContent = displayError; } } // --- DOM Manipulation (Rendering Todos) --- // Render the entire list of todos function renderTodoList(todos) { if (!todoList) return; todoList.innerHTML = ''; // Clear existing list if (todos.length === 0) { // Display a message within the todo list area if empty const emptyMsg = document.createElement('p'); emptyMsg.textContent = 'Noch keine Aufgaben vorhanden.'; emptyMsg.style.textAlign = 'center'; emptyMsg.style.color = '#888'; emptyMsg.style.padding = '20px 0'; todoList.appendChild(emptyMsg); } else { todos.forEach(todo => addTodoToDOM(todo)); } } // Add a single todo item to the DOM function addTodoToDOM(todo) { if (!todoList) return; // Remove the "no tasks" message if it exists and we are adding the first item const emptyMsg = todoList.querySelector('p'); if (emptyMsg && todoList.children.length === 1 && emptyMsg.parentNode === todoList) { emptyMsg.remove(); } const li = document.createElement('li'); li.classList.add('todo-item'); li.dataset.id = todo.id; // Store todo ID on the element if (todo.is_completed) { li.classList.add('completed'); } const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = todo.is_completed; const span = document.createElement('span'); span.textContent = todo.task; const deleteButton = document.createElement('button'); deleteButton.textContent = 'Löschen'; deleteButton.classList.add('btn-delete'); // Use specific class for deletion li.appendChild(checkbox); li.appendChild(span); li.appendChild(deleteButton); // Prepend to show newest todos first todoList.prepend(li); }