Feat: added rating showing in game card in categories, fixed maybe later button, changed empty state, fixed copy issue, added karma showing, added remove review text, added empty state for games with no reviews, fixed sorting buttons, fixed shift in the page

This commit is contained in:
Moyasee
2025-10-05 20:32:41 +03:00
parent 8653e62dce
commit 6667e00c91
16 changed files with 448 additions and 158 deletions

View File

@@ -72,7 +72,12 @@
display: flex;
color: globals.$muted-color;
font-size: 12px;
align-items: flex-end;
align-items: center;
// Ensure star rating is properly aligned
.star-rating {
align-items: center;
}
}
&__title-container {

View File

@@ -1,4 +1,4 @@
import { DownloadIcon, PeopleIcon, StarIcon } from "@primer/octicons-react";
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import type { GameStats } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -7,6 +7,7 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { StarRating } from "../star-rating/star-rating";
import { useCallback, useState, useMemo } from "react";
import { useFormat, useRepacks } from "@renderer/hooks";
@@ -107,12 +108,14 @@ export function GameCard({ game, ...props }: GameCardProps) {
{stats ? numberFormatter.format(stats.playerCount) : "…"}
</span>
</div>
{stats?.averageScore && (
<div className="game-card__specifics-item">
<StarIcon />
<span>{stats.averageScore.toFixed(1)}</span>
</div>
)}
<div className="game-card__specifics-item">
<StarRating
rating={stats?.averageScore || null}
size={14}
showCalculating={!!(stats && stats.averageScore === null)}
calculatingText={t("calculating")}
/>
</div>
</div>
</div>
</div>

View File

@@ -18,3 +18,4 @@ export * from "./debrid-badge/debrid-badge";
export * from "./context-menu/context-menu";
export * from "./game-context-menu/game-context-menu";
export * from "./game-context-menu/use-game-actions";
export * from "./star-rating/star-rating";

View File

@@ -0,0 +1 @@
export * from "./star-rating";

View File

@@ -0,0 +1,54 @@
@use "../../scss/globals.scss";
.star-rating {
display: flex;
align-items: center;
gap: 2px;
&__star {
color: globals.$muted-color;
transition: color ease 0.2s;
&--filled {
color: #ffffff;
}
&--empty {
color: globals.$muted-color;
}
&--half {
color: #ffffff;
position: absolute;
top: 0;
left: 0;
clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
}
}
&__half-star {
position: relative;
display: inline-block;
}
&__value {
margin-left: 4px;
font-size: 12px;
color: globals.$muted-color;
font-weight: 500;
}
&__calculating-text,
&__no-rating-text {
margin-left: 4px;
font-size: 12px;
color: globals.$muted-color;
}
&--calculating,
&--no-rating {
.star-rating__star {
color: globals.$muted-color;
}
}
}

View File

@@ -0,0 +1,64 @@
import { StarIcon, StarFillIcon } from "@primer/octicons-react";
import "./star-rating.scss";
export interface StarRatingProps {
rating: number | null;
maxStars?: number;
size?: number;
showCalculating?: boolean;
calculatingText?: string;
}
export function StarRating({
rating,
maxStars = 5,
size = 12,
showCalculating = false,
calculatingText = "Calculating"
}: StarRatingProps) {
if (rating === null && showCalculating) {
return (
<div className="star-rating star-rating--calculating">
<StarIcon size={size} />
<span className="star-rating__calculating-text">{calculatingText}</span>
</div>
);
}
if (rating === null || rating === undefined) {
return (
<div className="star-rating star-rating--no-rating">
<StarIcon size={size} />
<span className="star-rating__no-rating-text"></span>
</div>
);
}
const filledStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
const emptyStars = maxStars - filledStars - (hasHalfStar ? 1 : 0);
return (
<div className="star-rating">
{Array.from({ length: filledStars }, (_, index) => (
<StarFillIcon key={`filled-${index}`} size={size} className="star-rating__star star-rating__star--filled" />
))}
{hasHalfStar && (
<div className="star-rating__half-star" key="half-star">
<StarIcon size={size} className="star-rating__star star-rating__star--empty" />
<StarFillIcon size={size} className="star-rating__star star-rating__star--half" />
</div>
)}
{Array.from({ length: emptyStars }, (_, index) => (
<StarIcon key={`empty-${index}`} size={size} className="star-rating__star star-rating__star--empty" />
))}
<span className="star-rating__value">{rating.toFixed(1)}</span>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { PencilIcon, TrashIcon, ClockIcon } from "@primer/octicons-react";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { PencilIcon, TrashIcon, ClockIcon, NoteIcon } from "@primer/octicons-react";
import { ThumbsUp, ThumbsDown, Star } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
@@ -20,28 +20,24 @@ import { useTranslation } from "react-i18next";
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
import { useUserDetails, useLibrary, useDate } from "@renderer/hooks";
import { useUserDetails, useLibrary, useDate, useToast } from "@renderer/hooks";
import { useSubscription } from "@renderer/hooks/use-subscription";
import "./game-details.scss";
// Helper function to get score color class
const getScoreColorClass = (score: number): string => {
if (score >= 0 && score <= 3) return "game-details__review-score--red";
if (score >= 4 && score <= 6) return "game-details__review-score--yellow";
if (score >= 7 && score <= 10) return "game-details__review-score--green";
if (score >= 1 && score <= 2) return "game-details__review-score--red";
if (score >= 3 && score <= 3) return "game-details__review-score--yellow";
if (score >= 4 && score <= 5) return "game-details__review-score--green";
return "";
};
// Helper function to process media elements for responsive display
const processMediaElements = (document: Document) => {
const $images = Array.from(document.querySelectorAll("img"));
$images.forEach(($image) => {
$image.loading = "lazy";
// Remove any inline width/height styles that might cause overflow
$image.removeAttribute("width");
$image.removeAttribute("height");
$image.removeAttribute("style");
// Set max-width to prevent overflow
$image.style.maxWidth = "100%";
$image.style.width = "auto";
$image.style.height = "auto";
@@ -51,11 +47,9 @@ const processMediaElements = (document: Document) => {
// Handle videos the same way
const $videos = Array.from(document.querySelectorAll("video"));
$videos.forEach(($video) => {
// Remove any inline width/height styles that might cause overflow
$video.removeAttribute("width");
$video.removeAttribute("height");
$video.removeAttribute("style");
// Set max-width to prevent overflow
$video.style.maxWidth = "100%";
$video.style.width = "auto";
$video.style.height = "auto";
@@ -63,16 +57,26 @@ const processMediaElements = (document: Document) => {
});
};
// Helper function to get score color class for select element
const getSelectScoreColorClass = (score: number): string => {
if (score >= 0 && score <= 3) return "game-details__review-score-select--red";
if (score >= 4 && score <= 7)
if (score >= 1 && score <= 2) return "game-details__review-score-select--red";
if (score >= 3 && score <= 3)
return "game-details__review-score-select--yellow";
if (score >= 8 && score <= 10)
if (score >= 4 && score <= 5)
return "game-details__review-score-select--green";
return "";
};
const getRatingText = (score: number, t: (key: string) => string): string => {
switch (score) {
case 1: return t("rating_very_negative");
case 2: return t("rating_negative");
case 3: return t("rating_neutral");
case 4: return t("rating_positive");
case 5: return t("rating_very_positive");
default: return "";
}
};
export function GameDetailsContent() {
const heroRef = useRef<HTMLDivElement | null>(null);
const navigate = useNavigate();
@@ -93,6 +97,7 @@ export function GameDetailsContent() {
const { userDetails, hasActiveSubscription } = useUserDetails();
const { updateLibrary } = useLibrary();
const { formatDistance } = useDate();
const { showSuccessToast, showErrorToast } = useToast();
const { setShowCloudSyncModal, getGameArtifacts } =
useContext(cloudSyncContext);
@@ -139,16 +144,13 @@ export function GameDetailsContent() {
const [totalReviewCount, setTotalReviewCount] = useState(0);
const [showReviewForm, setShowReviewForm] = useState(false);
// Review prompt banner state
const [showReviewPrompt, setShowReviewPrompt] = useState(false);
const [hasUserReviewed, setHasUserReviewed] = useState(false);
const [reviewCheckLoading, setReviewCheckLoading] = useState(false);
// Tiptap editor for review input
const editor = useEditor({
extensions: [
StarterKit.configure({
// Disable link extension to prevent automatic link rendering and XSS
link: false,
}),
],
@@ -159,14 +161,26 @@ export function GameDetailsContent() {
"data-placeholder": t("write_review_placeholder"),
},
handlePaste: (view, event) => {
// Strip formatting from pasted content to prevent overflow issues
const text = event.clipboardData?.getData("text/plain") || "";
const htmlContent = event.clipboardData?.getData("text/html") || "";
const plainText = event.clipboardData?.getData("text/plain") || "";
const currentText = view.state.doc.textContent;
const remainingChars = MAX_REVIEW_CHARS - currentText.length;
if (text && remainingChars > 0) {
if ((htmlContent || plainText) && remainingChars > 0) {
event.preventDefault();
const truncatedText = text.slice(0, remainingChars);
if (htmlContent) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlContent;
const textLength = tempDiv.textContent?.length || 0;
if (textLength <= remainingChars) {
return false;
}
}
const truncatedText = plainText.slice(0, remainingChars);
view.dispatch(view.state.tr.insertText(truncatedText));
return true;
}
@@ -177,7 +191,6 @@ export function GameDetailsContent() {
const text = editor.getText();
setReviewCharCount(text.length);
// Prevent typing beyond character limit
if (text.length > MAX_REVIEW_CHARS) {
const truncatedContent = text.slice(0, MAX_REVIEW_CHARS);
editor.commands.setContent(truncatedContent);
@@ -219,7 +232,6 @@ export function GameDetailsContent() {
const isCustomGame = game?.shop === "custom";
// Reviews functions
const checkUserReview = async () => {
if (!objectId || !userDetails) return;
@@ -229,11 +241,9 @@ export function GameDetailsContent() {
const hasReviewed = (response as any)?.hasReviewed || false;
setHasUserReviewed(hasReviewed);
// Show prompt only if user hasn't reviewed and has played the game
if (
!hasReviewed &&
game?.playTimeInMilliseconds &&
game.playTimeInMilliseconds > 0
!sessionStorage.getItem(`reviewPromptDismissed_${objectId}`)
) {
setShowReviewPrompt(true);
}
@@ -258,7 +268,6 @@ export function GameDetailsContent() {
reviewsSortBy
);
// Handle the response structure: { totalCount: number, reviews: Review[] }
const reviewsData = (response as any)?.reviews || [];
const reviewCount = (response as any)?.totalCount || 0;
@@ -286,7 +295,6 @@ export function GameDetailsContent() {
try {
await window.electron.voteReview(shop, objectId, reviewId, voteType);
// Reload reviews to get updated vote counts
loadReviews(true);
} catch (error) {
console.error(`Failed to ${voteType} review:`, error);
@@ -303,40 +311,40 @@ export function GameDetailsContent() {
try {
await window.electron.deleteReview(shop, objectId, reviewToDelete);
// Reload reviews after deletion
loadReviews(true);
setShowDeleteReviewModal(false);
setReviewToDelete(null);
showSuccessToast(t("review_deleted_successfully"));
} catch (error) {
console.error("Failed to delete review:", error);
showErrorToast(t("review_deletion_failed"));
}
};
const handleSubmitReview = async () => {
console.log("handleSubmitReview called");
console.log("game:", game);
console.log("objectId:", objectId);
const reviewHtml = editor?.getHTML() || "";
console.log("reviewHtml:", reviewHtml);
console.log("reviewScore:", reviewScore);
console.log("submittingReview:", submittingReview);
const reviewText = editor?.getText() || "";
if (
!objectId ||
!reviewHtml.trim() ||
reviewScore === null ||
submittingReview ||
reviewCharCount > MAX_REVIEW_CHARS
) {
console.log("Early return - validation failed");
if (!objectId) {
return;
}
if (!reviewText.trim()) {
showErrorToast(t("review_cannot_be_empty"));
return;
}
if (submittingReview || reviewCharCount > MAX_REVIEW_CHARS) {
return;
}
if (reviewScore === null) {
return;
}
console.log("Starting review submission...");
setSubmittingReview(true);
try {
console.log("Calling window.electron.createGameReview...");
await window.electron.createGameReview(
shop,
objectId,
@@ -344,27 +352,25 @@ export function GameDetailsContent() {
reviewScore
);
console.log("Review submitted successfully");
editor?.commands.clearContent();
setReviewScore(null);
await loadReviews(true); // Reload reviews after submission
setShowReviewForm(false); // Hide the review form after successful submission
setShowReviewPrompt(false); // Hide the prompt banner
setHasUserReviewed(true); // Update the review status
showSuccessToast(t("review_submitted_successfully"));
await loadReviews(true);
setShowReviewForm(false);
setShowReviewPrompt(false);
setHasUserReviewed(true);
} catch (error) {
console.error("Failed to submit review:", error);
showErrorToast(t("review_submission_failed"));
} finally {
setSubmittingReview(false);
console.log("Review submission completed");
}
};
// Review prompt banner handlers
const handleReviewPromptYes = () => {
setShowReviewPrompt(false);
setShowReviewForm(true);
// Scroll to review form
setTimeout(() => {
const reviewFormElement = document.querySelector(
".game-details__review-form"
@@ -380,13 +386,18 @@ export function GameDetailsContent() {
const handleReviewPromptLater = () => {
setShowReviewPrompt(false);
if (objectId) {
sessionStorage.setItem(`reviewPromptDismissed_${objectId}`, 'true');
}
};
const handleSortChange = (newSortBy: string) => {
setReviewsSortBy(newSortBy);
setReviewsPage(0);
setHasMoreReviews(true);
loadReviews(true);
if (newSortBy !== reviewsSortBy) {
setReviewsSortBy(newSortBy);
setReviewsPage(0);
setHasMoreReviews(true);
loadReviews(true);
}
};
const toggleBlockedReview = (reviewId: string) => {
@@ -408,22 +419,19 @@ export function GameDetailsContent() {
}
};
// Load reviews when component mounts or sort changes
useEffect(() => {
if (objectId && (game || shop)) {
loadReviews(true);
checkUserReview(); // Check if user has reviewed this game
checkUserReview();
}
}, [game, shop, objectId, reviewsSortBy, userDetails]);
// Load more reviews when page changes
useEffect(() => {
if (reviewsPage > 0) {
loadReviews(false);
}
}, [reviewsPage]);
// Helper function to get image with custom asset priority
const getImageWithCustomPriority = (
customUrl: string | null | undefined,
originalUrl: string | null | undefined,
@@ -540,7 +548,6 @@ export function GameDetailsContent() {
{game?.shop !== "custom" &&
showReviewPrompt &&
userDetails &&
game?.playTimeInMilliseconds &&
!hasUserReviewed &&
!reviewCheckLoading && (
<ReviewPromptBanner
@@ -637,36 +644,30 @@ export function GameDetailsContent() {
<div className="game-details__review-form-bottom">
<div className="game-details__review-score-container">
<label className="game-details__review-score-label">
{t("rating")}
</label>
<select
className={`game-details__review-score-select ${
reviewScore
? getSelectScoreColorClass(reviewScore)
: ""
}`}
value={reviewScore || ""}
onChange={(e) =>
setReviewScore(
e.target.value ? Number(e.target.value) : null
)
}
>
<option value="" disabled>
{t("select_rating")}
</option>
<option value={1}>1/10</option>
<option value={2}>2/10</option>
<option value={3}>3/10</option>
<option value={4}>4/10</option>
<option value={5}>5/10</option>
<option value={6}>6/10</option>
<option value={7}>7/10</option>
<option value={8}>8/10</option>
<option value={9}>9/10</option>
<option value={10}>10/10</option>
</select>
<div className="game-details__star-rating">
{[1, 2, 3, 4, 5].map((starValue) => (
<button
key={starValue}
type="button"
className={`game-details__star ${
reviewScore && starValue <= reviewScore
? "game-details__star--filled"
: "game-details__star--empty"
} ${
reviewScore && starValue <= reviewScore
? getSelectScoreColorClass(reviewScore)
: ""
}`}
onClick={() => setReviewScore(starValue)}
title={getRatingText(starValue, t)}
>
<Star
size={24}
fill={reviewScore && starValue <= reviewScore ? "currentColor" : "none"}
/>
</button>
))}
</div>
</div>
<button
@@ -683,6 +684,7 @@ export function GameDetailsContent() {
? t("submitting")
: t("submit_review")}
</button>
</div>
</div>
</>
@@ -716,7 +718,9 @@ export function GameDetailsContent() {
{!reviewsLoading && reviews.length === 0 && (
<div className="game-details__reviews-empty">
<div className="game-details__reviews-empty-icon">📝</div>
<div className="game-details__reviews-empty-icon">
<NoteIcon size={48} />
</div>
<h4 className="game-details__reviews-empty-title">
{t("no_reviews_yet")}
</h4>
@@ -770,10 +774,23 @@ export function GameDetailsContent() {
</div>
</div>
</div>
<div
className={`game-details__review-score ${getScoreColorClass(review.score)}`}
>
{review.score}/10
<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
@@ -842,6 +859,7 @@ export function GameDetailsContent() {
title={t("delete_review")}
>
<TrashIcon size={16} />
<span>{t("remove_review")}</span>
</button>
)}
{review.isBlocked &&

View File

@@ -55,10 +55,31 @@ $hero-height: 300px;
flex-wrap: wrap;
}
&__review-message {
padding: calc(globals.$spacing-unit * 1);
border-radius: 4px;
font-size: globals.$small-font-size;
font-weight: 500;
margin-top: calc(globals.$spacing-unit * 1);
border: 1px solid;
&--success {
background: rgba(34, 197, 94, 0.1);
color: #86efac;
border-color: rgba(34, 197, 94, 0.3);
}
&--error {
background: rgba(239, 68, 68, 0.1);
color: #fca5a5;
border-color: rgba(239, 68, 68, 0.3);
}
}
&__review-score-container {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
}
&__review-score-label {
@@ -104,6 +125,59 @@ $hero-height: 300px;
}
}
&__star-rating {
display: flex;
align-items: center;
gap: 4px;
}
&__star {
background: none;
border: none;
color: #666666;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
color: #ffffff;
background-color: rgba(255, 255, 255, 0.1);
transform: scale(1.1);
}
&--filled {
color: #ffffff;
&.game-details__review-score-select--red {
color: #e74c3c;
}
&.game-details__review-score-select--yellow {
color: #f39c12;
}
&.game-details__review-score-select--green {
color: #27ae60;
}
}
&--empty {
color: #666666;
&:hover {
color: #ffffff;
}
}
svg {
fill: currentColor;
}
}
&__reviews-sort {
display: flex;
flex-direction: column;
@@ -191,16 +265,13 @@ $hero-height: 300px;
&__reviews-empty {
text-align: center;
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__reviews-empty-icon {
font-size: 48px;
margin-bottom: calc(globals.$spacing-unit * 2);
opacity: 0.6;
color: rgba(255, 255, 255, 0.6);
}
&__reviews-empty-title {
@@ -341,6 +412,7 @@ $hero-height: 300px;
color: #f44336;
cursor: pointer;
transition: all 0.2s ease;
gap: 6px;
&:hover {
background: rgba(244, 67, 54, 0.2);
@@ -387,32 +459,39 @@ $hero-height: 300px;
}
}
&__review-score {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
padding: calc(globals.$spacing-unit * 0.5) calc(globals.$spacing-unit * 1);
border-radius: 4px;
font-size: globals.$small-font-size;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.15);
&__review-score-stars {
display: flex;
align-items: center;
gap: 2px;
}
// Color variants based on score
&--red {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border-color: rgba(239, 68, 68, 0.4);
&__review-star {
color: #666666;
transition: color 0.2s ease;
cursor: default;
&--filled {
color: #ffffff;
&.game-details__review-score--red {
color: #fca5a5;
}
&.game-details__review-score--yellow {
color: #fcd34d;
}
&.game-details__review-score--green {
color: #86efac;
}
}
&--yellow {
background: rgba(245, 158, 11, 0.2);
color: #fcd34d;
border-color: rgba(245, 158, 11, 0.4);
&--empty {
color: #666666;
}
&--green {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
border-color: rgba(34, 197, 94, 0.4);
svg {
fill: currentColor;
}
}

View File

@@ -4,7 +4,7 @@
background: rgba(255, 255, 255, 0.02);
border-radius: 8px;
padding: calc(globals.$spacing-unit * 2);
margin-bottom: calc(globals.$spacing-unit * 3);
margin-bottom: calc(globals.$spacing-unit * 1.5);
border: 1px solid rgba(255, 255, 255, 0.05);
&__content {

View File

@@ -18,7 +18,7 @@ export function ReviewPromptBanner({
<div className="review-prompt-banner__content">
<div className="review-prompt-banner__text">
<span className="review-prompt-banner__playtime">
You&apos;ve seemed to enjoy this game
{t("you_seemed_to_enjoy_this_game")}
</span>
<span className="review-prompt-banner__question">
{t("would_you_recommend_this_game")}

View File

@@ -35,7 +35,9 @@ export function ReviewSortOptions({
};
const handleMostVotedClick = () => {
onSortChange("most_voted");
if (sortBy !== "most_voted") {
onSortChange("most_voted");
}
};
const isDateActive = sortBy === "newest" || sortBy === "oldest";

View File

@@ -5,7 +5,7 @@ import type {
UserAchievement,
} from "@types";
import { useTranslation } from "react-i18next";
import { Button, Link } from "@renderer/components";
import { Button, Link, StarRating } from "@renderer/components";
import { gameDetailsContext } from "@renderer/context";
import { useDate, useFormat, useUserDetails } from "@renderer/hooks";
@@ -227,15 +227,18 @@ export function Sidebar() {
<p>{numberFormatter.format(stats?.playerCount)}</p>
</div>
{stats?.averageScore && (
<div className="stats__category">
<p className="stats__category-title">
<StarIcon size={18} />
{t("rating_count")}
</p>
<p>{stats.averageScore.toFixed(1)}/10</p>
</div>
)}
<div className="stats__category">
<p className="stats__category-title">
<StarIcon size={18} />
{t("rating_count")}
</p>
<StarRating
rating={stats?.averageScore || 0}
size={16}
showCalculating={!!(stats && stats.averageScore === null)}
calculatingText={t("calculating", { ns: "game_card" })}
/>
</div>
</div>
</SidebarSection>
)}

View File

@@ -6,13 +6,16 @@ import { Award } from "lucide-react";
import "./user-karma-box.scss";
export function UserKarmaBox() {
const { isMe } = useContext(userProfileContext);
const { isMe, userProfile } = useContext(userProfileContext);
const { userDetails } = useUserDetails();
const { t } = useTranslation("user_profile");
const { numberFormatter } = useFormat();
// Only show karma for the current user (me)
if (!isMe || !userDetails) return null;
// Get karma from userDetails (for current user) or userProfile (for other users)
const karma = isMe ? userDetails?.karma : userProfile?.karma;
// Don't show if karma is not available
if (karma === undefined || karma === null) return null;
return (
<div>
@@ -24,7 +27,7 @@ export function UserKarmaBox() {
<div className="user-karma__content">
<div className="user-karma__stats-row">
<p className="user-karma__description">
<Award size={20} /> {numberFormatter.format(userDetails.karma)}{" "}
<Award size={20} /> {numberFormatter.format(karma)}{" "}
{t("karma_count")}
</p>
</div>