mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-02-01 19:15:07 +01:00
Compare commits
12 Commits
feat/revie
...
fix/game_a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e912b3b8d | ||
|
|
e71211f1aa | ||
|
|
a946f3bd5a | ||
|
|
374b62983b | ||
|
|
0cd4c3ccf6 | ||
|
|
7b97663b3a | ||
|
|
68e2e2a772 | ||
|
|
39979292e2 | ||
|
|
60ae7d40fa | ||
|
|
63b6b0b44e | ||
|
|
82c0dc0d97 | ||
|
|
1cba3f350c |
@@ -1,4 +1,11 @@
|
|||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -164,6 +171,8 @@ export function GameDetailsContent() {
|
|||||||
const [hasUserReviewed, setHasUserReviewed] = useState(false);
|
const [hasUserReviewed, setHasUserReviewed] = useState(false);
|
||||||
const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
|
const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
|
||||||
|
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
// Check if the current game is in the user's library
|
// Check if the current game is in the user's library
|
||||||
const isGameInLibrary = useMemo(() => {
|
const isGameInLibrary = useMemo(() => {
|
||||||
if (!library || !shop || !objectId) return false;
|
if (!library || !shop || !objectId) return false;
|
||||||
@@ -225,6 +234,14 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBackdropOpacity(1);
|
setBackdropOpacity(1);
|
||||||
|
|
||||||
|
// Cleanup: abort any pending review requests when objectId changes
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [objectId]);
|
}, [objectId]);
|
||||||
|
|
||||||
const handleCloudSaveButtonClick = () => {
|
const handleCloudSaveButtonClick = () => {
|
||||||
@@ -256,7 +273,7 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const isCustomGame = game?.shop === "custom";
|
const isCustomGame = game?.shop === "custom";
|
||||||
|
|
||||||
const checkUserReview = async () => {
|
const checkUserReview = useCallback(async () => {
|
||||||
if (!objectId || !userDetails) return;
|
if (!objectId || !userDetails) return;
|
||||||
|
|
||||||
setReviewCheckLoading(true);
|
setReviewCheckLoading(true);
|
||||||
@@ -265,51 +282,77 @@ export function GameDetailsContent() {
|
|||||||
const hasReviewed = (response as any)?.hasReviewed || false;
|
const hasReviewed = (response as any)?.hasReviewed || false;
|
||||||
setHasUserReviewed(hasReviewed);
|
setHasUserReviewed(hasReviewed);
|
||||||
|
|
||||||
|
const twoHoursInMilliseconds = 2 * 60 * 60 * 1000;
|
||||||
|
const hasEnoughPlaytime =
|
||||||
|
game &&
|
||||||
|
game.playTimeInMilliseconds >= twoHoursInMilliseconds &&
|
||||||
|
!game.hasManuallyUpdatedPlaytime;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!hasReviewed &&
|
!hasReviewed &&
|
||||||
|
hasEnoughPlaytime &&
|
||||||
!sessionStorage.getItem(`reviewPromptDismissed_${objectId}`)
|
!sessionStorage.getItem(`reviewPromptDismissed_${objectId}`)
|
||||||
) {
|
) {
|
||||||
setShowReviewPrompt(true);
|
setShowReviewPrompt(true);
|
||||||
|
setShowReviewForm(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to check user review:", error);
|
console.error("Failed to check user review:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setReviewCheckLoading(false);
|
setReviewCheckLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [objectId, userDetails, shop, game]);
|
||||||
|
|
||||||
const loadReviews = async (reset = false) => {
|
const loadReviews = useCallback(
|
||||||
if (!objectId) return;
|
async (reset = false) => {
|
||||||
|
if (!objectId) return;
|
||||||
|
|
||||||
setReviewsLoading(true);
|
if (abortControllerRef.current) {
|
||||||
try {
|
abortControllerRef.current.abort();
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHasMoreReviews(reviewsData.length === 20);
|
const abortController = new AbortController();
|
||||||
} catch (error) {
|
abortControllerRef.current = abortController;
|
||||||
console.error("Failed to load reviews:", error);
|
|
||||||
} finally {
|
setReviewsLoading(true);
|
||||||
setReviewsLoading(false);
|
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 (
|
const handleVoteReview = async (
|
||||||
reviewId: string,
|
reviewId: string,
|
||||||
@@ -396,7 +439,6 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const handleReviewPromptYes = () => {
|
const handleReviewPromptYes = () => {
|
||||||
setShowReviewPrompt(false);
|
setShowReviewPrompt(false);
|
||||||
setShowReviewForm(true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const reviewFormElement = document.querySelector(
|
const reviewFormElement = document.querySelector(
|
||||||
@@ -413,6 +455,7 @@ export function GameDetailsContent() {
|
|||||||
|
|
||||||
const handleReviewPromptLater = () => {
|
const handleReviewPromptLater = () => {
|
||||||
setShowReviewPrompt(false);
|
setShowReviewPrompt(false);
|
||||||
|
setShowReviewForm(false);
|
||||||
if (objectId) {
|
if (objectId) {
|
||||||
sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true");
|
sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, "true");
|
||||||
}
|
}
|
||||||
@@ -451,13 +494,13 @@ export function GameDetailsContent() {
|
|||||||
loadReviews(true);
|
loadReviews(true);
|
||||||
checkUserReview();
|
checkUserReview();
|
||||||
}
|
}
|
||||||
}, [game, shop, objectId, reviewsSortBy, userDetails]);
|
}, [game, shop, objectId, loadReviews, checkUserReview]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (reviewsPage > 0) {
|
if (reviewsPage > 0) {
|
||||||
loadReviews(false);
|
loadReviews(false);
|
||||||
}
|
}
|
||||||
}, [reviewsPage]);
|
}, [reviewsPage, loadReviews]);
|
||||||
|
|
||||||
// Initialize previousVotesRef for new reviews
|
// Initialize previousVotesRef for new reviews
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -773,216 +816,234 @@ export function GameDetailsContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{reviews.map((review) => (
|
<div
|
||||||
<div key={review.id} className="game-details__review-item">
|
style={{
|
||||||
{review.isBlocked &&
|
opacity: reviewsLoading && reviews.length > 0 ? 0.5 : 1,
|
||||||
!visibleBlockedReviews.has(review.id) ? (
|
transition: "opacity 0.2s ease",
|
||||||
<div className="game-details__blocked-review-simple">
|
}}
|
||||||
Review from blocked user —{" "}
|
>
|
||||||
<button
|
{reviews.map((review) => (
|
||||||
className="game-details__blocked-review-show-link"
|
<div
|
||||||
onClick={() => toggleBlockedReview(review.id)}
|
key={review.id}
|
||||||
>
|
className="game-details__review-item"
|
||||||
Show
|
>
|
||||||
</button>
|
{review.isBlocked &&
|
||||||
</div>
|
!visibleBlockedReviews.has(review.id) ? (
|
||||||
) : (
|
<div className="game-details__blocked-review-simple">
|
||||||
<>
|
Review from blocked user —{" "}
|
||||||
<div className="game-details__review-header">
|
<button
|
||||||
<div className="game-details__review-user">
|
className="game-details__blocked-review-show-link"
|
||||||
{review.user?.profileImageUrl && (
|
onClick={() => toggleBlockedReview(review.id)}
|
||||||
<button
|
>
|
||||||
className="game-details__review-avatar-button"
|
Show
|
||||||
onClick={() =>
|
</button>
|
||||||
review.user?.id &&
|
</div>
|
||||||
navigate(`/profile/${review.user.id}`)
|
) : (
|
||||||
}
|
<>
|
||||||
title={review.user.displayName || "User"}
|
<div className="game-details__review-header">
|
||||||
>
|
<div className="game-details__review-user">
|
||||||
<img
|
{review.user?.profileImageUrl && (
|
||||||
src={review.user.profileImageUrl}
|
<button
|
||||||
alt={review.user.displayName || "User"}
|
className="game-details__review-avatar-button"
|
||||||
className="game-details__review-avatar"
|
onClick={() =>
|
||||||
/>
|
review.user?.id &&
|
||||||
</button>
|
navigate(`/profile/${review.user.id}`)
|
||||||
)}
|
}
|
||||||
<div className="game-details__review-user-info">
|
title={review.user.displayName || "User"}
|
||||||
<button
|
>
|
||||||
className="game-details__review-display-name game-details__review-display-name--clickable"
|
<img
|
||||||
onClick={() =>
|
src={review.user.profileImageUrl}
|
||||||
review.user?.id &&
|
alt={review.user.displayName || "User"}
|
||||||
navigate(`/profile/${review.user.id}`)
|
className="game-details__review-avatar"
|
||||||
}
|
/>
|
||||||
>
|
</button>
|
||||||
{review.user?.displayName || "Anonymous"}
|
)}
|
||||||
</button>
|
<div className="game-details__review-user-info">
|
||||||
<div className="game-details__review-date">
|
<button
|
||||||
<ClockIcon size={12} />
|
className="game-details__review-display-name game-details__review-display-name--clickable"
|
||||||
{formatDistance(
|
onClick={() =>
|
||||||
new Date(review.createdAt),
|
review.user?.id &&
|
||||||
new Date(),
|
navigate(`/profile/${review.user.id}`)
|
||||||
{ addSuffix: true }
|
}
|
||||||
)}
|
>
|
||||||
|
{review.user?.displayName || "Anonymous"}
|
||||||
|
</button>
|
||||||
|
<div className="game-details__review-date">
|
||||||
|
<ClockIcon size={12} />
|
||||||
|
{formatDistance(
|
||||||
|
new Date(review.createdAt),
|
||||||
|
new Date(),
|
||||||
|
{ addSuffix: true }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="game-details__review-score-stars"
|
||||||
|
title={getRatingText(review.score, t)}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5].map((starValue) => (
|
||||||
|
<Star
|
||||||
|
key={starValue}
|
||||||
|
size={20}
|
||||||
|
fill={
|
||||||
|
starValue <= review.score
|
||||||
|
? "currentColor"
|
||||||
|
: "none"
|
||||||
|
}
|
||||||
|
className={`game-details__review-star ${
|
||||||
|
starValue <= review.score
|
||||||
|
? "game-details__review-star--filled"
|
||||||
|
: "game-details__review-star--empty"
|
||||||
|
} ${
|
||||||
|
starValue <= review.score
|
||||||
|
? getScoreColorClass(review.score)
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="game-details__review-score-stars"
|
className="game-details__review-content"
|
||||||
title={getRatingText(review.score, t)}
|
dangerouslySetInnerHTML={{
|
||||||
>
|
__html: sanitizeHtml(review.reviewHtml),
|
||||||
{[1, 2, 3, 4, 5].map((starValue) => (
|
}}
|
||||||
<Star
|
/>
|
||||||
key={starValue}
|
<div className="game-details__review-actions">
|
||||||
size={20}
|
<div className="game-details__review-votes">
|
||||||
fill={
|
<motion.button
|
||||||
starValue <= review.score
|
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
|
||||||
? "currentColor"
|
onClick={() =>
|
||||||
: "none"
|
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)
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="game-details__review-content"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: sanitizeHtml(review.reviewHtml),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="game-details__review-actions">
|
|
||||||
<div className="game-details__review-votes">
|
|
||||||
<motion.button
|
|
||||||
className={`game-details__vote-button game-details__vote-button--upvote ${review.hasUpvoted ? "game-details__vote-button--active" : ""}`}
|
|
||||||
onClick={() =>
|
|
||||||
handleVoteReview(review.id, "upvote")
|
|
||||||
}
|
|
||||||
animate={
|
|
||||||
review.hasUpvoted
|
|
||||||
? {
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
transition: { duration: 0.3 },
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ThumbsUp size={16} />
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.span
|
|
||||||
key={review.upvotes || 0}
|
|
||||||
custom={
|
|
||||||
(review.upvotes || 0) >
|
|
||||||
(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)}
|
|
||||||
</motion.span>
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.button>
|
|
||||||
<motion.button
|
|
||||||
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
|
|
||||||
onClick={() =>
|
|
||||||
handleVoteReview(review.id, "downvote")
|
|
||||||
}
|
|
||||||
animate={
|
|
||||||
review.hasDownvoted
|
|
||||||
? {
|
|
||||||
scale: [1, 1.2, 1],
|
|
||||||
transition: { duration: 0.3 },
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ThumbsDown size={16} />
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.span
|
|
||||||
key={review.downvotes || 0}
|
|
||||||
custom={
|
|
||||||
(review.downvotes || 0) >
|
|
||||||
(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)}
|
|
||||||
</motion.span>
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
{userDetails?.id === review.user?.id && (
|
|
||||||
<button
|
|
||||||
className="game-details__delete-review-button"
|
|
||||||
onClick={() => handleDeleteReview(review.id)}
|
|
||||||
title={t("delete_review")}
|
|
||||||
>
|
|
||||||
<TrashIcon size={16} />
|
|
||||||
<span>{t("remove_review")}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{review.isBlocked &&
|
|
||||||
visibleBlockedReviews.has(review.id) && (
|
|
||||||
<button
|
|
||||||
className="game-details__blocked-review-hide-link"
|
|
||||||
onClick={() => toggleBlockedReview(review.id)}
|
|
||||||
>
|
>
|
||||||
Hide
|
<ThumbsUp size={16} />
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.span
|
||||||
|
key={review.upvotes || 0}
|
||||||
|
custom={
|
||||||
|
(review.upvotes || 0) >
|
||||||
|
(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)}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
className={`game-details__vote-button game-details__vote-button--downvote ${review.hasDownvoted ? "game-details__vote-button--active" : ""}`}
|
||||||
|
onClick={() =>
|
||||||
|
handleVoteReview(review.id, "downvote")
|
||||||
|
}
|
||||||
|
animate={
|
||||||
|
review.hasDownvoted
|
||||||
|
? {
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
transition: { duration: 0.3 },
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ThumbsDown size={16} />
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.span
|
||||||
|
key={review.downvotes || 0}
|
||||||
|
custom={
|
||||||
|
(review.downvotes || 0) >
|
||||||
|
(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)}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
{userDetails?.id === review.user?.id && (
|
||||||
|
<button
|
||||||
|
className="game-details__delete-review-button"
|
||||||
|
onClick={() => handleDeleteReview(review.id)}
|
||||||
|
title={t("delete_review")}
|
||||||
|
>
|
||||||
|
<TrashIcon size={16} />
|
||||||
|
<span>{t("remove_review")}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
{review.isBlocked &&
|
||||||
</>
|
visibleBlockedReviews.has(review.id) && (
|
||||||
)}
|
<button
|
||||||
</div>
|
className="game-details__blocked-review-hide-link"
|
||||||
))}
|
onClick={() =>
|
||||||
|
toggleBlockedReview(review.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Hide
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasMoreReviews && !reviewsLoading && (
|
{hasMoreReviews && !reviewsLoading && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ export function EditGameModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
|
const setCustomGameAssets = useCallback((game: LibraryGame | Game) => {
|
||||||
|
// Check if assets were removed (URLs are null but original paths exist)
|
||||||
|
const iconRemoved = !game.iconUrl && (game as any).originalIconPath;
|
||||||
|
const logoRemoved = !game.logoImageUrl && (game as any).originalLogoPath;
|
||||||
|
const heroRemoved =
|
||||||
|
!game.libraryHeroImageUrl && (game as any).originalHeroPath;
|
||||||
|
|
||||||
setAssetPaths({
|
setAssetPaths({
|
||||||
icon: extractLocalPath(game.iconUrl),
|
icon: extractLocalPath(game.iconUrl),
|
||||||
logo: extractLocalPath(game.logoImageUrl),
|
logo: extractLocalPath(game.logoImageUrl),
|
||||||
@@ -85,10 +91,25 @@ export function EditGameModal({
|
|||||||
(game as any).originalHeroPath ||
|
(game as any).originalHeroPath ||
|
||||||
extractLocalPath(game.libraryHeroImageUrl),
|
extractLocalPath(game.libraryHeroImageUrl),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set removed assets state based on whether assets were explicitly removed
|
||||||
|
setRemovedAssets({
|
||||||
|
icon: iconRemoved,
|
||||||
|
logo: logoRemoved,
|
||||||
|
hero: heroRemoved,
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setNonCustomGameAssets = useCallback(
|
const setNonCustomGameAssets = useCallback(
|
||||||
(game: LibraryGame) => {
|
(game: LibraryGame) => {
|
||||||
|
// Check if assets were removed (custom URLs are null but original paths exist)
|
||||||
|
const iconRemoved =
|
||||||
|
!game.customIconUrl && (game as any).customOriginalIconPath;
|
||||||
|
const logoRemoved =
|
||||||
|
!game.customLogoImageUrl && (game as any).customOriginalLogoPath;
|
||||||
|
const heroRemoved =
|
||||||
|
!game.customHeroImageUrl && (game as any).customOriginalHeroPath;
|
||||||
|
|
||||||
setAssetPaths({
|
setAssetPaths({
|
||||||
icon: extractLocalPath(game.customIconUrl),
|
icon: extractLocalPath(game.customIconUrl),
|
||||||
logo: extractLocalPath(game.customLogoImageUrl),
|
logo: extractLocalPath(game.customLogoImageUrl),
|
||||||
@@ -111,6 +132,13 @@ export function EditGameModal({
|
|||||||
extractLocalPath(game.customHeroImageUrl),
|
extractLocalPath(game.customHeroImageUrl),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set removed assets state based on whether assets were explicitly removed
|
||||||
|
setRemovedAssets({
|
||||||
|
icon: iconRemoved,
|
||||||
|
logo: logoRemoved,
|
||||||
|
hero: heroRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
setDefaultUrls({
|
setDefaultUrls({
|
||||||
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
|
icon: shopDetails?.assets?.iconUrl || game.iconUrl || null,
|
||||||
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
|
logo: shopDetails?.assets?.logoImageUrl || game.logoImageUrl || null,
|
||||||
@@ -148,8 +176,12 @@ export function EditGameModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAssetDisplayPath = (assetType: AssetType): string => {
|
const getAssetDisplayPath = (assetType: AssetType): string => {
|
||||||
// Use original path if available, otherwise fall back to display path
|
// If asset was removed, don't show any path
|
||||||
return originalAssetPaths[assetType] || assetDisplayPaths[assetType];
|
if (removedAssets[assetType]) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Use display path first, then fall back to original path
|
||||||
|
return assetDisplayPaths[assetType] || originalAssetPaths[assetType];
|
||||||
};
|
};
|
||||||
|
|
||||||
const setAssetPath = (assetType: AssetType, path: string): void => {
|
const setAssetPath = (assetType: AssetType, path: string): void => {
|
||||||
@@ -221,18 +253,11 @@ export function EditGameModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRestoreDefault = (assetType: AssetType) => {
|
const handleRestoreDefault = (assetType: AssetType) => {
|
||||||
if (game && isCustomGame(game)) {
|
// Mark asset as removed and clear paths (for both custom and non-custom games)
|
||||||
// For custom games, mark asset as removed and clear paths
|
setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
|
||||||
setRemovedAssets((prev) => ({ ...prev, [assetType]: true }));
|
setAssetPath(assetType, "");
|
||||||
setAssetPath(assetType, "");
|
setAssetDisplayPath(assetType, "");
|
||||||
setAssetDisplayPath(assetType, "");
|
// Don't clear originalAssetPaths - keep them for reference but don't use them for display
|
||||||
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
|
||||||
} else {
|
|
||||||
// For non-custom games, clear custom assets (restore to shop defaults)
|
|
||||||
setAssetPath(assetType, "");
|
|
||||||
setAssetDisplayPath(assetType, "");
|
|
||||||
setOriginalAssetPaths((prev) => ({ ...prev, [assetType]: "" }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOriginalTitle = (): string => {
|
const getOriginalTitle = (): string => {
|
||||||
@@ -402,10 +427,28 @@ export function EditGameModal({
|
|||||||
|
|
||||||
// Helper function to prepare non-custom game assets
|
// Helper function to prepare non-custom game assets
|
||||||
const prepareNonCustomGameAssets = () => {
|
const prepareNonCustomGameAssets = () => {
|
||||||
|
const hasIconPath = assetPaths.icon;
|
||||||
|
let customIconUrl: string | null = null;
|
||||||
|
if (!removedAssets.icon && hasIconPath) {
|
||||||
|
customIconUrl = `local:${assetPaths.icon}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLogoPath = assetPaths.logo;
|
||||||
|
let customLogoImageUrl: string | null = null;
|
||||||
|
if (!removedAssets.logo && hasLogoPath) {
|
||||||
|
customLogoImageUrl = `local:${assetPaths.logo}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasHeroPath = assetPaths.hero;
|
||||||
|
let customHeroImageUrl: string | null = null;
|
||||||
|
if (!removedAssets.hero && hasHeroPath) {
|
||||||
|
customHeroImageUrl = `local:${assetPaths.hero}`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customIconUrl: assetPaths.icon ? `local:${assetPaths.icon}` : null,
|
customIconUrl,
|
||||||
customLogoImageUrl: assetPaths.logo ? `local:${assetPaths.logo}` : null,
|
customLogoImageUrl,
|
||||||
customHeroImageUrl: assetPaths.hero ? `local:${assetPaths.hero}` : null,
|
customHeroImageUrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -439,9 +482,15 @@ export function EditGameModal({
|
|||||||
customIconUrl,
|
customIconUrl,
|
||||||
customLogoImageUrl,
|
customLogoImageUrl,
|
||||||
customHeroImageUrl,
|
customHeroImageUrl,
|
||||||
customOriginalIconPath: originalAssetPaths.icon || undefined,
|
customOriginalIconPath: removedAssets.icon
|
||||||
customOriginalLogoPath: originalAssetPaths.logo || undefined,
|
? undefined
|
||||||
customOriginalHeroPath: originalAssetPaths.hero || undefined,
|
: originalAssetPaths.icon || undefined,
|
||||||
|
customOriginalLogoPath: removedAssets.logo
|
||||||
|
? undefined
|
||||||
|
: originalAssetPaths.logo || undefined,
|
||||||
|
customOriginalHeroPath: removedAssets.hero
|
||||||
|
? undefined
|
||||||
|
: originalAssetPaths.hero || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -484,6 +533,23 @@ export function EditGameModal({
|
|||||||
hero: false,
|
hero: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear all asset paths to ensure clean state
|
||||||
|
setAssetPaths({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
setAssetDisplayPaths({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
setOriginalAssetPaths({
|
||||||
|
icon: "",
|
||||||
|
logo: "",
|
||||||
|
hero: "",
|
||||||
|
});
|
||||||
|
|
||||||
if (isCustomGame(game)) {
|
if (isCustomGame(game)) {
|
||||||
setCustomGameAssets(game);
|
setCustomGameAssets(game);
|
||||||
// Clear default URLs for custom games
|
// Clear default URLs for custom games
|
||||||
|
|||||||
Reference in New Issue
Block a user