diff --git a/README.md b/README.md index 6fc6d095..61263197 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,15 @@ -

- - Project Banner Image - -

+# Happy thoughts Project – TypeScript edition -# Happy thoughts Project +## Converting to TypeScript -In this week's project, you'll be able to practice your React state skills by fetching and posting data to an API. +In this project I converted a previous project to TypeScript in order to learn. It was definiately a challenge trying to understand the syntax, where to use what and so on. I will need to use it more to feel comfortable writing it for sure. -## Getting Started with the Project +## The project -### Dependency Installation & Startup Development Server +In this project the trickiest part was to figure out how to structure the code. I wanted the create custom hooks for my GET and POST requests to make the code leaner and easier to read, but figuring out how to use it after I created it took a while. -Once cloned, navigate to the project's root directory and this project uses npm (Node Package Manager) to manage its dependencies. +If I had more time I would probably break out different parts into separate components. -The command below is a combination of installing dependencies, opening up the project on VS Code and it will run a development server on your terminal. +## View it live -```bash -npm i && code . && npm run dev -``` - -### The Problem - -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? - -### View it live - -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. - -## Instructions - - - See instructions of this project - +[See it live »](https://674c3c26ce500e0008dacc87--happy-thoughts-by-helene.netlify.app/) diff --git a/index.html b/index.html index 21cce4e0..6eb32404 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,26 @@ - - - Happy Thought - Project - Week 7 + + + + Happy thoughts by Helene Westrin
- + diff --git a/package.json b/package.json index 74245b0c..dbdf3b3b 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,22 @@ "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", "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "@vitejs/plugin-react": "^4.0.3", - "eslint": "^8.45.0", + "eslint": "^8.57.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "typescript": "^5.7.2", "vite": "^4.4.5" } } diff --git a/src/App.jsx b/src/App.jsx deleted file mode 100644 index 1091d431..00000000 --- a/src/App.jsx +++ /dev/null @@ -1,3 +0,0 @@ -export const App = () => { - return
Find me in src/app.jsx!
; -}; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..a145bfcb --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState } from "react"; +import useGet from "./hooks/useGet"; +import usePost from "./hooks/usePost"; +import useLocalStorage from "./hooks/useLocalStorage"; +import { CreateHappyThought } from "./components/CreateHappyThought"; +import { CreateHappyThoughtSkeleton } from "./components/ui/CreateHappyThoughtSkeleton"; +import { HappyThought } from "./components/HappyThought"; +import { HappyThoughtSkeleton } from "./components/ui/HappyThoughtSkeleton"; + +export type HappyThoughtType = { + _id: string; + message: string; + hearts: number; + createdAt: string; +}; + +export const App = () => { + const [happyThoughts, setHappyThoughts] = useState([]); + const [thought, setThought] = useState(""); + const [processingLikes, setProcessingLikes] = useState<{ + [key: string]: boolean; + }>({}); + const [likedThoughts, setLikedThoughts] = useLocalStorage( + "likedThoughts", + [] + ); + + const { + data: happyThoughtsData, + isLoading, + error, + } = useGet( + "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts" + ); + + // 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: string) => { + // Prevent liking a thought that is already liked + if (processingLikes[id] || likedThoughts.includes(id)) return; + + setProcessingLikes((prev) => ({ ...prev, [id]: true })); + + try { + 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 + ) + ); + + // Update likedThoughts + setLikedThoughts((prevLiked) => [...prevLiked, id]); + } catch (err) { + console.error("Failed to like the happy thought:", err); + } finally { + setProcessingLikes((prev) => ({ ...prev, [id]: false })); + } + }; + + const handleLikeClick = (id: string) => () => handleLike(id); + + if (error) { + const errorMessage = + error.name === "AbortError" + ? "Request was canceled. Please retry." + : error.message || "Unknown error occurred"; + + return ( +
+

Error loading thoughts: {errorMessage}

+
+ ); + } + + if (isLoading) { + return ( + <> +
+ +
+
+ {Array.from({ length: 20 }, (_, index) => ( + + ))} +
+ + ); + } + + return ( + <> +
+ +
+
+ {happyThoughts.map((happyThought) => ( + + ))} +
+ + ); +}; diff --git a/src/assets/examples/finished-example.png b/src/assets/examples/finished-example.png deleted file mode 100644 index cd845e6f..00000000 Binary files a/src/assets/examples/finished-example.png and /dev/null differ diff --git a/src/assets/happy-thoughts.svg b/src/assets/happy-thoughts.svg deleted file mode 100644 index 63ab9f0d..00000000 --- a/src/assets/happy-thoughts.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/assets/icons/IconLoading.tsx b/src/assets/icons/IconLoading.tsx new file mode 100644 index 00000000..8bc5f60b --- /dev/null +++ b/src/assets/icons/IconLoading.tsx @@ -0,0 +1,61 @@ +declare global { + namespace JSX { + interface IntrinsicElements { + animate: React.SVGProps; + animateTransform: React.SVGProps; + } + } +} + +export function IconLoading(props: React.SVGProps) { + return ( + + + + + + + + + + + + ); +} diff --git a/src/assets/icons/error.svg b/src/assets/icons/error.svg new file mode 100644 index 00000000..d933729f --- /dev/null +++ b/src/assets/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/heart-filled.svg b/src/assets/icons/heart-filled.svg new file mode 100644 index 00000000..26b7f245 --- /dev/null +++ b/src/assets/icons/heart-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/heart-outline.svg b/src/assets/icons/heart-outline.svg new file mode 100644 index 00000000..668dc454 --- /dev/null +++ b/src/assets/icons/heart-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/CreateHappyThought.css b/src/components/CreateHappyThought.css new file mode 100644 index 00000000..2ce6070a --- /dev/null +++ b/src/components/CreateHappyThought.css @@ -0,0 +1,85 @@ +.create-thought__container { + display: flex; + flex-flow: row wrap; + + border: 2px solid var(--black); + box-shadow: -0.5rem 0.5rem 0 0 var(--black); + background-color: var(--gray-200); + + padding: 1.25rem; +} + +.create-thought__title { + display: flex; + flex-direction: column; + width: 100%; + margin-top: 0; +} + +.create-thought__form { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + + label, + output { + font-size: 0.875rem; + font-weight: 600; + } + + label { + margin-bottom: 1rem; + } + + textarea { + font-family: "IBM Plex Mono", "Courier New", "Courier", sans-serif; + font-size: 1rem; + padding: 0.875rem; + resize: none; + width: 100%; + border: 1px solid var(--black); + border-radius: 0; + transition: box-shadow 0.3s ease-out; + + &:hover { + box-shadow: -0.25rem 0.25rem 0 0 var(--gray-500); + } + + &:focus { + outline: none; + box-shadow: -0.25rem 0.25rem 0 0 var(--black); + } + } +} + +.info { + display: flex; + align-items: center; + gap: 0.75rem; + + font-size: 0.8em; + font-weight: 600; + + transition: all 0.3s ease; + + p { + margin: 0 0 0.15rem; + } +} + +.info--error { + background-color: white; + border: 2px solid var(--red); + padding: 0.85rem 1rem; +} + +.create-thought__character-count { + display: block; + width: 100%; + text-align: right; +} + +.create-thought__button { + width: 100%; +} diff --git a/src/components/CreateHappyThought.tsx b/src/components/CreateHappyThought.tsx new file mode 100644 index 00000000..9ee31d0c --- /dev/null +++ b/src/components/CreateHappyThought.tsx @@ -0,0 +1,166 @@ +import { useState, FormEvent } from "react"; +import usePost from "../hooks/usePost"; +import { Button } from "./ui/Button"; +import { IconLoading } from "../assets/icons/IconLoading"; +import errorIcon from "../assets/icons/error.svg"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import "./CreateHappyThought.css"; +import { HappyThoughtType } from "@/App"; + +export interface CreateHappyThoughtProps { + thought: string; + setThought: React.Dispatch>; + setHappyThoughts: React.Dispatch>; + isLoading: boolean; +} + +export const CreateHappyThought: React.FC = ({ + thought, + setThought, + setHappyThoughts, + isLoading, +}) => { + const { postData, isPosting } = usePost(); + const [isFocused, setIsFocused] = useState(false); + const [error, setError] = useState(false); + const minLength = 4; + const maxLength = 140; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (thought.length >= minLength) { + try { + // Send the post request + const result = await postData( + "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts", + { message: thought } + ); + + // Clear the input field + setThought(""); + + // Update the happy thoughts with the new full thought object returned by the API + setHappyThoughts((previousThoughts) => [result, ...previousThoughts]); + + setError(false); + } catch (err) { + console.error("Error:", err); + } + } else { + setError(true); + } + }; + + return ( + +
+

+ {isLoading ? ( + + ) : ( + "Share a happy thought" + )} +

+
+ + {isLoading ? ( + + ) : ( +