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 - Zoe #104

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
34 changes: 17 additions & 17 deletions README.md
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/
51 changes: 51 additions & 0 deletions api/api.js
Copy link
Contributor

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 👍

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();
};
9 changes: 7 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Love it!

</head>
<body>
<div id="root"></div>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"date-fns": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
91 changes: 90 additions & 1 deletion src/App.jsx
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>
);
};
40 changes: 40 additions & 0 deletions src/components/SingleThought.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable react/prop-types */
Copy link
Contributor

Choose a reason for hiding this comment

The 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), {
Copy link
Contributor

Choose a reason for hiding this comment

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

// adds the "ago" suffix to the time
addSuffix: true,
})}
</p>
</div>
</div>
);
};
41 changes: 41 additions & 0 deletions src/components/ThoughtForm.jsx
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
};
26 changes: 26 additions & 0 deletions src/components/ThoughtList.jsx
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>
);
};
Loading