Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 <your-repo-url>
-cd happy-thoughts
-npm install
-npm start


This project was created as part of the Technigo Frontend Bootcamp.
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": {
"dayjs": "^1.11.19",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down
85 changes: 82 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<h1>Happy Thoughts</h1>
)
}
<main>
<ThoughtForm
thought={thought}
setThought={setThought}
handleSubmit={handleSubmit}
/>
{loading && <LoadingSpinner />}
<ThoughtList thoughts={thoughts} onLike={handleLike} />
</main>
);
};
11 changes: 11 additions & 0 deletions src/components/LoadingSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";

//A loading spinner displayed while the page loads
export const LoadingSpinner = () => {
return (
<div className="loading-container">
<div id="loader"></div>
<p>Loading…</p>
</div>
);
};
41 changes: 41 additions & 0 deletions src/components/ThoughtCard.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<article className="thought-card">
{/* The thought text */}
<p>{message}</p>

{/* Row with like button, heart count, and timestamp */}
<div className="like-container">
{/* Heart button – calls onLike when clicked */}
<button className="like-button" onClick={onLike}>
❤️
</button>
{/* Number of hearts */}
<span className="like-count">x {hearts}</span>
{/* Timestamp */}
<span className="time-ago">{displayTime}</span>
</div>
</article>
);
};
24 changes: 24 additions & 0 deletions src/components/ThoughtList.jsx
Original file line number Diff line number Diff line change
@@ -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
<section className="thoughts-list">
{/* Loop through the array of thoughts and render one ThoughtCard per item */}
{thoughts.map((item) => (
<ThoughtCard
key={item._id}
message={item.message}
hearts={item.hearts}
createdAt={item.createdAt}
// Pass down a function that calls onLike with the correct _id
// This allows each ThoughtCard to like *its own* thought
onLike={() => onLike(item._id)}
/>
))}
</section>
);
};
82 changes: 82 additions & 0 deletions src/components/form.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="form-container">
<form onSubmit={handleLocalSubmit}>
<h2>What is making you happy right now?</h2>

{/* Controlled textarea:
- value comes from App.jsx
- onChange updates the "thought" state in App
*/}
<textarea
rows="4"
value={thought}
onChange={(event) => {
setThought(event.target.value);
setError(""); // Clear error when user starts typing again
}}
/>

{/* Character counter with color feedback */}
<p
style={{
color: thought.length > MAX_THOUGHT_LENGTH ? "red" : "gray",
fontSize: "12px",
}}
>
{MAX_THOUGHT_LENGTH - thought.length} characters remaining
</p>

{/* Show error message only if error has text */}
{error && <p style={{ color: "red", fontSize: "12px" }}>{error}</p>}

<button type="submit">❤️ Send Happy Thought ❤️</button>
</form>
</div>
);
};
44 changes: 44 additions & 0 deletions src/data/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Base URL for all API requests
const API_URL = "https://happy-thoughts-api-4ful.onrender.com/thoughts";

// GET: Fetch the 20 latest thoughts
export const getThoughts = async () => {
const res = await fetch(API_URL); // Send a GET request to the API

if (!res.ok) {
throw new Error("Could not fetch thoughts");
}

const data = await res.json();
return data;
};

// POST: Create a new happy thought
export const postThought = async (message) => {
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});

if (!res.ok) {
throw new Error("Could not post new thought");
}

const data = await res.json();
return data;
};

// POST: Like a specific thought
export const likeThought = async (id) => {
const res = await fetch(`${API_URL}/${id}/like`, {
method: "POST",
});

if (!res.ok) {
throw new Error("Could not send like");
}

const data = await res.json();
return data;
};
13 changes: 13 additions & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//Creates two constants (numbers)
export const MAX_THOUGHT_LENGTH = 140;
export const MIN_THOUGHT_LENGTH = 5;

//Base URL for all API request
export const API_URL = "https://happy-thoughts-api-4ful.onrender.com/thoughts";

//Creates an object containing error messages.
export const ERROR_MESSAGES = {
empty: "Message cannot be empty.",
tooShort: "Message must be at least 5 characters.",
tooLong: "Message cannot be longer than 140 characters.",
};
Loading