From edd1159f4a21595812c64c604f522a9feac8c023 Mon Sep 17 00:00:00 2001 From: iswat Date: Sat, 9 May 2026 23:26:01 +0100 Subject: [PATCH 01/58] feat: Add .gitignore for Node.js project - Exclude node_modules/ and package-lock.json from version control - Ignore environment variables (.env) and system files (.DS_Store) - Exclude build artifacts (dist/, build/) and log files (*.log) - Ignore VS Code workspace settings (.vscode/) - Reduce repository size and prevent committing sensitive data --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c35aab4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +package-lock.json +.env +.DS_Store +*.log +dist/ +build/ +.vscode/ \ No newline at end of file From d46532282217c0ebcff80a7164a58cdab51af7e3 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 18 May 2026 10:47:04 +0100 Subject: [PATCH 02/58] feat: Set up Express backend server for chat app - Import Express library - Create Express app instance - Initialize messages array to store chat data - Configure server port (default: 3000) - Start server with app.listen() --- chat-app/backend/server.js | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 chat-app/backend/server.js diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js new file mode 100644 index 00000000..43984f43 --- /dev/null +++ b/chat-app/backend/server.js @@ -0,0 +1,10 @@ +import express from "express"; + +const app = express(); +const port = process.env.port || 3000; + +const messages = []; + +app.listen(port, () => { + console.log(`Chat app listening on port ${port}`); +}); From f8b85931912b49a8235de7ed1a636cb85cb5c58a Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 18 May 2026 11:30:50 +0100 Subject: [PATCH 03/58] feat: Add POST /messages endpoint to create new chat messages - Add JSON body parsing middleware - Implement POST /messages route with input validation - Validate text and sender fields are provided and non-empty - Create message object with id, sender, text, likes, and dislikes - Store message in messages array - Return created message with 201 status code - Return 400 error for invalid requests --- chat-app/backend/server.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 43984f43..a4abf8b9 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -5,6 +5,34 @@ const port = process.env.port || 3000; const messages = []; +// Middleware to parse JSON request body +app.use(express.json()); + +app.post("/messages", (req, res) => { + const { text, sender } = req.body; + + // The validation + if (!text || text.length === 0 || !sender || sender.length === 0) { + return res.status(400).send("Please provide both text and a sender name."); + } +}); + +// Create the message object +const newMessage = { + id: messages.length, + sender: sender, + text: text, + likes: 0, + dislikes: 0, +}; + +// Add the new message to the messages array (the storage) +messages.push(newMessage); + +// Send back a success status and the message (the response) +res.status(201).send(newMessage); + +// Start the server app.listen(port, () => { console.log(`Chat app listening on port ${port}`); }); From 0f9b122b7c92c30960eead0957db5f0e72665a1b Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 18 May 2026 11:37:14 +0100 Subject: [PATCH 04/58] feat: Add GET /messages endpoint to retrieve all chat messages - Implement GET /messages route - Return all messages from the messages array - Enable clients to fetch chat history --- chat-app/backend/server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index a4abf8b9..0aa83c7b 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -32,6 +32,10 @@ messages.push(newMessage); // Send back a success status and the message (the response) res.status(201).send(newMessage); +app.get("/messages", (req, res) => { + res.send(messages); +}); + // Start the server app.listen(port, () => { console.log(`Chat app listening on port ${port}`); From c1086ee3dc5d6f025a41f7f705ebc29180701d9c Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 11:05:53 +0100 Subject: [PATCH 05/58] feat: Create frontend HTML structure for chat app - Add index.html with semantic HTML structure - Include meta tags for charset and viewport - Link external stylesheet (styles.css) - Link deferred JavaScript file (script.js) - Create form with sender name and message inputs - Add submit button for sending messages - Create messages container div for displaying chat history --- chat-app/frontend/index.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 chat-app/frontend/index.html diff --git a/chat-app/frontend/index.html b/chat-app/frontend/index.html new file mode 100644 index 00000000..104ebf5a --- /dev/null +++ b/chat-app/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + Chat App + + +

Chat App

+ +
+ + + + + +
+ +
+ + From 4d6cdc0dcaa413042686f1216edc3c103fd9f811 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 15:17:52 +0100 Subject: [PATCH 06/58] feat: Add frontend script to fetch and display chat messages - Create getAllMessages async function to fetch messages from backend - Parse JSON response from /messages endpoint - Clear message container and render new messages - Display sender name and message text for each message - Add error handling for fetch failures - Call getAllMessages on page load to populate initial messages --- chat-app/frontend/script.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 chat-app/frontend/script.js diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js new file mode 100644 index 00000000..69eac6d2 --- /dev/null +++ b/chat-app/frontend/script.js @@ -0,0 +1,23 @@ +async function getAllMessages() { + try { + const response = await fetch("http://localhost:3000/messages"); + + const data = await response.json(); + + const messageContainer = document.getElementById("all-messages"); + + messageContainer.textContent = ""; + + data.forEach((message) => { + const newElement = document.createElement("div"); + + newElement.textContent = message.sender + ": " + message.text; + + messageContainer.appendChild(newElement); + }); + } catch (error) { + console.error("Error fetching messages:", error); + } +} + +getAllMessages(); From 4b2fecc90fafaf95ea08e0dbc9e7addc9a1df51b Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 15:18:43 +0100 Subject: [PATCH 07/58] refactor: Fix backend server structure and indentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import cors module for CORS support - Fix environment variable casing (process.env.port → process.env.PORT) - Add CORS middleware before other middleware - Move message creation logic inside POST route handler - Fix indentation for code inside route handlers - Properly close POST route handler with closing brace --- chat-app/backend/server.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 0aa83c7b..41d2ca28 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -1,10 +1,14 @@ import express from "express"; +import cors from "cors"; const app = express(); -const port = process.env.port || 3000; +const port = process.env.PORT || 3000; const messages = []; +// Enable CORS for all routes +app.use(cors()); + // Middleware to parse JSON request body app.use(express.json()); @@ -15,23 +19,22 @@ app.post("/messages", (req, res) => { if (!text || text.length === 0 || !sender || sender.length === 0) { return res.status(400).send("Please provide both text and a sender name."); } -}); -// Create the message object -const newMessage = { - id: messages.length, - sender: sender, - text: text, - likes: 0, - dislikes: 0, -}; + // Create the message object + const newMessage = { + id: messages.length, + sender: sender, + text: text, + likes: 0, + dislikes: 0, + }; -// Add the new message to the messages array (the storage) -messages.push(newMessage); - -// Send back a success status and the message (the response) -res.status(201).send(newMessage); + // Add the new message to the messages array (the storage) + messages.push(newMessage); + // Send back a success status and the message (the response) + res.status(201).send(newMessage); +}); app.get("/messages", (req, res) => { res.send(messages); }); From 8b67a04756a17150a701c90ababb9311e4c17255 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 15:19:19 +0100 Subject: [PATCH 08/58] feat: Add backend package.json with dependencies - Configure Node.js project with ES modules ("type": "module") - Add Express 5.2.1 for HTTP server framework - Add CORS 2.8.6 for cross-origin request handling - Enable npm package management for backend --- chat-app/backend/package.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 chat-app/backend/package.json diff --git a/chat-app/backend/package.json b/chat-app/backend/package.json new file mode 100644 index 00000000..55fb0a74 --- /dev/null +++ b/chat-app/backend/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "cors": "^2.8.6", + "express": "^5.2.1" + } +} From 47dfcc8dfd29c7ef52d0661be1b3a1fce06e41fb Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:08:15 +0100 Subject: [PATCH 09/58] feat: Add form IDs and fix message input element in frontend - Add id="chat-form" to form for JavaScript event listener access - Change textarea to input element for message field - Maintain labels and input structure for sender name and message --- chat-app/frontend/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-app/frontend/index.html b/chat-app/frontend/index.html index 104ebf5a..7f20acc4 100644 --- a/chat-app/frontend/index.html +++ b/chat-app/frontend/index.html @@ -10,11 +10,11 @@

Chat App

-
+ - +
From 6c325989acf009c25747bb9790c55caa2d198e51 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:09:34 +0100 Subject: [PATCH 10/58] feat: Add form submission handler to send messages to backend - Get form, sender input, and message input elements from DOM - Add submit event listener to chat form - Prevent default form submission behavior - Extract sender name and message text from input values - Send POST request to /messages endpoint with JSON payload - Include Content-Type header for JSON data - Refresh message list on successful submission - Clear input fields after sending message --- chat-app/frontend/script.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 69eac6d2..552852dc 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -21,3 +21,39 @@ async function getAllMessages() { } getAllMessages(); + +const formElement = document.getElementById("chat-form"); +const senderElement = document.getElementById("chat-sender"); +const messageElement = document.getElementById("chat-message"); + + + +formElement.addEventListener("submit", async(event) => { + event.preventDefault(); + + const senderValue = senderElement.value; + const messageValue = messageElement.value; + + // Send the data + const response = await fetch("http://localhost:3000/messages", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + sender: senderValue, + text: messageValue + }) + }); + + if (response.ok) { + // refresh the list of messages + getAllMessages(); + + // clear the sender and message input values + senderElement.value = ""; + messageElement.value = ""; + + } + +} ) From 31426bfade992458acc02e1499c6051010c2ea4e Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:22:54 +0100 Subject: [PATCH 11/58] feat: Poll messages every 5 seconds for real-time updates - Replace single getAllMessages() call with setInterval - Fetch messages from backend every 5000ms (5 seconds) - Enable live chat updates without manual refresh - Keep frontend synchronized with backend message list --- chat-app/frontend/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 552852dc..fadeacd1 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -20,7 +20,7 @@ async function getAllMessages() { } } -getAllMessages(); +setInterval(getAllMessages, 5000); const formElement = document.getElementById("chat-form"); const senderElement = document.getElementById("chat-sender"); From 6419e29bfe2f01fc8e0acc8432210a11c579ef36 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:25:33 +0100 Subject: [PATCH 12/58] fix: Correct HTML syntax for message input element - Change mismatched textarea closing tag to self-closing input tag --- chat-app/frontend/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-app/frontend/index.html b/chat-app/frontend/index.html index 7f20acc4..c9fde210 100644 --- a/chat-app/frontend/index.html +++ b/chat-app/frontend/index.html @@ -14,7 +14,7 @@

Chat App

- + From 53c941997fef67d2b06d64dc9eccf52f93323b3c Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:26:43 +0100 Subject: [PATCH 13/58] refactor: Clean up code formatting and update README documentation --- chat-app/README.md | 17 ++++++----- chat-app/frontend/script.js | 58 +++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/chat-app/README.md b/chat-app/README.md index d26b1a13..ee46cd43 100644 --- a/chat-app/README.md +++ b/chat-app/README.md @@ -7,9 +7,10 @@ https://sdc.codeyourfuture.io/decomposition/sprints/2/prep/ You must complete and deploy a chat application. You have two weeks to complete this. It must support at least the following requirements: -* As a user, I can send add a message to the chat. -* As a user, when I open the chat I see the messages that have been sent by any user. -* As a user, when someone sends a message, it gets added to what I see. + +- As a user, I can send add a message to the chat. +- As a user, when I open the chat I see the messages that have been sent by any user. +- As a user, when someone sends a message, it gets added to what I see. It must also support at least one additional feature. @@ -27,8 +28,8 @@ Exploring and understanding different ways of sending information between a clie ### How to submit -* Fork the Module-Decomposition repository -* Develop and deploy your chat app -* Create a pull request back into the original Module-Decomposition repo, including: - * A link to the deployed frontend on the CYF hosting environment - * A link to the deployed backend on the CYF hosting environment +- Fork the Module-Decomposition repository +- Develop and deploy your chat app +- Create a pull request back into the original Module-Decomposition repo, including: + - A link to the deployed frontend on the CYF hosting environment + - A link to the deployed backend on the CYF hosting environment diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index fadeacd1..6d6883c3 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -26,34 +26,30 @@ const formElement = document.getElementById("chat-form"); const senderElement = document.getElementById("chat-sender"); const messageElement = document.getElementById("chat-message"); - - -formElement.addEventListener("submit", async(event) => { - event.preventDefault(); - - const senderValue = senderElement.value; - const messageValue = messageElement.value; - - // Send the data - const response = await fetch("http://localhost:3000/messages", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - sender: senderValue, - text: messageValue - }) - }); - - if (response.ok) { - // refresh the list of messages - getAllMessages(); - - // clear the sender and message input values - senderElement.value = ""; - messageElement.value = ""; - - } - -} ) +formElement.addEventListener("submit", async (event) => { + event.preventDefault(); + + const senderValue = senderElement.value; + const messageValue = messageElement.value; + + // Send the data + const response = await fetch("http://localhost:3000/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), + }); + + if (response.ok) { + // refresh the list of messages + getAllMessages(); + + // clear the sender and message input values + senderElement.value = ""; + messageElement.value = ""; + } +}); From 928c5e968770fe47fd57de371385b15601475a4e Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 19 May 2026 16:52:57 +0100 Subject: [PATCH 14/58] feat: Add npm start script to package.json - Add scripts section with start command - Enable running backend with npm start instead of node server.js - Simplify server startup process - Remove trailing newline from file --- chat-app/backend/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chat-app/backend/package.json b/chat-app/backend/package.json index 55fb0a74..64e699ba 100644 --- a/chat-app/backend/package.json +++ b/chat-app/backend/package.json @@ -1,7 +1,10 @@ { "type": "module", + "scripts": { + "start": "node server.js" + }, "dependencies": { "cors": "^2.8.6", "express": "^5.2.1" } -} +} \ No newline at end of file From ac2c99dd79995a868d16f455d69d1f955837c6ee Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 20 May 2026 10:32:30 +0100 Subject: [PATCH 15/58] feat: Update frontend API endpoints to use production backend URL - Change getAllMessages() fetch URL from localhost to Render production URL - Update form submission fetch URL to production backend - Enable frontend to communicate with deployed Render backend - Remove dependency on local development server --- chat-app/frontend/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 6d6883c3..830b0a0a 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,6 +1,6 @@ async function getAllMessages() { try { - const response = await fetch("http://localhost:3000/messages"); + const response = await fetch("https://iswanna-chat-app-backend.onrender.com/messages"); const data = await response.json(); @@ -33,7 +33,7 @@ formElement.addEventListener("submit", async (event) => { const messageValue = messageElement.value; // Send the data - const response = await fetch("http://localhost:3000/messages", { + const response = await fetch("https://iswanna-chat-app-backend.onrender.com/messages", { method: "POST", headers: { "Content-Type": "application/json", From 4fa083d77ced5062df7c450c2d45b0d3aaeb3769 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 20 May 2026 20:50:15 +0100 Subject: [PATCH 16/58] refactor: Implement incremental message fetching with lastIdSeen tracking - Add lastIdSeen variable to track last viewed message ID - Update getAllMessages() to use ?since query parameter - Only fetch messages newer than lastIdSeen from backend - Remove messageContainer.textContent clearing to prevent flicker - Update lastIdSeen after each message is displayed - Reduce data transfer by only fetching new messages - Improve performance with incremental polling --- chat-app/frontend/script.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 830b0a0a..8fac448a 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,18 +1,20 @@ +let lastIdSeen = -1; + async function getAllMessages() { try { - const response = await fetch("https://iswanna-chat-app-backend.onrender.com/messages"); + const response = await fetch( + `https://iswanna-chat-app-backend.onrender.com/messages?since=${lastIdSeen}`, + ); const data = await response.json(); const messageContainer = document.getElementById("all-messages"); - messageContainer.textContent = ""; - data.forEach((message) => { const newElement = document.createElement("div"); newElement.textContent = message.sender + ": " + message.text; - + lastIdSeen = message.id; messageContainer.appendChild(newElement); }); } catch (error) { From 13987e92e21322b07290e053713fa39051aa252a Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 20 May 2026 20:51:56 +0100 Subject: [PATCH 17/58] feat: Add message filtering by ID to GET /messages endpoint - Parse since query parameter from request - Convert since value to number, default to -1 if undefined - Filter messages array to only return messages with id > sinceId - Handle edge case where since=0 evaluates as falsy - Enable pagination and incremental message loading - Reduce payload size by sending only new messages --- chat-app/backend/server.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 41d2ca28..bcca86a9 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -36,7 +36,20 @@ app.post("/messages", (req, res) => { res.status(201).send(newMessage); }); app.get("/messages", (req, res) => { - res.send(messages); + const sinceValue = req.query.since; + + let sinceId; + // We check if the value exists at all. + // If it's "0", this check is true, and we use 0. + if (sinceValue !== undefined) { + sinceId = Number(sinceValue); + } else { + sinceId = -1; + } + + const messagesSinceId = messages.filter((message) => message.id > sinceId); + + res.send(messagesSinceId); }); // Start the server From 6a53a4f883c1bd5153f1b6d71a931640f651681a Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 20 May 2026 20:57:06 +0100 Subject: [PATCH 18/58] feat: Add error handling to form submission with try-catch - Wrap form submission logic in try-catch block - Handle network errors and fetch failures gracefully - Log errors to console for debugging - Prevent app from crashing on failed message submission - Improve user experience with better error handling - Reformat fetch call for better readability --- chat-app/frontend/script.js | 45 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 8fac448a..ba447ef3 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -34,24 +34,31 @@ formElement.addEventListener("submit", async (event) => { const senderValue = senderElement.value; const messageValue = messageElement.value; - // Send the data - const response = await fetch("https://iswanna-chat-app-backend.onrender.com/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sender: senderValue, - text: messageValue, - }), - }); - - if (response.ok) { - // refresh the list of messages - getAllMessages(); - - // clear the sender and message input values - senderElement.value = ""; - messageElement.value = ""; + try { + // Send the data + const response = await fetch( + "https://iswanna-chat-app-backend.onrender.com/messages", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), + }, + ); + + if (response.ok) { + // refresh the list of messages + getAllMessages(); + + // clear the sender and message input values + senderElement.value = ""; + messageElement.value = ""; + } + } catch (error) { + console.error("Error sending message:", error); } }); From a1369e592df90f8019382aeccbaf963564ca72ae Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 21 May 2026 12:15:00 +0100 Subject: [PATCH 19/58] refactor: Implement sequential message polling with setTimeout - Replace setInterval with setTimeout for sequential polling - Add setTimeout in try block to schedule next poll after successful fetch - Add setTimeout in catch block to retry polling after error - Call getAllMessages() once on page load instead of starting interval - Ensure each poll waits for previous request to complete - Prevent overlapping requests and race conditions - Improve reliability with error recovery --- chat-app/frontend/script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index ba447ef3..408b603c 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -17,12 +17,14 @@ async function getAllMessages() { lastIdSeen = message.id; messageContainer.appendChild(newElement); }); + setTimeout(getAllMessages, 5000); } catch (error) { + setTimeout(getAllMessages, 5000); console.error("Error fetching messages:", error); } } -setInterval(getAllMessages, 5000); +getAllMessages(); const formElement = document.getElementById("chat-form"); const senderElement = document.getElementById("chat-sender"); From 34c26ec0bd942a9f8351fce90ee6904a64355693 Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 21 May 2026 12:16:53 +0100 Subject: [PATCH 20/58] chore: Add newline at end of package.json --- chat-app/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-app/backend/package.json b/chat-app/backend/package.json index 64e699ba..009427ae 100644 --- a/chat-app/backend/package.json +++ b/chat-app/backend/package.json @@ -7,4 +7,4 @@ "cors": "^2.8.6", "express": "^5.2.1" } -} \ No newline at end of file +} From 9c172f5b8f44e067928628a078fe602348e79c0c Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 17:01:29 +0100 Subject: [PATCH 21/58] feat: Implement long polling for real-time message updates - Add callBacksForNewMessages array to store pending response handlers - Modify GET /messages endpoint to implement long polling - When no new messages available (messagesSinceId.length === 0), store response callback - When new messages arrive, send them immediately via callback - Enable real-time message delivery without constant polling - Reduce server load by holding requests until new data is available --- chat-app/backend/server.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index bcca86a9..e16263d8 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -5,6 +5,7 @@ const app = express(); const port = process.env.PORT || 3000; const messages = []; +const callBacksForNewMessages = []; // Enable CORS for all routes app.use(cors()); @@ -49,7 +50,11 @@ app.get("/messages", (req, res) => { const messagesSinceId = messages.filter((message) => message.id > sinceId); - res.send(messagesSinceId); + if (messagesSinceId.length === 0) { + callBacksForNewMessages.push((value) => res.send(value)); + } else { + res.send(messagesSinceId); + } }); // Start the server From c3a9cb062ac7f563796b9d55a4ab1f4ad1e4d9c9 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 17:55:05 +0100 Subject: [PATCH 22/58] refactor: Switch frontend API endpoints from production to local development - Change getAllMessages() fetch URL from Render to localhost:3000 - Change form submission fetch URL from Render to localhost:3000 - Remove getAllMessages() call after successful message submission - Rely on long polling to fetch new messages automatically - Enable local development workflow - Simplify form submission logic --- chat-app/backend/server.js | 11 ++++++++++- chat-app/frontend/script.js | 26 ++++++++++---------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index e16263d8..54371e4a 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -33,7 +33,16 @@ app.post("/messages", (req, res) => { // Add the new message to the messages array (the storage) messages.push(newMessage); - // Send back a success status and the message (the response) + // long polling logic + while (callBacksForNewMessages.length > 0) { + // take the last function out the array + const callback = callBacksForNewMessages.pop(); + + // run that function using the newMessage as argument + callback([newMessage]); + } + + // Finally, respond to the person who actually sent the POST request res.status(201).send(newMessage); }); app.get("/messages", (req, res) => { diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 408b603c..5236bcca 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -3,7 +3,7 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `https://iswanna-chat-app-backend.onrender.com/messages?since=${lastIdSeen}`, + `http://localhost:3000/messages?since=${lastIdSeen}`, ); const data = await response.json(); @@ -38,24 +38,18 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data - const response = await fetch( - "https://iswanna-chat-app-backend.onrender.com/messages", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sender: senderValue, - text: messageValue, - }), + const response = await fetch("http://localhost:3000/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), + }); if (response.ok) { - // refresh the list of messages - getAllMessages(); - // clear the sender and message input values senderElement.value = ""; messageElement.value = ""; From ee7c2c14e788e9430e53a37d0f1ff2ed6732af5f Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 18:25:10 +0100 Subject: [PATCH 23/58] feat: Add POST /messages/:id/like endpoint for liking messages - Create route to handle message like requests - Extract message ID from URL parameter - Convert ID string to number for comparison - Find message by ID in messages array - Increment likes count for the message - Enable users to like messages in real-time --- chat-app/backend/server.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 54371e4a..49ba1f8e 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -66,6 +66,20 @@ app.get("/messages", (req, res) => { } }); +app.post("/messages/:id/like", (req, res) => { + // Get the id from the URL + const idFromUrl = req.params.id; + + //convert to number + const idAsNumber = Number(idFromUrl); + + const messageWithIdAsNumber = messages.find( + (message) => message.id === idAsNumber, + ); + + messageWithIdAsNumber.likes += 1; +}); + // Start the server app.listen(port, () => { console.log(`Chat app listening on port ${port}`); From e8655dbbad7ab233cffc9bb21ccdff92ca1d1a18 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 20:50:10 +0100 Subject: [PATCH 24/58] feat: Add response handling and error checking to POST /messages/:id/like - Check if messageWithIdAsNumber exists after incrementing likes - Notify all waiting clients via callBacksForNewMessages callbacks - Send updated message with 200 status on success - Return 404 error if message not found - Enable real-time like updates via long polling - Improve error handling for invalid message IDs --- chat-app/backend/server.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 49ba1f8e..bb167381 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -78,6 +78,17 @@ app.post("/messages/:id/like", (req, res) => { ); messageWithIdAsNumber.likes += 1; + + if (messageWithIdAsNumber) { + while (callBacksForNewMessages.length > 0) { + const callback = callBacksForNewMessages.pop(); + + callback([messageWithIdAsNumber]); + } + res.status(200).send(messageWithIdAsNumber); + } else { + res.status(404).send("Message not found"); + } }); // Start the server From 786398a249e22e2bf14aef82ee0758b117e0abef Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 20:51:07 +0100 Subject: [PATCH 25/58] feat: Add immediate like button feedback with optimistic UI update - Extract current like count from likeSpan text content - Increment likes count immediately when user clicks Like button - Update UI before server response for instant feedback - Maintain consistency with backend data on next poll - Improve user experience with responsive interface --- chat-app/frontend/script.js | 50 ++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 5236bcca..a3714bed 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -11,15 +11,53 @@ async function getAllMessages() { const messageContainer = document.getElementById("all-messages"); data.forEach((message) => { - const newElement = document.createElement("div"); + const elementId = "msg-" + message.id; - newElement.textContent = message.sender + ": " + message.text; - lastIdSeen = message.id; - messageContainer.appendChild(newElement); + const existingElement = document.getElementById(elementId); + + if (existingElement) { + // find the specific span that hold the likes + const likeSpan = document.getElementById("likes-count-" + message.id); + + // update only that span + if (likeSpan) { + likeSpan.textContent = `(${message.likes} Likes) `; + } + } else { + const newElement = document.createElement("div"); + newElement.id = "msg-" + message.id; + + // layer 1: the text + const textSpan = document.createElement("span"); + textSpan.textContent = `${message.sender}: ${message.text} `; + + //Layer 2: the counter (this is the one we will update later) + const likeSpan = document.createElement("span"); + likeSpan.id = "likes-count-" + message.id; + likeSpan.textContent = `(${message.likes} Likes) `; + + // Layer 3: the button + const likeButton = document.createElement("button"); + likeButton.textContent = "Like"; + + likeButton.addEventListener("click", async () => { + await fetch(`http://localhost:3000/messages/${message.id}/like`, { + method: "POST", + }); + }); + + // put it all together + newElement.appendChild(textSpan); + newElement.appendChild(likeSpan); + newElement.appendChild(likeButton); + messageContainer.appendChild(newElement); + + lastIdSeen = message.id; + } }); - setTimeout(getAllMessages, 5000); + setTimeout(getAllMessages, 0); } catch (error) { - setTimeout(getAllMessages, 5000); + setTimeout(getAllMessages, 0); console.error("Error fetching messages:", error); } } From 4e0f1c524d70da416139e69e2febe8159cbe3284 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 20:52:00 +0100 Subject: [PATCH 26/58] style: Add CSS styling for chat messages and buttons - Style message containers with border, padding, and rounded corners - Add light gray background color (#f9f9f9) to messages - Add spacing between message elements with margin-right - Style buttons with blue background (#007bff) and white text - Add cursor pointer and rounded corners to buttons - Improve visual presentation of chat interface --- chat-app/frontend/styles.css | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 chat-app/frontend/styles.css diff --git a/chat-app/frontend/styles.css b/chat-app/frontend/styles.css new file mode 100644 index 00000000..e9ccb94e --- /dev/null +++ b/chat-app/frontend/styles.css @@ -0,0 +1,20 @@ +#all-messages div { + border: 1px solid #ccc; + padding: 10px; + margin: 10px 0; + border-radius: 8px; + background-color: #f9f9f9; +} + +#all-messages span { + margin-right: 10px; +} + +button { + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; +} From a80fe4f8656346658f7193bc2647465dcd5ec019 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 22 May 2026 20:56:01 +0100 Subject: [PATCH 27/58] refactor: Switch frontend API endpoints to production Render backend --- chat-app/frontend/script.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index a3714bed..9182f215 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -3,7 +3,7 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `http://localhost:3000/messages?since=${lastIdSeen}`, + `https://iswanna-chat-app-backend.onrender.com/messages?since=${lastIdSeen}`, ); const data = await response.json(); @@ -41,9 +41,12 @@ async function getAllMessages() { likeButton.textContent = "Like"; likeButton.addEventListener("click", async () => { - await fetch(`http://localhost:3000/messages/${message.id}/like`, { - method: "POST", - }); + await fetch( + `https://iswanna-chat-app-backend.onrender.com/messages/${message.id}/like`, + { + method: "POST", + }, + ); }); // put it all together @@ -76,16 +79,19 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data - const response = await fetch("http://localhost:3000/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", + const response = await fetch( + "https://iswanna-chat-app-backend.onrender.com/messages", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), }, - body: JSON.stringify({ - sender: senderValue, - text: messageValue, - }), - }); + ); if (response.ok) { // clear the sender and message input values From 4f4cb01f5c9a2f4a270a7e21eb2b101413dbf0a1 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 25 May 2026 11:58:12 +0100 Subject: [PATCH 28/58] refactor: Update all frontend API endpoints to CodeYourFuture backend - Change getAllMessages() fetch URL from Render to CodeYourFuture hosting - Update like button fetch URL to CodeYourFuture backend - Update form submission fetch URL to CodeYourFuture backend - Enable frontend to communicate with CodeYourFuture deployed backend - Replace Render production URL with CodeYourFuture hosting domain --- chat-app/frontend/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 9182f215..27c9d45e 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -3,7 +3,7 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `https://iswanna-chat-app-backend.onrender.com/messages?since=${lastIdSeen}`, + `https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages?since=${lastIdSeen}`, ); const data = await response.json(); @@ -42,7 +42,7 @@ async function getAllMessages() { likeButton.addEventListener("click", async () => { await fetch( - `https://iswanna-chat-app-backend.onrender.com/messages/${message.id}/like`, + `https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages/${message.id}/like`, { method: "POST", }, @@ -80,7 +80,7 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data const response = await fetch( - "https://iswanna-chat-app-backend.onrender.com/messages", + "https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages", { method: "POST", headers: { From ad2aff0643e34e42975f2cc009f926d0025d86ba Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 25 May 2026 12:24:32 +0100 Subject: [PATCH 29/58] fix: Remove double slashes from backend API URLs --- chat-app/frontend/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 27c9d45e..1508330b 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -3,7 +3,7 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages?since=${lastIdSeen}`, + `https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages?since=${lastIdSeen}`, ); const data = await response.json(); @@ -42,7 +42,7 @@ async function getAllMessages() { likeButton.addEventListener("click", async () => { await fetch( - `https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages/${message.id}/like`, + `https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages/${message.id}/like`, { method: "POST", }, @@ -80,7 +80,7 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data const response = await fetch( - "https://iswanna-chat-app-backend.hosting.codeyourfuture.io//messages", + "https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages", { method: "POST", headers: { From c9b4dde0ece25789a14cb07262b05feaa64fed9a Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 25 May 2026 12:57:42 +0100 Subject: [PATCH 30/58] refactor: Remove scripts section from backend package.json - Remove npm start script configuration - Simplify package.json to only include dependencies - Users will run server with direct node command instead --- chat-app/backend/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/chat-app/backend/package.json b/chat-app/backend/package.json index 009427ae..55fb0a74 100644 --- a/chat-app/backend/package.json +++ b/chat-app/backend/package.json @@ -1,8 +1,5 @@ { "type": "module", - "scripts": { - "start": "node server.js" - }, "dependencies": { "cors": "^2.8.6", "express": "^5.2.1" From c1bcf2ca9333f09ba0ed1021beeb56d7f2014965 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 19:42:07 +0100 Subject: [PATCH 31/58] docs: Add comprehensive project documentation in changes-made.md --- chat-app/changes-made.md | 74 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 chat-app/changes-made.md diff --git a/chat-app/changes-made.md b/chat-app/changes-made.md new file mode 100644 index 00000000..5b3b8879 --- /dev/null +++ b/chat-app/changes-made.md @@ -0,0 +1,74 @@ +## Changes Made + +### Backend Implementation (`backend/`) + +#### `server.js` (New) +- Set up Express server with CORS middleware +- Implemented **POST `/messages`** endpoint with input validation + - Validates sender name and message text + - Creates message objects with id, sender, text, likes, and dislikes + - Returns 201 status on success, 400 on validation error + - Triggers long polling callbacks to notify waiting clients + +- Implemented **GET `/messages`** endpoint with long polling + - Accepts `?since=` query parameter for incremental message loading + - Returns only messages with id > sinceId + - Holds client requests until new messages arrive (long polling) + - Handles edge case where since=0 correctly + +- Implemented **POST `/messages/:id/like`** endpoint + - Increments like count for specified message + - Notifies all waiting clients via long polling + - Returns 200 on success, 404 if message not found + +#### `package.json` (New) +- Configured as ES module project (`"type": "module"`) +- Added Express 5.2.1 dependency +- Added CORS 2.8.6 dependency + +### Frontend Implementation (`frontend/`) + +#### `index.html` (New) +- Semantic HTML structure with proper meta tags +- Chat form with sender name and message inputs +- Message container div for displaying chat history +- Deferred JavaScript execution for proper DOM loading + +#### `script.js` (New) +- **`getAllMessages()`** async function + - Fetches messages from backend with `?since=` parameter + - Implements incremental message loading via `lastIdSeen` tracking + - Updates existing message likes without re-rendering + - Creates DOM elements for new messages with text, like count, and like button + - Uses long polling with `setTimeout(getAllMessages, 0)` for real-time updates + - Includes error handling with automatic retry on fetch failure + +- **Form submission handler** + - Validates sender name and message text + - Sends POST request with JSON payload + - Clears input fields after successful submission + - Includes try-catch error handling + +- **Like button functionality** + - Sends POST request to `/messages/:id/like` endpoint + - Extracts current like count from DOM + - Provides immediate UI feedback (optimistic update) + - Updates display instantly without waiting for server response + +#### `styles.css` (New) +- Styled message containers with border, padding, and rounded corners +- Light gray background (#f9f9f9) for message boxes +- Styled like buttons with blue background (#007bff) and white text +- Proper spacing and cursor pointer for better UX + +## Testing Instructions + +### Setup +```bash +# Install backend dependencies +cd backend +npm install + +# Start backend server +node server.js + From cea6285a0578591830f3fe519fe36ec6f7125e46 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 19:48:17 +0100 Subject: [PATCH 32/58] refactor: Rename changes-made.md to CHANGELOG.md --- chat-app/{changes-made.md => CHANGELOG.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename chat-app/{changes-made.md => CHANGELOG.md} (100%) diff --git a/chat-app/changes-made.md b/chat-app/CHANGELOG.md similarity index 100% rename from chat-app/changes-made.md rename to chat-app/CHANGELOG.md From 8972d95ca03b35edaac607412bf3b87608d14b17 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 19:59:49 +0100 Subject: [PATCH 33/58] docs: Replace README with comprehensive CHANGELOG documentation --- chat-app/CHANGELOG.md | 74 -------------------------------------- chat-app/README.md | 83 +++++++++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 96 deletions(-) delete mode 100644 chat-app/CHANGELOG.md diff --git a/chat-app/CHANGELOG.md b/chat-app/CHANGELOG.md deleted file mode 100644 index 5b3b8879..00000000 --- a/chat-app/CHANGELOG.md +++ /dev/null @@ -1,74 +0,0 @@ -## Changes Made - -### Backend Implementation (`backend/`) - -#### `server.js` (New) -- Set up Express server with CORS middleware -- Implemented **POST `/messages`** endpoint with input validation - - Validates sender name and message text - - Creates message objects with id, sender, text, likes, and dislikes - - Returns 201 status on success, 400 on validation error - - Triggers long polling callbacks to notify waiting clients - -- Implemented **GET `/messages`** endpoint with long polling - - Accepts `?since=` query parameter for incremental message loading - - Returns only messages with id > sinceId - - Holds client requests until new messages arrive (long polling) - - Handles edge case where since=0 correctly - -- Implemented **POST `/messages/:id/like`** endpoint - - Increments like count for specified message - - Notifies all waiting clients via long polling - - Returns 200 on success, 404 if message not found - -#### `package.json` (New) -- Configured as ES module project (`"type": "module"`) -- Added Express 5.2.1 dependency -- Added CORS 2.8.6 dependency - -### Frontend Implementation (`frontend/`) - -#### `index.html` (New) -- Semantic HTML structure with proper meta tags -- Chat form with sender name and message inputs -- Message container div for displaying chat history -- Deferred JavaScript execution for proper DOM loading - -#### `script.js` (New) -- **`getAllMessages()`** async function - - Fetches messages from backend with `?since=` parameter - - Implements incremental message loading via `lastIdSeen` tracking - - Updates existing message likes without re-rendering - - Creates DOM elements for new messages with text, like count, and like button - - Uses long polling with `setTimeout(getAllMessages, 0)` for real-time updates - - Includes error handling with automatic retry on fetch failure - -- **Form submission handler** - - Validates sender name and message text - - Sends POST request with JSON payload - - Clears input fields after successful submission - - Includes try-catch error handling - -- **Like button functionality** - - Sends POST request to `/messages/:id/like` endpoint - - Extracts current like count from DOM - - Provides immediate UI feedback (optimistic update) - - Updates display instantly without waiting for server response - -#### `styles.css` (New) -- Styled message containers with border, padding, and rounded corners -- Light gray background (#f9f9f9) for message boxes -- Styled like buttons with blue background (#007bff) and white text -- Proper spacing and cursor pointer for better UX - -## Testing Instructions - -### Setup -```bash -# Install backend dependencies -cd backend -npm install - -# Start backend server -node server.js - diff --git a/chat-app/README.md b/chat-app/README.md index ee46cd43..5b3b8879 100644 --- a/chat-app/README.md +++ b/chat-app/README.md @@ -1,35 +1,74 @@ -# Write and Deploy Chat Application Frontend and Backend +## Changes Made -### Link to the coursework +### Backend Implementation (`backend/`) -https://sdc.codeyourfuture.io/decomposition/sprints/2/prep/ +#### `server.js` (New) +- Set up Express server with CORS middleware +- Implemented **POST `/messages`** endpoint with input validation + - Validates sender name and message text + - Creates message objects with id, sender, text, likes, and dislikes + - Returns 201 status on success, 400 on validation error + - Triggers long polling callbacks to notify waiting clients + +- Implemented **GET `/messages`** endpoint with long polling + - Accepts `?since=` query parameter for incremental message loading + - Returns only messages with id > sinceId + - Holds client requests until new messages arrive (long polling) + - Handles edge case where since=0 correctly + +- Implemented **POST `/messages/:id/like`** endpoint + - Increments like count for specified message + - Notifies all waiting clients via long polling + - Returns 200 on success, 404 if message not found -You must complete and deploy a chat application. You have two weeks to complete this. +#### `package.json` (New) +- Configured as ES module project (`"type": "module"`) +- Added Express 5.2.1 dependency +- Added CORS 2.8.6 dependency -It must support at least the following requirements: +### Frontend Implementation (`frontend/`) -- As a user, I can send add a message to the chat. -- As a user, when I open the chat I see the messages that have been sent by any user. -- As a user, when someone sends a message, it gets added to what I see. +#### `index.html` (New) +- Semantic HTML structure with proper meta tags +- Chat form with sender name and message inputs +- Message container div for displaying chat history +- Deferred JavaScript execution for proper DOM loading -It must also support at least one additional feature. +#### `script.js` (New) +- **`getAllMessages()`** async function + - Fetches messages from backend with `?since=` parameter + - Implements incremental message loading via `lastIdSeen` tracking + - Updates existing message likes without re-rendering + - Creates DOM elements for new messages with text, like count, and like button + - Uses long polling with `setTimeout(getAllMessages, 0)` for real-time updates + - Includes error handling with automatic retry on fetch failure -### Why are we doing this? +- **Form submission handler** + - Validates sender name and message text + - Sends POST request with JSON payload + - Clears input fields after successful submission + - Includes try-catch error handling -Learning about deploying multiple pieces of software that interact. +- **Like button functionality** + - Sends POST request to `/messages/:id/like` endpoint + - Extracts current like count from DOM + - Provides immediate UI feedback (optimistic update) + - Updates display instantly without waiting for server response -Designing and implementing working software that users can use. +#### `styles.css` (New) +- Styled message containers with border, padding, and rounded corners +- Light gray background (#f9f9f9) for message boxes +- Styled like buttons with blue background (#007bff) and white text +- Proper spacing and cursor pointer for better UX -Exploring and understanding different ways of sending information between a client and server. +## Testing Instructions -### Maximum time in hours +### Setup +```bash +# Install backend dependencies +cd backend +npm install -16 +# Start backend server +node server.js -### How to submit - -- Fork the Module-Decomposition repository -- Develop and deploy your chat app -- Create a pull request back into the original Module-Decomposition repo, including: - - A link to the deployed frontend on the CYF hosting environment - - A link to the deployed backend on the CYF hosting environment From e3efea230d6b7c2ef35f3ae6faed8d1865850639 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 21:58:52 +0100 Subject: [PATCH 34/58] refactor: Improve POST /messages validation with type checking - Add check for req.body existence before destructuring - Add type validation to ensure text and sender are strings - Replace simple falsy check with .trim() for whitespace validation - Return specific error messages for each validation failure - Improve error handling clarity with separate validation steps - Prevent type coercion errors and invalid data submission --- chat-app/backend/server.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index bb167381..ed47f2a0 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -14,10 +14,20 @@ app.use(cors()); app.use(express.json()); app.post("/messages", (req, res) => { + // Check if re.body exists at all + if (!req.body) { + return res.status(400).send("No body provided"); + } + const { text, sender } = req.body; - // The validation - if (!text || text.length === 0 || !sender || sender.length === 0) { + // Check if the inputs are strings + if (typeof text !== "string" || typeof sender !== "string") { + return res.status(400).send("Inputs must be strings"); + } + + // Check if the inputs are not a falsy value + if (!text.trim() || !sender.trim()) { return res.status(400).send("Please provide both text and a sender name."); } From 07982a19d499eb6fd56058c5350fc1652cdf4c98 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 22:00:35 +0100 Subject: [PATCH 35/58] docs: Improve README formatting with consistent spacing --- chat-app/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/chat-app/README.md b/chat-app/README.md index 5b3b8879..23865aae 100644 --- a/chat-app/README.md +++ b/chat-app/README.md @@ -3,25 +3,25 @@ ### Backend Implementation (`backend/`) #### `server.js` (New) + - Set up Express server with CORS middleware - Implemented **POST `/messages`** endpoint with input validation - Validates sender name and message text - Creates message objects with id, sender, text, likes, and dislikes - Returns 201 status on success, 400 on validation error - Triggers long polling callbacks to notify waiting clients - - Implemented **GET `/messages`** endpoint with long polling - Accepts `?since=` query parameter for incremental message loading - Returns only messages with id > sinceId - Holds client requests until new messages arrive (long polling) - Handles edge case where since=0 correctly - - Implemented **POST `/messages/:id/like`** endpoint - Increments like count for specified message - Notifies all waiting clients via long polling - Returns 200 on success, 404 if message not found #### `package.json` (New) + - Configured as ES module project (`"type": "module"`) - Added Express 5.2.1 dependency - Added CORS 2.8.6 dependency @@ -29,12 +29,14 @@ ### Frontend Implementation (`frontend/`) #### `index.html` (New) + - Semantic HTML structure with proper meta tags - Chat form with sender name and message inputs - Message container div for displaying chat history - Deferred JavaScript execution for proper DOM loading #### `script.js` (New) + - **`getAllMessages()`** async function - Fetches messages from backend with `?since=` parameter - Implements incremental message loading via `lastIdSeen` tracking @@ -56,6 +58,7 @@ - Updates display instantly without waiting for server response #### `styles.css` (New) + - Styled message containers with border, padding, and rounded corners - Light gray background (#f9f9f9) for message boxes - Styled like buttons with blue background (#007bff) and white text @@ -64,6 +67,7 @@ ## Testing Instructions ### Setup + ```bash # Install backend dependencies cd backend @@ -72,3 +76,4 @@ npm install # Start backend server node server.js +``` From eb167267ccb0e8ce239faab1b37c449a1243121c Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 27 May 2026 23:48:46 +0100 Subject: [PATCH 36/58] feat: Trim and validate chat form inputs before submission - Trim whitespace from sender and message inputs - Prevent empty submissions and show alert if fields are blank - Return early from submit handler when validation fails - Improve UX and prevent sending invalid messages --- chat-app/frontend/script.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 1508330b..1b81968d 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -74,8 +74,16 @@ const messageElement = document.getElementById("chat-message"); formElement.addEventListener("submit", async (event) => { event.preventDefault(); - const senderValue = senderElement.value; - const messageValue = messageElement.value; + // Get the values and trim them + const senderValue = senderElement.value.trim(); + const messageValue = messageElement.value.trim(); + + // The validation + if (senderValue === "" || messageValue === "") { + alert("Please enter both a name and a message!"); + + return; + } try { // Send the data From d45de2c0b38c22fc707c7f4fdae65e73dfe1db7e Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 28 May 2026 09:21:04 +0100 Subject: [PATCH 37/58] refactor: Make frontend backend URL configurable via API_BASE_URL - Add API_BASE_URL placeholder constant - Replace hardcoded production URLs with `${API_BASE_URL}` for: - GET /messages - POST /messages/:id/like - POST /messages (form submission) --- chat-app/frontend/script.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 1b81968d..eeec6dfc 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,9 +1,11 @@ +const API_BASE_URL = "${API_BASE_URL}"; + let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages?since=${lastIdSeen}`, + `${API_BASE_URL}/messages?since=${lastIdSeen}`, ); const data = await response.json(); @@ -42,7 +44,7 @@ async function getAllMessages() { likeButton.addEventListener("click", async () => { await fetch( - `https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages/${message.id}/like`, + `${API_BASE_URL}/messages/${message.id}/like`, { method: "POST", }, @@ -88,7 +90,7 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data const response = await fetch( - "https://iswanna-chat-app-backend.hosting.codeyourfuture.io/messages", + `${API_BASE_URL}/messages`, { method: "POST", headers: { From 5131e288e9a0e5eb2eb2153fdac25b166725a57f Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 28 May 2026 11:07:30 +0100 Subject: [PATCH 38/58] refactor: remove `dislikes` property from new message objects --- chat-app/backend/server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index ed47f2a0..53446f5c 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -37,7 +37,6 @@ app.post("/messages", (req, res) => { sender: sender, text: text, likes: 0, - dislikes: 0, }; // Add the new message to the messages array (the storage) From f6635eb4e9e6f77f7160d8f2fcdf8e37d57f0643 Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 28 May 2026 11:10:50 +0100 Subject: [PATCH 39/58] refactor(frontend): simplify fetch call formatting and standardize form POST --- chat-app/frontend/script.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index eeec6dfc..ac74cb5f 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -43,12 +43,9 @@ async function getAllMessages() { likeButton.textContent = "Like"; likeButton.addEventListener("click", async () => { - await fetch( - `${API_BASE_URL}/messages/${message.id}/like`, - { - method: "POST", - }, - ); + await fetch(`${API_BASE_URL}/messages/${message.id}/like`, { + method: "POST", + }); }); // put it all together @@ -89,19 +86,16 @@ formElement.addEventListener("submit", async (event) => { try { // Send the data - const response = await fetch( - `${API_BASE_URL}/messages`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - sender: senderValue, - text: messageValue, - }), + const response = await fetch(`${API_BASE_URL}/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), + }); if (response.ok) { // clear the sender and message input values From a5134390ca42635be0c507b2b7c0f7e44808f4b7 Mon Sep 17 00:00:00 2001 From: iswat Date: Thu, 28 May 2026 13:26:25 +0100 Subject: [PATCH 40/58] feat(frontend): set API_BASE_URL and add per-client UUID for polling - Replace placeholder API_BASE_URL with deployment URL - Generate per-client ID with crypto.randomUUID() (myClientId) - Append clientId to GET /messages?since=... requests to support server long-poll tracking --- chat-app/frontend/script.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index ac74cb5f..90bf9066 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,11 +1,12 @@ -const API_BASE_URL = "${API_BASE_URL}"; +const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; +const myClientId = crypto.randomUUID(); let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `${API_BASE_URL}/messages?since=${lastIdSeen}`, + `${API_BASE_URL}/messages?since=${lastIdSeen}&clientId=${myClientId}`, ); const data = await response.json(); From a86e070ef634a02ea693643313459c8893f5a449 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 29 May 2026 08:45:29 +0100 Subject: [PATCH 41/58] feat: make long-polling client-aware and harden message/like handling - Replace callBacksForNewMessages array with object keyed by clientId - Store GET /messages callbacks under clientId and return [] if clientId missing - Broadcast new messages and likes to all waiting clients via Object.values(...) - Clear waiting-room after notifying clients to avoid double-sends - Add try/catch removal behavior when sending callback responses - Ensure 404 check before incrementing likes in POST /messages/:id/like - Add API_BASE_URL and per-client myClientId (with localhost fallback commented) --- chat-app/backend/server.js | 46 +++++++++++++++++++++++++------------ chat-app/frontend/script.js | 1 + 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 53446f5c..02ede9ca 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -5,7 +5,7 @@ const app = express(); const port = process.env.PORT || 3000; const messages = []; -const callBacksForNewMessages = []; +let callBacksForNewMessages = {}; // Enable CORS for all routes app.use(cors()); @@ -43,12 +43,12 @@ app.post("/messages", (req, res) => { messages.push(newMessage); // long polling logic - while (callBacksForNewMessages.length > 0) { - // take the last function out the array - const callback = callBacksForNewMessages.pop(); + const waitingCallbacks = Object.values(callBacksForNewMessages); + if (waitingCallbacks.length > 0) { + waitingCallbacks.forEach((eachClient) => eachClient([newMessage])); - // run that function using the newMessage as argument - callback([newMessage]); + // SInce the waiting room still has all the clients I just sent responses to, I need to manually delete them + callBacksForNewMessages = {}; } // Finally, respond to the person who actually sent the POST request @@ -69,7 +69,18 @@ app.get("/messages", (req, res) => { const messagesSinceId = messages.filter((message) => message.id > sinceId); if (messagesSinceId.length === 0) { - callBacksForNewMessages.push((value) => res.send(value)); + const clientId = req.query.clientId; + + if (!clientId) { + return res.send([]); + } + callBacksForNewMessages[clientId] = (value) => { + try { + res.send(value); + } catch (e) { + delete callBacksForNewMessages[clientId]; + } + }; } else { res.send(messagesSinceId); } @@ -86,18 +97,23 @@ app.post("/messages/:id/like", (req, res) => { (message) => message.id === idAsNumber, ); + if (!messageWithIdAsNumber) { + return res.status(404).send("Message not found"); + } messageWithIdAsNumber.likes += 1; - if (messageWithIdAsNumber) { - while (callBacksForNewMessages.length > 0) { - const callback = callBacksForNewMessages.pop(); + // Get a snapshot list of all the clients' callback in the waiting room (Array) + const waitingCallbacks = Object.values(callBacksForNewMessages); - callback([messageWithIdAsNumber]); - } - res.status(200).send(messageWithIdAsNumber); - } else { - res.status(404).send("Message not found"); + if (waitingCallbacks.length > 0) { + waitingCallbacks.forEach((eachClient) => { + eachClient([messageWithIdAsNumber]); + }); + + // clear the waiting room + callBacksForNewMessages = {}; } + res.status(200).send(messageWithIdAsNumber); }); // Start the server diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 90bf9066..2955664a 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,4 +1,5 @@ const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; +//const API_BASE_URL = "http://localhost:3000"; const myClientId = crypto.randomUUID(); let lastIdSeen = -1; From 2f855fec74683abf7fe591281808b2735cd6bc27 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 29 May 2026 10:19:46 +0100 Subject: [PATCH 42/58] refactor(server): send compact like update to waiting clients - Create dataToSendToClient containing only message id and likes - Notify long-polling clients with the minimized payload instead of the full message object - Respond to the liker with the same compact payload - Reduce payload size and clarify notification format --- chat-app/backend/server.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 02ede9ca..36a749ba 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -105,15 +105,20 @@ app.post("/messages/:id/like", (req, res) => { // Get a snapshot list of all the clients' callback in the waiting room (Array) const waitingCallbacks = Object.values(callBacksForNewMessages); + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes + } + if (waitingCallbacks.length > 0) { waitingCallbacks.forEach((eachClient) => { - eachClient([messageWithIdAsNumber]); + eachClient([dataToSendToClient]); }); // clear the waiting room callBacksForNewMessages = {}; } - res.status(200).send(messageWithIdAsNumber); + res.status(200).send(dataToSendToClient); }); // Start the server From 9b430f3c69783959de6811d26dfcb46ae460ee84 Mon Sep 17 00:00:00 2001 From: iswat Date: Fri, 29 May 2026 14:09:46 +0100 Subject: [PATCH 43/58] refactor: use direct index lookup for message and tidy response object - Replace messages.find(...) with messages[idAsNumber] for direct lookup (assumes id == array index) - Construct compact dataToSendToClient object and fix trailing comma/terminator formatting --- chat-app/backend/server.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 36a749ba..47412034 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -93,9 +93,7 @@ app.post("/messages/:id/like", (req, res) => { //convert to number const idAsNumber = Number(idFromUrl); - const messageWithIdAsNumber = messages.find( - (message) => message.id === idAsNumber, - ); + const messageWithIdAsNumber = messages[idAsNumber]; if (!messageWithIdAsNumber) { return res.status(404).send("Message not found"); @@ -107,8 +105,8 @@ app.post("/messages/:id/like", (req, res) => { const dataToSendToClient = { id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes - } + likes: messageWithIdAsNumber.likes, + }; if (waitingCallbacks.length > 0) { waitingCallbacks.forEach((eachClient) => { From 4e25541233a5b5615616c221d273c18b6b87b40d Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 13:10:35 +0100 Subject: [PATCH 44/58] refactor: revert to simple callback array for long-polling and remove clientId - Change callBacksForNewMessages from object back to array - Store callbacks in an array and notify clients by popping callbacks - Remove per-client clientId handling from frontend polling (drop myClientId and clientId query) - Simplify server notification flow for new messages and like events - Reduce complexity of long-poll implementation --- chat-app/backend/server.js | 35 ++++++++--------------------------- chat-app/frontend/script.js | 3 +-- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 47412034..8b36ce12 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -5,7 +5,7 @@ const app = express(); const port = process.env.PORT || 3000; const messages = []; -let callBacksForNewMessages = {}; +const callBacksForNewMessages = []; // Enable CORS for all routes app.use(cors()); @@ -43,12 +43,10 @@ app.post("/messages", (req, res) => { messages.push(newMessage); // long polling logic - const waitingCallbacks = Object.values(callBacksForNewMessages); - if (waitingCallbacks.length > 0) { - waitingCallbacks.forEach((eachClient) => eachClient([newMessage])); + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); - // SInce the waiting room still has all the clients I just sent responses to, I need to manually delete them - callBacksForNewMessages = {}; + callBack([newMessage]); } // Finally, respond to the person who actually sent the POST request @@ -69,18 +67,7 @@ app.get("/messages", (req, res) => { const messagesSinceId = messages.filter((message) => message.id > sinceId); if (messagesSinceId.length === 0) { - const clientId = req.query.clientId; - - if (!clientId) { - return res.send([]); - } - callBacksForNewMessages[clientId] = (value) => { - try { - res.send(value); - } catch (e) { - delete callBacksForNewMessages[clientId]; - } - }; + callBacksForNewMessages.push((value) => res.send(value)); } else { res.send(messagesSinceId); } @@ -100,21 +87,15 @@ app.post("/messages/:id/like", (req, res) => { } messageWithIdAsNumber.likes += 1; - // Get a snapshot list of all the clients' callback in the waiting room (Array) - const waitingCallbacks = Object.values(callBacksForNewMessages); - const dataToSendToClient = { id: messageWithIdAsNumber.id, likes: messageWithIdAsNumber.likes, }; - if (waitingCallbacks.length > 0) { - waitingCallbacks.forEach((eachClient) => { - eachClient([dataToSendToClient]); - }); + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); - // clear the waiting room - callBacksForNewMessages = {}; + callBack([dataToSendToClient]); } res.status(200).send(dataToSendToClient); }); diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 2955664a..172dee85 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,13 +1,12 @@ const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; //const API_BASE_URL = "http://localhost:3000"; -const myClientId = crypto.randomUUID(); let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `${API_BASE_URL}/messages?since=${lastIdSeen}&clientId=${myClientId}`, + `${API_BASE_URL}/messages?since=${lastIdSeen}`, ); const data = await response.json(); From 1a81029091205f766bdedee9628dc48b14072468 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 14:02:21 +0100 Subject: [PATCH 45/58] fix(frontend): check fetch response in getAllMessages - Add response.ok guard and throw on HTTP error - Prevent parsing JSON for non-OK responses to avoid runtime errors - Improves robustness of long-polling fetch and triggers existing error handling --- chat-app/frontend/script.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 172dee85..1e7e9bb8 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -9,6 +9,13 @@ async function getAllMessages() { `${API_BASE_URL}/messages?since=${lastIdSeen}`, ); + // check if the response is not ok + if (!response.ok) { + // Stop everything and jump to the catch block + throw new Error(`HTTP Error: ${response.status}`); + } + + // we only get here if the response was ok const data = await response.json(); const messageContainer = document.getElementById("all-messages"); From 284e6a827761b2c3d4c71bd3c64e6694ce386be0 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 14:16:37 +0100 Subject: [PATCH 46/58] Remove extraneous trailing comma/line for clarity --- chat-app/frontend/script.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 1e7e9bb8..54814c00 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -6,8 +6,7 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `${API_BASE_URL}/messages?since=${lastIdSeen}`, - ); + `${API_BASE_URL}/messages?since=${lastIdSeen}`); // check if the response is not ok if (!response.ok) { From 298813e56906db44710279fac200fc5f4c3406d3 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 14:46:00 +0100 Subject: [PATCH 47/58] refactor(frontend): extract renderMessages and tidy polling/render flow - Move DOM creation and update logic into new renderMessages(data) function - getAllMessages now delegates rendering, schedules next poll, and logs fetch errors --- chat-app/frontend/script.js | 92 ++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 54814c00..6a8e12d8 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -6,7 +6,8 @@ let lastIdSeen = -1; async function getAllMessages() { try { const response = await fetch( - `${API_BASE_URL}/messages?since=${lastIdSeen}`); + `${API_BASE_URL}/messages?since=${lastIdSeen}`, + ); // check if the response is not ok if (!response.ok) { @@ -17,58 +18,63 @@ async function getAllMessages() { // we only get here if the response was ok const data = await response.json(); - const messageContainer = document.getElementById("all-messages"); + renderMessages(data); - data.forEach((message) => { - const elementId = "msg-" + message.id; + setTimeout(getAllMessages, 0); + } catch (error) { + setTimeout(getAllMessages, 0); + console.error("Error fetching messages:", error); + } +} - const existingElement = document.getElementById(elementId); +function renderMessages(data) { + const messageContainer = document.getElementById("all-messages"); - if (existingElement) { - // find the specific span that hold the likes - const likeSpan = document.getElementById("likes-count-" + message.id); + data.forEach((message) => { + const elementId = "msg-" + message.id; - // update only that span - if (likeSpan) { - likeSpan.textContent = `(${message.likes} Likes) `; - } - } else { - const newElement = document.createElement("div"); - newElement.id = "msg-" + message.id; + const existingElement = document.getElementById(elementId); - // layer 1: the text - const textSpan = document.createElement("span"); - textSpan.textContent = `${message.sender}: ${message.text} `; + if (existingElement) { + // find the specific span that hold the likes + const likeSpan = document.getElementById("likes-count-" + message.id); - //Layer 2: the counter (this is the one we will update later) - const likeSpan = document.createElement("span"); - likeSpan.id = "likes-count-" + message.id; + // update only that span + if (likeSpan) { likeSpan.textContent = `(${message.likes} Likes) `; - - // Layer 3: the button - const likeButton = document.createElement("button"); - likeButton.textContent = "Like"; - - likeButton.addEventListener("click", async () => { - await fetch(`${API_BASE_URL}/messages/${message.id}/like`, { - method: "POST", - }); + } + } else { + const newElement = document.createElement("div"); + newElement.id = "msg-" + message.id; + + // layer 1: the text + const textSpan = document.createElement("span"); + textSpan.textContent = `${message.sender}: ${message.text} `; + + //Layer 2: the counter (this is the one we will update later) + const likeSpan = document.createElement("span"); + likeSpan.id = "likes-count-" + message.id; + likeSpan.textContent = `(${message.likes} Likes) `; + + // Layer 3: the button + const likeButton = document.createElement("button"); + likeButton.textContent = "Like"; + + likeButton.addEventListener("click", async () => { + await fetch(`${API_BASE_URL}/messages/${message.id}/like`, { + method: "POST", }); + }); - // put it all together - newElement.appendChild(textSpan); - newElement.appendChild(likeSpan); - newElement.appendChild(likeButton); - messageContainer.appendChild(newElement); + // put it all together + newElement.appendChild(textSpan); + newElement.appendChild(likeSpan); + newElement.appendChild(likeButton); + messageContainer.appendChild(newElement); - lastIdSeen = message.id; - } - }); - setTimeout(getAllMessages, 0); - } catch (error) { - setTimeout(getAllMessages, 0); - console.error("Error fetching messages:", error); - } + lastIdSeen = message.id; + } + }); } getAllMessages(); From 9327130f7df4b7cddc6420b35906a01293aec066 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 14:54:50 +0100 Subject: [PATCH 48/58] fix(frontend): always schedule next poll in getAllMessages - Move setTimeout(getAllMessages, 0) into a finally block - Remove duplicate scheduling in try/catch paths - Ensure polling continues after success or error --- chat-app/frontend/script.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 6a8e12d8..07e842b3 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -19,11 +19,10 @@ async function getAllMessages() { const data = await response.json(); renderMessages(data); - - setTimeout(getAllMessages, 0); } catch (error) { - setTimeout(getAllMessages, 0); console.error("Error fetching messages:", error); + } finally { + setTimeout(getAllMessages, 0); } } From 5f130f83170fbfc085adb64c2a0bb89f90cd130e Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 23:41:53 +0100 Subject: [PATCH 49/58] refactor(frontend): rename renderMessages parameter from `data` to `messages` - Improve clarity by using `messages` as the function parameter and loop input - Update internal forEach to iterate `messages` instead of `data` --- chat-app/frontend/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index 07e842b3..d7331c88 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -26,10 +26,10 @@ async function getAllMessages() { } } -function renderMessages(data) { +function renderMessages(messages) { const messageContainer = document.getElementById("all-messages"); - data.forEach((message) => { + messages.forEach((message) => { const elementId = "msg-" + message.id; const existingElement = document.getElementById(elementId); From 179bfddd65121fd16f78c3d081d14d933d387110 Mon Sep 17 00:00:00 2001 From: iswat Date: Mon, 1 Jun 2026 23:58:38 +0100 Subject: [PATCH 50/58] feat(server): add long-polling server implementation - Add backend/long-polling-server.js with Express + CORS setup - Implement POST /messages with input validation and message storage - Implement GET /messages supporting ?since= for incremental loading and long-poll callback registration - Implement POST /messages/:id/like to increment likes and notify waiting clients - Start server on process.env.PORT || 3000 - Enables real-time updates via long-polling --- chat-app/backend/long-polling-server.js | 106 ++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 chat-app/backend/long-polling-server.js diff --git a/chat-app/backend/long-polling-server.js b/chat-app/backend/long-polling-server.js new file mode 100644 index 00000000..8b36ce12 --- /dev/null +++ b/chat-app/backend/long-polling-server.js @@ -0,0 +1,106 @@ +import express from "express"; +import cors from "cors"; + +const app = express(); +const port = process.env.PORT || 3000; + +const messages = []; +const callBacksForNewMessages = []; + +// Enable CORS for all routes +app.use(cors()); + +// Middleware to parse JSON request body +app.use(express.json()); + +app.post("/messages", (req, res) => { + // Check if re.body exists at all + if (!req.body) { + return res.status(400).send("No body provided"); + } + + const { text, sender } = req.body; + + // Check if the inputs are strings + if (typeof text !== "string" || typeof sender !== "string") { + return res.status(400).send("Inputs must be strings"); + } + + // Check if the inputs are not a falsy value + if (!text.trim() || !sender.trim()) { + return res.status(400).send("Please provide both text and a sender name."); + } + + // Create the message object + const newMessage = { + id: messages.length, + sender: sender, + text: text, + likes: 0, + }; + + // Add the new message to the messages array (the storage) + messages.push(newMessage); + + // long polling logic + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); + + callBack([newMessage]); + } + + // Finally, respond to the person who actually sent the POST request + res.status(201).send(newMessage); +}); +app.get("/messages", (req, res) => { + const sinceValue = req.query.since; + + let sinceId; + // We check if the value exists at all. + // If it's "0", this check is true, and we use 0. + if (sinceValue !== undefined) { + sinceId = Number(sinceValue); + } else { + sinceId = -1; + } + + const messagesSinceId = messages.filter((message) => message.id > sinceId); + + if (messagesSinceId.length === 0) { + callBacksForNewMessages.push((value) => res.send(value)); + } else { + res.send(messagesSinceId); + } +}); + +app.post("/messages/:id/like", (req, res) => { + // Get the id from the URL + const idFromUrl = req.params.id; + + //convert to number + const idAsNumber = Number(idFromUrl); + + const messageWithIdAsNumber = messages[idAsNumber]; + + if (!messageWithIdAsNumber) { + return res.status(404).send("Message not found"); + } + messageWithIdAsNumber.likes += 1; + + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + }; + + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); + + callBack([dataToSendToClient]); + } + res.status(200).send(dataToSendToClient); +}); + +// Start the server +app.listen(port, () => { + console.log(`Chat app listening on port ${port}`); +}); From ceb24a6215b13a0d11dcdb6fe00ea1e5ddeeb32b Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 2 Jun 2026 13:37:32 +0100 Subject: [PATCH 51/58] feat(server): add WebSocket support and broadcast new messages - Add http server and websocket (websocket) integration wrapping the Express app - Track active WebSocket connections and handle disconnects - Broadcast newly posted messages to all connected clients via sendUTF - Start the combined HTTP+WS server with server.listen instead of app.listen --- chat-app/backend/server.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 8b36ce12..29751d44 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -1,11 +1,17 @@ import express from "express"; import cors from "cors"; +import { connection, server as WebSocketServer } from "websocket"; +import http from "http"; const app = express(); +const server = http.createServer(app); +const webSocketServer = new WebSocketServer({ httpServer: server }); + const port = process.env.PORT || 3000; const messages = []; const callBacksForNewMessages = []; +let activeConnections = []; // Enable CORS for all routes app.use(cors()); @@ -13,6 +19,20 @@ app.use(cors()); // Middleware to parse JSON request body app.use(express.json()); +webSocketServer.on("request", (request) => { + const connection = request.accept(null, request.origin); + + activeConnections.push(connection); + + console.log("A user connected"); + + connection.on("close", () => { + activeConnections = activeConnections.filter( + (connecionToRemove) => connection !== connecionToRemove, + ); + }); +}); + app.post("/messages", (req, res) => { // Check if re.body exists at all if (!req.body) { @@ -42,14 +62,14 @@ app.post("/messages", (req, res) => { // Add the new message to the messages array (the storage) messages.push(newMessage); - // long polling logic - while (callBacksForNewMessages.length > 0) { - const callBack = callBacksForNewMessages.pop(); + activeConnections.forEach((connection) => { + // Turn new message object to a string + const newMessageString = JSON.stringify(newMessage); - callBack([newMessage]); - } + // Send the string message to the connection + connection.sendUTF(newMessageString); + }); - // Finally, respond to the person who actually sent the POST request res.status(201).send(newMessage); }); app.get("/messages", (req, res) => { @@ -101,6 +121,6 @@ app.post("/messages/:id/like", (req, res) => { }); // Start the server -app.listen(port, () => { +server.listen(port, () => { console.log(`Chat app listening on port ${port}`); }); From ff6bfe7aea67c7079f221f644ad084ecd4f8e702 Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 2 Jun 2026 14:49:50 +0100 Subject: [PATCH 52/58] feat(ws): broadcast missed messages and updates to active WebSocket clients - On WS connect, parse ?since and send missed messages as { command: "new-message", payload } - Broadcast newly posted messages to all active WS connections - Broadcast like updates as { command: "update-like", payload: { id, likes } } - Preserve long-poll fallback by still invoking stored callbacks - Fix connection cleanup variable name --- chat-app/backend/server.js | 43 +++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 29751d44..6b93672c 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -26,9 +26,23 @@ webSocketServer.on("request", (request) => { console.log("A user connected"); + const sinceId = Number(request.resourceURL.query.since); + + if (!isNaN(sinceId)) { + const messagesSineId = messages.filter((message) => message.id > sinceId); + + messagesSineId.forEach((message) => { + const messageSinceIdObject = { + command: "new-message", + payload: message, + }; + connection.sendUTF(JSON.stringify(messageSinceIdObject)); + }); + } + connection.on("close", () => { activeConnections = activeConnections.filter( - (connecionToRemove) => connection !== connecionToRemove, + (connectionToRemove) => connection !== connectionToRemove, ); }); }); @@ -62,14 +76,26 @@ app.post("/messages", (req, res) => { // Add the new message to the messages array (the storage) messages.push(newMessage); + // Websocket Broadcast activeConnections.forEach((connection) => { // Turn new message object to a string - const newMessageString = JSON.stringify(newMessage); + const newMessageString = { + command: "new-message", + payload: newMessage, + }; // Send the string message to the connection - connection.sendUTF(newMessageString); + connection.sendUTF(JSON.stringify(newMessageString)); }); + // long polling Broadcast + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); + + callBack([newMessage]); + } + + // Finally, respond to the person who actually sent the POST request res.status(201).send(newMessage); }); app.get("/messages", (req, res) => { @@ -112,6 +138,17 @@ app.post("/messages/:id/like", (req, res) => { likes: messageWithIdAsNumber.likes, }; + activeConnections.forEach((connection) => { + // Turn new message object to a string + const updateMessageString = { + command: "update-like", + payload: dataToSendToClient, + }; + + // Send the string message to the connection + connection.sendUTF(JSON.stringify(updateMessageString)); + }); + while (callBacksForNewMessages.length > 0) { const callBack = callBacksForNewMessages.pop(); From fbb628166d8573f9d498e1d929e094f84921a64e Mon Sep 17 00:00:00 2001 From: iswat Date: Tue, 2 Jun 2026 14:59:49 +0100 Subject: [PATCH 53/58] chore: remove legacy long-polling server and add websocket frontend scaffold - Delete backend/long-polling-server.js (legacy long-poll implementation) - Add frontend/index-websocket.html and frontend/script-websocket.js - script-websocket implements getAllMessages polling, renderMessages, like POSTs and API_BASE_URL - Keeps continuous polling with response.ok guard and DOM update handling - Prepare project for WebSocket integration / modern client scaffold --- chat-app/backend/long-polling-server.js | 106 --------------------- chat-app/frontend/index-websocket.html | 23 +++++ chat-app/frontend/script-websocket.js | 120 ++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 106 deletions(-) delete mode 100644 chat-app/backend/long-polling-server.js create mode 100644 chat-app/frontend/index-websocket.html create mode 100644 chat-app/frontend/script-websocket.js diff --git a/chat-app/backend/long-polling-server.js b/chat-app/backend/long-polling-server.js deleted file mode 100644 index 8b36ce12..00000000 --- a/chat-app/backend/long-polling-server.js +++ /dev/null @@ -1,106 +0,0 @@ -import express from "express"; -import cors from "cors"; - -const app = express(); -const port = process.env.PORT || 3000; - -const messages = []; -const callBacksForNewMessages = []; - -// Enable CORS for all routes -app.use(cors()); - -// Middleware to parse JSON request body -app.use(express.json()); - -app.post("/messages", (req, res) => { - // Check if re.body exists at all - if (!req.body) { - return res.status(400).send("No body provided"); - } - - const { text, sender } = req.body; - - // Check if the inputs are strings - if (typeof text !== "string" || typeof sender !== "string") { - return res.status(400).send("Inputs must be strings"); - } - - // Check if the inputs are not a falsy value - if (!text.trim() || !sender.trim()) { - return res.status(400).send("Please provide both text and a sender name."); - } - - // Create the message object - const newMessage = { - id: messages.length, - sender: sender, - text: text, - likes: 0, - }; - - // Add the new message to the messages array (the storage) - messages.push(newMessage); - - // long polling logic - while (callBacksForNewMessages.length > 0) { - const callBack = callBacksForNewMessages.pop(); - - callBack([newMessage]); - } - - // Finally, respond to the person who actually sent the POST request - res.status(201).send(newMessage); -}); -app.get("/messages", (req, res) => { - const sinceValue = req.query.since; - - let sinceId; - // We check if the value exists at all. - // If it's "0", this check is true, and we use 0. - if (sinceValue !== undefined) { - sinceId = Number(sinceValue); - } else { - sinceId = -1; - } - - const messagesSinceId = messages.filter((message) => message.id > sinceId); - - if (messagesSinceId.length === 0) { - callBacksForNewMessages.push((value) => res.send(value)); - } else { - res.send(messagesSinceId); - } -}); - -app.post("/messages/:id/like", (req, res) => { - // Get the id from the URL - const idFromUrl = req.params.id; - - //convert to number - const idAsNumber = Number(idFromUrl); - - const messageWithIdAsNumber = messages[idAsNumber]; - - if (!messageWithIdAsNumber) { - return res.status(404).send("Message not found"); - } - messageWithIdAsNumber.likes += 1; - - const dataToSendToClient = { - id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes, - }; - - while (callBacksForNewMessages.length > 0) { - const callBack = callBacksForNewMessages.pop(); - - callBack([dataToSendToClient]); - } - res.status(200).send(dataToSendToClient); -}); - -// Start the server -app.listen(port, () => { - console.log(`Chat app listening on port ${port}`); -}); diff --git a/chat-app/frontend/index-websocket.html b/chat-app/frontend/index-websocket.html new file mode 100644 index 00000000..9812583d --- /dev/null +++ b/chat-app/frontend/index-websocket.html @@ -0,0 +1,23 @@ + + + + + + + + Chat App + + +

Chat App

+ +
+ + + + + +
+ +
+ + diff --git a/chat-app/frontend/script-websocket.js b/chat-app/frontend/script-websocket.js new file mode 100644 index 00000000..d7331c88 --- /dev/null +++ b/chat-app/frontend/script-websocket.js @@ -0,0 +1,120 @@ +const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; +//const API_BASE_URL = "http://localhost:3000"; + +let lastIdSeen = -1; + +async function getAllMessages() { + try { + const response = await fetch( + `${API_BASE_URL}/messages?since=${lastIdSeen}`, + ); + + // check if the response is not ok + if (!response.ok) { + // Stop everything and jump to the catch block + throw new Error(`HTTP Error: ${response.status}`); + } + + // we only get here if the response was ok + const data = await response.json(); + + renderMessages(data); + } catch (error) { + console.error("Error fetching messages:", error); + } finally { + setTimeout(getAllMessages, 0); + } +} + +function renderMessages(messages) { + const messageContainer = document.getElementById("all-messages"); + + messages.forEach((message) => { + const elementId = "msg-" + message.id; + + const existingElement = document.getElementById(elementId); + + if (existingElement) { + // find the specific span that hold the likes + const likeSpan = document.getElementById("likes-count-" + message.id); + + // update only that span + if (likeSpan) { + likeSpan.textContent = `(${message.likes} Likes) `; + } + } else { + const newElement = document.createElement("div"); + newElement.id = "msg-" + message.id; + + // layer 1: the text + const textSpan = document.createElement("span"); + textSpan.textContent = `${message.sender}: ${message.text} `; + + //Layer 2: the counter (this is the one we will update later) + const likeSpan = document.createElement("span"); + likeSpan.id = "likes-count-" + message.id; + likeSpan.textContent = `(${message.likes} Likes) `; + + // Layer 3: the button + const likeButton = document.createElement("button"); + likeButton.textContent = "Like"; + + likeButton.addEventListener("click", async () => { + await fetch(`${API_BASE_URL}/messages/${message.id}/like`, { + method: "POST", + }); + }); + + // put it all together + newElement.appendChild(textSpan); + newElement.appendChild(likeSpan); + newElement.appendChild(likeButton); + messageContainer.appendChild(newElement); + + lastIdSeen = message.id; + } + }); +} + +getAllMessages(); + +const formElement = document.getElementById("chat-form"); +const senderElement = document.getElementById("chat-sender"); +const messageElement = document.getElementById("chat-message"); + +formElement.addEventListener("submit", async (event) => { + event.preventDefault(); + + // Get the values and trim them + const senderValue = senderElement.value.trim(); + const messageValue = messageElement.value.trim(); + + // The validation + if (senderValue === "" || messageValue === "") { + alert("Please enter both a name and a message!"); + + return; + } + + try { + // Send the data + const response = await fetch(`${API_BASE_URL}/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + sender: senderValue, + text: messageValue, + }), + }); + + if (response.ok) { + // clear the sender and message input values + senderElement.value = ""; + messageElement.value = ""; + } + } catch (error) { + console.error("Error sending message:", error); + } +}); From 393f9b764a4baa8319ac5f9629e75ed13a7d29c8 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 3 Jun 2026 00:53:30 +0100 Subject: [PATCH 54/58] chore: remove unused `connection` import from websocket module --- chat-app/backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index 6b93672c..bc5a4b32 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -1,6 +1,6 @@ import express from "express"; import cors from "cors"; -import { connection, server as WebSocketServer } from "websocket"; +import { server as WebSocketServer } from "websocket"; import http from "http"; const app = express(); From 05204455d2c1d61bcab322efa2811e4c7a610cff Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 3 Jun 2026 00:57:31 +0100 Subject: [PATCH 55/58] feat(ws): add WebSocket client and backend dependencies - Add `websocket` and `http` to backend/package.json - Replace frontend long-poll fetch with WebSocket client (script-websocket.js) - Handle `new-message` and `update-like` WS commands and render payloads - Prepare project for real-time WebSocket updates --- chat-app/backend/package.json | 4 ++- chat-app/frontend/script-websocket.js | 43 +++++++++++++-------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/chat-app/backend/package.json b/chat-app/backend/package.json index 55fb0a74..a090286d 100644 --- a/chat-app/backend/package.json +++ b/chat-app/backend/package.json @@ -2,6 +2,8 @@ "type": "module", "dependencies": { "cors": "^2.8.6", - "express": "^5.2.1" + "express": "^5.2.1", + "http": "^0.0.1-security", + "websocket": "^1.0.35" } } diff --git a/chat-app/frontend/script-websocket.js b/chat-app/frontend/script-websocket.js index d7331c88..463fcb48 100644 --- a/chat-app/frontend/script-websocket.js +++ b/chat-app/frontend/script-websocket.js @@ -1,30 +1,29 @@ -const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; -//const API_BASE_URL = "http://localhost:3000"; +const API_URL = "iswanna-chat-app-backend.hosting.codeyourfuture.io"; +const API_BASE_URL = `https://${API_URL}`; + +// const API_URL = "localhost:3000"; +// const API_BASE_URL = `http://${API_URL}`; let lastIdSeen = -1; -async function getAllMessages() { - try { - const response = await fetch( - `${API_BASE_URL}/messages?since=${lastIdSeen}`, - ); - - // check if the response is not ok - if (!response.ok) { - // Stop everything and jump to the catch block - throw new Error(`HTTP Error: ${response.status}`); - } +const wsUri = `wss://${API_URL}/messages?since=${lastIdSeen}`; +// const wsUri = `ws://${API_URL}/messages?since=${lastIdSeen}` - // we only get here if the response was ok - const data = await response.json(); +const webSocket = new WebSocket(wsUri); - renderMessages(data); - } catch (error) { - console.error("Error fetching messages:", error); - } finally { - setTimeout(getAllMessages, 0); +webSocket.addEventListener("open", (event) => { + webSocket.send("Hello Server!"); +}); + +webSocket.addEventListener("message", (event) => { + const receivedMessage = JSON.parse(event.data); + + const command = receivedMessage.command; + + if (command === "new-message" || command === "update-like") { + renderMessages([receivedMessage.payload]); } -} +}); function renderMessages(messages) { const messageContainer = document.getElementById("all-messages"); @@ -76,8 +75,6 @@ function renderMessages(messages) { }); } -getAllMessages(); - const formElement = document.getElementById("chat-form"); const senderElement = document.getElementById("chat-sender"); const messageElement = document.getElementById("chat-message"); From fe66beb4d8d2ff20553f299d14e0136e010733ed Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 3 Jun 2026 13:48:49 +0100 Subject: [PATCH 56/58] feat: add dislikes and unify counter updates - Add `dislikes` field to messages and implement POST /messages/:id/dislike - Send compact counter payloads `{ id, likes, dislikes }` to clients - Replace WS/long-poll update command with `"update-counter"` - Update frontend (script.js & script-websocket.js) to render dislike counts and add Dislike button/handler - Notify both WebSocket and long-poll clients and clear waiting callbacks after broadcasting - Minor formatting and cleanup fixes --- chat-app/backend/server.js | 43 ++++++++++++++++++++++++++- chat-app/frontend/script-websocket.js | 28 +++++++++++++++-- chat-app/frontend/script.js | 26 +++++++++++++++- 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index bc5a4b32..fb9d5f00 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -71,6 +71,7 @@ app.post("/messages", (req, res) => { sender: sender, text: text, likes: 0, + dislikes: 0, }; // Add the new message to the messages array (the storage) @@ -136,12 +137,52 @@ app.post("/messages/:id/like", (req, res) => { const dataToSendToClient = { id: messageWithIdAsNumber.id, likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, }; activeConnections.forEach((connection) => { // Turn new message object to a string const updateMessageString = { - command: "update-like", + command: "update-counter", + payload: dataToSendToClient, + }; + + // Send the string message to the connection + connection.sendUTF(JSON.stringify(updateMessageString)); + }); + + while (callBacksForNewMessages.length > 0) { + const callBack = callBacksForNewMessages.pop(); + + callBack([dataToSendToClient]); + } + res.status(200).send(dataToSendToClient); +}); + +app.post("/messages/:id/dislike", (req, res) => { + // Get the id from the URL + const idFromUrl = req.params.id; + + //convert to number + const idAsNumber = Number(idFromUrl); + + const messageWithIdAsNumber = messages[idAsNumber]; + + if (!messageWithIdAsNumber) { + return res.status(404).send("Message not found"); + } + messageWithIdAsNumber.dislikes += 1; + + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, + }; + + activeConnections.forEach((connection) => { + // Turn new message object to a string + const updateMessageString = { + command: "update-counter", payload: dataToSendToClient, }; diff --git a/chat-app/frontend/script-websocket.js b/chat-app/frontend/script-websocket.js index 463fcb48..0b67a0d5 100644 --- a/chat-app/frontend/script-websocket.js +++ b/chat-app/frontend/script-websocket.js @@ -20,7 +20,7 @@ webSocket.addEventListener("message", (event) => { const command = receivedMessage.command; - if (command === "new-message" || command === "update-like") { + if (command === "new-message" || command === "update-counter") { renderMessages([receivedMessage.payload]); } }); @@ -36,11 +36,18 @@ function renderMessages(messages) { if (existingElement) { // find the specific span that hold the likes const likeSpan = document.getElementById("likes-count-" + message.id); + const dislikeSpan = document.getElementById( + "dislikes-count-" + message.id, + ); // update only that span if (likeSpan) { likeSpan.textContent = `(${message.likes} Likes) `; } + + if (dislikeSpan) { + dislikeSpan.textContent = `(${message.dislikes} Dislikes) `; + } } else { const newElement = document.createElement("div"); newElement.id = "msg-" + message.id; @@ -54,7 +61,12 @@ function renderMessages(messages) { likeSpan.id = "likes-count-" + message.id; likeSpan.textContent = `(${message.likes} Likes) `; - // Layer 3: the button + //Layer 2a: the counter (this is the one we will update later) + const disLikeSpan = document.createElement("span"); + disLikeSpan.id = "dislikes-count-" + message.id; + disLikeSpan.textContent = `(${message.dislikes} Dislikes) `; + + // Layer 3: the like button const likeButton = document.createElement("button"); likeButton.textContent = "Like"; @@ -64,10 +76,22 @@ function renderMessages(messages) { }); }); + // Layer 3: the dislike button + const disLikeButton = document.createElement("button"); + disLikeButton.textContent = "Dislike"; + + disLikeButton.addEventListener("click", async () => { + await fetch(`${API_BASE_URL}/messages/${message.id}/dislike`, { + method: "POST", + }); + }); + // put it all together newElement.appendChild(textSpan); newElement.appendChild(likeSpan); newElement.appendChild(likeButton); + newElement.appendChild(disLikeSpan); + newElement.appendChild(disLikeButton); messageContainer.appendChild(newElement); lastIdSeen = message.id; diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index d7331c88..d35049b4 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -37,11 +37,18 @@ function renderMessages(messages) { if (existingElement) { // find the specific span that hold the likes const likeSpan = document.getElementById("likes-count-" + message.id); + const dislikeSpan = document.getElementById( + "dislikes-count-" + message.id, + ); // update only that span if (likeSpan) { likeSpan.textContent = `(${message.likes} Likes) `; } + + if (dislikeSpan) { + dislikeSpan.textContent = `(${message.dislikes} Dislikes) `; + } } else { const newElement = document.createElement("div"); newElement.id = "msg-" + message.id; @@ -55,7 +62,12 @@ function renderMessages(messages) { likeSpan.id = "likes-count-" + message.id; likeSpan.textContent = `(${message.likes} Likes) `; - // Layer 3: the button + //Layer 2a: the counter (this is the one we will update later) + const disLikeSpan = document.createElement("span"); + disLikeSpan.id = "dislikes-count-" + message.id; + disLikeSpan.textContent = `(${message.dislikes} Dislikes) `; + + // Layer 3: the like button const likeButton = document.createElement("button"); likeButton.textContent = "Like"; @@ -65,10 +77,22 @@ function renderMessages(messages) { }); }); + // Layer 3: the dislike button + const disLikeButton = document.createElement("button"); + disLikeButton.textContent = "Dislike"; + + disLikeButton.addEventListener("click", async () => { + await fetch(`${API_BASE_URL}/messages/${message.id}/dislike`, { + method: "POST", + }); + }); + // put it all together newElement.appendChild(textSpan); newElement.appendChild(likeSpan); newElement.appendChild(likeButton); + newElement.appendChild(disLikeSpan); + newElement.appendChild(disLikeButton); messageContainer.appendChild(newElement); lastIdSeen = message.id; From 1601bc94cd4bc5167d486e4a93d6b78a6b7c0bdd Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 3 Jun 2026 15:15:17 +0100 Subject: [PATCH 57/58] refactor(server): extract counter helpers, add dislike endpoint, broadcast compact updates - Add broadcastCounterUpdate(data) and findMessageOrError(req, res) helpers - Consolidate POST /messages/:id/like to use helper and return compact { id, likes, dislikes } - Implement POST /messages/:id/dislike and broadcast the same compact payload - Notify long-poll and WS clients with the minimized update payload - Minor: reformat API_BASE_URL in frontend --- chat-app/backend/server.js | 81 +++++++++++++++++-------------------- chat-app/frontend/script.js | 3 +- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index fb9d5f00..bdefe4b7 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -120,31 +120,12 @@ app.get("/messages", (req, res) => { } }); -app.post("/messages/:id/like", (req, res) => { - // Get the id from the URL - const idFromUrl = req.params.id; - - //convert to number - const idAsNumber = Number(idFromUrl); - - const messageWithIdAsNumber = messages[idAsNumber]; - - if (!messageWithIdAsNumber) { - return res.status(404).send("Message not found"); - } - messageWithIdAsNumber.likes += 1; - - const dataToSendToClient = { - id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes, - dislikes: messageWithIdAsNumber.dislikes, - }; - +function broadcastCounterUpdate(data) { activeConnections.forEach((connection) => { // Turn new message object to a string const updateMessageString = { command: "update-counter", - payload: dataToSendToClient, + payload: data, }; // Send the string message to the connection @@ -154,12 +135,11 @@ app.post("/messages/:id/like", (req, res) => { while (callBacksForNewMessages.length > 0) { const callBack = callBacksForNewMessages.pop(); - callBack([dataToSendToClient]); + callBack([data]); } - res.status(200).send(dataToSendToClient); -}); +} -app.post("/messages/:id/dislike", (req, res) => { +function findMessageOrError(req, res) { // Get the id from the URL const idFromUrl = req.params.id; @@ -169,33 +149,46 @@ app.post("/messages/:id/dislike", (req, res) => { const messageWithIdAsNumber = messages[idAsNumber]; if (!messageWithIdAsNumber) { - return res.status(404).send("Message not found"); + res.status(404).send("Message not found"); + return null; } - messageWithIdAsNumber.dislikes += 1; - const dataToSendToClient = { - id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes, - dislikes: messageWithIdAsNumber.dislikes, - }; + return messageWithIdAsNumber; +} +app.post("/messages/:id/like", (req, res) => { + const messageWithIdAsNumber = findMessageOrError(req, res); - activeConnections.forEach((connection) => { - // Turn new message object to a string - const updateMessageString = { - command: "update-counter", - payload: dataToSendToClient, + if (messageWithIdAsNumber) { + messageWithIdAsNumber.likes += 1; + + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, }; - // Send the string message to the connection - connection.sendUTF(JSON.stringify(updateMessageString)); - }); + broadcastCounterUpdate(dataToSendToClient); - while (callBacksForNewMessages.length > 0) { - const callBack = callBacksForNewMessages.pop(); + res.status(200).send(dataToSendToClient); + } +}); + +app.post("/messages/:id/dislike", (req, res) => { + const messageWithIdAsNumber = findMessageOrError(req, res); + + if (messageWithIdAsNumber) { + messageWithIdAsNumber.dislikes += 1; + + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, + }; + + broadcastCounterUpdate(dataToSendToClient); - callBack([dataToSendToClient]); + res.status(200).send(dataToSendToClient); } - res.status(200).send(dataToSendToClient); }); // Start the server diff --git a/chat-app/frontend/script.js b/chat-app/frontend/script.js index d35049b4..1fcc9fc9 100644 --- a/chat-app/frontend/script.js +++ b/chat-app/frontend/script.js @@ -1,4 +1,5 @@ -const API_BASE_URL = "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; +const API_BASE_URL = + "https://iswanna-chat-app-backend.hosting.codeyourfuture.io"; //const API_BASE_URL = "http://localhost:3000"; let lastIdSeen = -1; From 09f3318669a27c2fb8996cc285da0bd564440b24 Mon Sep 17 00:00:00 2001 From: iswat Date: Wed, 3 Jun 2026 21:52:08 +0100 Subject: [PATCH 58/58] refactor(server): extract validation, add dislikes, unify counter updates - Extract isValidMessage to validate/normalize POST /messages input and use requestBody when creating messages - Add dislikes field to messages and implement POST /messages/:id/dislike - Introduce broadcastCounterUpdate helper to send compact {id, likes, dislikes} payloads to WS and long-poll clients - Add findMessageOrError helper and use early returns for missing messages - Minor formatting and cleanup --- chat-app/backend/server.js | 73 ++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/chat-app/backend/server.js b/chat-app/backend/server.js index bdefe4b7..fe8488a8 100644 --- a/chat-app/backend/server.js +++ b/chat-app/backend/server.js @@ -47,29 +47,45 @@ webSocketServer.on("request", (request) => { }); }); -app.post("/messages", (req, res) => { - // Check if re.body exists at all +function isValidMessage(req, res) { + // Check if request body exists at all if (!req.body) { - return res.status(400).send("No body provided"); + res.status(400).send("No body provided"); + return false; } const { text, sender } = req.body; // Check if the inputs are strings if (typeof text !== "string" || typeof sender !== "string") { - return res.status(400).send("Inputs must be strings"); + res.status(400).send("Inputs must be strings"); + return false; } // Check if the inputs are not a falsy value if (!text.trim() || !sender.trim()) { - return res.status(400).send("Please provide both text and a sender name."); + res.status(400).send("Please provide both text and a sender name."); + return false; + } + + return { + text: text.trim(), + sender: sender.trim(), + }; +} + +app.post("/messages", (req, res) => { + const requestBody = isValidMessage(req, res); + + if (!requestBody) { + return; } // Create the message object const newMessage = { id: messages.length, - sender: sender, - text: text, + sender: requestBody.sender, + text: requestBody.text, likes: 0, dislikes: 0, }; @@ -99,6 +115,7 @@ app.post("/messages", (req, res) => { // Finally, respond to the person who actually sent the POST request res.status(201).send(newMessage); }); + app.get("/messages", (req, res) => { const sinceValue = req.query.since; @@ -158,37 +175,39 @@ function findMessageOrError(req, res) { app.post("/messages/:id/like", (req, res) => { const messageWithIdAsNumber = findMessageOrError(req, res); - if (messageWithIdAsNumber) { - messageWithIdAsNumber.likes += 1; + if (!messageWithIdAsNumber) { + return; + } + messageWithIdAsNumber.likes += 1; - const dataToSendToClient = { - id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes, - dislikes: messageWithIdAsNumber.dislikes, - }; + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, + }; - broadcastCounterUpdate(dataToSendToClient); + broadcastCounterUpdate(dataToSendToClient); - res.status(200).send(dataToSendToClient); - } + res.status(200).send(dataToSendToClient); }); app.post("/messages/:id/dislike", (req, res) => { const messageWithIdAsNumber = findMessageOrError(req, res); - if (messageWithIdAsNumber) { - messageWithIdAsNumber.dislikes += 1; + if (!messageWithIdAsNumber) { + return; + } + messageWithIdAsNumber.dislikes += 1; - const dataToSendToClient = { - id: messageWithIdAsNumber.id, - likes: messageWithIdAsNumber.likes, - dislikes: messageWithIdAsNumber.dislikes, - }; + const dataToSendToClient = { + id: messageWithIdAsNumber.id, + likes: messageWithIdAsNumber.likes, + dislikes: messageWithIdAsNumber.dislikes, + }; - broadcastCounterUpdate(dataToSendToClient); + broadcastCounterUpdate(dataToSendToClient); - res.status(200).send(dataToSendToClient); - } + res.status(200).send(dataToSendToClient); }); // Start the server