Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ecf16e4
started with the project
violacathrine Apr 30, 2025
1b2044f
styling
violacathrine May 1, 2025
e8be3a1
clean, styling etc
violacathrine May 5, 2025
24d1bb5
loader
violacathrine May 10, 2025
e24cb22
hearticons, styling etc
violacathrine May 10, 2025
487ac1e
env fil
violacathrine May 10, 2025
da8252e
final touches
violacathrine May 10, 2025
ee8c95c
favicon, logo, footer
violacathrine May 11, 2025
b30f561
color on isliked
violacathrine May 11, 2025
210378d
readme file
violacathrine May 11, 2025
8ddf21e
ready for handin
violacathrine May 11, 2025
599091a
last push
violacathrine May 11, 2025
84f5d57
ogtags
violacathrine May 11, 2025
e15e26e
finally done
violacathrine May 11, 2025
025540c
added attribute to icon
violacathrine May 17, 2025
a042b31
changed the apiurl to new one
violacathrine May 26, 2025
3651f3c
url
violacathrine May 28, 2025
1fafb7a
testing
violacathrine May 28, 2025
01ffee3
test
violacathrine May 28, 2025
1aff31e
test again
violacathrine May 28, 2025
546715c
changed color
violacathrine May 28, 2025
3a76df6
test
violacathrine May 28, 2025
f2d7b9a
color on timestamp
violacathrine May 28, 2025
ab9d9e9
color change
violacathrine May 28, 2025
380a437
test again
violacathrine May 28, 2025
ce2e7c9
test
violacathrine May 28, 2025
6cdf0af
new api
violacathrine Jun 11, 2025
76a7a93
testing api
violacathrine Jun 12, 2025
e5a68a7
api up n running
violacathrine Jun 12, 2025
1bdbe50
sorting messages
violacathrine Jun 12, 2025
e4ca486
wake up fetch function
violacathrine Jun 12, 2025
bdf222b
api-url
violacathrine Aug 6, 2025
631620e
test
violacathrine Aug 6, 2025
4345013
test
violacathrine Aug 7, 2025
576ed2c
styling, ready for deploy
violacathrine Sep 2, 2025
829397c
readme
violacathrine Sep 2, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ package-lock.json
*.njsproj
*.sln
*.sw?
.env
.env
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# Happy Thoughts

This is a cheerful, responsive messaging app where users can post short, happy messages and like each other's thoughts - built with React and Styled Components.

## 🌐 Live Demo

👉 [View the live site here](https://happythoughtsbyc.netlify.app/)

## ✨ Features

- Post new happy thoughts (min. 5 / max. 140 characters)
- Live character counter that turns red if over limit
- Like thoughts using the ❤️ button
- Likes are stored in `localStorage` to track what you’ve liked
- Optimistic UI updates when posting and liking
- Visually highlights liked posts
- Custom loading spinner while fetching/posting

## 🧱 Built With

- [React](https://reactjs.org/)
- [Styled Components](https://styled-components.com/)
- [Vite](https://vitejs.dev/)
- Public Happy Thoughts API (Technigo)
- HTML5, CSS3
- `localStorage` for persistent like tracking
- [Happy thoughts icons created by Smashicons - Flaticon](https://www.flaticon.com/free-icons/happy-thoughts)


## 👩‍💻 Created by

Created with ❤️ by **Cathi**
25 changes: 19 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@
<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" />
<link rel="icon" type="image/svg+xml" href="./happy-thoughts.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap"
rel="stylesheet"
/>
<title>Happy Thoughts</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content="Happy Thoughts by Cathi" />
<meta
property="og:description"
content="A happy place for happy thoughts! 💬✨"
/>
<meta
property="og:image"
content="https://happythoughtsbyc.netlify.app/thumbnail-ht.png"
/>
<meta property="og:type" content="website" />
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
<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 @@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"styled-components": "^6.1.17"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
Binary file added public/happy-thoughts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/thumbnail-ht.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

3 changes: 2 additions & 1 deletion pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Please include your Netlify link here.
Please include your Netlify link here.
https://happythoughtsbyc.netlify.app/
92 changes: 89 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,91 @@
import { useEffect, useState } from "react";
import { fetchThoughts, postThought, likeThought } from "./api/thoughts";
import { Form } from "./components/Form";
import { MessageList } from "./components/MessageList";
import { GlobalStyles } from "./GlobalStyles";
import { Loader } from "./components/Loader";
import { Logo } from "./components/Logo";
import { Footer } from "./components/Footer";
import {
getLikedThoughts,
saveLikedThought,
getLikeCount,
} from "./utils/localLikes";

export const App = () => {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [posting, setPosting] = useState(false);
const [likeCount, setLikeCount] = useState(getLikeCount());

// Get all messages
const getMessages = async () => {
try {
setLoading(true);
const data = await fetchThoughts();
setMessages(data);
} catch (error) {
console.error("Fetch failed", error);
} finally {
setLoading(false);
}
};

// Post new message
const handleNewMessage = async (message) => {
const optimisticThought = {
_id: Date.now().toString(),
message,
hearts: 0,
createdAt: new Date().toISOString(),
};

setMessages((prev) => [optimisticThought, ...prev]);
setPosting(true);

try {
await postThought(message);
await getMessages();
} catch (error) {
console.error("Post failed", error);
} finally {
setPosting(false);
}
};

const handleLike = async (id) => {
setMessages((prevMessages) =>
prevMessages.map((msg) =>
msg._id === id ? { ...msg, hearts: msg.hearts + 1 } : msg
)
);

if (!getLikedThoughts()[id]) {
saveLikedThought(id);
setLikeCount(getLikeCount());
}

try {
await likeThought(id);
} catch (error) {
console.error("Like failed", error);
}
};


useEffect(() => {
getMessages();
}, []);

return (
<h1>Happy Thoughts</h1>
)
}
<>
<GlobalStyles />
<Logo />
<h1>Happy Thoughts</h1>
<Form onSubmitMessage={handleNewMessage} posting={posting} />
{!loading && posting && <Loader />}
<MessageList messages={messages} loading={loading} onLike={handleLike} />
<Footer likeCount={likeCount} />
</>
);
};
31 changes: 31 additions & 0 deletions src/GlobalStyles.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createGlobalStyle } from "styled-components";

export const GlobalStyles = createGlobalStyle`
*,
*::before,
*::after {
box-sizing: border-box;
}

body {
display: flex;
justify-content: center;
background-color:rgba(224, 203, 200, 0.23);
font-family: roboto mono;
}

h1 {
display: flex;
justify-content: center;
font-family: verdana;
margin: 20px 0 16px;
font-size: 48px;
color:#FF5364
}

p {
line-height: 1.6;
margin: 0 0 16px;
font-size: 17px;
}
`;
30 changes: 30 additions & 0 deletions src/api/thoughts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const BASE_URL = import.meta.env.VITE_API_URL;

// GET messages
export const fetchThoughts = async () => {
const res = await fetch(`${BASE_URL}/thoughts`);
if (!res.ok) throw new Error("Failed to fetch thoughts");
return res.json();
};

// POST message
export const postThought = async (message) => {
const res = await fetch(`${BASE_URL}/thoughts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message }),
});
if (!res.ok) throw new Error("Failed to post message");
return res.json();
};

// LIKE message
export const likeThought = async (id) => {
const res = await fetch(`${BASE_URL}/thoughts/${id}/like`, {
method: "POST",
});
if (!res.ok) throw new Error("Failed to like message");
return res.json();
};
44 changes: 44 additions & 0 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import styled from "styled-components";
import { HeartIcon } from "./HeartIcon";

const FooterWrapper = styled.footer`
text-align: center;
font-size: 14px;
color: #666;
margin: 50px auto 20px;
padding: 10px;
`;

const FooterLink = styled.a`
color: inherit;
text-decoration: none;

&:hover {
color: #000;
}
`;

const IconInline = styled(HeartIcon)`
margin: 0 4px;
vertical-align: middle;
`;

export const Footer = ({ likeCount }) => {
return (
<FooterWrapper>
<p>
You’ve <IconInline /> {likeCount} thought{likeCount !== 1 ? "s" : ""}
</p>
<p>
© 2025{" "}
<FooterLink
href="https://github.com/violacathrine/js-project-happy-thoughts"
target="_blank"
rel="noopener noreferrer"
>
Cathi
</FooterLink>
</p>
</FooterWrapper>
);
};
92 changes: 92 additions & 0 deletions src/components/Form.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState } from "react";
import { HeartIcon } from "./HeartIcon";
import {
FormWrapper,
StyledForm,
StyledLabel,
StyledTextarea,
StyledInfoCharacterText,
StyledButton,
StyledErrorMessage,
} from "./Form.styles";

export const Form = ({ onSubmitMessage }) => {
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const maxLength = 140;
const isTooLong = message.length > maxLength;

const handleSubmit = (event) => {
event.preventDefault();

if (message.length < 5) {
setError("Your message is too short.");
return;
}

if (message.length > maxLength) {
setError("Your message is too long.");
return;
}

setError("");

onSubmitMessage(message);
setMessage("");
};

const handleInputChange = (e) => {
const newMessage = e.target.value;
setMessage(newMessage);

if (newMessage.length === 0) {
setError("");
} else if (newMessage.length < 5) {
setError("Your message is too short.");
} else if (newMessage.length > maxLength) {
setError("Your message is too long.");
} else {
setError("");
}
};

const handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};

const errorMessageElement = error ? (
<StyledErrorMessage>{error}</StyledErrorMessage>
) : null;


return (
<FormWrapper>
<StyledForm onSubmit={handleSubmit}>
<StyledLabel htmlFor="thought">
What's making you happy right now?
</StyledLabel>

<StyledTextarea
id="thought"
placeholder="Type your happy thought here..."
value={message}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>

<StyledInfoCharacterText $exceedsLimit={isTooLong}>
{maxLength - message.length} characters remaining
</StyledInfoCharacterText>

{errorMessageElement}

<StyledButton type="submit">
<HeartIcon /> Send Happy Thought <HeartIcon />
</StyledButton>
</StyledForm>
</FormWrapper>
);
};
Loading