Skip to content
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

Happy Thoughts 😊 Joyce Kuo #105

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
26 changes: 7 additions & 19 deletions README.md
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for sharing your process! I can relate to getting stuck with details 😅

Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,18 @@

# Happy thoughts Project

In this week's project, you'll be able to practice your React state skills by fetching and posting data to an API.
This project allows users to post happy thoughts, view the most recent posts, and like posts by clicking a button. It uses the useEffect hook to handle side effects such as fetching recent posts from an API and updating the time since each thought was posted in real time.

## Getting Started with the Project

### Dependency Installation & Startup Development Server

Once cloned, navigate to the project's root directory and this project uses npm (Node Package Manager) to manage its dependencies.
### The Problem

The command below is a combination of installing dependencies, opening up the project on VS Code and it will run a development server on your terminal.
I tried to keep it simple and keep the focus on managing state and lifecycle events, rather than getting distracted with design issues like which font to use. In some ways having a design to copy helps to streamline the design process, but having to work within certain limitations can also feel like a challenge. (I actually started to look into tools that could help me identify fonts, but then backtracked and decided I needed to have a working app first. 😅)

```bash
npm i && code . && npm run dev
```
I did request some help from ChatGPT for making the function to display the time lapsed since a happy thought was posted, but after looking at the code I felt like I could have come up with the function on my own.

### The Problem
One challenge I didn't expect was how to get the accessibility score in Lighthouse to be 95 or higher when the design showed a light gray text on a white background. I tried making the font bigger and bolder, but would still be just under the accesibility score requirement. In the end, I decided accessibility was a higher priority than copying the design perfectly, so I had to settle for a darker gray font color.

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
If I had more time, I would love to try to tackle the stretch goals, like triggering an animation when submitting a new thought.

### View it live

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.

## Instructions

<a href="instructions.md">
See instructions of this project
</a>
https://happy-thoughts-joyce.netlify.app/
67 changes: 66 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
import { useState, useEffect } from 'react';
import SubmitForm from './components/SubmitForm';
import { HappyThought } from './components/HappyThought';

const URL = 'https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts';

export const App = () => {
return <div>Find me in src/app.jsx!</div>;
const [thoughts, setThoughts] = useState([]);

// Fetch the most recent thoughts
useEffect(() => {
const fetchThoughts = async () => {
try {
const response = await fetch(URL);
const result = await response.json();
setThoughts(result); // Sets the array of 20 latest thoughts
} catch (error) {
console.error('Error fetching thoughts:', error);
}
};
fetchThoughts();
}, []);

// Adds a new thought to the list
const addThought = (newThought) => {
setThoughts((prevThoughts) => [newThought, ...prevThoughts.slice(0, 19)]);
};

// Like a thought
const handleLike = async (thoughtId) => {
const likeURL = `${URL}/${thoughtId}/like`;
try {
const response = await fetch(likeURL, {
method: 'POST',
})
if (response.ok) {
// Update hearts count for liked thought
setThoughts((prevThoughts) =>
prevThoughts.map((thought) =>
thought._id === thoughtId ? { ...thought, hearts: thought.hearts + 1 } : thought
)
);
}
} catch (error) {
console.error('Error liking the thought:', error);
}
};

return (
<div className="App">
<h1>Happy Thoughts 😊</h1>
<div className="content">
<SubmitForm onSubmit={addThought} />
<div className="HappyThoughts">
{thoughts.map((thought) => (
<HappyThought
key={thought._id}
thought={thought}
onLike={handleLike}
/>
))}
</div>
</div>
</div>
);
};

export default App;
63 changes: 63 additions & 0 deletions src/components/HappyThought.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';

// Function to calculate time difference
const timeAgo = (createdAt) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

But could maybe be abstracted into a utils folder?

const now = new Date();
const createdTime = new Date(createdAt);
const timeDiff = now - createdTime;

const seconds = Math.floor(timeDiff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (days > 0) {
return `${days} day${days > 1 ? 's' : ''} ago`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
} else {
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
}
};

export const HappyThought = ({ thought, onLike }) => {
// Track how long ago the thought was posted
const [timeSincePosted, setTimeSincePosted] = useState(timeAgo(thought.createdAt));

// Track whether the heart button has been clicked
const [isClicked, setIsClicked] = useState(false);

// Update time difference every minute
useEffect(() => {
const interval = setInterval(() => {
setTimeSincePosted(timeAgo(thought.createdAt));
}, 60000); // Update every 60 seconds
return () => clearInterval(interval);
}, [thought.createdAt]);

const handleLikeClick = () => {
if (!isClicked) {
onLike(thought._id); // Trigger like functionality
setIsClicked(true); // Set the clicked state to true to persist the clicked state
}
};


return (
<div className="happy-thought">
<p>{thought.message}</p>
<div className="thought-actions">
<button
className={`heart-button ${isClicked ? 'liked' : ''}`}// Change color if liked
onClick={handleLikeClick}
>🩷</button>
<span className="likes-count">x {thought.hearts}</span>
<span className="time-posted">{timeSincePosted}</span>
</div>
</div>
);
};


60 changes: 60 additions & 0 deletions src/components/SubmitForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react';
import PropTypes from 'prop-types';

export const SubmitForm = ({ onSubmit }) => {

const [thought, setThought] = useState('');
const [error, setError] = useState('');

const handleSubmit = async (event) => {
event.preventDefault();

const URL = 'https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts';

// Validate message length
if (thought.length < 5 || thought.length > 140) {
Copy link
Contributor

Choose a reason for hiding this comment

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

setError('Message must be between 5 and 140 characters.');
return;
}

try {
const res = await fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: thought }),
});
const newThought = await res.json();
onSubmit(newThought); // Passes new thought to App.jsx
setThought(''); // Clears input field
setError(''); // Clears any previous errors
} catch (error) {
console.log('Error posting thought:', error);
}
};

return (
<form onSubmit={handleSubmit} className="submit-form">
<label htmlFor="thought">What&apos;s making you happy right now?</label>
<textarea
type="text"
id="thought"
value={thought}
rows={2}
className="happy-thought-input"
placeholder="React is making me happy!!"
onChange={(e) => setThought(e.target.value)}
/>
<button type="submit">🩷 Send Happy Thought 🩷</button>
{error && <p className="error-message">{error}</p>}
</form>

);
};

SubmitForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
};

export default SubmitForm;
130 changes: 130 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,133 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

body,
html {
margin: 0;
padding: 2.5%;
width: 100%;
box-sizing: border-box;
}

h1 {
width: 100%;
font-family: 'Courier New', Courier, monospace;
text-align: center;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
margin-bottom: 25px;
}

.happy-thought {
font-weight: bold;
font-family: 'Courier New', Courier, monospace;
background-color: white;
padding: 16px;
border: 2px solid #666;
box-shadow: 5px 5px 0px rgb(0, 0, 0);
box-sizing: border-box;
width: 100%;
margin-bottom: 25px;
}

.submit-form {
display: flex;
flex-direction: column;
gap: 20px;
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
background-color: #e6e6e6;
border: 2px solid #666;
padding: 2%;
box-shadow: 5px 5px 0px rgb(0, 0, 0);
box-sizing: border-box;
width: 100%;
padding: 15px;
margin-bottom: 25px;
}

.happy-thought-input {
font-family: 'Courier New', Courier, monospace;
font-size: 18px;
width: 90%;
padding: 1em;
background-color: #ffffff;
border: 1px solid #bbb;
box-shadow: 2px rgb(0, 0, 0);
resize: none;
}

button {
font-size: 16px;
font-weight: bold;
color: black;
background-color: rgb(248, 170, 170);
border: none;
border-radius: 25px;
width: 250px;
height: 40px;
}

.heart-button {
background-color: #e6e6e6;
font-size: 1.4rem;
cursor: pointer;
border-radius: 50%;
width: 40px;
height: 40px;
margin-right: 10px;
display: flex;
justify-content: center;
align-items: center;
}

.heart-button.liked {
background-color: rgb(248, 170, 170);
}

.likes-count {
font-family: Arial, Helvetica, sans-serif;
color: #666;
font-weight: bold;
}

.thought-actions {
display: flex;
align-items: center;
}

.time-posted {
color: #747474;
font-weight: bolder;
margin-left: auto;
}

button:hover {
background-color: rgb(248, 170, 170);
}

.App {
display: flex;
flex-direction: column;
align-items: center;
}

.content {
font-size: 18px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}

.HappyThoughts {
width: 100%;
box-sizing: border-box;
}

/* Larger screen layout */
@media (min-width: 768px) {
.content {
max-width: 760px;
}
}