From 2a0f91d82b87d52d06acff5340359ff912f8118c Mon Sep 17 00:00:00 2001 From: HeleneWestrin Date: Tue, 22 Oct 2024 22:41:16 +0200 Subject: [PATCH 01/10] Created fetch for latest thoughts, POST request for liking a thought, HappyThought component with skeleton loader and some overall styling. --- package.json | 4 +- src/App.jsx | 106 ++++++++++++++++++++++++++++++- src/assets/heart-filled.svg | 1 + src/assets/heart-outline.svg | 1 + src/assets/icons/IconLoading.jsx | 52 +++++++++++++++ src/components/HappyThought.css | 63 ++++++++++++++++++ src/components/HappyThought.jsx | 65 +++++++++++++++++++ src/index.css | 37 +++++++++-- 8 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 src/assets/heart-filled.svg create mode 100644 src/assets/heart-outline.svg create mode 100644 src/assets/icons/IconLoading.jsx create mode 100644 src/components/HappyThought.css create mode 100644 src/components/HappyThought.jsx diff --git a/package.json b/package.json index 74245b0c..e6062af7 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "moment": "^2.30.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-loading-skeleton": "^3.5.0" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/src/App.jsx b/src/App.jsx index 1091d431..2d85721a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,107 @@ +import { useEffect, useState } from "react"; +import "react-loading-skeleton/dist/skeleton.css"; +import { HappyThought } from "./components/HappyThought"; + +// Skeleton Loader helper function +const renderSkeletonLoader = (Component, count, props) => { + const placeholderArray = Array(count).fill(); + return placeholderArray.map((_, index) => ( + + )); +}; + export const App = () => { - return
Find me in src/app.jsx!
; + const [happyThoughts, setHappyThoughts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [processingLikes, setProcessingLikes] = useState({}); + const [likedThoughts, setLikedThoughts] = useState([]); + + const fetchHappyThoughts = async () => { + try { + const response = await fetch( + "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts" + ); + const data = await response.json(); + setHappyThoughts(data); + } catch (error) { + console.error("Failed to fetch happy thoughts:", error); + } finally { + setIsLoading(false); + } + }; + + // Post request when user likes a happy thought + const handleLike = async (id) => { + // Prevent multiple likes by marking thought as "processing" after user clicks + setProcessingLikes((previousProcessing) => ({ + ...previousProcessing, + [id]: true, + })); + + try { + const response = await fetch( + `https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${id}/like`, + { + method: "POST", + } + ); + if (response.ok) { + //Update the local state to reflect the new like count + setHappyThoughts((previousThoughts) => + previousThoughts.map((thought) => + thought._id === id + ? { ...thought, hearts: thought.hearts + 1 } + : thought + ) + ); + + // Update the likedThoughts array and save it to localStorage + const updatedLikedThoughts = [...likedThoughts, id]; + setLikedThoughts(updatedLikedThoughts); + localStorage.setItem( + "likedThoughts", + JSON.stringify(updatedLikedThoughts) + ); + } + } catch (error) { + console.error("Failed to like the happy thought:", error); + } finally { + // Remove processing state after the liked has been processed + setProcessingLikes((previousProcessing) => ({ + ...previousProcessing, + [id]: false, + })); + } + }; + + // Load likedThoughts on initial load from localStorage + useEffect(() => { + const savedLikedThoughts = + JSON.parse(localStorage.getItem("likedThoughts")) || []; + setLikedThoughts(savedLikedThoughts); + }, []); + + // Fetch happy thoughts on inital load + useEffect(() => { + fetchHappyThoughts(); + }, []); + + return ( +
+ {isLoading + ? renderSkeletonLoader(HappyThought, 20, { isLoading: true }) + : happyThoughts.map((happyThought) => ( + handleLike(happyThought._id)} + isProcessing={processingLikes[happyThought._id]} + isAlreadyLiked={likedThoughts.includes(happyThought._id)} + /> + ))} +
+ ); }; diff --git a/src/assets/heart-filled.svg b/src/assets/heart-filled.svg new file mode 100644 index 00000000..26b7f245 --- /dev/null +++ b/src/assets/heart-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/heart-outline.svg b/src/assets/heart-outline.svg new file mode 100644 index 00000000..668dc454 --- /dev/null +++ b/src/assets/heart-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/IconLoading.jsx b/src/assets/icons/IconLoading.jsx new file mode 100644 index 00000000..ecf06e0e --- /dev/null +++ b/src/assets/icons/IconLoading.jsx @@ -0,0 +1,52 @@ +export function IconLoading(props) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/components/HappyThought.css b/src/components/HappyThought.css new file mode 100644 index 00000000..13d3b228 --- /dev/null +++ b/src/components/HappyThought.css @@ -0,0 +1,63 @@ +.happy-thought { + display: flex; + flex-flow: row wrap; + border: 2px solid black; + box-shadow: 8px 8px 0 0 black; + padding: 0.875rem; +} + +.happy-thought__title { + flex-basis: 100%; + margin: 0 0 1rem; + overflow-wrap: anywhere; +} + +.happy-thought__footer { + display: flex; + justify-content: space-between; + flex-basis: 100%; + align-items: flex-end; + align-self: flex-end; + + font-size: 0.85em; +} + +.happy-thought__likes, +.happy-thought__timestamp { + color: #666; +} + +.happy-thought__likes { + display: flex; + align-items: center; + gap: 0.5rem; + + span { + position: relative; + display: inline-flex; + gap: 0.25rem; + top: -0.125rem; + + i { + font-style: normal; + } + } +} + +.happy-thought__like-btn { + display: flex; + align-items: center; + height: 2.25rem; + width: 2.25rem; + border-radius: 2.25rem; + border: none; + + font-size: 1.25em; + cursor: pointer; + + background-color: #ddd; + + &:hover { + background-color: #eee; + } +} diff --git a/src/components/HappyThought.jsx b/src/components/HappyThought.jsx new file mode 100644 index 00000000..989cf884 --- /dev/null +++ b/src/components/HappyThought.jsx @@ -0,0 +1,65 @@ +import Skeleton from "react-loading-skeleton"; +import moment from "moment"; +import "react-loading-skeleton/dist/skeleton.css"; +import "./HappyThought.css"; +import HeartOutlinePath from "../assets/heart-outline.svg"; +import HeartFilledPath from "../assets/heart-filled.svg"; +import { IconLoading } from "../assets/icons/IconLoading"; + +const HeartOutline = () => { + return ; +}; + +const HeartFilled = () => { + return ; +}; + +console.log(HeartFilled); + +export const HappyThought = ({ + message, + likes, + timestamp, + isLoading, + onLike, + isProcessing, + isAlreadyLiked, +}) => { + return ( +
+

+ {isLoading ? : message} +

+
+
+ {isLoading ? ( + + ) : ( + <> + + + x + {likes} + + + )} +
+
+ {timestamp ? moment(timestamp).fromNow() : } +
+
+
+ ); +}; diff --git a/src/index.css b/src/index.css index 4558f538..a750ed87 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,38 @@ +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&display=swap"); + :root { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + font-family: "IBM Plex Mono", "Courier New", "Courier", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; +*, +*:before, +*:after { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 1rem; + + @media (min-width: 768px) { + padding: 2rem; + } +} + +main { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr)); + gap: 1rem; + + @media (min-width: 768px) { + gap: 2rem; + } +} + +h2 { + font-size: 1.125rem; + font-weight: 600; } From 92bdf3745c8277d7d51937d546ba6e911922f26b Mon Sep 17 00:00:00 2001 From: HeleneWestrin Date: Thu, 24 Oct 2024 00:26:05 +0200 Subject: [PATCH 02/10] Created hooks for useGet and usePost. Created baseline for posting a thought. Added validation for the textarea and a character count. --- src/App.jsx | 153 ++++++++++++-------------- src/components/CreateHappyThought.css | 6 + src/components/CreateHappyThought.jsx | 70 ++++++++++++ src/components/HappyThought.css | 12 +- src/components/HappyThought.jsx | 2 - src/hooks/useGet.jsx | 29 +++++ src/hooks/useLocalStorage.jsx | 33 ++++++ src/hooks/usePost.jsx | 35 ++++++ src/index.css | 22 ++++ src/utils/renderSkeletonLoader.jsx | 7 ++ 10 files changed, 278 insertions(+), 91 deletions(-) create mode 100644 src/components/CreateHappyThought.css create mode 100644 src/components/CreateHappyThought.jsx create mode 100644 src/hooks/useGet.jsx create mode 100644 src/hooks/useLocalStorage.jsx create mode 100644 src/hooks/usePost.jsx create mode 100644 src/utils/renderSkeletonLoader.jsx diff --git a/src/App.jsx b/src/App.jsx index 2d85721a..74e6149b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,107 +1,94 @@ import { useEffect, useState } from "react"; -import "react-loading-skeleton/dist/skeleton.css"; +import useGet from "./hooks/useGet"; +import usePost from "./hooks/usePost"; +import useLocalStorage from "./hooks/useLocalStorage"; +import { renderSkeletonLoader } from "./utils/renderSkeletonLoader"; +import { CreateHappyThought } from "./components/CreateHappyThought"; import { HappyThought } from "./components/HappyThought"; -// Skeleton Loader helper function -const renderSkeletonLoader = (Component, count, props) => { - const placeholderArray = Array(count).fill(); - return placeholderArray.map((_, index) => ( - - )); -}; - export const App = () => { + const { + data: happyThoughtsData, + isLoading, + error, + } = useGet("https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts"); + const [happyThoughts, setHappyThoughts] = useState([]); - const [isLoading, setIsLoading] = useState(true); const [processingLikes, setProcessingLikes] = useState({}); - const [likedThoughts, setLikedThoughts] = useState([]); + const [likedThoughts, setLikedThoughts] = useLocalStorage( + "likedThoughts", + [] + ); - const fetchHappyThoughts = async () => { - try { - const response = await fetch( - "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts" - ); - const data = await response.json(); - setHappyThoughts(data); - } catch (error) { - console.error("Failed to fetch happy thoughts:", error); - } finally { - setIsLoading(false); + // Update happyThoughts state when data is fetched + useEffect(() => { + if (happyThoughtsData) { + setHappyThoughts(happyThoughtsData); } - }; + }, [happyThoughtsData]); + + const { postData } = usePost(); // Post request when user likes a happy thought const handleLike = async (id) => { - // Prevent multiple likes by marking thought as "processing" after user clicks - setProcessingLikes((previousProcessing) => ({ - ...previousProcessing, - [id]: true, - })); + if (processingLikes[id]) return; // Prevent duplicate processing + + setProcessingLikes((prev) => ({ ...prev, [id]: true })); try { - const response = await fetch( - `https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${id}/like`, - { - method: "POST", - } + await postData( + `https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${id}/like` + ); + + // Update happyThoughts state to increment the likes count + setHappyThoughts((prevThoughts) => + prevThoughts.map((thought) => + thought._id === id + ? { ...thought, hearts: thought.hearts + 1 } + : thought + ) ); - if (response.ok) { - //Update the local state to reflect the new like count - setHappyThoughts((previousThoughts) => - previousThoughts.map((thought) => - thought._id === id - ? { ...thought, hearts: thought.hearts + 1 } - : thought - ) - ); - // Update the likedThoughts array and save it to localStorage - const updatedLikedThoughts = [...likedThoughts, id]; - setLikedThoughts(updatedLikedThoughts); - localStorage.setItem( - "likedThoughts", - JSON.stringify(updatedLikedThoughts) - ); - } - } catch (error) { - console.error("Failed to like the happy thought:", error); + // Update likedThoughts + setLikedThoughts((prevLiked) => [...prevLiked, id]); + } catch (err) { + console.error("Failed to like the happy thought:", err); } finally { - // Remove processing state after the liked has been processed - setProcessingLikes((previousProcessing) => ({ - ...previousProcessing, - [id]: false, - })); + setProcessingLikes((prev) => ({ ...prev, [id]: false })); } }; - // Load likedThoughts on initial load from localStorage - useEffect(() => { - const savedLikedThoughts = - JSON.parse(localStorage.getItem("likedThoughts")) || []; - setLikedThoughts(savedLikedThoughts); - }, []); + const handleLikeClick = (id) => () => handleLike(id); - // Fetch happy thoughts on inital load - useEffect(() => { - fetchHappyThoughts(); - }, []); + if (error) { + return
Error loading thoughts: {error.message}
; + } + + if (isLoading) { + return ( +
{renderSkeletonLoader(HappyThought, 20, { isLoading: true })}
+ ); + } return ( -
- {isLoading - ? renderSkeletonLoader(HappyThought, 20, { isLoading: true }) - : happyThoughts.map((happyThought) => ( - handleLike(happyThought._id)} - isProcessing={processingLikes[happyThought._id]} - isAlreadyLiked={likedThoughts.includes(happyThought._id)} - /> - ))} -
+ <> + +
+ {happyThoughts.map((happyThought) => ( + + ))} +
+ ); }; diff --git a/src/components/CreateHappyThought.css b/src/components/CreateHappyThought.css new file mode 100644 index 00000000..3bf51702 --- /dev/null +++ b/src/components/CreateHappyThought.css @@ -0,0 +1,6 @@ +.create-thought__form { + textarea { + resize: none; + width: 100%; + } +} diff --git a/src/components/CreateHappyThought.jsx b/src/components/CreateHappyThought.jsx new file mode 100644 index 00000000..e50ed41e --- /dev/null +++ b/src/components/CreateHappyThought.jsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import "./CreateHappyThought.css"; + +export const CreateHappyThought = () => { + const [thought, setThought] = useState(""); + const [isFocused, setIsFocused] = useState(false); + const [error, setError] = useState(false); + const minLength = 4; + const maxLength = 140; + + const handleSubmit = (e) => { + e.preventDefault(); + if (thought.length < minLength) { + setError(true); + } else { + // Proceed with form submission logic + console.log("Form submitted:", thought); + // Reset form and state + setThought(""); + setError(false); + } + }; + + return ( + <> +

Share a happy thought

+
+ +