-
Notifications
You must be signed in to change notification settings - Fork 35
C24 Possum Ekaterina Belova Chat log #25
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
base: main
Are you sure you want to change the base?
Changes from all commits
d91b1a0
202327b
d8330d5
c1db4dd
c12ceb4
323c4e0
14b8af9
5d6daa3
d5138f8
600f8dd
c7445dc
deddd0e
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 |
|---|---|---|
| @@ -1,14 +1,53 @@ | ||
| import './App.css'; | ||
| import { useState } from 'react'; | ||
| import ChatLog from './components/ChatLog.jsx'; | ||
| import messages from './data/messages.json'; | ||
| import Header from './components/Header.jsx'; | ||
|
|
||
| const App = () => { | ||
| const [entries, setEntries] = useState(messages); | ||
| const [fontColorForLocalSender, setFontColorForLocalSender] = useState('black'); | ||
| const [fontColorForRemoteSender, setFontColorForRemoteSender] = useState('black'); | ||
| const localSender = 'Vladimir'; | ||
|
|
||
| const totalLikes = entries.reduce((total, entry) => { | ||
| return entry.isLiked ? total + 1 : total; | ||
| }, 0); | ||
|
|
||
| const toggleHeart = (entryId) => { | ||
| setEntries(entries => { | ||
| return entries.map(entry => { | ||
|
Comment on lines
+18
to
+19
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. Nice job using the callback-style for updating the state. |
||
| if (entry.id === entryId) { | ||
| return { ...entry, isLiked: !entry.isLiked }; | ||
| } else { | ||
| return entry; | ||
| } | ||
| }); | ||
| }); | ||
| }; | ||
|
|
||
| const chooseFontColor = (senderName, color) => { | ||
| if (senderName === localSender) { | ||
| setFontColorForLocalSender(color); | ||
| } else { | ||
| setFontColorForRemoteSender(color); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div id="App"> | ||
| <header> | ||
| <h1>Application title</h1> | ||
| </header> | ||
| <Header | ||
| totalLikes={totalLikes} | ||
| chooseFontColor={chooseFontColor} | ||
| /> | ||
| <main> | ||
| {/* Wave 01: Render one ChatEntry component | ||
| Wave 02: Render ChatLog component */} | ||
| <ChatLog | ||
| entries={entries} | ||
| localSender={localSender} | ||
| onToggleHeart={toggleHeart} | ||
| fontColorForLocalSender={fontColorForLocalSender} | ||
| fontColorForRemoteSender={fontColorForRemoteSender} | ||
| /> | ||
| </main> | ||
| </div> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,21 +1,40 @@ | ||||||||||||
| import './ChatEntry.css'; | ||||||||||||
| import PropTypes from 'prop-types'; | ||||||||||||
| import TimeStamp from './TimeStamp'; | ||||||||||||
|
|
||||||||||||
| const ChatEntry = (props) => { | ||||||||||||
| const isLocal = props.sender === props.localSender; | ||||||||||||
|
Comment on lines
+5
to
+6
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. Destructuring Destructuring props also has some benefits:
Ultimately, you'll adapt what you do depending on what your team does.
Suggested change
|
||||||||||||
| const entryClass = isLocal ? 'local' : 'remote'; | ||||||||||||
| const isLiked = props.isLiked ? '❤️' : '🤍'; | ||||||||||||
|
||||||||||||
| const isLikedButtonClicked = () => { | ||||||||||||
| props.onToggleHeart(props.id); | ||||||||||||
| }; | ||||||||||||
|
Comment on lines
+9
to
+11
|
||||||||||||
|
|
||||||||||||
| const fontColor = isLocal ? props.fontColorForLocalSender : props.fontColorForRemoteSender; | ||||||||||||
|
|
||||||||||||
| 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 ${entryClass}`}> | ||||||||||||
| <h2 className="entry-name">{props.sender}</h2> | ||||||||||||
| <section className="entry-bubble"> | ||||||||||||
|
Comment on lines
+17
to
18
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. While not strictly required by the HTML specification, it is a strong best practice and highly recommended that You could put this |
||||||||||||
| <p>Replace with body of ChatEntry</p> | ||||||||||||
| <p className="entry-time">Replace with TimeStamp component</p> | ||||||||||||
| <button className="like">🤍</button> | ||||||||||||
| <p className={`entry-body ${fontColor}`}>{props.body}</p> | ||||||||||||
| <div className="entry-attributes"> | ||||||||||||
| <p className="entry-time"> | ||||||||||||
| <TimeStamp time={props.timeStamp} /> | ||||||||||||
|
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. Nice job using the supplied |
||||||||||||
| </p> | ||||||||||||
| <button className="like" onClick={isLikedButtonClicked}>{isLiked}</button> | ||||||||||||
|
||||||||||||
| </div> | ||||||||||||
| </section> | ||||||||||||
| </replace-with-relevant-semantic-element> | ||||||||||||
| </article> | ||||||||||||
| ); | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| ChatEntry.propTypes = { | ||||||||||||
| // Fill with correct proptypes | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| export default ChatEntry; | ||||||||||||
|
|
||||||||||||
| ChatEntry.propTypes = { | ||||||||||||
| sender: PropTypes.string.isRequired, | ||||||||||||
| body: PropTypes.string.isRequired, | ||||||||||||
| timeStamp: PropTypes.string.isRequired, | ||||||||||||
| localSender: PropTypes.string, | ||||||||||||
| isLiked: PropTypes.bool, | ||||||||||||
| onToggleHeart: PropTypes.func, | ||||||||||||
|
||||||||||||
| onToggleHeart: PropTypes.func, | |
| onToggleHeart: PropTypes.func, | |
| id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, | |
| fontColorForLocalSender: PropTypes.string.isRequired, | |
| fontColorForRemoteSender: PropTypes.string.isRequired, |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,39 @@ | ||||||||||
| import './ChatLog.css'; | ||||||||||
| import ChatEntry from './ChatEntry'; | ||||||||||
| import PropTypes from 'prop-types'; | ||||||||||
|
|
||||||||||
| const ChatLog = ({entries, localSender, onToggleHeart, fontColorForLocalSender, fontColorForRemoteSender}) => { | ||||||||||
|
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. Nice work destructuring your props here 👍 |
||||||||||
| return ( | ||||||||||
| <section className="chat-log"> | ||||||||||
| {entries.map((entry) => ( | ||||||||||
|
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. Another common way you'll see components organized using map is to store the result into a variable we use in the JSX result: const chatEntries = entries.map((entry) => {
return (
<ChatEntry
// your props
/>
);
});
return <section className="chat-log">{chatEntries}</section>; |
||||||||||
| <ChatEntry | ||||||||||
| key={entry.id} | ||||||||||
|
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. 👍 The |
||||||||||
| id={entry.id} | ||||||||||
| sender={entry.sender} | ||||||||||
| body={entry.body} | ||||||||||
| timeStamp={entry.timeStamp} | ||||||||||
| localSender={localSender} | ||||||||||
| isLiked={entry.isLiked} | ||||||||||
| onToggleHeart={onToggleHeart} | ||||||||||
| fontColorForLocalSender={fontColorForLocalSender} | ||||||||||
| fontColorForRemoteSender={fontColorForRemoteSender} | ||||||||||
| /> | ||||||||||
| ))} | ||||||||||
| </section> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| export default ChatLog; | ||||||||||
|
|
||||||||||
| ChatLog.propTypes = { | ||||||||||
| entries: PropTypes.arrayOf( | ||||||||||
| PropTypes.shape({ | ||||||||||
| id: PropTypes.number.isRequired, | ||||||||||
| sender: PropTypes.string.isRequired, | ||||||||||
| body: PropTypes.string.isRequired, | ||||||||||
| timeStamp: PropTypes.string.isRequired, | ||||||||||
| }) | ||||||||||
| ).isRequired, | ||||||||||
| localSender: PropTypes.string, | ||||||||||
| onToggleHeart: PropTypes.func, | ||||||||||
|
||||||||||
| onToggleHeart: PropTypes.func, | |
| onToggleHeart: PropTypes.func, | |
| fontColorForLocalSender: PropTypes.string, | |
| fontColorForRemoteSender: PropTypes.string, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| #App header { | ||
| background-color: #2c2c2c; | ||
| color: #fff; | ||
| padding-bottom: 0.5rem; | ||
| position: fixed; | ||
| width: 100%; | ||
| z-index: 100; | ||
| text-align: center; | ||
| align-items: center; | ||
| } | ||
|
|
||
| #App h1 { | ||
| font-size: 1.5em; | ||
| text-align: center; | ||
| display: inline-block; | ||
| } | ||
|
|
||
| #App header section { | ||
| background-color: #e0ffff; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||||||||||||||
| import { useState } from 'react'; | ||||||||||||||||||
| import './Header.css'; | ||||||||||||||||||
| import HeartCounter from './HeartCounter.jsx'; | ||||||||||||||||||
| import SenderColorPicker from './SenderColorPicker.jsx'; | ||||||||||||||||||
|
|
||||||||||||||||||
| const Header = ({ totalLikes, chooseFontColor }) => { | ||||||||||||||||||
| const [localColor, setLocalColor] = useState('white'); | ||||||||||||||||||
| const [remoteColor, setRemoteColor] = useState('white'); | ||||||||||||||||||
|
Comment on lines
+7
to
+8
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. Instead of using 2 pieces of state to track colors for the local and remote sender, we can use one piece with an object for the initial state. Wrt to state, it's beneficial to evaluate what pieces of state do we really need and how can we simplify state so that our project won't be overly complicated.
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| const changeFontColor = (senderName, color) => { | ||||||||||||||||||
| if (senderName === 'Vladimir') { | ||||||||||||||||||
| setLocalColor(color); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| setRemoteColor(color); | ||||||||||||||||||
| } | ||||||||||||||||||
| chooseFontColor(senderName, color); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <header> | ||||||||||||||||||
| <h1> | ||||||||||||||||||
| Chat between <span className={localColor}>Vladimir</span> and <span className={remoteColor}>Estragon</span> | ||||||||||||||||||
|
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. Can you think of a way to dynamically get the senders' names without hardcoding "Vladimir" and "Estragon" here? |
||||||||||||||||||
| </h1> | ||||||||||||||||||
|
|
||||||||||||||||||
| <section> | ||||||||||||||||||
| <SenderColorPicker senderName="Vladimir" chooseFontColor={changeFontColor} /> | ||||||||||||||||||
| <HeartCounter totalLikes={totalLikes} /> | ||||||||||||||||||
| <SenderColorPicker senderName="Estragon" chooseFontColor={changeFontColor} /> | ||||||||||||||||||
| </section> | ||||||||||||||||||
| </header> | ||||||||||||||||||
| ); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export default Header; | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||
| #App .widget { | ||||||
| display: inline-block; | ||||||
| line-height: 0.5em; | ||||||
| border-radius: 10px; | ||||||
| color: black; | ||||||
| font-size: 0.8em; | ||||||
| padding-left: 1em; | ||||||
| padding-right: 1em; | ||||||
| } | ||||||
|
|
||||||
| #App #heartWidget { | ||||||
| font-size: 1.5em; | ||||||
| margin: 1em; | ||||||
| } | ||||||
|
|
||||||
| #App span { | ||||||
|
||||||
| #App span { | |
| #App #heartWidget span { |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSS rules defined here (.widget, #heartWidget, span) appear to be component-specific styles but are placed in HeartCounter.css. Since these styles are shared across multiple components (HeartCounter and potentially SenderColorPicker use .widget class), consider moving these to a shared stylesheet or the main App.css file to avoid duplication and maintain consistency.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import './HeartCounter.css'; | ||
|
|
||
| const HeartCounter = ({ totalLikes }) => { | ||
| return ( | ||
| <div className="widget" id="heartWidget"> | ||
| {totalLikes} ❤️s | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default HeartCounter; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| span { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: inline-block | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+3
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .red { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: #b22222 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .orange { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: #e6ac00 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .yellow { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: #e6e600 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .green { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: green | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .blue { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: blue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .purple { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: purple | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .black { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+29
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .red { | |
| color: #b22222 | |
| } | |
| .orange { | |
| color: #e6ac00 | |
| } | |
| .yellow { | |
| color: #e6e600 | |
| } | |
| .green { | |
| color: green | |
| } | |
| .blue { | |
| color: blue | |
| } | |
| .purple { | |
| color: purple | |
| } | |
| .black { | |
| .senderColorPicker .red { | |
| color: #b22222 | |
| } | |
| .senderColorPicker .orange { | |
| color: #e6ac00 | |
| } | |
| .senderColorPicker .yellow { | |
| color: #e6e600 | |
| } | |
| .senderColorPicker .green { | |
| color: green | |
| } | |
| .senderColorPicker .blue { | |
| color: blue | |
| } | |
| .senderColorPicker .purple { | |
| color: purple | |
| } | |
| .senderColorPicker .black { |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The selector '.senderColorPicker' uses camelCase but doesn't match the actual class name 'sender-color-picker' (kebab-case) used in the JSX component. This CSS rule will not apply to any elements. The selector should be '.sender-color-picker span' to match the component's class name.
| .senderColorPicker span { | |
| .sender-color-picker span { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❗️👀 The same concept (“what color does each sender use?”) is being represented in 3 components (
App,Header, andSenderColorPicker), which is a strong signal that state placement needs to be cleaned up. There is no longer a single source of truth, but 3 sources of truth!Again, my eyebrows were raised when I saw another place where there is state that is related to color.
Now that I see how the 3 components work together, I see that
SenderColorPickeronly needs to trigger a change. It does not need to “own” or persist the color itself andHeaderdoesn't need to either.When I think about
App:Headershould:- Have no color state (remove it)
SenderColorPickershould:- Have no color state (remove it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧠 We should identify all the components need to know about color and identify their closest common ancestor.
We should also really consider what each component should know and it should be responsible for. This is why we put so much emphasis on pre-planning your components during the activity related to task-list-front-end (where the first wave was looking at a mockup and identifying what components you needed and how data should flow between the components).
For future projects it will be critical to do this kind of planning for your components before you start writing code so you don't create many different sources of truth by introducing redundant pieces of state.