-
-
Notifications
You must be signed in to change notification settings - Fork 56
West Midlands | 26 March SDC | Iswat Bello | Sprint 2 | chat app #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Iswanna
wants to merge
58
commits into
CodeYourFuture:main
Choose a base branch
from
Iswanna:feature/chat-app
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
58 commits
Select commit
Hold shift + click to select a range
edd1159
feat: Add .gitignore for Node.js project
Iswanna d465322
feat: Set up Express backend server for chat app
Iswanna f8b8593
feat: Add POST /messages endpoint to create new chat messages
Iswanna 0f9b122
feat: Add GET /messages endpoint to retrieve all chat messages
Iswanna c1086ee
feat: Create frontend HTML structure for chat app
Iswanna 4d6cdc0
feat: Add frontend script to fetch and display chat messages
Iswanna 4b2fecc
refactor: Fix backend server structure and indentation
Iswanna 8b67a04
feat: Add backend package.json with dependencies
Iswanna 47dfcc8
feat: Add form IDs and fix message input element in frontend
Iswanna 6c32598
feat: Add form submission handler to send messages to backend
Iswanna 31426bf
feat: Poll messages every 5 seconds for real-time updates
Iswanna 6419e29
fix: Correct HTML syntax for message input element
Iswanna 53c9419
refactor: Clean up code formatting and update README documentation
Iswanna 928c5e9
feat: Add npm start script to package.json
Iswanna ac2c99d
feat: Update frontend API endpoints to use production backend URL
Iswanna 4fa083d
refactor: Implement incremental message fetching with lastIdSeen trac…
Iswanna 13987e9
feat: Add message filtering by ID to GET /messages endpoint
Iswanna 6a53a4f
feat: Add error handling to form submission with try-catch
Iswanna a1369e5
refactor: Implement sequential message polling with setTimeout
Iswanna 34c26ec
chore: Add newline at end of package.json
Iswanna 9c172f5
feat: Implement long polling for real-time message updates
Iswanna c3a9cb0
refactor: Switch frontend API endpoints from production to local deve…
Iswanna ee7c2c1
feat: Add POST /messages/:id/like endpoint for liking messages
Iswanna e8655db
feat: Add response handling and error checking to POST /messages/:id/…
Iswanna 786398a
feat: Add immediate like button feedback with optimistic UI update
Iswanna 4e0f1c5
style: Add CSS styling for chat messages and buttons
Iswanna a80fe4f
refactor: Switch frontend API endpoints to production Render backend
Iswanna 4f4cb01
refactor: Update all frontend API endpoints to CodeYourFuture backend
Iswanna ad2aff0
fix: Remove double slashes from backend API URLs
Iswanna c9b4dde
refactor: Remove scripts section from backend package.json
Iswanna c1bcf2c
docs: Add comprehensive project documentation in changes-made.md
Iswanna cea6285
refactor: Rename changes-made.md to CHANGELOG.md
Iswanna 8972d95
docs: Replace README with comprehensive CHANGELOG documentation
Iswanna e3efea2
refactor: Improve POST /messages validation with type checking
Iswanna 07982a1
docs: Improve README formatting with consistent spacing
Iswanna eb16726
feat: Trim and validate chat form inputs before submission
Iswanna d45de2c
refactor: Make frontend backend URL configurable via API_BASE_URL
Iswanna 5131e28
refactor: remove `dislikes` property from new message objects
Iswanna f6635eb
refactor(frontend): simplify fetch call formatting and standardize fo…
Iswanna a513439
feat(frontend): set API_BASE_URL and add per-client UUID for polling
Iswanna a86e070
feat: make long-polling client-aware and harden message/like handling
Iswanna 2f855fe
refactor(server): send compact like update to waiting clients
Iswanna 9b430f3
refactor: use direct index lookup for message and tidy response object
Iswanna 4e25541
refactor: revert to simple callback array for long-polling and remove…
Iswanna 1a81029
fix(frontend): check fetch response in getAllMessages
Iswanna 284e6a8
Remove extraneous trailing comma/line for clarity
Iswanna 298813e
refactor(frontend): extract renderMessages and tidy polling/render flow
Iswanna 9327130
fix(frontend): always schedule next poll in getAllMessages
Iswanna 5f130f8
refactor(frontend): rename renderMessages parameter from `data` to `m…
Iswanna 179bfdd
feat(server): add long-polling server implementation
Iswanna ceb24a6
feat(server): add WebSocket support and broadcast new messages
Iswanna ff6bfe7
feat(ws): broadcast missed messages and updates to active WebSocket c…
Iswanna fbb6281
chore: remove legacy long-polling server and add websocket frontend s…
Iswanna 393f9b7
chore: remove unused `connection` import from websocket module
Iswanna 0520445
feat(ws): add WebSocket client and backend dependencies
Iswanna fe66beb
feat: add dislikes and unify counter updates
Iswanna 1601bc9
refactor(server): extract counter helpers, add dislike endpoint, broa…
Iswanna 09f3318
refactor(server): extract validation, add dislikes, unify counter upd…
Iswanna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "type": "module", | ||
| "dependencies": { | ||
| "cors": "^2.8.6", | ||
| "express": "^5.2.1", | ||
| "http": "^0.0.1-security", | ||
| "websocket": "^1.0.35" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| import express from "express"; | ||
| import cors from "cors"; | ||
| import { 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()); | ||
|
|
||
| // 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"); | ||
|
|
||
| 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( | ||
| (connectionToRemove) => connection !== connectionToRemove, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| function isValidMessage(req, res) { | ||
| // Check if request body exists at all | ||
| if (!req.body) { | ||
| 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") { | ||
| res.status(400).send("Inputs must be strings"); | ||
| return false; | ||
| } | ||
|
|
||
| // Check if the inputs are not a falsy value | ||
| if (!text.trim() || !sender.trim()) { | ||
| 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: requestBody.sender, | ||
| text: requestBody.text, | ||
| likes: 0, | ||
| dislikes: 0, | ||
| }; | ||
|
|
||
| // 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 = { | ||
| command: "new-message", | ||
| payload: newMessage, | ||
| }; | ||
|
|
||
| // Send the string message to the connection | ||
| 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); | ||
|
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)); | ||
| } else { | ||
| res.send(messagesSinceId); | ||
| } | ||
| }); | ||
|
|
||
| function broadcastCounterUpdate(data) { | ||
| activeConnections.forEach((connection) => { | ||
| // Turn new message object to a string | ||
| const updateMessageString = { | ||
| command: "update-counter", | ||
| payload: data, | ||
| }; | ||
|
|
||
| // Send the string message to the connection | ||
| connection.sendUTF(JSON.stringify(updateMessageString)); | ||
| }); | ||
|
|
||
| while (callBacksForNewMessages.length > 0) { | ||
| const callBack = callBacksForNewMessages.pop(); | ||
|
|
||
| callBack([data]); | ||
| } | ||
| } | ||
|
|
||
| function findMessageOrError(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) { | ||
| res.status(404).send("Message not found"); | ||
| return null; | ||
| } | ||
|
|
||
| return messageWithIdAsNumber; | ||
| } | ||
| app.post("/messages/:id/like", (req, res) => { | ||
| const messageWithIdAsNumber = findMessageOrError(req, res); | ||
|
|
||
| if (!messageWithIdAsNumber) { | ||
| return; | ||
| } | ||
| messageWithIdAsNumber.likes += 1; | ||
|
|
||
| const dataToSendToClient = { | ||
| id: messageWithIdAsNumber.id, | ||
| likes: messageWithIdAsNumber.likes, | ||
| dislikes: messageWithIdAsNumber.dislikes, | ||
| }; | ||
|
|
||
| broadcastCounterUpdate(dataToSendToClient); | ||
|
|
||
| res.status(200).send(dataToSendToClient); | ||
| }); | ||
|
|
||
| app.post("/messages/:id/dislike", (req, res) => { | ||
| const messageWithIdAsNumber = findMessageOrError(req, res); | ||
|
|
||
| if (!messageWithIdAsNumber) { | ||
| return; | ||
| } | ||
| messageWithIdAsNumber.dislikes += 1; | ||
|
|
||
| const dataToSendToClient = { | ||
| id: messageWithIdAsNumber.id, | ||
| likes: messageWithIdAsNumber.likes, | ||
| dislikes: messageWithIdAsNumber.dislikes, | ||
| }; | ||
|
|
||
| broadcastCounterUpdate(dataToSendToClient); | ||
|
|
||
| res.status(200).send(dataToSendToClient); | ||
| }); | ||
|
|
||
| // Start the server | ||
| server.listen(port, () => { | ||
| console.log(`Chat app listening on port ${port}`); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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-websocket.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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.