-
-
Notifications
You must be signed in to change notification settings - Fork 61
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
Changes from 48 commits
edd1159
d465322
f8b8593
0f9b122
c1086ee
4d6cdc0
4b2fecc
8b67a04
47dfcc8
6c32598
31426bf
6419e29
53c9419
928c5e9
ac2c99d
4fa083d
13987e9
6a53a4f
a1369e5
34c26ec
9c172f5
c3a9cb0
ee7c2c1
e8655db
786398a
4e0f1c5
a80fe4f
4f4cb01
ad2aff0
c9b4dde
c1bcf2c
cea6285
8972d95
e3efea2
07982a1
eb16726
d45de2c
5131e28
f6635eb
a513439
a86e070
2f855fe
9b430f3
4e25541
1a81029
284e6a8
298813e
9327130
5f130f8
179bfdd
ceb24a6
ff6bfe7
fbb6281
393f9b7
0520445
fe66beb
1601bc9
09f3318
f867a49
25efc39
a908ae4
bf6423f
fd03bd1
643af70
092748c
ae3c8e9
0877702
e7119a7
491ff49
3b9a337
d99b054
5366cb1
b52fa30
3a3c205
52742d0
33e4afc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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/ |
| 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 | ||
|
|
||
| ``` |
| 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" | ||
| } | ||
| } |
| 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."); | ||
| } | ||
|
|
||
| // 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); | ||
|
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)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Frontend: If the connection times out, my code catches the error and retries using the same ID. 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}`); | ||
| }); | ||
| 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> |
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have replaced the parameter |
||
| 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); | ||
| } | ||
| }); | ||
| 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; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.