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
14 changes: 12 additions & 2 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
padding-right: 2em;
padding-bottom: 5rem;
padding-top: 10rem;
text-align: center;
}

#App h1 {
Expand All @@ -31,7 +32,7 @@
}

#App .widget {
display: inline-block;
display: block;
line-height: 0.5em;
border-radius: 10px;
color: black;
Expand All @@ -42,7 +43,16 @@

#App #heartWidget {
font-size: 1.5em;
margin: 1em
margin: 1em;
padding-top: 1em;
padding-bottom: 0.8em;
}

#App header h1,
#App #heartWidget {
line-height: 3rem;
margin: 0;
padding: 0;
}

#App span {
Expand Down
41 changes: 36 additions & 5 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
import './App.css';
import messages from './data/messages.json';
import ChatLog from './components/ChatLog';
import { useState } from 'react';

const SENDER_COLORS = {
Vladimir: 'red',
Estragon: 'green',
};

const App = () => {
const [chatMessages, setChatMessages] = useState(messages);

const toggleLikeChoice = (id) => {
setChatMessages(chatMessages => {
return chatMessages.map(message => {
if (message.id === id) {
return { ...message, liked: !message.liked };
} else {
return message;
}
});
});
};
Comment on lines +15 to +24
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.


const countLikes = chatMessages.filter(msg => msg.liked).length;

const participants = Array.from(new Set(chatMessages.map(msg => msg.sender)));
const headerTitle = `Chat between ${participants.join(' and ')}`;
Comment on lines +28 to +29
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I like that you dynamically calculate the participants names for the header. This could accommodate a situation where the senders are not named Vladimir or Estragon. How might you use participants to dynamically generate the keys for the SENDER_COLORS object on lines 6-9?


return (
<div id="App">
<header>
<h1>Application title</h1>
<h1 className="header">{headerTitle}</h1>
<section id="heartWidget" className="widget">{countLikes} ❤️s</section>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

section tags are recommended to have a heading within them. Use of an h2 around the like count would avoid validation warnings, as well as resemble the sample site more closely.

</header>
<main>
{/* Wave 01: Render one ChatEntry component
Wave 02: Render ChatLog component */}
<main className="main">
<ChatLog
entries={chatMessages}
toggleLikeChoice={toggleLikeChoice}
senderColors={SENDER_COLORS}
/>
</main>
</div>
);
};

export default App;
export default App;
7 changes: 7 additions & 0 deletions src/components/ChatEntry.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@ button {
}

.chat-entry .entry-time {
display: block;
color: #bbb;
font-size: x-small;
margin-bottom: 0.1rem;
margin-right: 0.5rem;
}

.chat-entry .like {
display: block;
margin-top: 0.3rem;
margin-left: auto;
}

/* Chat bubble arrow styling */
.chat-entry .entry-bubble::before {
content: '';
Expand Down
29 changes: 20 additions & 9 deletions src/components/ChatEntry.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import './ChatEntry.css';
import PropTypes from 'prop-types';
import TimeStamp from './TimeStamp';

const ChatEntry = () => {
const ChatEntry = ({ id, sender, body, timeStamp, toggleLikeChoice, liked, color }) => {
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 destructuring the variables instead of just passing props into the parameter list 👍

const isLocal = sender === 'Vladimir';
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>
<section className="entry-bubble">
<p>Replace with body of ChatEntry</p>
<p className="entry-time">Replace with TimeStamp component</p>
<button className="like">🤍</button>
<article className={`chat-entry ${isLocal ? '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 work computing the value of isLocal 👍

<h2 className="entry-name">{sender}</h2>
<section className={`entry-bubble ${color}`}>
<p>{body}</p>
<span className="entry-time">
<TimeStamp time={timeStamp} />
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 use of the TimeStamp component that was supplied.

</span>
<button className="like" onClick={() => toggleLikeChoice(id)}>{liked ? '❤️' : '🤍'}</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👍 Passing the id of this message lets the logic defined in the App component find the correct chat entry to update.

</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,
toggleLikeChoice: PropTypes.func,
color: PropTypes.string
};

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

const ChatLog = ({ entries, toggleLikeChoice = () => {}, senderColors = {} }) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should pass a function as a prop to a child component just by variable name (like how you did for in the ChatEntry component.

We don't need to provide a default no-op function. We might use no-op functions when we build a library that other developers will use in their programs and that library surfaces an option for developer-supplied functions.

Here, if no function is passed then our component shouldn't work.

The functions passed as props are part of the component’s contract and the parent is responsible for passing it. If it’s missing, that’s a usage error rather than something to silently ignore.

Suggested change
const ChatLog = ({ entries, toggleLikeChoice = () => {}, senderColors = {} }) => {
const ChatLog = ({ entries, toggleLikeChoice, senderColors}) => {

const chatEntryComponents = 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.

👍 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}
toggleLikeChoice={toggleLikeChoice}
liked={entry.liked}
color={senderColors?.[entry.sender]}
/>
);
});

return (
<section className="chat-log">
{chatEntryComponents}
</section>
);
};

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,
toggleLikeChoice: PropTypes.func,
senderColors: PropTypes.object,
};

export default ChatLog;