Skip to content
Closed
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
edd1159
feat: Add .gitignore for Node.js project
Iswanna May 9, 2026
d465322
feat: Set up Express backend server for chat app
Iswanna May 18, 2026
f8b8593
feat: Add POST /messages endpoint to create new chat messages
Iswanna May 18, 2026
0f9b122
feat: Add GET /messages endpoint to retrieve all chat messages
Iswanna May 18, 2026
c1086ee
feat: Create frontend HTML structure for chat app
Iswanna May 19, 2026
4d6cdc0
feat: Add frontend script to fetch and display chat messages
Iswanna May 19, 2026
4b2fecc
refactor: Fix backend server structure and indentation
Iswanna May 19, 2026
8b67a04
feat: Add backend package.json with dependencies
Iswanna May 19, 2026
47dfcc8
feat: Add form IDs and fix message input element in frontend
Iswanna May 19, 2026
6c32598
feat: Add form submission handler to send messages to backend
Iswanna May 19, 2026
31426bf
feat: Poll messages every 5 seconds for real-time updates
Iswanna May 19, 2026
6419e29
fix: Correct HTML syntax for message input element
Iswanna May 19, 2026
53c9419
refactor: Clean up code formatting and update README documentation
Iswanna May 19, 2026
928c5e9
feat: Add npm start script to package.json
Iswanna May 19, 2026
ac2c99d
feat: Update frontend API endpoints to use production backend URL
Iswanna May 20, 2026
4fa083d
refactor: Implement incremental message fetching with lastIdSeen trac…
Iswanna May 20, 2026
13987e9
feat: Add message filtering by ID to GET /messages endpoint
Iswanna May 20, 2026
6a53a4f
feat: Add error handling to form submission with try-catch
Iswanna May 20, 2026
a1369e5
refactor: Implement sequential message polling with setTimeout
Iswanna May 21, 2026
34c26ec
chore: Add newline at end of package.json
Iswanna May 21, 2026
9c172f5
feat: Implement long polling for real-time message updates
Iswanna May 22, 2026
c3a9cb0
refactor: Switch frontend API endpoints from production to local deve…
Iswanna May 22, 2026
ee7c2c1
feat: Add POST /messages/:id/like endpoint for liking messages
Iswanna May 22, 2026
e8655db
feat: Add response handling and error checking to POST /messages/:id/…
Iswanna May 22, 2026
786398a
feat: Add immediate like button feedback with optimistic UI update
Iswanna May 22, 2026
4e0f1c5
style: Add CSS styling for chat messages and buttons
Iswanna May 22, 2026
a80fe4f
refactor: Switch frontend API endpoints to production Render backend
Iswanna May 22, 2026
4f4cb01
refactor: Update all frontend API endpoints to CodeYourFuture backend
Iswanna May 25, 2026
ad2aff0
fix: Remove double slashes from backend API URLs
Iswanna May 25, 2026
c9b4dde
refactor: Remove scripts section from backend package.json
Iswanna May 25, 2026
c1bcf2c
docs: Add comprehensive project documentation in changes-made.md
Iswanna May 27, 2026
cea6285
refactor: Rename changes-made.md to CHANGELOG.md
Iswanna May 27, 2026
8972d95
docs: Replace README with comprehensive CHANGELOG documentation
Iswanna May 27, 2026
e3efea2
refactor: Improve POST /messages validation with type checking
Iswanna May 27, 2026
07982a1
docs: Improve README formatting with consistent spacing
Iswanna May 27, 2026
eb16726
feat: Trim and validate chat form inputs before submission
Iswanna May 27, 2026
d45de2c
refactor: Make frontend backend URL configurable via API_BASE_URL
Iswanna May 28, 2026
5131e28
refactor: remove `dislikes` property from new message objects
Iswanna May 28, 2026
f6635eb
refactor(frontend): simplify fetch call formatting and standardize fo…
Iswanna May 28, 2026
a513439
feat(frontend): set API_BASE_URL and add per-client UUID for polling
Iswanna May 28, 2026
a86e070
feat: make long-polling client-aware and harden message/like handling
Iswanna May 29, 2026
2f855fe
refactor(server): send compact like update to waiting clients
Iswanna May 29, 2026
9b430f3
refactor: use direct index lookup for message and tidy response object
Iswanna May 29, 2026
4e25541
refactor: revert to simple callback array for long-polling and remove…
Iswanna Jun 1, 2026
1a81029
fix(frontend): check fetch response in getAllMessages
Iswanna Jun 1, 2026
284e6a8
Remove extraneous trailing comma/line for clarity
Iswanna Jun 1, 2026
298813e
refactor(frontend): extract renderMessages and tidy polling/render flow
Iswanna Jun 1, 2026
9327130
fix(frontend): always schedule next poll in getAllMessages
Iswanna Jun 1, 2026
5f130f8
refactor(frontend): rename renderMessages parameter from `data` to `m…
Iswanna Jun 1, 2026
179bfdd
feat(server): add long-polling server implementation
Iswanna Jun 1, 2026
ceb24a6
feat(server): add WebSocket support and broadcast new messages
Iswanna Jun 2, 2026
ff6bfe7
feat(ws): broadcast missed messages and updates to active WebSocket c…
Iswanna Jun 2, 2026
fbb6281
chore: remove legacy long-polling server and add websocket frontend s…
Iswanna Jun 2, 2026
393f9b7
chore: remove unused `connection` import from websocket module
Iswanna Jun 2, 2026
0520445
feat(ws): add WebSocket client and backend dependencies
Iswanna Jun 2, 2026
fe66beb
feat: add dislikes and unify counter updates
Iswanna Jun 3, 2026
1601bc9
refactor(server): extract counter helpers, add dislike endpoint, broa…
Iswanna Jun 3, 2026
09f3318
refactor(server): extract validation, add dislikes, unify counter upd…
Iswanna Jun 3, 2026
f867a49
style(frontend): add spacing between buttons
Iswanna Jun 3, 2026
25efc39
Rename file from README.md to CHANGES-MADE.md
Iswanna Jun 15, 2026
a908ae4
Create a new README.md file: add hybrid real-time chat overview, tech…
Iswanna Jun 15, 2026
bf6423f
Update README: fix Live Backend URL to point to /messages endpoint
Iswanna Jun 15, 2026
fd03bd1
Remove redundant "Getting Started" subheading from README.md
Iswanna Jun 15, 2026
643af70
Split long-polling and WebSocket frontends into separate directories.
Iswanna Jun 17, 2026
092748c
Change websocket message card background color
Iswanna Jun 17, 2026
ae3c8e9
Separate polling and WebSocket frontends documentation, and update We…
Iswanna Jun 17, 2026
0877702
Update long-polling frontend link in README
Iswanna Jun 17, 2026
e7119a7
Rename WebSocket frontend files to standard names
Iswanna Jun 17, 2026
491ff49
Update WebSocket HTML to load renamed script file
Iswanna Jun 17, 2026
3b9a337
Add WebSocket frontend live link to README
Iswanna Jun 17, 2026
d99b054
Update docs to reflect WebSocket frontend file renames
Iswanna Jun 17, 2026
5366cb1
Add main landmark and redesign chat frontends with flex layouts
Iswanna Jun 17, 2026
b52fa30
Simplify frontend backgrounds and button styling
Iswanna Jun 17, 2026
3a3c205
docs(readme): update project origin and lessons learned
Iswanna Jun 18, 2026
52742d0
docs(readme): remove live backend link
Iswanna Jun 18, 2026
33e4afc
Update the deployed link for the websocket client
Iswanna Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
package-lock.json
.env
.DS_Store
*.log
dist/
build/
.vscode/
87 changes: 66 additions & 21 deletions chat-app/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,79 @@
# 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)

You must complete and deploy a chat application. You have two weeks to complete this.
- 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

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.
#### `package.json` (New)

It must also support at least one additional feature.
- Configured as ES module project (`"type": "module"`)
- Added Express 5.2.1 dependency
- Added CORS 2.8.6 dependency

### Why are we doing this?
### Frontend Implementation (`frontend/`)

Learning about deploying multiple pieces of software that interact.
#### `index.html` (New)

Designing and implementing working software that users can use.
- 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

Exploring and understanding different ways of sending information between a client and server.
#### `script.js` (New)

### Maximum time in hours
- **`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

16
- **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

### How to submit
- **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

* 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
#### `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

```
7 changes: 7 additions & 0 deletions chat-app/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "module",
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1"
}
}
106 changes: 106 additions & 0 deletions chat-app/backend/server.js
Original file line number Diff line number Diff line change
@@ -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.");
}
Comment thread
cjyuan marked this conversation as resolved.

// 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);
Comment thread
cjyuan marked this conversation as resolved.
});
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you expect to happen if no new message arrives within 30 minutes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect the connection to be closed by the hosting environment or the browser.
To handle this, I added Unique Client IDs:

Frontend: If the connection times out, my code catches the error and retries using the same ID.
Backend: I replaced the array with an Object. The server uses the Client ID to overwrite the old connection with the new one.

This ensures the server's memory doesn't fill up with 'ghost' connections, even if no messages arrive for 30 minutes.

} 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}`);
});
23 changes: 23 additions & 0 deletions chat-app/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="styles.css" />
<script src="script.js" defer></script>
<title>Chat App</title>
</head>
<body>
<h1>Chat App</h1>

<form id="chat-form">
<label for="chat-sender">Name</label>
<input type="text" id="chat-sender" />
<label for="chat-message">Message</label>
<input id="chat-message" />
<button type="submit">Send</button>
</form>

<div id="all-messages"></div>
</body>
</html>
120 changes: 120 additions & 0 deletions chat-app/frontend/script.js
Original file line number Diff line number Diff line change
@@ -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(data) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

messages would probably a clearer name for this parameter.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have replaced the parameter data with messages in the renderMessages function.

const messageContainer = document.getElementById("all-messages");

data.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);
}
});
20 changes: 20 additions & 0 deletions chat-app/frontend/styles.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading