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
35 changes: 31 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import './App.css';
import { useState } from 'react';
import messagesData from './data/messages.json';
import ChatLog from './components/ChatLog';

const App = () => {
const [messageList, setMessageList] = useState(messagesData);

Choose a reason for hiding this comment

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

⭐️


const likeToggleFunc = (messageId) => {
const messages = messageList.map(message => {
if (message.id === messageId) {
return { ...message, liked: !message.liked };
} else {
return message;
}
});

setMessageList(messages);
};
Comment on lines +9 to +19

Choose a reason for hiding this comment

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

Nice work on using .map to map over your entries! Very functional! Since you are using a simple conditional block here, we could make this more succinct by using a ternary instead like so:

const toggleLike = (id) =>
  setEntries(entries.map(entry =>
    entry.id === id ? { ...entry, liked: !entry.liked } : entry
  ));

When you find yourself with simple checks like these, more often than not a ternary could be implemented instead for conciseness but still maintaining some kind of readability.


const totalLikes = messageList
.filter((message) => message.liked)
.length;
Comment on lines +21 to +23

Choose a reason for hiding this comment

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

You could also do this with the.reduce method like so:

const  totalLikes  =  chatMessages.reduce((count,  message)  => {  
 return count  + (message.liked ? 1 : 0)
},  0);

As for the formatting, I think you could leave this code on a single since it isn't longer than 80 characters.

Suggested change
const totalLikes = messageList
.filter((message) => message.liked)
.length;
const totalLikes = messageList.filter((message) => message.liked).length;


return (
<div id="App">
<header>
<h1>Application title</h1>
<h1>Chat between Vladimir and Estragon</h1>
<div>
{totalLikes} ❤️s
Comment on lines +28 to +30

Choose a reason for hiding this comment

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

⭐️

</div>
</header>
<main>
{/* Wave 01: Render one ChatEntry component
Wave 02: Render ChatLog component */}
<ChatLog
entries={messageList}
likeToggleFunc={likeToggleFunc}
localSender="Vladimir"
Comment on lines +35 to +37

Choose a reason for hiding this comment

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

A couple of things here:

  • You have the data named messageList but then name the prop entries, is there a reason as to why you chose not to consistently name key in the prop object messageList? Though there is nothing technically wrong with what you have, for readability you will want to stick with consistent naming. It will help yourself and other developers keep track of what data is coming from where.

  • For your likeToggleFunc, I would name it something like onLikeToggle or onLike, this more readily communicates that the function will be used for an event.

  • You hard coded the local sender here, but what could be do to make this more dynamic? What would happen here if the messages no longer with Vladimir?

@omatesh

/>
</main>
</div>
);
};

export default App;
export default App;
76 changes: 45 additions & 31 deletions src/components/ChatEntry.css
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
button {
background: none;
color: inherit;
border: none;
padding: 10px;
font: inherit;
cursor: pointer;
outline: inherit;
color: inherit;
border: none;
padding: 10px;
font: inherit;
cursor: pointer;
outline: inherit;
}

.chat-entry {
display: flex; /* Flex container for horizontal alignment */
margin: 1rem;
flex-direction: column; /* Stack name on top of bubble */
}

.chat-entry:last-child {
margin-bottom: 0;
.chat-entry.local {
justify-content: flex-start; /* Align entire entry left */
align-items: flex-start; /* Align items to the start horizontally */
}

.chat-entry.remote {
justify-content: flex-end; /* Align entire entry right */
align-items: flex-end; /* Align items to the end horizontally */
}

/* Sender name styling with full width and alignment */
.chat-entry .entry-name {
font-size: medium;
margin-bottom: 0.5rem;
width: 100%;
}

.chat-entry.local .entry-name {
text-align: left;
}

.chat-entry.remote .entry-name {
text-align: right;
}

/* Remove any margin on bubbles for flex alignment */
.chat-entry.local .entry-bubble,
.chat-entry.remote .entry-bubble {
margin-left: 0;
margin-right: 0;
}

.chat-entry .entry-bubble {
Expand All @@ -30,11 +60,6 @@ button {
background-color: #fefea2;
}

.chat-entry .entry-name {
font-size: medium;
margin-bottom: 0.5rem;
}

.chat-entry .entry-time {
color: #bbb;
font-size: x-small;
Expand All @@ -48,18 +73,17 @@ button {
height: 22px;
width: 44px;
clip-path: polygon(100% 0, 0 0, 50% 100%);

position: absolute;
top: 0;
}

/* "local" messages are shown on the left side */
.chat-entry.local {
text-align: left;
/* "local" message bubble arrow and hover */
.chat-entry.local .entry-bubble {
background-color: #ffffe0;
}

.chat-entry.local .entry-time {
text-align: right;
.chat-entry.local .entry-bubble:hover {
background-color: #fefea2;
}

.chat-entry.local .entry-bubble::before {
Expand All @@ -71,30 +95,20 @@ button {
background-color: #fefea2;
}

/* "remote" messages are shown on the right side, in blue */
.chat-entry.remote {
text-align: right;
}

/* "remote" messages shown on the right side in blue */
.chat-entry.remote .entry-bubble {
background-color: #e0ffff;
margin-left: auto;
margin-right: 0;
}

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

.chat-entry.remote .entry-time {
text-align: left;
}

.chat-entry.remote .entry-bubble::before {
background-color: #e0ffff;
right: -18px;
}

.chat-entry.remote .entry-bubble:hover::before {
background-color: #a9f6f6;
}
}
29 changes: 22 additions & 7 deletions src/components/ChatEntry.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import PropTypes from 'prop-types';
import './ChatEntry.css';
import TimeStamp from './TimeStamp';

const ChatEntry = (props) => {
const likeButtonClicked = () => {
props.likeToggleFunc(props.id);
};
Comment on lines +6 to +8

Choose a reason for hiding this comment

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

⭐️


const ChatEntry = () => {
return (
<div className="chat-entry local">
<h2 className="entry-name">Replace with name of sender</h2>
<div className={props.senderType === 'local' ? 'chat-entry local' : 'chat-entry remote'}>

Choose a reason for hiding this comment

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

You could also assign this ternary logic to a variable before your return statement for readability. However, note that you have very similar logic here and in ChatLog.jsx. How could you refactor this logic to DRY up your components? @omatesh

<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">Time Sent: <TimeStamp time={props.timeStamp} /></p>

Choose a reason for hiding this comment

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

👍🏿

<button className="like" onClick={likeButtonClicked}>
{props.liked ? '❤️' : '🤍'}
Comment on lines +16 to +17

Choose a reason for hiding this comment

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

The button function could also be implemented like so:

<button  className='like'  onClick={()  => props.likeToggleFunc(props.id)}>{liked  ?  '❤️':  '🤍'}</button>

</button>
</section>
</div>
);
};

ChatEntry.propTypes = {
// Fill with correct proptypes
id: PropTypes.number.isRequired,
sender: PropTypes.string.isRequired,
senderType: PropTypes.oneOf(['local', 'remote']).isRequired,

Choose a reason for hiding this comment

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

🫡

body: PropTypes.string.isRequired,
timeStamp: PropTypes.string.isRequired,
liked: PropTypes.bool.isRequired,
likeToggleFunc: PropTypes.func.isRequired,
};

export default ChatEntry;

3 changes: 3 additions & 0 deletions src/components/ChatLog.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
margin: auto;
max-width: 50rem;
}
.chat-log ul {
list-style: none;
}
46 changes: 46 additions & 0 deletions src/components/ChatLog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ChatEntry from './ChatEntry';
import './ChatLog.css';
import PropTypes from 'prop-types';

const ChatLog = (props) => {
const messageComponents = props.entries.map((message) => {

Choose a reason for hiding this comment

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

Once again, the naming here is switching back and forth between entries and message. I would stick with one for consistency.

const senderType = message.sender === props.localSender ? 'local' : 'remote';

Choose a reason for hiding this comment

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

👍🏿


return (
<li key={message.id}>
<ChatEntry
id={message.id}
sender={message.sender}
senderType={senderType}
body={message.body}
timeStamp={message.timeStamp}
liked={message.liked}
likeToggleFunc={props.likeToggleFunc}
/>
</li>
Comment on lines +10 to +20

Choose a reason for hiding this comment

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

Since the names of the keys on entry are the same as the names your are setting on ChatEntry attributes/you are using all of the values in entry you could do something something like this to save you a few keystrokes:

const  chatComponents  =  entries.map((entry)  =>  {
  return(
    <ChatEntry
	{...entry}
	onLikeToggle={onLikeBtnToggle}
	key={entry.id}
	/>
  );
});

Though, what you have is more verbose! The suggestion I have above can be used when we want to use pass every key/value pair to the child component. If not, then we wouldn't follow the suggested pattern since it would be passing impertinent information to a component, putting it at risk to accidentally change a piece and be more bug prone.

);
});

return (
<section className="chat-log">
<ul>{messageComponents}</ul>

Choose a reason for hiding this comment

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

👍🏿

</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,
likeToggleFunc: PropTypes.func.isRequired,
localSender: PropTypes.string.isRequired,

};
Comment on lines +31 to +44

Choose a reason for hiding this comment

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

Nice work including prop types for this component! Just a quick reminder: in React v19, PropTypes are deprecated. Moving forward, it's best to use TypeScript for type checking, as it's the recommended and more robust solution for ensuring type safety across your application.


export default ChatLog;