diff --git a/labs/02/css/style.css b/labs/02/css/style.css new file mode 100644 index 0000000..171f53a --- /dev/null +++ b/labs/02/css/style.css @@ -0,0 +1,173 @@ +/* + Chat Gipity glossed over some CSS for me + so I dont have to claw out my eyes looking + at my own hot garbage styling +*/ + +:root { + color: #172033; + background-color: #f5f7fb; + font-family: Arial, Helvetica, sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; +} + +main { + width: min(92%, 48rem); + margin: 3rem auto; +} + +h1 { + margin: 0; + font-size: clamp(1.8rem, 5vw, 2.6rem); +} + +.landing-page { + padding: 3rem; + text-align: center; + background-color: white; + border: 1px solid #d8deea; + border-radius: 0.75rem; + box-shadow: 0 0.75rem 2rem rgb(23 32 51 / 8%); +} + +.student-name { + margin: 0.75rem 0 2rem; + color: #536078; + font-size: 1.15rem; +} + +.page-links { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 1rem; +} + +.button-link, +.back-link { + display: inline-block; + color: white; + background-color: #315fc4; + padding: 0.75rem 1.2rem; + border-radius: 0.4rem; + text-decoration: none; +} + +.notes-page { + min-height: 36rem; + padding: 2rem; + background-color: white; + border: 1px solid #b9c4d8; + border-radius: 0.5rem; +} + +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 2rem; +} + +.status { + min-height: 1.2rem; + margin: 0.4rem 0 0; + color: #536078; + white-space: nowrap; +} + +.notes { + display: grid; + gap: 1rem; +} + +.note { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 1rem; +} + +.note textarea { + min-height: 7rem; + padding: 0.8rem; + color: #172033; + background-color: #eef3ff; + border: 1px solid #8398c2; + border-radius: 0.35rem; + font: inherit; + resize: vertical; +} + +button { + padding: 0.65rem 1rem; + border: 0; + border-radius: 0.35rem; + font: inherit; + font-weight: 600; + cursor: pointer; +} + +.remove-button { + color: white; + background-color: #c43e45; +} + +.add-button-control { + margin-top: 1rem; + color: white; + background-color: #2f7a42; +} + +.back-link { + margin-top: 2rem; + background-color: #536078; +} + +.reader-note { + min-height: 5rem; + margin: 0; + padding: 1rem; + color: white; + background-color: #315fc4; + border-radius: 0.35rem; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.empty-message { + color: #536078; +} + +@media (max-width: 36rem) { + main { + margin: 1rem auto; + } + + .landing-page, + .notes-page { + padding: 1.25rem; + } + + .page-header, + .note { + grid-template-columns: 1fr; + flex-direction: column; + } + + .status { + white-space: normal; + } + + .remove-button { + justify-self: start; + } +} diff --git a/labs/02/index.html b/labs/02/index.html new file mode 100644 index 0000000..53ada5d --- /dev/null +++ b/labs/02/index.html @@ -0,0 +1,22 @@ + + + + + + + + + +
+

+
+ + +
+ + + + diff --git a/labs/02/js/script.js b/labs/02/js/script.js new file mode 100644 index 0000000..2e2f146 --- /dev/null +++ b/labs/02/js/script.js @@ -0,0 +1,204 @@ +import { STRINGS } from "../lang/messages/en/user.js"; + +const STORAGE_KEY = "comp4537-lab2-notes"; +const UPDATE_INTERVAL = 2000; + +class NoteButton { + constructor(label, className, clickHandler) { + this.element = document.createElement("button"); + this.element.type = "button"; + this.element.classList.add(className); + this.element.textContent = label; + this.element.addEventListener("click", clickHandler); + } + + getElement() { + return this.element; + } +} + +class Note { + constructor(id, content, removeHandler) { + this.id = id; + this.container = document.createElement("article"); + this.textArea = document.createElement("textarea"); + this.removeButton = new NoteButton( + STRINGS.REMOVE_BUTTON, + "remove-button", + () => removeHandler(this) + ); + + this.container.classList.add("note"); + this.textArea.value = content; + this.textArea.placeholder = STRINGS.NOTE_PLACEHOLDER; + this.textArea.setAttribute("aria-label", STRINGS.NOTE_PLACEHOLDER); + + this.container.append( + this.textArea, + this.removeButton.getElement() + ); + } + + getElement() { + return this.container; + } + + getData() { + return { + id: this.id, + content: this.textArea.value, + }; + } + + remove() { + this.container.remove(); + } +} + +class NoteStorage { + load() { + const storedNotes = localStorage.getItem(STORAGE_KEY); + + if (storedNotes === null) { + return []; + } + + try { + const notes = JSON.parse(storedNotes); + return Array.isArray(notes) ? notes : []; + } catch { + return []; + } + } + + save(notes) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(notes)); + } +} + +class Writer { + constructor(storage) { + this.storage = storage; + this.notes = []; + this.notesElement = document.getElementById("notes"); + this.statusElement = document.getElementById("status"); + this.addButtonElement = document.getElementById("add-button"); + } + + init() { + document.title = STRINGS.WRITER_TITLE; + document.getElementById("page-title").textContent = STRINGS.WRITER_TITLE; + document.getElementById("back-link").textContent = STRINGS.BACK_LINK; + + const addButton = new NoteButton( + STRINGS.ADD_BUTTON, + "add-button-control", + () => this.addNote() + ); + + this.addButtonElement.replaceWith(addButton.getElement()); + + for (const noteData of this.storage.load()) { + this.createNote(noteData.id, noteData.content); + } + + window.setInterval(() => this.saveNotes(), UPDATE_INTERVAL); + } + + addNote() { + const id = `${Date.now()}-${Math.random()}`; + this.createNote(id, ""); + } + + createNote(id, content) { + const note = new Note(id, content, (selectedNote) => { + this.removeNote(selectedNote); + }); + + this.notes.push(note); + this.notesElement.appendChild(note.getElement()); + } + + removeNote(note) { + this.notes = this.notes.filter((currentNote) => currentNote !== note); + note.remove(); + this.saveNotes(); + } + + saveNotes() { + const noteData = this.notes.map((note) => note.getData()); + this.storage.save(noteData); + this.statusElement.textContent = STRINGS.STORED_AT + this.getCurrentTime(); + } + + getCurrentTime() { + return new Date().toLocaleTimeString(); + } +} + +class Reader { + constructor(storage) { + this.storage = storage; + this.notesElement = document.getElementById("notes"); + this.statusElement = document.getElementById("status"); + } + + init() { + document.title = STRINGS.READER_TITLE; + document.getElementById("page-title").textContent = STRINGS.READER_TITLE; + document.getElementById("back-link").textContent = STRINGS.BACK_LINK; + + this.updateNotes(); + window.setInterval(() => this.updateNotes(), UPDATE_INTERVAL); + window.addEventListener("storage", (event) => { + if (event.key === STORAGE_KEY) { + this.updateNotes(); + } + }); + } + + updateNotes() { + const notes = this.storage.load(); + this.notesElement.replaceChildren(); + + if (notes.length === 0) { + const message = document.createElement("p"); + message.classList.add("empty-message"); + message.textContent = STRINGS.EMPTY_NOTES; + this.notesElement.appendChild(message); + } else { + for (const note of notes) { + const noteElement = document.createElement("p"); + noteElement.classList.add("reader-note"); + noteElement.textContent = note.content; + this.notesElement.appendChild(noteElement); + } + } + + this.statusElement.textContent = STRINGS.UPDATED_AT + this.getCurrentTime(); + } + + getCurrentTime() { + return new Date().toLocaleTimeString(); + } +} + +class LandingPage { + init() { + document.title = STRINGS.INDEX_TITLE; + document.getElementById("page-title").textContent = STRINGS.INDEX_TITLE; + document.getElementById("writer-link").textContent = STRINGS.WRITER_LINK; + document.getElementById("reader-link").textContent = STRINGS.READER_LINK; + } +} + +const storage = new NoteStorage(); +const pageName = window.location.pathname.split("/").pop() || "index.html"; + +if (pageName === "writer.html") { + new Writer(storage).init(); +} else if (pageName === "reader.html") { + new Reader(storage).init(); +} else { + new LandingPage().init(); +} diff --git a/labs/02/lang/messages/en/user.js b/labs/02/lang/messages/en/user.js new file mode 100644 index 0000000..aa12317 --- /dev/null +++ b/labs/02/lang/messages/en/user.js @@ -0,0 +1,14 @@ +export const STRINGS = { + INDEX_TITLE: "Lab 1: JSON, Object Constructor, localStorage", + WRITER_LINK: "Write Notes", + READER_LINK: "Read Notes", + WRITER_TITLE: "Writer", + READER_TITLE: "Reader", + ADD_BUTTON: "Add Note", + REMOVE_BUTTON: "Remove", + BACK_LINK: "Back to Home", + NOTE_PLACEHOLDER: "Enter a note...", + EMPTY_NOTES: "There are no notes to display.", + STORED_AT: "Stored at: ", + UPDATED_AT: "Updated at: ", +}; diff --git a/labs/02/reader.html b/labs/02/reader.html new file mode 100644 index 0000000..9d26bf0 --- /dev/null +++ b/labs/02/reader.html @@ -0,0 +1,23 @@ + + + + + + + + + +
+ + +
+ + +
+ + + + diff --git a/labs/02/writer.html b/labs/02/writer.html new file mode 100644 index 0000000..4e1da44 --- /dev/null +++ b/labs/02/writer.html @@ -0,0 +1,24 @@ + + + + + + + + + +
+ + +
+ + + +
+ + + +