diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index 2e85a0e1..94da6a06 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -1,4 +1,11 @@ -import { useContext, useEffect, useMemo, useRef, useState } from "react"; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { PencilIcon, TrashIcon, @@ -164,6 +171,8 @@ export function GameDetailsContent() { const [hasUserReviewed, setHasUserReviewed] = useState(false); const [reviewCheckLoading, setReviewCheckLoading] = useState(false); + const abortControllerRef = useRef(null); + // Check if the current game is in the user's library const isGameInLibrary = useMemo(() => { if (!library || !shop || !objectId) return false; @@ -225,6 +234,14 @@ export function GameDetailsContent() { useEffect(() => { setBackdropOpacity(1); + + // Cleanup: abort any pending review requests when objectId changes + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + }; }, [objectId]); const handleCloudSaveButtonClick = () => { @@ -256,7 +273,7 @@ export function GameDetailsContent() { const isCustomGame = game?.shop === "custom"; - const checkUserReview = async () => { + const checkUserReview = useCallback(async () => { if (!objectId || !userDetails) return; setReviewCheckLoading(true); @@ -265,51 +282,77 @@ export function GameDetailsContent() { const hasReviewed = (response as any)?.hasReviewed || false; setHasUserReviewed(hasReviewed); + const twoHoursInMilliseconds = 2 * 60 * 60 * 1000; + const hasEnoughPlaytime = + game && + game.playTimeInMilliseconds >= twoHoursInMilliseconds && + !game.hasManuallyUpdatedPlaytime; + if ( !hasReviewed && + hasEnoughPlaytime && !sessionStorage.getItem(`reviewPromptDismissed_${objectId}`) ) { setShowReviewPrompt(true); + setShowReviewForm(true); } } catch (error) { console.error("Failed to check user review:", error); } finally { setReviewCheckLoading(false); } - }; + }, [objectId, userDetails, shop, game]); - const loadReviews = async (reset = false) => { - if (!objectId) return; + const loadReviews = useCallback( + async (reset = false) => { + if (!objectId) return; - setReviewsLoading(true); - try { - const skip = reset ? 0 : reviewsPage * 20; - const response = await window.electron.getGameReviews( - shop, - objectId, - 20, - skip, - reviewsSortBy - ); - - const reviewsData = (response as any)?.reviews || []; - const reviewCount = (response as any)?.totalCount || 0; - - if (reset) { - setReviews(reviewsData); - setReviewsPage(0); - setTotalReviewCount(reviewCount); - } else { - setReviews((prev) => [...prev, ...reviewsData]); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); } - setHasMoreReviews(reviewsData.length === 20); - } catch (error) { - console.error("Failed to load reviews:", error); - } finally { - setReviewsLoading(false); - } - }; + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setReviewsLoading(true); + try { + const skip = reset ? 0 : reviewsPage * 20; + const response = await window.electron.getGameReviews( + shop, + objectId, + 20, + skip, + reviewsSortBy + ); + + if (abortController.signal.aborted) { + return; + } + + const reviewsData = (response as any)?.reviews || []; + const reviewCount = (response as any)?.totalCount || 0; + + if (reset) { + setReviews(reviewsData); + setReviewsPage(0); + setTotalReviewCount(reviewCount); + } else { + setReviews((prev) => [...prev, ...reviewsData]); + } + + setHasMoreReviews(reviewsData.length === 20); + } catch (error) { + if (!abortController.signal.aborted) { + console.error("Failed to load reviews:", error); + } + } finally { + if (!abortController.signal.aborted) { + setReviewsLoading(false); + } + } + }, + [objectId, shop, reviewsPage, reviewsSortBy] + ); const handleVoteReview = async ( reviewId: string, @@ -396,7 +439,6 @@ export function GameDetailsContent() { const handleReviewPromptYes = () => { setShowReviewPrompt(false); - setShowReviewForm(true); setTimeout(() => { const reviewFormElement = document.querySelector( @@ -413,6 +455,7 @@ export function GameDetailsContent() { const handleReviewPromptLater = () => { setShowReviewPrompt(false); + setShowReviewForm(false); if (objectId) { sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true"); } @@ -451,13 +494,13 @@ export function GameDetailsContent() { loadReviews(true); checkUserReview(); } - }, [game, shop, objectId, reviewsSortBy, userDetails]); + }, [game, shop, objectId, loadReviews, checkUserReview]); useEffect(() => { if (reviewsPage > 0) { loadReviews(false); } - }, [reviewsPage]); + }, [reviewsPage, loadReviews]); // Initialize previousVotesRef for new reviews useEffect(() => { @@ -773,216 +816,234 @@ export function GameDetailsContent() { )} - {reviews.map((review) => ( -
- {review.isBlocked && - !visibleBlockedReviews.has(review.id) ? ( -
- Review from blocked user —{" "} - -
- ) : ( - <> -
-
- {review.user?.profileImageUrl && ( - - )} -
- -
- - {formatDistance( - new Date(review.createdAt), - new Date(), - { addSuffix: true } - )} +
0 ? 0.5 : 1, + transition: "opacity 0.2s ease", + }} + > + {reviews.map((review) => ( +
+ {review.isBlocked && + !visibleBlockedReviews.has(review.id) ? ( +
+ Review from blocked user —{" "} + +
+ ) : ( + <> +
+
+ {review.user?.profileImageUrl && ( + + )} +
+ +
+ + {formatDistance( + new Date(review.createdAt), + new Date(), + { addSuffix: true } + )} +
+
+ {[1, 2, 3, 4, 5].map((starValue) => ( + + ))} +
- {[1, 2, 3, 4, 5].map((starValue) => ( - +
+
+ + handleVoteReview(review.id, "upvote") + } + animate={ + review.hasUpvoted + ? { + scale: [1, 1.2, 1], + transition: { duration: 0.3 }, + } + : {} } - className={`game-details__review-star ${ - starValue <= review.score - ? "game-details__review-star--filled" - : "game-details__review-star--empty" - } ${ - starValue <= review.score - ? getScoreColorClass(review.score) - : "" - }`} - /> - ))} -
-
-
-
-
- - handleVoteReview(review.id, "upvote") - } - animate={ - review.hasUpvoted - ? { - scale: [1, 1.2, 1], - transition: { duration: 0.3 }, - } - : {} - } - > - - - - (previousVotesRef.current.get(review.id) - ?.upvotes || 0) - } - variants={{ - enter: (isIncreasing: boolean) => ({ - y: isIncreasing ? 10 : -10, - opacity: 0, - }), - center: { y: 0, opacity: 1 }, - exit: (isIncreasing: boolean) => ({ - y: isIncreasing ? -10 : 10, - opacity: 0, - }), - }} - initial="enter" - animate="center" - exit="exit" - transition={{ duration: 0.2 }} - onAnimationComplete={() => { - previousVotesRef.current.set(review.id, { - upvotes: review.upvotes || 0, - downvotes: review.downvotes || 0, - }); - }} - > - {formatNumber(review.upvotes || 0)} - - - - - handleVoteReview(review.id, "downvote") - } - animate={ - review.hasDownvoted - ? { - scale: [1, 1.2, 1], - transition: { duration: 0.3 }, - } - : {} - } - > - - - - (previousVotesRef.current.get(review.id) - ?.downvotes || 0) - } - variants={{ - enter: (isIncreasing: boolean) => ({ - y: isIncreasing ? 10 : -10, - opacity: 0, - }), - center: { y: 0, opacity: 1 }, - exit: (isIncreasing: boolean) => ({ - y: isIncreasing ? -10 : 10, - opacity: 0, - }), - }} - initial="enter" - animate="center" - exit="exit" - transition={{ duration: 0.2 }} - onAnimationComplete={() => { - previousVotesRef.current.set(review.id, { - upvotes: review.upvotes || 0, - downvotes: review.downvotes || 0, - }); - }} - > - {formatNumber(review.downvotes || 0)} - - - -
- {userDetails?.id === review.user?.id && ( - - )} - {review.isBlocked && - visibleBlockedReviews.has(review.id) && ( -
+ {userDetails?.id === review.user?.id && ( + )} -
- - )} -
- ))} + {review.isBlocked && + visibleBlockedReviews.has(review.id) && ( + + )} +
+ + )} +
+ ))} +
{hasMoreReviews && !reviewsLoading && (