Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a0b1053
Add: Components and skeleton of code in each
marinalendt-png Dec 2, 2025
51d9da7
Add: styling to global style, MCard and MForm
marinalendt-png Dec 3, 2025
93213b9
add: input, textarea, button, select and label gets font-family inherit
marinalendt-png Dec 4, 2025
c91a500
remove: font from html, not in use
marinalendt-png Dec 4, 2025
46a3a31
add: styling to App
marinalendt-png Dec 4, 2025
857f535
remove: strictmode from main, otherwise the font-family is not showin…
marinalendt-png Dec 4, 2025
9f3a099
function: set likes to change and the button to change color
marinalendt-png Dec 4, 2025
3a12150
add: function to messageForm
marinalendt-png Dec 5, 2025
d57e68b
add: fav-icon and canonical links in html
marinalendt-png Dec 5, 2025
5b1c83c
add: comments in MessageForm, App and MessageCard
marinalendt-png Dec 5, 2025
23d82a7
add: og:image
marinalendt-png Dec 5, 2025
39c168a
change: og-image in html
marinalendt-png Dec 5, 2025
66dc967
change: resize og:img
marinalendt-png Dec 5, 2025
5d0e1c7
add: api-function to api.js
marinalendt-png Dec 8, 2025
d7bd1b9
change: async instead of .then
marinalendt-png Dec 9, 2025
0913d5b
fix: bug with time and api fetch
marinalendt-png Dec 9, 2025
55b3fc5
Clean: cleaning up code
marinalendt-png Dec 10, 2025
ef4caa4
fix: small fixes
marinalendt-png Dec 10, 2025
4072408
fix: commenting in api.js
marinalendt-png Dec 10, 2025
daa6012
fix: bug in likes
marinalendt-png Dec 10, 2025
8b3569a
clean: last check
marinalendt-png Dec 11, 2025
0e239c1
small fixes
marinalendt-png Dec 11, 2025
af7409e
Add: read me file
marinalendt-png Dec 11, 2025
43b227a
Rename API.js to api.js
marinalendt-png Dec 11, 2025
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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# Happy Thoughts
# Happy Thoughts #
A small web app to share short, positive messages. Users can write tiny happy notes and read messages from others.

# Demo #
https://project-happy-thoughts-ml.netlify.app/

# Features #
- You can write messages (5-140 characters)
- Like other messages
- Shows how long ago messages were posted
- Works on mobile and desktop

# Tech #
- React
- Styled components
- Day.js
- Fetch API

# API #

- GET /thoughts - get recent messages
- POST /thoughts - send a new message
- POST /thought/:id/like - like a message


###### ENJOY #######
46 changes: 32 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
<!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 Thoughts</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" />

<!-- for SEO and Description -->
<title>Happy Thoughts — Share small joyful messages</title>
<meta name="description"
content="Happy Thoughts — a small app to share short, positive messages. Write and read tiny happy notes." />
<link rel="canonical" href="https://project-happy-thoughts-ml.netlify.app/" />
<meta name="robots" content="index, follow" />

<!-- Favicon -->
<link rel="icon" href="/assets/og-image.png" />
<meta name="theme-color" content="#ffb3b3" />

<!-- Social preview -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Happy Thoughts — Share small joyful messages" />
<meta property="og:description" content="Share short, positive messages with others." />
<meta property="og:url" content="https://project-happy-thoughts-ml.netlify.app/" />
<meta property="og:image" content="/assets/og-image.png" />

</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": {
"dayjs": "^1.11.19",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"styled-components": "^6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
Binary file added public/assets/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 126 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,129 @@
import React, { useState, useEffect } from 'react';
import styled from "styled-components";
import { MessageForm } from './components/MessageForm';
import { MessageCard } from './components/MessageCard';
import { GlobalStyle } from "./styles/GlobalStyle";
import { fetchThoughts, postThought, likeThought } from "./api.js";

// App - the main component for the application. It handles the list of messages och passes them down function to child components
export const App = () => {

// States that stores all submitted messages, shows when loading and when error
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null)

// When the app starts, we get thoughts from API. The data is normalized and stored in messages (state). Runs only once when the app loads (empty array [])
useEffect(() => {
const loadThoughts = async () => {
try {
setIsLoading(true); //shows text when loading
setError(null); //reset previous errors
const data = await fetchThoughts(); //calling api.js

const normalized = data.map((t) => ({
id: t._id,
text: t.message,
likes: t.hearts,
time: new Date(t.createdAt).getTime(),
}));

setMessages(normalized);
} catch (err) {
console.error(err);
setError("Could not fetch thoughts. Please try again later.");
} finally {
setIsLoading(false);
}
};

loadThoughts();
}, []);

// addMessage - this function is called when MessageForm submits next time. It creates a message object and adds it to the start of the list
const addMessage = async (text) => {
try {
const newThought = await postThought({ message: text }); //Sending to API

//Making the object easier to read in the app by normalizing it.
const normalized = {
id: newThought._id,
text: newThought.message,
likes: newThought.hearts,
time: new Date(newThought.createdAt).getTime()
};

setMessages(prev => [normalized, ...prev]);
setError(null); // add the newest message at the top
} catch (err) {
console.error(err);
setError("Could not send your thought. Make sure you have 5-140 characters.");
}
};

// Updates the like count for a message both locally and in the API
const handleLike = async (id) => {
setMessages(prev =>
prev.map(msg =>
msg.id === id ? { ...msg, likes: msg.likes + 1 } : msg
)
);

try {
const updatedThought = await likeThought(id);
setMessages(prev =>
prev.map(msg =>
msg.id === id ? { ...msg, likes: updatedThought.hearts } : msg
)
)
} catch (err) {
console.error("Could not like thought", err);
}
};

return (
<h1>Happy Thoughts</h1>
)
<>
<GlobalStyle />
<AppContainer>
<MessageForm onSend={addMessage} />
{isLoading && <p>Loading thoughts...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}

<MessageList>
{messages.map((msg) => (
<MessageCard
key={msg.id}
message={msg}
onLike={handleLike} />
))}
</MessageList>
</AppContainer>
</>
);
}

// ===== Styled Components ===== //

const AppContainer = styled.main`
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 20px;
`;

const MessageList = styled.section`
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;

@media (min-width: 480px) {
gap: 16px;
}
`;



32 changes: 32 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const BASE_URL = "https://happy-thoughts-api-4ful.onrender.com/thoughts";

// GET - fetches the 20 most recent thoughts from the API. A list of messages will be shown as the app loads.
export const fetchThoughts = async () => {
const res = await fetch(BASE_URL);
const data = await res.json();
return data;
};

// POST - creates a new thought. The API wants a JSON body with a "message" property. If it is successful, the API returns a full thought object back.
export const postThought = async (message) => {
const res = await fetch(BASE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(message)
});

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

// POST - Like a thought. This function sends a like to the API using the thoughts ID. The API then sends back an updated thought, including a new heart count.
export const likeThought = async (thoughtId) => {
const res = await fetch(`${BASE_URL}/${thoughtId}/like`, {
method: "POST",
});

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