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
9 changes: 9 additions & 0 deletions .firebase/hosting.YnVpbGQ.cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
asset-manifest.json,1675364990843,dece47109c0ca1e64c1b5e94618f11bfa9d69585a6a8c297d281df22928b0ec7
manifest.json,1675364988458,5fb9316df9fcb631af8259f9a91b476804b4d68258e1c4144017c018f2b63aa3
index.html,1675364990843,d7d5a09cf7feab5366cf2d0dbe3c9989a15fe8749268439a902cd564b3e09f40
favicon.ico,1675364988457,b72f7455f00e4e58792d2bca892abb068e2213838c0316d6b7a0d6d16acd1955
static/js/main.95edea91.js.LICENSE.txt,1675364990845,65c6ecb6192f97a7b3e674b68ee6129941ddc83db480b46ce95eaea24ff40121
static/css/main.a6e8f088.css.map,1675364990845,12abce3ad8340f964b204829f9fb593ab92dd96d24cd074a8f1380636efd11bb
static/css/main.a6e8f088.css,1675364990845,76e1ca20fd6535a8bba07534184f8a7a6eba7698a209b025cca6fb5c763df7fb
static/js/main.95edea91.js,1675364990845,11186382d3ddf88d44e43905aeb9c485a2e600a7316fd34fc3241184d679ea57
static/js/main.95edea91.js.map,1675364990845,c45e64aadb10424bf980a4a465c3c7225448e3129425eca8eb25e3486b390fd8
5 changes: 5 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "react-chatlog"
}
}
10 changes: 10 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
25 changes: 21 additions & 4 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

#App {
background-color: #87cefa;
}
Expand All @@ -11,6 +12,7 @@
z-index: 100;
text-align: center;
align-items: center;

}

#App main {
Expand All @@ -26,31 +28,46 @@
display: inline-block;
}

#App h3 {
margin-block-start: 0.2em;
margin-block-end: 0.2em;
}


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

#header-container {
color: #222;
display: flex;
justify-content: space-evenly;
margin: auto;
max-width: 50rem;
}

#App .widget {
display: inline-block;
line-height: 0.5em;
line-height: 1em;
border-radius: 10px;
color: black;
font-size:0.8em;
font-size: 0.8em;
padding-left: 1em;
padding-right: 1em;
font-weight: bold;
}

#App #heartWidget {
font-size: 1.5em;
margin: 1em
margin: 1em;
}

#App span {
display: inline-block
}

.red {
color: #b22222
color: #b22222
}

.orange {
Expand Down
88 changes: 81 additions & 7 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,92 @@
import React from 'react';
import React, {useState} from 'react';
import './App.css';
import chatMessages from './data/messages.json';
import MESSAGES from './data/messages.json';
import ChatLog from './components/ChatLog';
import ColorButtons from './components/ColorButtons'

let senders = []; //MESSAGES can have many senders
for (let message of MESSAGES) {
if (!senders.includes(message.sender)){
senders.push(message.sender);
};
};
//This chatLog has only two people
const localSender = senders[0];
const remoteSender = senders[1];
let initialLiked = 0;
for (let message of MESSAGES) {
if (message.liked){
initialLiked++
};
};

const App = () => {
const [chatMessages, setChatMessages] = useState(MESSAGES);
const [likesCount, setLikesCount] = useState(initialLiked);

Choose a reason for hiding this comment

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

Since the liked status of a message lives in the chatMessages data we should avoid holding an extra piece of state that we need to manually keep in sync. We can remove likesCount declared on line 25 and use a higher order function like array.reduce to take our list of messages and reduce it down to a single value (our like count).

// This could be returned from a helper function
// totalLikes is a variable that accumulates a value as we loop over each entry in chatEntries
const likesCount = chatMessages.reduce((totalLikes, currentMessage) => {
    // If currentMessage.liked is true add 1 to totalLikes, else add 0
    return (totalLikes += currentMessage.liked ? 1 : 0);
}, 0); // The 0 here sets the initial value of totalLikes to 0

const [localColor, setLocalColor] = useState('black');
const [remoteColor, setRemoteColor] = useState('black');

const updateChats = (updatedChat) => {
const chats = chatMessages.map((chat) => {
if (updatedChat.id === chat.id){
return updatedChat;
} else {
return chat;
}

});
setChatMessages(chats);
let likes = 0;
for (const chat of chats){
if (chat.liked){
likes++
}
};
setLikesCount(likes);
}

const handleLocalColor = (changedColor) => {
setLocalColor(changedColor)}

const handleRemoteColor = (changedColor) => {
setRemoteColor(changedColor);
}

return (
<div id="App">
<section id="App">
<header>
<h1>Application title</h1>
<h1>
{`Chat between `}
<span className={`${localColor}`}>{localSender}</span>
{` and `}
<span className={`${remoteColor}`}>{remoteSender}</span>
</h1>
<section>
<section id='header-container'>
<section id='header-local'>
<h3 className={localColor}>{localSender}'s color</h3>
<ColorButtons onChange={handleLocalColor} />
</section>
<span className='widget' id='heartWidget'>{likesCount} ❤️s</span>
<section id='header-remote'>
<h3 className={remoteColor}>{remoteSender}'s color</h3>
<ColorButtons onChange={handleRemoteColor} />
</section>
</section>
</section>
</header>
<main>
{/* Wave 01: Render one ChatEntry component
Wave 02: Render ChatLog component */}
<section>
{<ChatLog
entries={chatMessages}
onUpdate={updateChats}
localColor={localColor}
remoteColor={remoteColor}
localSender={localSender}
/>}
</section>
</main>
</div>
</section>
);
};

Expand Down
1 change: 1 addition & 0 deletions src/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Wave 03: clicking like button and rendering App', () => {
// Arrange
const { container } = render(<App />)
const buttons = container.querySelectorAll('button.like')
console.log(buttons)

Choose a reason for hiding this comment

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

Gentle reminder that we want to take a scan through our files and remove diagnostic/debugging code before opening PRs.

const firstButton = buttons[0]
const lastButton = buttons[buttons.length - 1]

Expand Down
35 changes: 28 additions & 7 deletions src/components/ChatEntry.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
import React from 'react';
import '../App.css';
import './ChatEntry.css';
import PropTypes from 'prop-types';
import TimeStamp from './TimeStamp';

const ChatEntry = ({id, sender, body, timeStamp, liked, onUpdate, color, location}) => {

const toggleHeart = () => {
return onUpdate({
id,
sender,
body,
timeStamp,
liked: !liked,
})
};
Comment on lines +9 to +17

Choose a reason for hiding this comment

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

I would consider passing the id of the message clicked to onUpdate and having the App code handle the new object creation. When ChatEntry creates the new object for the App state, it takes some responsibility for managing those contents. If we want the responsibility of managing the state to live solely with App, we would want it to handle defining the new message object.

This made me think of a related concept in secure design for APIs. Imagine we had an API for creating and updating messages, and it has an endpoint /<msg_id>/like meant to update a true/false liked value. We could have that endpoint accept a body in the request and let the user send an object with data for the message's record (similar to passing a message object from ChatEntry to App), but the user could choose to send any data for those values. If the endpoint only takes in an id and handles updating the liked status for the message itself, there is less opportunity for user error or malicious action.


const ChatEntry = (props) => {
return (
<div className="chat-entry local">
<h2 className="entry-name">Replace with name of sender</h2>
<div className={`chat-entry ${location}`}>
<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 className={`${color}`}>{body}</p>
<p className="entry-time">
<TimeStamp time = {timeStamp}/></p>
<button className="like" onClick={toggleHeart}>{liked? '❤️' : '🤍'}</button>
</section>
</div>
);
};

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

export default ChatEntry;
16 changes: 8 additions & 8 deletions src/components/ChatEntry.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import ChatEntry from "./ChatEntry";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import ChatEntry from './ChatEntry';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

describe("Wave 01: ChatEntry", () => {
describe('Wave 01: ChatEntry', () => {
beforeEach(() => {
render(
<ChatEntry
Expand All @@ -14,15 +14,15 @@ describe("Wave 01: ChatEntry", () => {
);
});

test("renders without crashing and shows the sender", () => {
test('renders without crashing and shows the sender', () => {
expect(screen.getByText(/Joe Biden/)).toBeInTheDocument();
});

test("that it will display the body", () => {
test('that it will display the body', () => {
expect(screen.getByText(/Get out by 8am/)).toBeInTheDocument();
});

test("that it will display the time", () => {
test('that it will display the time', () => {
expect(screen.getByText(/\d+ years ago/)).toBeInTheDocument();
});
});
54 changes: 54 additions & 0 deletions src/components/ChatLog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import ChatEntry from './ChatEntry';
import './ChatLog.css';

const ChatLog = ({entries, onUpdate, localColor, remoteColor, localSender}) => {
return <ul className="chat-log">
{entries.map((entry) => (

Choose a reason for hiding this comment

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

It is a little hard to understand the structure of the JSX with the mapping embedded here, I suggest pulling the mapping out and storing the result of it in a variable that you can then place in the JSX.

(entry.sender === localSender)?
<ChatEntry
// key={entry.id}
key={entry.timeStamp}
id={entry.id}
sender={entry.sender}
body={entry.body}
timeStamp={entry.timeStamp}
liked={entry.liked}
onUpdate = {onUpdate}
color={localColor}
location='local'
/> :
<ChatEntry
// key={entry.id}
key={entry.timeStamp}
id={entry.id}
sender={entry.sender}
body={entry.body}
timeStamp={entry.timeStamp}
liked={entry.liked}
onUpdate = {onUpdate}
color={remoteColor}
location='remote'
/>
Comment on lines +9 to +33

Choose a reason for hiding this comment

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

There's a lot of duplication between the 2 ChatEntry components, we could create a couple variables above where we create the ChatEntry to determine the color and location values which would let us keep just one of the ChatEntry declarations.

let color = remoteColor
let location = 'remote'
if (entry.sender === localSender) {
    color = localColor
    location = 'local'
}

<ChatEntry
    // key={entry.id}
    key={entry.timeStamp}
    id={entry.id}       
    sender={entry.sender}
    body={entry.body}
    timeStamp={entry.timeStamp}
    liked={entry.liked}
    onUpdate = {onUpdate}
    color={color}
    location={location}
/>

))}
</ul>
};

ChatLog.propTypes = {
entries: PropTypes.arrayOf(
PropTypes.shape({

Choose a reason for hiding this comment

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

Really nice use of PropTypes.

id:PropTypes.number,
sender:PropTypes.string.isRequired,
body:PropTypes.string.isRequired,
timeStamp:PropTypes.string.isRequired,
liked:PropTypes.bool,
})
).isRequired,
onUpdate: PropTypes.func,
localColor: PropTypes.string,
remoteColor: PropTypes.string,
localSender: PropTypes.string,
};

export default ChatLog;
Loading