diff --git a/debugging/book-library/index.html b/debugging/book-library/index.html index 23acfa71..a883b7ba 100644 --- a/debugging/book-library/index.html +++ b/debugging/book-library/index.html @@ -1,96 +1,97 @@ - + - - + Library Books + + + - - + + + -
+

Library

Add books to your virtual library

-
+ + +
+ + +
+
+ + - + + -
-
- - - - - - - - + type="number" + class="form-control" + id="pages" + name="pages" + min="1" + step="1" + required + > + +
+ + +
+ + +
-
- - - - - - - - - - - - - - - - - - - -
TitleAuthorNumber of PagesRead
+ + + + + + + + + + + + + +
TitleAuthorNumber of PagesReadActions
+
- + - + \ No newline at end of file diff --git a/debugging/book-library/script.js b/debugging/book-library/script.js index 75ce6c1d..df5ebf65 100644 --- a/debugging/book-library/script.js +++ b/debugging/book-library/script.js @@ -1,103 +1,240 @@ -let myLibrary = []; +class Book { + constructor(title, author, pages, check) { + this.title = title; + this.author = author; + this.pages = pages; + this.check = check; + } +} -window.addEventListener("load", function (e) { - populateStorage(); - render(); -}); +const myLibrary = []; + +// DOM Node References +const bookForm = document.getElementById("book-form"); +const titleInput = document.getElementById("title"); +const authorInput = document.getElementById("author"); +const pagesInput = document.getElementById("pages"); +const checkInput = document.getElementById("check"); +const displayTableBody = document.querySelector("#display tbody"); + +// ----------------------------------------------------------------------------- +// NON-BLOCKING NOTIFICATION SYSTEM (BOOTSTRAP 4 COMPATIBLE) +// ----------------------------------------------------------------------------- + +function showNotification(message, type = "success") { + const notificationArea = + document.getElementById("notification-area") || + (() => { + const el = document.createElement("div"); + el.id = "notification-area"; + el.style.cssText = + "position: fixed; top: 20px; right: 20px; z-index: 1060; max-width: 350px;"; + document.body.appendChild(el); + return el; + })(); + + const alertEl = document.createElement("div"); + alertEl.className = `alert alert-${type} alert-dismissible fade show shadow-sm mb-2`; + alertEl.role = "alert"; + + // Bootstrap 4 syntax: data-dismiss instead of data-bs-dismiss + alertEl.innerHTML = ` +
${message}
+ + `; + + notificationArea.appendChild(alertEl); + + setTimeout(() => { + alertEl.classList.remove("show"); + setTimeout(() => alertEl.remove(), 200); + }, 3500); +} + +// ----------------------------------------------------------------------------- +// LOCAL STORAGE PERSISTENCE +// ----------------------------------------------------------------------------- + +const STORAGE_KEY = "myLibraryData"; + +function saveLibraryToStorage() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(myLibrary)); +} + +function loadLibraryFromStorage() { + const storedData = localStorage.getItem(STORAGE_KEY); -function populateStorage() { - if (myLibrary.length == 0) { - let book1 = new Book("Robison Crusoe", "Daniel Defoe", "252", true); - let book2 = new Book( + if (storedData) { + const parsedData = JSON.parse(storedData); + myLibrary.length = 0; + parsedData.forEach((b) => + myLibrary.push(new Book(b.title, b.author, b.pages, b.check)) + ); + } else { + const book1 = new Book("Robinson Crusoe", "Daniel Defoe", 252, true); + const book2 = new Book( "The Old Man and the Sea", "Ernest Hemingway", - "127", + 127, true ); - myLibrary.push(book1); - myLibrary.push(book2); - render(); + myLibrary.push(book1, book2); + saveLibraryToStorage(); } } -const title = document.getElementById("title"); -const author = document.getElementById("author"); -const pages = document.getElementById("pages"); -const check = document.getElementById("check"); - -//check the right input from forms and if its ok -> add the new book (object in array) -//via Book function and start render function -function submit() { - if ( - title.value == null || - title.value == "" || - pages.value == null || - pages.value == "" - ) { - alert("Please fill all fields!"); - return false; - } else { - let book = new Book(title.value, title.value, pages.value, check.checked); - library.push(book); - render(); +// ----------------------------------------------------------------------------- +// PREPROCESSING PIPELINE (Normalization & Validation) +// ----------------------------------------------------------------------------- + +function normalizeString(str) { + if (typeof str !== "string") return ""; + return str.trim().replace(/\s+/g, " "); +} + +function preprocessBookInput(rawTitle, rawAuthor, rawPages, rawCheck) { + const cleanTitle = normalizeString(rawTitle); + const cleanAuthor = normalizeString(rawAuthor); + const cleanPagesNum = Number(rawPages); + const cleanCheck = Boolean(rawCheck); + + const errors = []; + + if (cleanTitle === "") { + errors.push("Title cannot be empty or contain only space characters."); + } else if (cleanTitle.length > 150) { + errors.push("Title must be 150 characters or less."); + } + + if (cleanAuthor === "") { + errors.push("Author cannot be empty or contain only space characters."); + } else if (cleanAuthor.length > 100) { + errors.push("Author must be 100 characters or less."); + } + + if (!Number.isInteger(cleanPagesNum) || cleanPagesNum <= 0) { + errors.push("Pages must be a whole positive number greater than 0."); + } + + if (errors.length > 0) { + showNotification( + `Validation Error:
${errors.join("
")}`, + "danger" + ); + return null; } + + return { + title: cleanTitle, + author: cleanAuthor, + pages: cleanPagesNum, + check: cleanCheck, + }; } -function Book(title, author, pages, check) { - this.title = title; - this.author = author; - this.pages = pages; - this.check = check; +// ----------------------------------------------------------------------------- +// EVENT HANDLERS & DYNAMIC RENDERING +// ----------------------------------------------------------------------------- + +function handleFormSubmit(event) { + event.preventDefault(); + + const processedData = preprocessBookInput( + titleInput.value, + authorInput.value, + pagesInput.value, + checkInput.checked + ); + + if (!processedData) return; + + const book = new Book( + processedData.title, + processedData.author, + processedData.pages, + processedData.check + ); + + myLibrary.push(book); + saveLibraryToStorage(); + bookForm.reset(); + render(); + + const tempDiv = document.createElement("div"); + tempDiv.textContent = book.title; + showNotification(`"${tempDiv.innerHTML}" added to your library.`, "success"); } function render() { - let table = document.getElementById("display"); - let rowsNumber = table.rows.length; - //delete old table - for (let n = rowsNumber - 1; n > 0; n-- { - table.deleteRow(n); - } - //insert updated row and cells - let length = myLibrary.length; - for (let i = 0; i < length; i++) { - let row = table.insertRow(1); - let titleCell = row.insertCell(0); - let authorCell = row.insertCell(1); - let pagesCell = row.insertCell(2); - let wasReadCell = row.insertCell(3); - let deleteCell = row.insertCell(4); - titleCell.innerHTML = myLibrary[i].title; - authorCell.innerHTML = myLibrary[i].author; - pagesCell.innerHTML = myLibrary[i].pages; - - //add and wait for action for read/unread button - let changeBut = document.createElement("button"); - changeBut.id = i; - changeBut.className = "btn btn-success"; - wasReadCell.appendChild(changeBut); - let readStatus = ""; - if (myLibrary[i].check == false) { - readStatus = "Yes"; - } else { - readStatus = "No"; - } - changeBut.innerText = readStatus; - - changeBut.addEventListener("click", function () { - myLibrary[i].check = !myLibrary[i].check; + displayTableBody.innerHTML = ""; + + const fragment = document.createDocumentFragment(); + + myLibrary.forEach((book, index) => { + const rowEl = document.createElement("tr"); + + // Text cells (Title, Author, Pages) + ["title", "author", "pages"].forEach((prop) => { + const cellEl = document.createElement("td"); + cellEl.textContent = book[prop]; + rowEl.appendChild(cellEl); + }); + + // Read status toggle button + const wasReadCellEl = document.createElement("td"); + const toggleBtn = document.createElement("button"); + toggleBtn.type = "button"; + toggleBtn.className = book.check + ? "btn btn-success btn-sm" + : "btn btn-secondary btn-sm"; + toggleBtn.textContent = book.check ? "Yes" : "No"; + + toggleBtn.addEventListener("click", () => { + book.check = !book.check; + saveLibraryToStorage(); render(); }); - //add delete button to every row and render again - let delButton = document.createElement("button"); - delBut.id = i + 5; - deleteCell.appendChild(delBut); - delBut.className = "btn btn-warning"; - delBut.innerHTML = "Delete"; - delBut.addEventListener("clicks", function () { - alert(`You've deleted title: ${myLibrary[i].title}`); - myLibrary.splice(i, 1); + wasReadCellEl.appendChild(toggleBtn); + rowEl.appendChild(wasReadCellEl); + + // Delete button + const deleteCellEl = document.createElement("td"); + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "btn btn-danger btn-sm"; + deleteBtn.textContent = "Delete"; + + deleteBtn.addEventListener("click", () => { + const deletedTitle = book.title; + myLibrary.splice(index, 1); + saveLibraryToStorage(); render(); + + const tempDiv = document.createElement("div"); + tempDiv.textContent = deletedTitle; + showNotification(`"${tempDiv.innerHTML}" deleted successfully.`, "info"); }); - } + + deleteCellEl.appendChild(deleteBtn); + rowEl.appendChild(deleteCellEl); + + fragment.appendChild(rowEl); + }); + + displayTableBody.appendChild(fragment); } + +document.addEventListener("DOMContentLoaded", () => { + loadLibraryFromStorage(); + render(); + + // Guard check placed directly within the listener assignment + if (bookForm) { + bookForm.addEventListener("submit", (event) => { + handleFormSubmit(event); + }); + } +});