Skip to content

Commit

Permalink
Merge pull request #15 from ITurres/fix/excessive-likebutton-requests
Browse files Browse the repository at this point in the history
Issue #14 🎫: Fix Excessive Involvement API Requests Triggered by -LikeButton- UI Component.
  • Loading branch information
ITurres authored Mar 9, 2024
2 parents df361b4 + cffadb4 commit f9d6eca
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 43 deletions.
66 changes: 44 additions & 22 deletions src/components/UI/LikeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ import '../../styles/animations/loading-icon.scss';

import involvement from '../../services/involvementAPI/involvementAPI.ts';

interface Like {
// * disable camelcase since 'item_id' is the name of the property in the API response.
// eslint-disable-next-line camelcase
item_id: string;
likes: number;
error?: boolean;
}
interface LikeButtonProps {
itemId: string;
projectsLikes: Like[];
}

const LikeButton: React.FC<LikeButtonProps> = ({ itemId }) => {
const LikeButton: React.FC<LikeButtonProps> = ({ itemId, projectsLikes }) => {
const [wasLiked, setWasLiked] = useState(false);
const [likeCount, setLikeCount] = useState(0);
const [error, setError] = useState(false);
Expand All @@ -41,61 +49,77 @@ const LikeButton: React.FC<LikeButtonProps> = ({ itemId }) => {
// ! terminate the loading state after a certain time.
// ? this is to prevent the loading state from being stuck.
// * and show the user the like button again.
const setLoadingToFalse = useCallback(() => {
const setLoadingFalseAfterTimeout = useCallback(() => {
setTimeout(() => {
setLoading(false);
}, maximumLoadingTime);
}, []);

interface Like {
// * disable camelcase since 'item_id' is the name of the property in the API response.
// eslint-disable-next-line camelcase
item_id: string;
likes: number;
}

const updateLikeCount = useCallback(async () => {
const likes: Like[] = (await involvement.getLikes()) as Like[];
const updateLikeCount = useCallback(() => {
// ? When the Involvement API App ID is new, it will return an empty ary with an empty object
// ? while fetching the projectsLikes at 'projectsPage',
// * here we will receive an array with one empty object, thats the 1 length.
// * so just terminate the loading state, and show the like button with no likes.
if (projectsLikes.length === 1) {
setLoadingFalseAfterTimeout();
return;
}

if (likes.length === 0) {
// ? If an error was catch at the 'projectsPage' while fetching the projectsLikes,
// ? we will receive an array with one object with the error property set to true.
if (projectsLikes[0].error) {
setError(true);
// ? at this point the loading spinner is still showing.
// * so terminate the loading state, to show the error message.
setLoadingToFalse();
setLoadingFalseAfterTimeout();
return;
}

const likedProject = likes.find((like) => like.item_id === itemId);
const likedProject = projectsLikes.find(
(project) => project.item_id === itemId,
) as Like | undefined;

if (likedProject) {
setLikeCount(likedProject.likes);
// ? at this point the loading spinner is still showing because of the initial API request.
// * so terminate the loading state, to show the like button with the like count.
setLoadingToFalse();
setLoadingFalseAfterTimeout();
} else {
// * If a project has no likes, then terminate the loading state,
// * to show the like button with no likes.
setLoadingFalseAfterTimeout();
}
}, [itemId, setLoadingToFalse]);
}, [itemId, setLoadingFalseAfterTimeout, projectsLikes]);

useEffect(() => {
updateLikeCount();
}, [itemId, updateLikeCount]);

const handleLikeSubmit = async () => {
setWasLiked((liked) => !liked);
// ? Since 'wasLiked' was false, now it will be set to true. This is to
// ? trigger the function 'styleButton' to add the 'liked' class to the button.
setWasLiked((wasLiked) => !wasLiked);

// * shows an instant update of the like count.
if (!wasLiked) {
setLikeCount((count) => count + 1);
// * set 'wasLiked' to false, so that the user can like the project again.
setWasLiked((wasLiked) => !wasLiked);
}

if (error) {
if (projectsLikes[0].error || error) {
// ? at this point, there was an error already, so user will attempt to like again.
// * so trigger the loading state again.
setLoading(true);
// * after some time, terminate the loading state, to show like button again.
setLoadingToFalse();
setLoadingFalseAfterTimeout();
// * and finally show the error message.
setError(true);
return;
}

// ? If after all, the error state is still false, then the user can like the project.
// * i.e. POST the like to the API.
const likePosted = await involvement.postLike(itemId);

if (likePosted === 'error') {
Expand All @@ -104,8 +128,6 @@ const LikeButton: React.FC<LikeButtonProps> = ({ itemId }) => {
}

setLoading(false);
// * after posting a like, update the like count.
updateLikeCount();
};

if (loading) {
Expand Down Expand Up @@ -137,7 +159,7 @@ const LikeButton: React.FC<LikeButtonProps> = ({ itemId }) => {
{(likeCount > 0 || error) && (
<span className="like-button__text">
&nbsp;
{!error ? likeCount : 'Error'}
{error ? 'Error' : likeCount }
</span>
)}
</button>
Expand Down
12 changes: 10 additions & 2 deletions src/components/UI/SplideCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import getRandomId from '../../utils/getRandomId.ts';
/* eslint-disable no-undef */
interface SplideCarouselProps {
projectsGroup: Array<{}>;
projectsLikes: Array<{}>;
}

const SplideCarousel: React.FC<SplideCarouselProps> = ({ projectsGroup }) => (
const SplideCarousel: React.FC<SplideCarouselProps> = ({
projectsGroup,
projectsLikes,
}) => (
<>
<Splide
aria-label="Projects carousel"
Expand All @@ -39,7 +43,11 @@ const SplideCarousel: React.FC<SplideCarouselProps> = ({ projectsGroup }) => (
}}
>
{projectsGroup.map((projectData) => (
<SplideCarouselSlide key={getRandomId()} projectData={projectData} />
<SplideCarouselSlide
key={getRandomId()}
projectData={projectData}
projectsLikes={projectsLikes}
/>
))}
</Splide>
</>
Expand Down
4 changes: 3 additions & 1 deletion src/components/UI/SplideCarouselSlide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ interface SplideCarouselSlideProps {
sourceCodeHref: string;
};
};
projectsLikes: Array<{}>;
}

const SplideCarouselSlide: React.FC<SplideCarouselSlideProps> = ({
projectData,
projectsLikes,
}) => (
<SplideSlide>
<div className="splide__slide__container">
Expand All @@ -53,7 +55,7 @@ const SplideCarouselSlide: React.FC<SplideCarouselSlideProps> = ({
<summary>Description</summary>
<p>{projectData.data.description.join(' ')}</p>
</details>
<LikeButton itemId={projectData.id} />
<LikeButton itemId={projectData.id} projectsLikes={projectsLikes} />
</div>
<div className="splide__slide__overlay__text__stack text-hue-rotate">
{projectData.data.stack.map((stackItem) => (
Expand Down
33 changes: 31 additions & 2 deletions src/components/pages/ProjectsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect, useCallback } from 'react';

import '../../styles/pages/ProjectsPage.scss';

Expand All @@ -8,14 +8,43 @@ import SplideCarousel from '../UI/SplideCarousel.tsx';
import getRandomId from '../../utils/getRandomId.ts';
import setPageTitle from '../../utils/setPageTitle.ts';

import involvement from '../../services/involvementAPI/involvementAPI.ts';

const ProjectsPage: React.FC = () => {
setPageTitle('My projects 😊');

const [projectsLikes, setProjectsLikes] = useState([{}]);

const getProjectsLikes = useCallback(async () => {
try {
const requestedProjectsLikes = (await involvement.getLikes()) as Array<{}>;

setProjectsLikes(requestedProjectsLikes);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
setProjectsLikes([
{
error: true,
message: error,
},
]);
}
}, []);

useEffect(() => {
getProjectsLikes();
}, [getProjectsLikes]);

return (
<div className="blend-in-out">
<main className="projects-page container">
{Object.values(projects).map((projectGroup) => (
<SplideCarousel key={getRandomId()} projectsGroup={projectGroup} />
<SplideCarousel
key={getRandomId()}
projectsGroup={projectGroup}
projectsLikes={projectsLikes}
/>
))}
</main>
</div>
Expand Down
30 changes: 14 additions & 16 deletions src/services/involvementAPI/involvementAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,23 @@ const postLike = async (itemId: string) => {
};

const getLikes = async () => {
try {
const response = await fetch(`${baseURL}${appId}/likes/`, {
method: 'GET',
});

if (!response.ok) return [];
const response = await fetch(`${baseURL}${appId}/likes/`, {
method: 'GET',
});

// * Only parse the response if it's JSON.
// * specially when the involvementAPI : 'appId' is new, the response to be parsed wont be JSON.
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const likes = await response.json();
if (likes instanceof Array) return likes;
}
if (!response.ok) {
throw new Error(`Error fetching likes: ${response.statusText}`);
}

return [];
} catch (error) {
return error;
// * Only parse the response if it's JSON.
// * specially when the involvementAPI : 'appId' is new, the response to be parsed wont be JSON.
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const likes = await response.json();
if (likes instanceof Array) return likes;
}

return [];
};

const involvement = {
Expand Down

0 comments on commit f9d6eca

Please sign in to comment.