-
Notifications
You must be signed in to change notification settings - Fork 145
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 - Zoe #104
base: main
Are you sure you want to change the base?
Happy thoughts - Zoe #104
Changes from all commits
4da5b41
2fb190c
a0cb784
3dbc4e8
91e6fff
219583d
0455e82
8dc15bf
413809e
d86f163
04b0cad
23ff80f
f5af1ff
9dd1545
ec220a0
a15f05e
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,35 +1,35 @@ | ||
<h1 align="center"> | ||
<a href=""> | ||
<img src="/src/assets/happy-thoughts.svg" alt="Project Banner Image"> | ||
</a> | ||
</h1> | ||
|
||
# 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. | ||
In this project, the goal was to create an interactive user interface where users could submit "happy thoughts" (messages) and like each other's thoughts. The challenge was to fetch existing thoughts from an API, post new thoughts, and implement a liking feature, all while keeping the UI responsive and user-friendly. This project was also designed to help practice working with React's state management and communicating with an API using `fetch`. | ||
|
||
## 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 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. | ||
|
||
```bash | ||
npm i && code . && npm run dev | ||
``` | ||
|
||
### The Problem | ||
## Tools and Techniques Used | ||
|
||
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? | ||
- React: I used React for building the user interface and managing state across different components. | ||
- Fetch API: I used the Fetch API to communicate with the Happy Thoughts API, both for fetching and posting data. | ||
- Component Structure: I broke down the app into reusable components (ThoughtForm, ThoughtList, and SingleThought) to maintain code modularity and improve readability. | ||
- React Hooks: I used useState to manage local state and useEffect to fetch data from the API when the component first rendered. | ||
- Optimistic UI Updates: I applied optimistic UI updates to make the app feel more responsive by immediately updating the UI without waiting for the server. | ||
- Error Handling: I added basic error handling to ensure the app could deal with network issues gracefully. | ||
- API File Separation: To keep the code clean, I moved all the API interaction logic into a separate api.js file. This allowed me to centralize API functions and keep the components focused on the UI logic. | ||
|
||
### View it live | ||
### Technologies Used | ||
|
||
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. | ||
- React: For building the user interface | ||
- JavaScript (ES6+): For handling asynchronous API calls and logic | ||
- CSS: For basic styling of the app | ||
- Fetch API: For making HTTP requests to the backend API | ||
- Node.js and npm: For managing dependencies and running the development server | ||
|
||
## Instructions | ||
### View it live | ||
|
||
<a href="instructions.md"> | ||
See instructions of this project | ||
</a> | ||
https://happythoughts-api.netlify.app/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// This function fetches a list of thoughts (messages) from the API. | ||
export const fetchThoughts = async () => { | ||
// Makes a GET request to the "happy-thoughts" API to retrieve all thoughts. | ||
const res = await fetch( | ||
"https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts" | ||
); | ||
// if the request was not successful, throw an error | ||
if (!res.ok) { | ||
throw new Error("Failed to fetch thoughts"); | ||
} | ||
|
||
// Converts the response (which is in JSON format) into a JavaScript object | ||
const data = await res.json(); | ||
|
||
// Sort the fetched thoughts by their creation date, so the most recent ones come first. | ||
return data.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); | ||
}; | ||
|
||
// This function posts a new message (thought) to the API | ||
export const postThought = async (newMessage) => { | ||
// Makes a POST request to the API to create a new thought | ||
// "newMessage" is the user's text/thought | ||
const res = await fetch( | ||
"https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts", | ||
{ | ||
method: "POST", // POST method is used to send new data to the server | ||
headers: { | ||
"Content-Type": "application/json", // Tells the server we're sending JSON data | ||
}, | ||
body: JSON.stringify({ message: newMessage }), // The new thought content, converted to JSON format | ||
} | ||
); | ||
if (!res.ok) { | ||
throw new Error("Failed to post thought"); | ||
} | ||
return await res.json(); | ||
}; | ||
|
||
// This function sends a "like" (heart) for a specific thought by its ID | ||
export const likeThought = async (thoughtId) => { | ||
const res = await fetch( | ||
`https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${thoughtId}/like`, | ||
{ | ||
method: "POST", // POST method is used because we're updating the server by adding a like | ||
} | ||
); | ||
if (!res.ok) { | ||
throw new Error("Failed to like thought"); | ||
} | ||
return await res.json(); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,14 @@ | |
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Happy Thought - Project - Week 7</title> | ||
<title>Happy Thoughts</title> | ||
|
||
<!-- Favicon with heart emoji --> | ||
<link | ||
rel="icon" | ||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%E2%9D%A4%EF%B8%8F%3C/text%3E%3C/svg%3E" | ||
/> | ||
Comment on lines
+8
to
+12
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. Love it! |
||
</head> | ||
<body> | ||
<div id="root"></div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,92 @@ | ||
import { useState, useEffect } from "react"; | ||
import { ThoughtForm } from "./components/ThoughtForm"; | ||
import { ThoughtList } from "./components/ThoughtList"; | ||
import { fetchThoughts, postThought, likeThought } from "../api/api"; // Import API functions | ||
|
||
export const App = () => { | ||
return <div>Find me in src/app.jsx!</div>; | ||
// "thoughts" is the state that stores the list of thoughts, initially set as an empty array. | ||
// "setThoughts" is the function used to update the state. | ||
const [thoughts, setThoughts] = useState([]); | ||
|
||
// Fetch thoughts when the component mounts | ||
useEffect(() => { | ||
const loadThoughts = async () => { | ||
try { | ||
// Fetch thoughts from API | ||
const thoughtsData = await fetchThoughts(); | ||
// Update state with fetched thoughts | ||
setThoughts(thoughtsData); | ||
} catch (error) { | ||
console.error("Error fetching thoughts:", error); // Handle any errors | ||
} | ||
}; | ||
|
||
loadThoughts(); // Call the function to fetch thoughts when the component loads | ||
}, []); // Empty dependency array means the effect only runs once | ||
|
||
// Handles form submission (new thought) | ||
const handleFormSubmit = async (newMessage) => { | ||
// Creates a new thought with a temporary ID (before it gets stored on the server) | ||
const newThought = { | ||
_id: Math.random().toString(36).substring(2, 9), // Generates a random temporary ID for the new thought | ||
message: newMessage, // The actual user's text/message | ||
hearts: 0, // Initialize hearts (likes) to 0 | ||
createdAt: new Date().toISOString(), // Sets the current date and time | ||
}; | ||
|
||
// Optimistically add the new thought to the state so it appears immediately in the UI, before the server stores it in the database | ||
// 'Optimistic UI update' | ||
setThoughts((prevThoughts) => [newThought, ...prevThoughts]); | ||
|
||
try { | ||
// Post the new thought to the server (send it to the backend) | ||
const createdThoughtFromServer = await postThought(newMessage); | ||
// After receiving the response from the server, | ||
// replaces the temporary thought with the real one from the server (which has the correct ID) | ||
setThoughts((prevThoughts) => | ||
prevThoughts.map((thought) => | ||
thought._id === newThought._id ? createdThoughtFromServer : thought | ||
) | ||
); | ||
} catch (error) { | ||
// If error, remove the optimistic thought from state | ||
console.error("Error posting the new thought:", error); | ||
|
||
// checks if the _id of the current thought in the array is different from the newThought._id. If the _id is different, the thought will be included in the new list. If the same, will be excluded | ||
setThoughts((prevThoughts) => | ||
prevThoughts.filter((thought) => thought._id !== newThought._id) | ||
); | ||
} | ||
}; | ||
|
||
// handleLike is called when the user clicks the like (heart) button on a thought | ||
const handleLike = async (thoughtId) => { | ||
try { | ||
// Send a request to the server to "like" the thought by its ID | ||
const updatedThought = await likeThought(thoughtId); // Like thought via API | ||
// Update the state with the new heart count (received from the server) | ||
setThoughts((prevThoughts) => | ||
prevThoughts.map((thought) => | ||
thought._id === updatedThought._id ? updatedThought : thought | ||
) | ||
); | ||
} catch (error) { | ||
console.error("Error liking the thought:", error); | ||
} | ||
}; | ||
|
||
return ( | ||
// A container div to wrap the form and the list | ||
<div> | ||
{/* Render the ThoughtForm component */} | ||
{/* Pass the handleFormSubmit function as a prop called 'onFormSubmit' */} | ||
{/* This allows ThoughtForm to call this function when a new thought is submitted */} | ||
<ThoughtForm onFormSubmit={handleFormSubmit} /> | ||
{/* Render the ThoughtList component */} | ||
{/* Pass the array of thoughts and the handleLike function as props */} | ||
{/* 'thoughts' prop is an array that ThoughtList will use to display the list of thoughts */} | ||
{/* 'onLike' prop is a function that ThoughtList will use when the like button is clicked */} | ||
<ThoughtList thoughts={thoughts} onLike={handleLike} /> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* eslint-disable react/prop-types */ | ||
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. You can do this in an eslint config file as well |
||
|
||
/* Imports a helper function from the 'date-fns' library, | ||
to format a date to show how much time has passed since it occurred. */ | ||
import { formatDistanceToNow } from "date-fns"; | ||
|
||
export const SingleThought = ({ thought, onLike }) => { | ||
return ( | ||
<div className="thought"> | ||
{/* Displays the 'message' property of the 'thought' object, which contains the user's message. */} | ||
<p>{thought.message}</p> | ||
{/* Footer section for the thought, which includes the heart button, likes count, and the time */} | ||
<div className="thought-footer"> | ||
{/* Separate the heart button and likes count */} | ||
<button className="heart-button" onClick={() => onLike(thought._id)}> | ||
{/* Emoji displayed as a heart in the button */} | ||
<span role="img" aria-label="heart"> | ||
❤️ | ||
</span> | ||
</button> | ||
{/* Displays the number of likes the thought has. | ||
- 'thought.hearts' contains the count of likes the thought has received. | ||
*/} | ||
<span className="likes-count">x {thought.hearts}</span> | ||
|
||
<p> | ||
{/* Displays how long ago the thought was posted. | ||
- 'thought.createdAt' is the timestamp when the thought was created. | ||
- 'formatDistanceToNow' from 'date-fns' formats the time, | ||
like "2 minutes ago". | ||
*/} | ||
{formatDistanceToNow(new Date(thought.createdAt), { | ||
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. ⭐ |
||
// adds the "ago" suffix to the time | ||
addSuffix: true, | ||
})} | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { useState } from "react"; | ||
|
||
// eslint-disable-next-line react/prop-types | ||
export const ThoughtForm = ({ onFormSubmit }) => { | ||
// Declare a state variable to hold the user's input | ||
// 'message' is the current value of the input, and 'setMessage' is used to update it | ||
const [message, setMessage] = useState(""); | ||
// Function to handle form submission | ||
const handleSubmit = (event) => { | ||
// Prevent the form's default behavior of reloading the page when submitted | ||
event.preventDefault(); | ||
if (message.length < 5 || message.length > 140) { | ||
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! |
||
alert("Message must be between 5 and 140 characters."); // Show an alert if it's not valid | ||
return;// Stop the function if the message is invalid | ||
} | ||
onFormSubmit(message); | ||
setMessage(""); // Clear the form | ||
}; | ||
|
||
return ( | ||
<form onSubmit={handleSubmit}> | ||
<h1 htmlFor="thought">What’s making you happy right now?</h1> | ||
<input | ||
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 input element is generally used for shorter text input, so maybe you'd wanna use a textare instead? |
||
id="thought" | ||
type="text" | ||
value={message} | ||
onChange={(e) => setMessage(e.target.value)} | ||
placeholder="React is making me happy!" | ||
/> | ||
<button type="submit"> | ||
<span role="img" aria-label="heart"> | ||
❤️ | ||
</span>{" "} | ||
Send Happy Thought{" "} | ||
<span role="img" aria-label="heart"> | ||
❤️ | ||
</span> | ||
</button> | ||
</form> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* eslint-disable react/prop-types */ | ||
// ThoughtList acts as the parent component that passes props to SingleThought | ||
// It receives 'thoughts' and 'onLike' as props from its parent component (App.jsx) | ||
|
||
import { SingleThought } from "./SingleThought"; | ||
|
||
// 'thoughts'= array of thought objects, | ||
// each containing the message, hearts, and creation time | ||
// 'onLike' = a function (callback), handles the likes for a specific thought. | ||
export const ThoughtList = ({ thoughts, onLike }) => { | ||
return ( | ||
<div> | ||
{/* 'thoughts' array is iterated over using the .map() method. | ||
For each thought in the array, it renders a SingleThought component. | ||
Each thought object is passed as a prop to the SingleThought component. | ||
This allows SingleThought to know which thought's details to display.*/} | ||
{thoughts.map((thought) => ( | ||
<SingleThought | ||
key={thought._id} //Each thought in the array has a unique '_id' property, which is used as the 'key' | ||
thought={thought} | ||
onLike={onLike} // Pass the onLike handler to the SingleThought component | ||
/> | ||
))} | ||
</div> | ||
); | ||
}; |
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.
Nice that you abstracted these functions 👍