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
29 changes: 27 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import './App.css';
import ChatLog from './components/ChatLog';
import entriesData from './data/messages.json';
import { useState } from 'react';

const countTotalLikes = entriesData => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❗️👀 Functions that are only used within a specific component, like countTotalLikes is only used inside the App component on line 33, should be defined inside the component to make the code more readable because it keeps related logic together.

Therefore, this function should be defined in the App component (like you do with handleLikes on line 18).

return entriesData.reduce((acc, entry) => {
if (!entry.liked) {
return acc;
}
return acc + 1;
Comment on lines +8 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A more concise way to write this looks like:
return acc + (entry.liked ? 1 : 0);

}, 0);
};

const App = () => {
const [entryData, setEntriesData] = useState(entriesData);

function handleLikes(id) {
setEntriesData(entryData => {
return entryData.map(entry => {
Comment on lines +19 to +20
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 using the callback-style for updating the state.

if (entry.id === id) {
return { ...entry, liked: !entry.liked};
}
return entry;
});
});
};

return (
<div id="App">
<header>
<h1>Application title</h1>
<h2>{countTotalLikes(entryData)} ❤️s</h2>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❗️ ^^ above on line 32, you should have changed the title so that the header rendered "Chat between Vladimir and Estragon"

Can you think of a way to dynamically get the senders' names without hardcoding "Vladimir" and "Estragon" here?

</header>
<main>
{/* Wave 01: Render one ChatEntry component
Wave 02: Render ChatLog component */}
<ChatLog entries={entryData} onLikeEntry={handleLikes}></ChatLog>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prefer to have opening tag, closing tag, and props all on separate lines to help with readability. React doesn't care how you write the component so the key is to be consistent in your project.

Also since ChatLog doesn't render any child elements or content within its opening and closing tags, you can use a self closing tag here.

Suggested change
<ChatLog entries={entryData} onLikeEntry={handleLikes}></ChatLog>
<ChatLog
entries={entryData}
onLikeEntry={handleLikes}>
/>

</main>
</div>
);
Expand Down
27 changes: 18 additions & 9 deletions src/components/ChatEntry.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import './ChatEntry.css';
import TimeStamp from './TimeStamp';
import PropTypes from 'prop-types';

const ChatEntry = (props) => {
Copy link
Copy Markdown

@yangashley yangashley Dec 22, 2025

Choose a reason for hiding this comment

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

Destructuring props can be a matter of preference. I prefer to destructure props so that I don't need to type props. every time I want to get an attribute from the object.

Destructuring props also has some benefits:

  • you know every possible prop right away.
  • you know when a prop is being unused, in order to remove it.
  • knowing which props are being used will also remind you what you need to validate

Ultimately, you'll adapt what you do depending on what your team does.


const chatDisplay = props.sender === 'Vladimir' ? 'local' : 'remote';
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 writing logic to determine the class to apply to the component so that the components show up with 'local' vs 'remote' styles.


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>
<div className={`chat-entry ${chatDisplay}`}>
<h2 className="entry-name">{props.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>{props.body}</p>
<p className="entry-time"><TimeStamp time={props.timeStamp} /></p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good work using the provided TimeStamp component.

<button className="like" onClick={() => props.onLike(props.id)}>{props.liked ? '❤️' : '🤍'}</button>
</section>
</replace-with-relevant-semantic-element>
</div>
);
};

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.isRequired
};

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 './ChatLog.css';
import ChatEntry from './ChatEntry';
import PropTypes from 'prop-types';

const ChatLog = (props) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Like my comment above in ChatEntry, I prefer to destructure props.


const getChatEntries = (entries) => {
return entries.map((entry) => {
return (<ChatEntry
key= {entry.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.

Nitpick: incorrect white spacing

Suggested change
key= {entry.id}
key={entry.id}

Take care to check your style using a linter or fix up the issues while you're writing the code so that your work conforms to convention and guidelines.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 The key attribute is important for React to be able to detect certain kinds of data changes in an efficient manner. We're also using the id for our own id prop, so it might feel redundant to pass both, but one is for our logic and one is for React internals.

id={entry.id}
sender={entry.sender}
body={entry.body}
timeStamp={entry.timeStamp}
liked={entry.liked}
onLike={props.onLikeEntry}>
</ChatEntry>
);
});
};

return (
<div className='chat-log'>
{getChatEntries(props.entries)}
</div>
);

};

ChatLog.propTypes = {
entries: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
sender: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
timeStamp: PropTypes.string.isRequired,
liked: PropTypes.bool.isRequired
})
).isRequired,
onLikeEntry: PropTypes.func.isRequired
};

export default ChatLog;