diff --git a/README.md b/README.md index 41ebece2..e076b314 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# Happy Thoughts +Happy Thoughts – React Project (Technigo) + +A React-based “Happy Thoughts” application built as part of a Technigo assignment. The app allows users to post uplifting messages and send hearts (likes) to others’ thoughts. All data is stored and fetched from a shared public API. + +Features: +Post new happy thoughts (5–140 characters) +Fetch and display the 20 most recent thoughts +Send likes to individual thoughts +Real-time UI updates after submitting or liking a thought +Loading states and error handling (optional stretch goals) + +What I Learned: +How to use React component lifecycle and the useEffect hook +Managing state and controlled forms in React +Integrating a frontend with an external REST API +Handling POST requests and updating the UI based on API responses +Working with tools like Postman for testing endpoints + +API Endpoints: + +Get recent thoughts: +GET https://happy-thoughts-api-4ful.onrender.com/thoughts + +Post a new thought: +POST https://happy-thoughts-api-4ful.onrender.com/thoughts + +Like a thought: +POST https://happy-thoughts-api-4ful.onrender.com/thoughts/:THOUGHT_ID/like + +Technologies Used: +-React +-JavaScript +-Fetch API +-CSS/Tailwind/Styled Components (anpassa beroende på vad du använde) + +Installation: +-Clone the project and install dependencies: +-git clone +-cd happy-thoughts +-npm install +-npm start + + +This project was created as part of the Technigo Frontend Bootcamp. diff --git a/package.json b/package.json index 2f66d295..5058efc3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "dayjs": "^1.11.19", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/src/App.jsx b/src/App.jsx index 07f2cbdf..4b3e769d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,84 @@ +// Import React and two hooks: +// - useState: lets the component store and update data +// - useEffect: lets the component run code when it mounts, updates, or unmounts +import React, { useState, useEffect } from "react"; + +//Global Styling +import "./index.css"; + +//Custom components +import { ThoughtForm } from "./components/form"; +import { LoadingSpinner } from "./components/LoadingSpinner"; +import { ThoughtList } from "./components/ThoughtList"; + +// Constants and API helper functions +import { getThoughts, postThought, likeThought } from "./data/api"; + export const App = () => { + // State for the text inside the form input + const [thought, setThought] = useState(""); + // State for the list of all thoughts from the API + const [thoughts, setThoughts] = useState([]); + // State for showing a loading spinner (true = loading) + const [loading, setLoading] = useState(false); + + // useEffect runs ONCE when the App mounts (because of the empty dependency array []) + useEffect(() => { + const fetchThoughts = async () => { + setLoading(true); // Show the spinner + try { + //All the thoughts from API + const data = await getThoughts(); + // Save the list to state → this triggers a re-render + setThoughts(data); + } catch (error) { + } finally { + // Always hide the spinner when done (success or failure) + setLoading(false); + } + }; + // Run the function when the component mounts + fetchThoughts(); + }, []); + + // Handles form submission for creating a new thought + const handleSubmit = async (event) => { + event.preventDefault(); // Prevents page reload + // Prevent sending an empty message + if (thought.trim() === "") return; + try { + // Send the new thought to the API + const newThoughtFromAPI = await postThought(thought); + // Update our local list by adding the new thought to the top + setThoughts((prevThoughts) => [newThoughtFromAPI, ...prevThoughts]); + // Clear the text input + setThought(""); + } catch (error) {} + }; + + // Handles clicking the heart button (liking a thought) + const handleLike = async (id) => { + try { + const updatedThought = await likeThought(id); + // Update only the thought that was changed + setThoughts((prevThoughts) => + prevThoughts.map((thought) => + thought._id === updatedThought._id ? updatedThought : thought + ) + ); + } catch (error) {} + }; + + // JSX returned by the App component – this is what appears on the page return ( -

Happy Thoughts

- ) -} +
+ + {loading && } + +
+ ); +}; diff --git a/src/components/LoadingSpinner.jsx b/src/components/LoadingSpinner.jsx new file mode 100644 index 00000000..b8459adf --- /dev/null +++ b/src/components/LoadingSpinner.jsx @@ -0,0 +1,11 @@ +import React from "react"; + +//A loading spinner displayed while the page loads +export const LoadingSpinner = () => { + return ( +
+
+

Loading…

+
+ ); +}; diff --git a/src/components/ThoughtCard.jsx b/src/components/ThoughtCard.jsx new file mode 100644 index 00000000..abae668a --- /dev/null +++ b/src/components/ThoughtCard.jsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; + +//Enable Day.js plugin +dayjs.extend(relativeTime); + +export const ThoughtCard = ({ message, createdAt, hearts, onLike }) => { + // Convert the given timestamp into a Day.js date object + const created = dayjs(createdAt); + const now = dayjs(); + // Calculate how many days have passed since the thought was created + const diffInDays = now.diff(created, "day"); + + // Decide how the time should be displayed + let displayTime; + if (diffInDays < 7) { + displayTime = created.fromNow(); + } else { + displayTime = created.format("YYYY-MM-DD"); + } + + return ( +
+ {/* The thought text */} +

{message}

+ + {/* Row with like button, heart count, and timestamp */} +
+ {/* Heart button – calls onLike when clicked */} + + {/* Number of hearts */} + x {hearts} + {/* Timestamp */} + {displayTime} +
+
+ ); +}; diff --git a/src/components/ThoughtList.jsx b/src/components/ThoughtList.jsx new file mode 100644 index 00000000..f4b57ea3 --- /dev/null +++ b/src/components/ThoughtList.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { ThoughtCard } from "./ThoughtCard"; +// ThoughtList receives two props: +// - thoughts: an array of thought objects to display +// - onLike: a function that handles liking a thought +export const ThoughtList = ({ thoughts, onLike }) => { + return ( + // Container for all the thought cards +
+ {/* Loop through the array of thoughts and render one ThoughtCard per item */} + {thoughts.map((item) => ( + onLike(item._id)} + /> + ))} +
+ ); +}; diff --git a/src/components/form.jsx b/src/components/form.jsx new file mode 100644 index 00000000..8d27f6ca --- /dev/null +++ b/src/components/form.jsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; + +import { + MAX_THOUGHT_LENGTH, + MIN_THOUGHT_LENGTH, + ERROR_MESSAGES, +} from "../data/constants"; + +// ThoughtForm receives three props from App: +// - thought: the current text inside the textarea +// - setThought: function to update the textarea value +// - handleSubmit: function from App that sends the POST request +export const ThoughtForm = ({ thought, setThought, handleSubmit }) => { + // Local state for showing validation error messages + const [error, setError] = useState(""); + + // This function runs when the user submits the form + const handleLocalSubmit = (event) => { + event.preventDefault(); // Stop page reload + + // VALIDATION CHECKS + // 1. Empty text (after trimming spaces) + if (thought.trim().length === 0) { + setError(ERROR_MESSAGES.empty); + return; + } + + // 2. Too short + if (thought.trim().length < MIN_THOUGHT_LENGTH) { + setError(ERROR_MESSAGES.tooShort); + return; + } + + // 3. Too long + if (thought.length > MAX_THOUGHT_LENGTH) { + setError(ERROR_MESSAGES.tooLong); + return; + } + + // If everything is OK, clear errors + setError(""); + + // Call App's submit function to actually post the thought + handleSubmit(event); + }; + + return ( +
+
+

What is making you happy right now?

+ + {/* Controlled textarea: + - value comes from App.jsx + - onChange updates the "thought" state in App + */} +