Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 7 additions & 1 deletion src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#App header section {
background-color: #e0ffff;
padding: 0.2em;
}

#App .widget {
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The selector uses .widget class but the actual element has id="widget". CSS class selectors start with . but this should be an id selector #widget or the HTML should use className="widget" instead of id="widget".

Suggested change
#App .widget {
#App #widget {

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -71,4 +72,9 @@

.purple {
color: purple
}
}

h2 {
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The h2 rule is too broad and will affect all h2 elements in the application. Since this appears to be styling for the widget section specifically, consider using a more specific selector like #widget h2 to avoid unintended side effects on other h2 elements.

Suggested change
h2 {
#App .widget h2 {

Copilot uses AI. Check for mistakes.
color: black;

}
67 changes: 64 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,75 @@
import './App.css';
import {useState} from 'react';
import messagesJSON from './data/messages.json';
import ChatLog from './components/ChatLog';


const countTotalLikes = (messageData) => {
return messageData.reduce((acc, entry) => acc + entry.liked, 0);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The countTotalLikes function treats liked as a number and sums it up, but according to the PropTypes definitions in both ChatEntry and ChatLog, liked is a boolean. This will result in incorrect counting - instead of counting the number of likes, it will sum boolean values (true = 1, false = 0), which happens to work but is semantically incorrect and confusing. The function should check if entry.liked is true and count those instances.

Suggested change
return messageData.reduce((acc, entry) => acc + entry.liked, 0);
return messageData.reduce((acc, entry) => acc + (entry.liked === true ? 1 : 0), 0);

Copilot uses AI. Check for mistakes.
//explain what reduce is doing here
//
};

const toggleLike = (messageId) => {
return {
...messageId,
liked: !messageId.liked,
};
};
Comment on lines +7 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 Nice job determining the total likes based on the like data of each message. We don't need an additional piece of state to track this, since it can be derived from the existing state we are tracking.

I like that you wrapped your reduce call in a well-named helper function. This is one way that we can make using reduce a little more understandable for other programmers reading our code, since the syntax can be a little confusing.

Since the message data is passed in, we could even relocate this function completely out of this function. This is definitely business logic, not rendering logic. So it can be managed separately from our React components.


const App = () => {

const [messageData, setMessagesData] = useState(messagesJSON);

const likeMessage = (messageId) => {
setMessagesData((prevMessagesData) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 Another benefit to using the callback style of setter is that it now no longer depends on anything in the enclosing scope (the current state value is passed in as a parameter), so this whole chunk of logic for finding and updating the message could be moved out to a helper function!

return prevMessagesData.map((entry) => {
if (entry.id === messageId) {
return toggleLike(entry);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 Toggling is the only business logic we have for messages in this small app, but we can imagine if there are other operations, those could all have their own functions and potentially be grouped into a class (or at least be collected in a separate file).

Of course, when the true data representation is actually coming from a backend, we might omit such helpers entirely, and just get the updated record as a JSON response, using it directly to replace the previous entry in the list, freeing the frontend from needing to know anything about how to manage the record at all!

} else {
return entry;
}
});
});
};

const likeCount = countTotalLikes(messageData);

const findSenderNames = (messageData) => {
const senderNames = messageData.map((entry) => {
return entry.sender;
});
return new Set(senderNames);
};
Comment on lines +38 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 Great way to get a list of the users in the messages. Sets preserve insertion order in JS, so we know that the first encountered sender would appear first in the list. Extracting the senders this way also scales to however many senders could be in this conversation.


const setHeaderTitle = (messageData) => {
const senderNames = Array.from(findSenderNames(messageData));
return senderNames.reduce((acc, name, index) => {
if (index === senderNames.length - 1 && index !== 0) {
return `${acc} and ${name}`;
} else if (index === 0) {
return name;
} else {
return `${acc}, ${name}`;
}
}, '');
Comment on lines +47 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fancy! And it handles the regular rule format of "item, item and item" (personally, I'm an Oxford comma fan, so would prefer "item, item, and item").

While not a major concern for any reasonable conversation group size, keep in mind that each of the new intermediate strings created while reduce iterates will grow longer and longer, tending towards quadratic time with the total length of the names. Instead, we could determine the part of the list that gets joined with ",", and handle those in one join call, then tack on the "and" part. There is still some extra copying, but it's a fixed number, so ternding more towards linear. Alernatively, we could build up a list of all the parts (names as well as separators), then join them with no separator in a single pass. This is the closest approach to a typical "string buffer/string builder" approach.

};

return (
<div id="App">
<header>
<h1>Application title</h1>
<h1>{setHeaderTitle(messageData)}</h1>
<section id="widget">
<h2>
{likeCount} ❤️s
</h2>
</section>
</header>
<main>
{/* Wave 01: Render one ChatEntry component
Wave 02: Render ChatLog component */}
<ChatLog
entries={messageData}
onLike={likeMessage}>
</ChatLog>
</main>
</div>
);
Expand Down
7 changes: 6 additions & 1 deletion src/components/ChatEntry.css
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,9 @@ button {

.chat-entry.remote .entry-bubble:hover::before {
background-color: #a9f6f6;
}
}





Comment on lines +100 to +105
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Try to avoid making stray changes to other files. When reviewing the contents of a commit, if we see a change like this, it would be preferable to revert it rather than committing it.

31 changes: 22 additions & 9 deletions src/components/ChatEntry.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import './ChatEntry.css';
import PropTypes from 'prop-types';
import TimeStamp from './TimeStamp';

const ChatEntry = ({ id, sender, body, timeStamp, liked, onLike }) => {

const handleLike = () => {
onLike(id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Relaxing the isRequired on the onLike proptypes avoids the warning during tests, but as a result, we should also check that onLike actually has a value before calling it so that we don't end up trying to invoke undefined, which would result in an error.

};

const like = liked ? '❤️' : '🤍';

const ChatEntry = () => {
return (
// Replace the outer tag name with a semantic element that fits our use case
<replace-with-relevant-semantic-element className="chat-entry local">
<h2 className="entry-name">Replace with name of sender</h2>
<article className="chat-entry local">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 article is a more appropriate choice for a semantic tag, since one of its uses is to represent self-contained, reusable page elements.

<h2 className="entry-name">{sender}</h2>
<section className="entry-bubble">
<p>Replace with body of ChatEntry</p>
<p className="entry-time">Replace with TimeStamp component</p>
<button className="like">🤍</button>
<p>{body}</p>
<p className="entry-time"><TimeStamp time={timeStamp} /></p>
<button onClick={handleLike} className="like">{like}</button>
</section>
</replace-with-relevant-semantic-element>
</article>
);
};

ChatEntry.propTypes = {
// Fill with correct proptypes
id: PropTypes.number.isRequired,
sender: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
timeStamp: PropTypes.string.isRequired,
liked: PropTypes.bool.isRequired,
onLike: PropTypes.func,
};

export default ChatEntry;
43 changes: 43 additions & 0 deletions src/components/ChatLog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ChatEntry from './ChatEntry';
import './ChatLog.css';
import PropTypes from 'prop-types';


const ChatLog = ({ entries, onLike }) => {
const chatEntries = (chatLog) => {
return chatLog.map((entry) => {
return (
<ChatEntry
key={entry.id}
id={entry.id}
sender={entry.sender}
body={entry.body}
timeStamp={entry.timeStamp}
liked={entry.liked}
onLike={onLike}
/>
);
});
};

return (
<div className="chat-log">
{chatEntries(entries)}
</div>
);
};

ChatLog.propTypes = {
entries: PropTypes.arrayOf(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 This matches the expected name now.

PropTypes.shape({
id: PropTypes.number.isRequired,
sender: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
timeStamp: PropTypes.string.isRequired,
liked: PropTypes.bool.isRequired,
})
).isRequired,
onLike: PropTypes.func,
};

export default ChatLog;