Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
55e01f9
First layout
tildetilde Apr 28, 2025
6767015
Merge pull request #1 from tildetilde/initial-sprint
tildetilde Apr 29, 2025
82c1882
added margins to app-div
Idahel Apr 30, 2025
1af5aec
Added components LikeCounter, ThoughtItem and Spinner.
tildetilde May 6, 2025
8814253
Merge pull request #3 from tildetilde/structuring
tildetilde May 6, 2025
c0396cf
Merge pull request #2 from tildetilde/margins
Idahel May 6, 2025
bea3f7d
Update App.tsx
tildetilde May 6, 2025
ad8be80
Merge pull request #4 from tildetilde/making-data-names-match-api
tildetilde May 6, 2025
bf24693
Updated spinner
tildetilde May 6, 2025
5dcc96c
Merge pull request #5 from tildetilde/loading-state
tildetilde May 6, 2025
5d392cd
Updated spinner
tildetilde May 7, 2025
70ca5c6
Merge pull request #6 from tildetilde/liked-thoughts
tildetilde May 7, 2025
f3895b4
Created MyLikeCounter
tildetilde May 7, 2025
73409d4
Merge pull request #7 from tildetilde/liked-thoughts
tildetilde May 7, 2025
7396664
trying to fix conflicts
Idahel May 7, 2025
88569dc
fixed conflicts and updated post to API
Idahel May 7, 2025
abde80e
Merge pull request #9 from tildetilde/post-api
Idahel May 7, 2025
e06aae1
fixed the error message when clicking submit button
Idahel May 7, 2025
0ee661a
Merge pull request #10 from tildetilde/fix-max-length
Idahel May 7, 2025
6b401b3
Changed back to happy instead of thankful and changed timing of mylik…
tildetilde May 8, 2025
76e5344
Merge pull request #11 from tildetilde/happy-thoughts-fix
tildetilde May 8, 2025
fb4176f
Update index.html
tildetilde May 8, 2025
25bf3f5
Merge branch 'main' of https://github.com/tildetilde/js-project-happy…
tildetilde May 8, 2025
edc18a8
Pulsating heart when liking post
tildetilde May 8, 2025
b65f41a
Merge pull request #12 from tildetilde/pulsating-heart
tildetilde May 8, 2025
b64d6b6
added hover effect on posts
Idahel May 8, 2025
ae904b3
Merge pull request #13 from tildetilde/hover-effect
Idahel May 8, 2025
58d88fe
sounds, font and textarea
tildetilde May 9, 2025
e2e0e19
Update README.md
tildetilde May 13, 2025
416a511
new icon
tildetilde May 13, 2025
2d9fe30
Merge branch 'main' of https://github.com/tildetilde/js-project-happy…
tildetilde May 13, 2025
a4109a3
Added new API URL
tildetilde May 27, 2025
decf135
Improved accessibility
tildetilde May 28, 2025
ad33d33
Added my own API
tildetilde Jun 4, 2025
8a82673
Added delete feature
tildetilde Jun 4, 2025
d42ff69
Update ThoughtItem.tsx
tildetilde Jun 4, 2025
46f4ff8
Added update message function
tildetilde Jun 5, 2025
d442b56
add inline edit support with Enter to save and Escape to cancel
tildetilde Jun 5, 2025
7c1c106
Update ThoughtForm.tsx
tildetilde Jun 5, 2025
fa548f8
Fixing signup and login form
tildetilde Jun 12, 2025
f020a93
-
tildetilde Jun 12, 2025
89148be
Authorization improvement
tildetilde Jun 13, 2025
ef2cd6a
Regex validation
tildetilde Jun 13, 2025
18ac7ad
Update SignupForm.tsx
tildetilde Jun 13, 2025
e6aad39
jwt-decode
tildetilde Jun 13, 2025
39c2a92
Updated buttons for log in och sign up
tildetilde Jun 15, 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# Happy Thoughts


https://happyping.netlify.app/
11 changes: 6 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<link rel="icon" type="image/svg+xml" href="/heart.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Happy Thoughts</title>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@
"preview": "vite preview"
},
"dependencies": {
"jwt-decode": "^4.0.0",
"lucide-react": "^0.503.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@tailwindcss/postcss": "^4.1.4",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
"vite": "^6.2.0"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file added public/ding.wav
Binary file not shown.
1 change: 1 addition & 0 deletions public/heart.svg
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/heartbeat.wav
Binary file not shown.
Binary file added public/love.m4a
Binary file not shown.
Binary file added public/pling.m4a
Binary file not shown.
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

5 changes: 0 additions & 5 deletions src/App.jsx

This file was deleted.

283 changes: 283 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"use client";
import React, { useState, useEffect } from "react";
import { jwtDecode } from "jwt-decode";
import ThoughtForm from "./components/ThoughtForm";
import ThoughtList from "./components/ThoughtList";
import Spinner from "./components/Spinner";
import MyLikedThoughts from "./components/MyLikedThoughts";
import LoginForm from "./components/LoginForm";
import SignupForm from "./components/SignupForm";

export type Thought = {
id: string;
message: string;
likes: number;
timestamp: Date;
createdBy: string;
};

type DecodedToken = {
id: string;
exp: number;
iat: number;
};

const API_BASE = "https://happy-thoughts-api-5hw3.onrender.com";
const plingSound = "/ding.wav";

export default function App() {
const [token, setToken] = useState<string | null>(
localStorage.getItem("token")
);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(!!token);
const [loading, setLoading] = useState(false);
const [thoughts, setThoughts] = useState<Thought[]>([]);
const [likedThoughtIds, setLikedThoughtIds] = useState<string[]>([]);
const [posting, setPosting] = useState(false);
const [showLogin, setShowLogin] = useState(false);
const [showSignup, setShowSignup] = useState(false);
const [currentUserId, setCurrentUserId] = useState<string | null>(null);

const login = (newToken: string) => {
localStorage.setItem("token", newToken);
setToken(newToken);
setIsLoggedIn(true);
};

const logout = () => {
localStorage.removeItem("token");
localStorage.removeItem("likedThoughts");
setToken(null);
setIsLoggedIn(false);
setCurrentUserId(null);
setLikedThoughtIds([]);
};

useEffect(() => {
const fetchThoughts = async () => {
setLoading(true);
try {
const response = await fetch(`${API_BASE}/thoughts`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

const data = await response.json();
const transformedData: Thought[] = data.thoughts.map((item: any) => ({
id: item._id,
message: item.message,
likes: item.hearts,
timestamp: new Date(item.createdAt),
createdBy: item.createdBy,
}));

setThoughts(transformedData);
} catch (error) {
console.error("Error fetching thoughts:", error);
} finally {
setLoading(false);
}
};

fetchThoughts();

const stored = localStorage.getItem("likedThoughts");
if (stored) {
setLikedThoughtIds(JSON.parse(stored));
}
}, []);

useEffect(() => {
if (token) {
try {
const decoded = jwtDecode<DecodedToken>(token);
setCurrentUserId(decoded.id);
} catch (err) {
console.error("Invalid token", err);
setCurrentUserId(null);
}
}
}, [token]);

useEffect(() => {
if (isLoggedIn) {
setShowLogin(false);
setShowSignup(false);
}
}, [isLoggedIn]);

const addThought = async (message: string) => {
if (!token) {
alert("Please log in to post a thought.");
return;
}

setPosting(true);
try {
const response = await fetch(`${API_BASE}/thoughts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ message }),
});

if (!response.ok) {
console.error(`HTTP error! Status: ${response.status}`);
} else {
const newThoughtData = await response.json();
const newThought: Thought = {
id: newThoughtData._id,
message: newThoughtData.message,
likes: newThoughtData.hearts,
timestamp: new Date(newThoughtData.createdAt),
createdBy: newThoughtData.createdBy,
};
setThoughts([newThought, ...thoughts]);

const audio = new Audio(plingSound);

Choose a reason for hiding this comment

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

I love this

audio.volume = 0.2;
audio.play();
}
} catch (error: any) {
console.error("Error posting thought:", error);
} finally {
setPosting(false);
}
};

const handleEdit = async (id: string, newMessage: string) => {
try {
const response = await fetch(`${API_BASE}/thoughts/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ message: newMessage }),
});

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

const updated = await response.json();

setThoughts((prev) =>
prev.map((thought) =>
thought.id === id
? {
...thought,
message: updated.message || newMessage,
timestamp: new Date(updated.updatedAt || thought.timestamp),
}
: thought
)
);
} catch (error) {
console.error("Failed to edit thought", error);
}
};

const handleDelete = async (id: string) => {
try {
const response = await fetch(`${API_BASE}/thoughts/${id}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

setThoughts((prev) => prev.filter((thought) => thought.id !== id));
} catch (error) {
console.error("Failed to delete thought", error);
}
};

const handleLike = async (id: string) => {

Choose a reason for hiding this comment

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

To make the code easier to read, it could be a good idea to split it into a separate component. This way, it might also be easier to update things in the future.

setThoughts((prev) =>
prev.map((thought) =>
thought.id === id ? { ...thought, likes: thought.likes + 1 } : thought
)
);

try {
await fetch(`${API_BASE}/thoughts/${id}/like`, {
method: "PATCH",
});
} catch (error) {
console.error("Failed to send like to API", error);
}

setLikedThoughtIds((prev) => {
if (prev.includes(id)) return prev;
const updated = [...prev, id];
localStorage.setItem("likedThoughts", JSON.stringify(updated));
return updated;
});
};

return (
<div className="mx-auto max-w-md my-4 space-y-4 px-4 sm:px-4 md:px-0 lg:px-0 xl:px-0">
<nav className="flex justify-between items-center mb-4">
{isLoggedIn ? (
<button
onClick={logout}
className="text-sm text-pink-600 hover:underline"
>
Log out
</button>
) : (
<div className="flex gap-4">
<button
onClick={() => {
setShowLogin(true);
setShowSignup(false);
}}
className="text-gray-700 text-sm hover:underline"
>
Log in
</button>
<button
onClick={() => {
setShowSignup(true);
setShowLogin(false);
}}
className="text-gray-700 text-sm hover:underline"
>
Sign up
</button>
</div>
)}
</nav>

{!isLoggedIn && showLogin && <LoginForm onLogin={login} />}
{!isLoggedIn && showSignup && <SignupForm onSignup={login} />}

<ThoughtForm onSubmit={addThought} isPosting={posting} />

{loading ? (
<Spinner />
) : (
<>
<ThoughtList
thoughts={thoughts}
onLike={handleLike}
onDelete={handleDelete}
onEdit={handleEdit}
currentUserId={currentUserId}
/>
<MyLikedThoughts
thoughts={thoughts}
likedThoughtIds={likedThoughtIds}
/>
</>
)}
</div>
);
}
20 changes: 20 additions & 0 deletions src/components/LikeCounter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";

type LikeCounterProps = {
count: number;
};

function LikeCounter({ count }: LikeCounterProps) {
return (
<>
<span className="sr-only">
{count === 1 ? "1 like" : `${count} likes`}
</span>
<span aria-hidden="true" className="text-gray-700">
x {count}
</span>
</>
);
}

export default LikeCounter;
Loading