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 Project #96

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
923f8c7
- added the components Header, HappyThought, ThoughtForm and ThoughtList
gittebe Oct 24, 2024
95808ff
-worked on CSS
gittebe Oct 24, 2024
c213610
- added a submit button component,
gittebe Oct 25, 2024
0b98bf3
- added like LikeButton
gittebe Oct 25, 2024
7dd735c
- added function to update likes
gittebe Oct 25, 2024
bbe3f36
- fixed a bug by replacing two prop names
gittebe Oct 25, 2024
58af9d4
- added the time component
gittebe Oct 25, 2024
0cd9b8a
- restructured the components by adding two new js files fot the hook…
gittebe Oct 26, 2024
d57a9c4
- worked on the CSS
gittebe Oct 26, 2024
de22a47
- added a @media for the submit button
gittebe Oct 26, 2024
83c84e9
-changed the color of the time and hearts counter to make them access…
gittebe Oct 26, 2024
17d1008
- worked on CSS
gittebe Oct 26, 2024
593d84c
- added netlify link
gittebe Oct 26, 2024
35f8b7b
- added character Counter
gittebe Oct 26, 2024
5378baf
- toggle for the like button
gittebe Oct 26, 2024
334bda4
- added an animation
gittebe Oct 26, 2024
cf1e325
- max-length of characters changed
gittebe Oct 26, 2024
b20f2e2
- worked on the like function
gittebe Oct 29, 2024
0f57ab5
- worked on the like button
gittebe Oct 30, 2024
0475f75
- I tidied up the code to improve readability
gittebe Nov 10, 2024
7fcdf6a
- cleaned code and added README
gittebe Nov 22, 2024
22c4f25
- changed README
gittebe Nov 22, 2024
f662baa
- connected to happy-thoughts-api backend
gittebe Dec 18, 2024
856c707
- changed api
gittebe Dec 18, 2024
98eaed6
- changed api
gittebe Dec 18, 2024
b0fb28f
- added netlify link
gittebe Dec 18, 2024
b7c2f65
- fixed url
gittebe Dec 18, 2024
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
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
<h1 align="center">
<a href="">
<img src="/src/assets/happy-thoughts.svg" alt="Project Banner Image">
</a>
</h1>
# Happy Thoughts App
This project is a React-based application that allows users to share their happy thoughts with others. Users can submit short messages (thoughts), and view a list of all submitted thoughts along with their like counts. The app also includes features such as validation for thought submission, a character counter, and a fun heart animation when submitting thoughts.

# Happy thoughts Project
Key Features:
Users can enter their thoughts in a form and submit them to the app.
Thoughts are limited to a maximum of 140 characters and must be at least 5 characters long.
On successful submission, the thought is added to the list of thoughts, and the input is cleared.

In this week's project, you'll be able to practice your React state skills by fetching and posting data to an API.
Like Thoughts: Each thought has a like button that allows users to "like" a thought by clicking on a heart icon. The number of likes (hearts) is updated both locally and on the backend.
Users can "unlike" a thought by clicking the heart again.

## Getting Started with the Project
Local storage is used to persist liked thoughts across page reloads.

### Dependency Installation & Startup Development Server
Heart Animation: A heart animation appears when a user submits a new thought, providing a delightful visual confirmation.

Once cloned, navigate to the project's root directory and this project uses npm (Node Package Manager) to manage its dependencies.
Character Count: As users type their thought, a character counter is displayed to show how many characters remain before reaching the 140-character limit.

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.
Validation messages appear if the user attempts to submit a thought that doesn't meet the requirements.

```bash
npm i && code . && npm run dev
```
Time Ago:
Each thought is tagged with the time it was submitted (e.g., "3 minutes ago", "2 days ago").
The time difference updates every minute to reflect the most accurate "time ago" format.

### The Problem
Responsive Design: The app is responsive and works well on mobile and desktop screens, ensuring a smooth experience across different devices.

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?
# Tech Stack
React: Used for creating interactive UI components, managing state, and handling events.

### View it live
CSS: Custom styling is used to design a clean and user-friendly interface.

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.
Local Storage: Local storage is utilized to remember liked thoughts, even after page refreshes.

## Instructions
PropTypes: Ensures the correctness of props passed to React components.

<a href="instructions.md">
See instructions of this project
</a>
API Integration: API calls are made to store and fetch thoughts from a backend server (mocked with functions postThought and likeThought).

Hooks: The app utilizes custom hooks (useState, useEffect, useFetchThoughts) for managing state and side effects.

## view it live
https://post-happy-thoughts.netlify.app/
24 changes: 13 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<!DOCTYPE html>
<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>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Happy Thought - Project - Week 7</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>

</html>
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-fireworks": "^1.0.4"
},
"devDependencies": {
"@types/react": "^18.2.15",
Expand Down
3 changes: 1 addition & 2 deletions pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
## Netlify link
Add your Netlify link here.
PS. Don't forget to add it in your readme as well.
https://post-happy-thoughts.netlify.app

## Collaborators
Add your collaborators here. Write their GitHub usernames in square brackets. If there's more than one, separate them with a comma, like this:
Expand Down
12 changes: 10 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { HappyThoughts } from "./components/HappyThoughts";
import { HappyThoughtsHeader } from "./components/Header";

export const App = () => {
return <div>Find me in src/app.jsx!</div>;
};
return (
<>
<HappyThoughtsHeader />
<HappyThoughts />
</>
);
};
25 changes: 25 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// const BASE_URL = "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts"

const BASE_URL = "https://project-happy-thoughts-api-production-5d1d.up.railway.app/thoughts"

// fetching data from the API
export const fetchThoughts = async () => {
const response = await fetch(BASE_URL)
return response.json()
}

// function to send the message as a JSON-object to the url via POST
export const postThought = async (message) => {
const response = await fetch(BASE_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message })
})
return response.ok
}

// function to send a POST request to the server to like a message/thought
export const likeThought = async (thoughtId) => {
const response = await fetch(`${BASE_URL}/${thoughtId}/like`, { method: "POST" })
if (!response.ok) throw new Error("Failed to like thought")
}
Copy link

Choose a reason for hiding this comment

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

Smart to make the API functions standalone! That makes it easier to reuse them across other components since you don't have to duplicate the code.

46 changes: 46 additions & 0 deletions src/components/HappyThoughts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react";
import { ThoughtsForm } from "./form/ThoughtForm";
import { ThoughtList } from "./list/ThoughtList";
import { useFetchThoughts } from "../hooks";
import { postThought, likeThought } from "../api";

export const HappyThoughts = () => {
const [newThought, setNewThought] = useState("")
const { thoughts, setThoughts, loading, getThoughts } = useFetchThoughts()

// this function handles the submission of the thoughts form.
// It prevents the default form submission behavior, checks if the input is not empty and then calls the postThought functions to send the new thought to the API. If the post is successful, it clears the input and refreshes the list of the thoughts (getThoughts)
const handleFormSubmit = async (e) => {
e.preventDefault()
if (!newThought.trim()) return
const success = await postThought(newThought)
if (success) {
setNewThought("")
getThoughts()
}
}

// this function handles the likes of the thought by its ID. It calls the function likeThought, and if successful, updates the local state with the new number of hearts by mapping over the previous thoughts
const handleLike = async (thoughtId, isClicked) => {
try {
await likeThought(thoughtId)
setThoughts((prevThoughts) =>
prevThoughts.map((thought) =>
thought._id === thoughtId ? { ...thought, hearts: thought.hearts + (isClicked ? 1 : -1) } : thought
))
} catch (error) {
console.error("Error liking thought", error)
}
}

return (
<div className="content">
<ThoughtsForm
newThought={newThought}
setNewThought={setNewThought}
handleFormSubmit={handleFormSubmit}
/>
{loading ? <p>Loading...</p> : <ThoughtList thoughts={thoughts} onLike={handleLike} />}
</div>
)
}
10 changes: 10 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const HappyThoughtsHeader = () => {
return (
<header>
<div className='header-container'>
<h1>Uplifting Moments</h1>
<h2>Share, Like, and Celebrate What Makes Life Beautiful</h2>
</div>
</header>
)
}
18 changes: 18 additions & 0 deletions src/components/form/Animation.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from "prop-types";
import "./animation.css";

export const HeartAnimation = ({ isVisible }) => {
return (
<>
{isVisible && (
<div className="heart-animation">
❤️
</div>
)}
</>
)
}

HeartAnimation.propTypes = {
isVisible: PropTypes.bool.isRequired,
}
70 changes: 70 additions & 0 deletions src/components/form/ThoughtForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import PropTypes from "prop-types";
import "./form.css";
import { SubmitButton } from "./submitButton/SubmitButton";
import { CharacterCounter } from "./characterCounter/CharacterCounter";
import { useState } from "react";
import { HeartAnimation } from "./Animation";

export const ThoughtsForm = ({ newThought, setNewThought, handleFormSubmit }) => {
const maxChars = 140
const minChars = 5
const [error, setError] = useState("")
const [showHeart, setShowHeart] = useState(false)

const handleInputChange = (e) => {
setNewThought(e.target.value)
setError("")
}

const handleSubmit = (e) => {
e.preventDefault()
// Validation checks
if (newThought.length === 0) {
setError("The message cannot be empty.");
} else if (newThought.length < minChars) {
setError(`The message must be at least ${minChars} characters.`);
} else if (newThought.length > maxChars) {
setError(`The message cannot exceed ${maxChars} characters.`);
} else {
setError("")
setShowHeart(true)
handleFormSubmit(e)
setNewThought("")
setTimeout(() => {
setShowHeart(false)
}, 2000);
}
}

return (
<>
<div className="form-container">
<p>What is making you happy right now?</p>
<form onSubmit={handleSubmit}>
<textarea
type="text"
value={newThought}
onChange={handleInputChange}
placeholder="Type here..."
maxLength="maxChars"
/>
<section className="counter-container">
<CharacterCounter currentLength={newThought.length} maxChars={maxChars} />
</section>
{error && <p className="error-message">{error}</p>}
<section className="submit-button-container">
<SubmitButton />
</section>
</form>
</div>
<HeartAnimation isVisible={showHeart} />
</>
)
}

// Props Validation
ThoughtsForm.propTypes = {
newThought: PropTypes.string.isRequired,
setNewThought: PropTypes.func.isRequired,
handleFormSubmit: PropTypes.func.isRequired,
}
35 changes: 35 additions & 0 deletions src/components/form/animation.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.heart-animation {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 200px;
color: red;
animation: pulse 0.6s ease-in-out forwards, scale 0.6s ease-in-out forwards;
pointer-events: none;
z-index: 1000;
}

@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(1);
}

50% {
transform: translate(-50%, -50%) scale(1.2);
}

100% {
transform: translate(-50%, -50%) scale(1);
}
}

@keyframes scale {
0% {
opacity: 1;
}

100% {
opacity: 0;
}
}
18 changes: 18 additions & 0 deletions src/components/form/characterCounter/CharacterCounter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from "prop-types";
import "./characterCounter.css";

export const CharacterCounter = ({ currentLength, maxChars }) => {
const charsLeft = maxChars - currentLength
const isOverLimit = currentLength > 140

return (
<div className={`character-count ${isOverLimit ? "warning" : ""}`}>
{charsLeft} characters left
</div>
)
}

CharacterCounter.propTypes = {
currentLength: PropTypes.number.isRequired,
maxChars: PropTypes.number.isRequired,
}
Copy link

Choose a reason for hiding this comment

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

I really liked this counter and it worked like a charm too! It gives the app even more of an old-school vibe, which I love. The isOverLimit logic is clear and easy to follow. Great job!

13 changes: 13 additions & 0 deletions src/components/form/characterCounter/characterCounter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.counter-container {
display: flex;
justify-content: end;
width: 90%;
}

.character-count {
font-weight: bold;
}

.character-count.warning {
color: red;
}
Loading