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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+