-
Notifications
You must be signed in to change notification settings - Fork 53
Happy thoughts project #44
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
base: main
Are you sure you want to change the base?
Changes from all commits
f9230fa
7765281
7ba0b00
4896ae6
ecd5191
e53f536
80679ee
6fc817d
1eba4d9
f04010e
2f6f021
5881538
9c333b8
c36bb9a
0b396e5
dd39912
9c8ff7a
eb70d80
a72cf8e
a10f8eb
f3663b2
96f62f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,3 @@ | ||
| # Happy Thoughts | ||
|
|
||
| Netlify link: https://happy-thoughts-25.netlify.app/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
|
||
This file was deleted.
| 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 | ||
| useEffect(() => { | ||
| const fetchMessages = () => { | ||
| fetch("https://happy-thoughts-api-4ful.onrender.com/thoughts") | ||
|
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. 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 | ||
|
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. 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) | ||
|
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. Good! :) |
||
| },[]) | ||
|
|
||
| // Update local storage when likedPosts changes | ||
|
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. Nice work with liked posts and local storage, works great! |
||
| useEffect(() => { | ||
| localStorage.setItem("likedPosts", JSON.stringify(likedPosts)) | ||
| }, [likedPosts]) | ||
|
|
||
| // Post new message to API | ||
|
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. Any reason you are using different API methods (async/await vs .then) between fetching the data, posting messages and likes? Just curious! :)
Author
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. 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="© ❤️ 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 }; | ||
| } | ||
| ` | ||
| 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 }; | ||
| ` |
| 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; | ||
| ` |
| 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%; | ||
| } | ||
| ` |
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.
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!
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.
Fixed! Thanks for the advise!