Skip to content
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

Happy thoughts – TypeScript edition #113

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
36 changes: 8 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
<h1 align="center">
<a href="">
<img src="/src/assets/happy-thoughts.svg" alt="Project Banner Image">
</a>
</h1>
# 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

<a href="instructions.md">
See instructions of this project
</a>
[See it live »](https://674c3c26ce500e0008dacc87--happy-thoughts-by-helene.netlify.app/)
22 changes: 18 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Happy Thought - Project - Week 7</title>
<link
rel="icon"
type="image/svg+xml"
href="/src/assets/icons/heart-filled.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<meta
name="description"
content="A place to post your happy thoughts. TypeScript edition."
/>
<title>Happy thoughts by Helene Westrin</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
3 changes: 0 additions & 3 deletions src/App.jsx

This file was deleted.

131 changes: 131 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<HappyThoughtType[]>([]);
const [thought, setThought] = useState<string>("");
const [processingLikes, setProcessingLikes] = useState<{
[key: string]: boolean;
}>({});
const [likedThoughts, setLikedThoughts] = useLocalStorage<string[]>(
"likedThoughts",
[]
);

const {
data: happyThoughtsData,
isLoading,
error,
} = useGet<HappyThoughtType[]>(
"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<HappyThoughtType>();

// 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 (
<main>
<p>Error loading thoughts: {errorMessage}</p>
</main>
);
}

if (isLoading) {
return (
<>
<main>
<CreateHappyThoughtSkeleton />
</main>
<section aria-label="Latest posted thoughts are loading">
{Array.from({ length: 20 }, (_, index) => (
<HappyThoughtSkeleton key={index} />
))}
</section>
</>
);
}

return (
<>
<main>
<CreateHappyThought
thought={thought}
setThought={setThought}
isLoading={false}
setHappyThoughts={setHappyThoughts}
/>
</main>
<section aria-label="Latest posted thoughts">
{happyThoughts.map((happyThought) => (
<HappyThought
key={happyThought._id}
message={happyThought.message}
likes={happyThought.hearts}
timestamp={happyThought.createdAt}
isLoading={false}
onLike={handleLikeClick(happyThought._id)}
isProcessing={!!processingLikes[happyThought._id]}
isAlreadyLiked={likedThoughts.includes(happyThought._id)}
/>
))}
</section>
</>
);
};
Binary file removed src/assets/examples/finished-example.png
Binary file not shown.
18 changes: 0 additions & 18 deletions src/assets/happy-thoughts.svg

This file was deleted.

61 changes: 61 additions & 0 deletions src/assets/icons/IconLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
declare global {
namespace JSX {
interface IntrinsicElements {
animate: React.SVGProps<SVGElement>;
animateTransform: React.SVGProps<SVGElement>;
}
}
}

export function IconLoading(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
{...props}
>
<g
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
>
<path
strokeDasharray={16}
strokeDashoffset={16}
d="M12 3c4.97 0 9 4.03 9 9"
>
<animate
fill="freeze"
attributeName="stroke-dashoffset"
dur="0.2s"
values="16;0"
/>
<animateTransform
attributeName="transform"
dur="1s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
<path
strokeDasharray={64}
strokeDashoffset={64}
strokeOpacity={0.3}
d="M12 3c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9c0 -4.97 4.03 -9 9 -9Z"
>
<animate
fill="freeze"
attributeName="stroke-dashoffset"
dur="1s"
values="64;0"
/>
</path>
</g>
</svg>
);
}
1 change: 1 addition & 0 deletions src/assets/icons/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/heart-filled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/heart-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 85 additions & 0 deletions src/components/CreateHappyThought.css
Original file line number Diff line number Diff line change
@@ -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%;
}
Loading