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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# Happy Thoughts

Netlify link: https://happy-thoughts-25.netlify.app/
5 changes: 4 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<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="./public/favicon.ico" />
<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=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Merriweather:ital,opsz,wght@0,18..144,300..900;1,18..144,300..900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Quicksand:[email protected]&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Happy Thoughts</title>
</head>
Expand Down
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/favicon.ico
Binary file not shown.
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

216 changes: 215 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,219 @@
import { useState, useEffect, useRef } from "react"
import styled from "styled-components"

import { ThemeProvider } from "styled-components"
import { GlobalStyles } from "./styling/globalStyles.js"
import { theme } from "./styling/theme.js"
import { Header } from "./components/layout/Header.js"
import { Hero } from "./components/layout/Hero.js"
import { Footer } from "./components/layout/Footer.js"
import { InputCard } from "./components/input/InputCard.jsx"
import { MessageList } from "./components/messages/MessageList.jsx"
import { HeartLoader } from "./styling/LoadingAnime.jsx"

export const App = () => {

const scrollRef = useRef(null)

const [messages, setMessages] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [scroll, setScroll] = useState(false)

// State to track liked posts in local storage
const [likedPosts, setLikedPosts] = useState(() => {
const saved = localStorage.getItem("likedPosts")
return saved ? JSON.parse(saved) : []
})

// fetch messages from API + interval polling

Choose a reason for hiding this comment

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

Maybe would be good to add some error handling in the fetch function, i.e. so that an error message is thrown if the API fetch has errors or fails!

Copy link
Author

Choose a reason for hiding this comment

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

Fixed! Thanks for the advise!

useEffect(() => {
const fetchMessages = () => {
fetch("https://happy-thoughts-api-4ful.onrender.com/thoughts")

Choose a reason for hiding this comment

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

Keeping global reused texts/numbers like this API URL in constants in a shared file makes it a lot easer to change them once they have to be changed! :)

.then(res => {
if (!res.ok) {
throw new Error(`Failed to fetch: ${res.status}`)
}
return res.json()
})
.then(data => {
setMessages(data)
setLoading(false)
setError(null)
})
.catch(error => {
console.error("Error fetching messages:", error)
setError("Something went wrong. Please try again ❤️")
setLoading(false)
})
}

fetchMessages() // Initial fetch

// Set interval to fetch messages every 30 seconds

Choose a reason for hiding this comment

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

I wonder if i's maybe more efficient to use a state as dependency here (in the useEffect where you have the fetch function), instead of re-fetching every 30 seconds. In other words, so that it only re-fetches when it needs to, for example when there is an update which would be after you send a new message or like.

...But the it's maybe hard to know when there has been an update coming from someone posting from another app to the API :D. Well, just some reflections!

const intervalID = setInterval(fetchMessages, 30000)

// Cleanup interval on component unmount
return () => clearInterval(intervalID)

Choose a reason for hiding this comment

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

Good! :)

},[])

// Update local storage when likedPosts changes

Choose a reason for hiding this comment

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

Nice work with liked posts and local storage, works great!

useEffect(() => {
localStorage.setItem("likedPosts", JSON.stringify(likedPosts))
}, [likedPosts])

// Post new message to API

Choose a reason for hiding this comment

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

Any reason you are using different API methods (async/await vs .then) between fetching the data, posting messages and likes? Just curious! :)

Copy link
Author

Choose a reason for hiding this comment

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

No, just for practice:)

const addMessage = async (newText) => {
try {
const response = await fetch("https://happy-thoughts-api-4ful.onrender.com/thoughts",
{
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ message: newText})
})

if (!response.ok) {
throw new Error("Failed to post your message 💔")
}

const data = await response.json()
setMessages(prev => [data, ...prev])
setScroll(true)

} catch (error) {
console.error("Error posting message:", error)
setError("Something went wrong. Please try again ❤️")
}
}

useEffect(() => {
if (scroll && scrollRef.current) {
scrollRef.current.scrollTo({
top: 0,
behavior: "smooth"
})
setScroll(false)
}
},[messages, scroll])

// Send like to API
const increaseHeart = async (id) => {
const message = messages.find(msg => msg._id === id)

try {
const response = await fetch(`https://happy-thoughts-api-4ful.onrender.com/thoughts/${message._id}/like`,
{ method: "POST"}
)

if (!response.ok) {
throw new Error("Failed to send like 💔")
}

const updated = messages.map(msg =>
msg._id === id
? { ...msg, hearts: msg.hearts + 1 }
: msg
)
setMessages(updated)

// Update likedPosts state
setLikedPosts(prev =>
prev.includes(id) ? prev : [...prev, id]
)

} catch (error) {
console.error("Error liking message:", error)
setError("Failed to send like ❤️‍🩹 Try again!")
}
}

return (
<h1>Happy Thoughts</h1>
<>
<ThemeProvider theme={theme}>
<GlobalStyles />
<AppContainer>

<Header likedCount={likedPosts.length} />

<Hero text="Happy Thoughts"/>

<CardWrapper>
<InputCard onSubmit={addMessage} />
</CardWrapper>

{error && <ErrorBox>{error}</ErrorBox>}

{loading ? (
<LoadingWrapper>
<HeartLoader />
<p>Loading Happy Thoughts...</p>
</LoadingWrapper>
) : (
<ScrollArea ref={scrollRef}>
<CardWrapper>
<MessageList
messages={messages}
onLike={increaseHeart}
/>
</CardWrapper>
</ScrollArea>
)}

<Footer text="&copy; ❤️ Happy Thoughts ❤️"/>

</AppContainer>
</ThemeProvider>
</>
)
}

const AppContainer = styled.div`
display: flex;
flex-direction: column;
height: 100vh;
`
const CardWrapper = styled.div`
width: 600px;
max-width: 85%;
margin: 0 auto;
`
const ErrorBox = styled.div`
margin: 50px auto 10px;
border-radius: 6px;
width: fit-content;
text-align: center;
`
const LoadingWrapper = styled.div`
display: flex;
overflow-y: auto;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
font-size: 20px;
color: ${({ theme }) => theme.colors.text };
`

const ScrollArea = styled.div`
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 0 50px;
padding-left: 12px;

/* Scrollbar Styling */
&::-webkit-scrollbar {
width: 12px;
}

&::-webkit-scrollbar-thumb {
background-color: ${({ theme }) => theme.colors.primary };
border-radius: 8px;
}

&::-webkit-scrollbar-track {
background-color: ${({ theme }) => theme.colors.formBackground };
}
`
30 changes: 30 additions & 0 deletions src/components/input/InputCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import styled from "styled-components"
import { InputForm } from "./InputForm.jsx"

export const InputCard = ({onSubmit}) => {
return (
<InputCardSection>
<StyledInputCard>
<p>What's making you happy right now?</p>

<InputForm onSubmit={onSubmit} />

</StyledInputCard>
</InputCardSection>
)
}

const InputCardSection = styled.section`
/* width: 100%; */
`

const StyledInputCard = styled.div`
display: flex;
flex-direction: column;
justify-content: flex-start;
border: 2px solid ${({ theme }) => theme.colors.border };
height: auto;
padding: 20px;
background-color: ${({ theme }) => theme.colors.formBackground };
box-shadow: 7px 7px ${({ theme }) => theme.colors.border };
`
77 changes: 77 additions & 0 deletions src/components/input/InputForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useState } from "react"
import styled, { useTheme } from "styled-components"
import { SubmitButton } from "./SubmitButton.jsx"


export const InputForm = ({ onSubmit }) => {
const [textInput, setTextInput] = useState("")
const theme = useTheme()

const isValid = textInput.length >= 5 && textInput.length <= 140
const showError = textInput.length > 0 && !isValid

const handleSubmit = (event) => {
event.preventDefault()
if (!isValid) return

onSubmit(textInput)
setTextInput("") // Clear the input after submission
}

// Change color of character count based on length
const getColor = () => {
const length = textInput.length
if (length >= 130) return theme.colors.inputLimit
return theme.colors.text
}

return (
<StyledForm onSubmit={handleSubmit}>
<textarea
onChange={event => setTextInput(event.target.value)}
value={textInput}
placeholder="React is making me happy!"
/>
<StyledP style={{ color: getColor() }}>{textInput.length} / 140</StyledP>
{showError && (
<ErrorText>
Message must be between 5 and 140 characters
</ErrorText>
)}

<SubmitButton type="submit" disabled={!isValid}/>
</StyledForm>
)
}

const StyledForm = styled.form`
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-top: 10px;
width: 100%;
height: 150px;

textarea {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 5px 10px;
border: 2px solid #ccc;
border-radius: 5px;
white-space: pre-wrap;
word-wrap: break-word;
}
`

const StyledP = styled.p`
font-size: 12px;
margin-bottom: 10px;
padding: 0;
`
const ErrorText = styled.p`
font-size: 12px;
color: ${({ theme }) => theme.colors.inputLimit };
margin-bottom: 10px;
`
31 changes: 31 additions & 0 deletions src/components/input/SubmitButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import styled from "styled-components";

export const SubmitButton = ( textInput ) => {
return (
<StyledButton
type="submit"
disabled={textInput.length === 0 || textInput.length > 140}
>
❤️ Send Happy Thought ❤️
</StyledButton>
)
}

const StyledButton = styled.button`
background-color: ${({ theme }) => theme.colors.primary};
border: none;
border-radius: 50px;
width: 50%;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
&:hover {
background-color:${({ theme }) => theme.colors.secondary};
}
@media ${({ theme }) => theme.breakpoints.mobile} {
width: 100%;
}
`
Loading