generated from Technigo/react-vite-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 53
Ida and Tilde happyping #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tildetilde
wants to merge
46
commits into
Technigo:main
Choose a base branch
from
tildetilde:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
55e01f9
First layout
tildetilde 6767015
Merge pull request #1 from tildetilde/initial-sprint
tildetilde 82c1882
added margins to app-div
Idahel 1af5aec
Added components LikeCounter, ThoughtItem and Spinner.
tildetilde 8814253
Merge pull request #3 from tildetilde/structuring
tildetilde c0396cf
Merge pull request #2 from tildetilde/margins
Idahel bea3f7d
Update App.tsx
tildetilde ad8be80
Merge pull request #4 from tildetilde/making-data-names-match-api
tildetilde bf24693
Updated spinner
tildetilde 5dcc96c
Merge pull request #5 from tildetilde/loading-state
tildetilde 5d392cd
Updated spinner
tildetilde 70ca5c6
Merge pull request #6 from tildetilde/liked-thoughts
tildetilde f3895b4
Created MyLikeCounter
tildetilde 73409d4
Merge pull request #7 from tildetilde/liked-thoughts
tildetilde 7396664
trying to fix conflicts
Idahel 88569dc
fixed conflicts and updated post to API
Idahel abde80e
Merge pull request #9 from tildetilde/post-api
Idahel e06aae1
fixed the error message when clicking submit button
Idahel 0ee661a
Merge pull request #10 from tildetilde/fix-max-length
Idahel 6b401b3
Changed back to happy instead of thankful and changed timing of mylik…
tildetilde 76e5344
Merge pull request #11 from tildetilde/happy-thoughts-fix
tildetilde fb4176f
Update index.html
tildetilde 25bf3f5
Merge branch 'main' of https://github.com/tildetilde/js-project-happy…
tildetilde edc18a8
Pulsating heart when liking post
tildetilde b65f41a
Merge pull request #12 from tildetilde/pulsating-heart
tildetilde b64d6b6
added hover effect on posts
Idahel ae904b3
Merge pull request #13 from tildetilde/hover-effect
Idahel 58d88fe
sounds, font and textarea
tildetilde e2e0e19
Update README.md
tildetilde 416a511
new icon
tildetilde 2d9fe30
Merge branch 'main' of https://github.com/tildetilde/js-project-happy…
tildetilde a4109a3
Added new API URL
tildetilde decf135
Improved accessibility
tildetilde ad33d33
Added my own API
tildetilde 8a82673
Added delete feature
tildetilde d42ff69
Update ThoughtItem.tsx
tildetilde 46f4ff8
Added update message function
tildetilde d442b56
add inline edit support with Enter to save and Escape to cancel
tildetilde 7c1c106
Update ThoughtForm.tsx
tildetilde fa548f8
Fixing signup and login form
tildetilde f020a93
-
tildetilde 89148be
Authorization improvement
tildetilde ef2cd6a
Regex validation
tildetilde 18ac7ad
Update SignupForm.tsx
tildetilde e6aad39
jwt-decode
tildetilde 39c2a92
Updated buttons for log in och sign up
tildetilde File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,4 @@ | ||
| # Happy Thoughts | ||
|
|
||
|
|
||
| https://happyping.netlify.app/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export default { | ||
| plugins: { | ||
| tailwindcss: {}, | ||
| autoprefixer: {}, | ||
| }, | ||
| }; |
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| 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) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this