diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b54fe2fb..1af953fe 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -222,12 +222,22 @@ "sort_most_voted": "Most Voted", "rating": "Rating", "rating_stats": "Rating", - "select_rating": "Select Rating", + "rating_very_negative": "Very Negative", + "rating_negative": "Negative", + "rating_neutral": "Neutral", + "rating_positive": "Positive", + "rating_very_positive": "Very Positive", "submit_review": "Submit Review", "submitting": "Submitting...", + "review_submitted_successfully": "Review submitted successfully!", + "review_submission_failed": "Failed to submit review. Please try again.", + "review_cannot_be_empty": "Review text field cannot be empty.", + "review_deleted_successfully": "Review deleted successfully.", + "review_deletion_failed": "Failed to delete review. Please try again.", "loading_reviews": "Loading reviews...", "loading_more_reviews": "Loading more reviews...", "load_more_reviews": "Load More Reviews", + "you_seemed_to_enjoy_this_game": "You've seemed to enjoy this game", "would_you_recommend_this_game": "Would you like to leave a review to this game?", "yes": "Yes", "maybe_later": "Maybe Later", @@ -329,6 +339,7 @@ "filter_by_source": "Filter by source", "no_repacks_found": "No sources found for this game", "delete_review": "Delete review", + "remove_review": "Remove Review", "delete_review_modal_title": "Are you sure you want to delete your review?", "delete_review_modal_description": "This action cannot be undone.", "delete_review_modal_delete_button": "Delete", @@ -548,7 +559,8 @@ "game_card": { "available_one": "Available", "available_other": "Available", - "no_downloads": "No downloads available" + "no_downloads": "No downloads available", + "calculating": "Calculating" }, "binary_not_found_modal": { "title": "Programs not installed", @@ -654,7 +666,7 @@ "game_added_to_pinned": "Game added to pinned", "karma": "Karma", "karma_count": "karma", - "karma_description": "Earned from positive likes on your reviews" + "karma_description": "Earned from positive likes on reviews" }, "achievement": { "achievement_unlocked": "Achievement unlocked", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 8992a4a0..45177eaf 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -189,10 +189,14 @@ "refuse_nsfw_content": "Назад", "stats": "Статистика", "player_count": "Активные игроки", + "rating_count": "Рейтинг", "warning": "Внимание:", "hydra_needs_to_remain_open": "Для этой загрузки Hydra должна оставаться открытой до завершения. Если Hydra закроется до завершения, вы потеряете прогресс.", "achievements": "Достижения", "achievements_count": "Достижения {{unlockedCount}}/{{achievementsCount}}", + "show_more": "Показать больше", + "reviews": "Отзывы", + "leave_a_review": "Оставить отзыв", "cloud_save": "Облачное сохранение", "cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве", "backups": "Резервные копии", @@ -271,7 +275,41 @@ "backup_unfrozen": "Резервная копия откреплена", "backup_freeze_failed": "Не удалось закрепить резервную копию", "backup_freeze_failed_description": "Вы должны оставить как минимум один свободный слот для автоматических резервных копий", - "manual_playtime_tooltip": "Это время игры было обновлено вручную" + "manual_playtime_tooltip": "Это время игры было обновлено вручную", + "write_review_placeholder": "Поделитесь своими мыслями об этой игре...", + "sort_newest": "Новые", + "no_reviews_yet": "Пока нет отзывов", + "be_first_to_review": "Будьте первым, кто поделится своими мыслями об этой игре!", + "sort_oldest": "Старые", + "sort_highest_score": "Высший балл", + "sort_lowest_score": "Низший балл", + "sort_most_voted": "Самые популярные", + "rating": "Рейтинг", + "rating_stats": "Рейтинг", + "rating_very_negative": "Очень негативный", + "rating_negative": "Негативный", + "rating_neutral": "Нейтральный", + "rating_positive": "Позитивный", + "rating_very_positive": "Очень позитивный", + "submit_review": "Отправить отзыв", + "submitting": "Отправка...", + "remove_review": "Удалить отзыв", + "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?", + "delete_review_modal_description": "Это действие нельзя отменить.", + "delete_review_modal_delete_button": "Удалить", + "delete_review_modal_cancel_button": "Отмена", + "review_submitted_successfully": "Отзыв успешно отправлен!", + "review_submission_failed": "Не удалось отправить отзыв. Попробуйте еще раз.", + "review_cannot_be_empty": "Поле отзыва не может быть пустым.", + "review_deleted_successfully": "Отзыв успешно удален.", + "review_deletion_failed": "Не удалось удалить отзыв. Попробуйте еще раз.", + "loading_reviews": "Загрузка отзывов...", + "loading_more_reviews": "Загрузка дополнительных отзывов...", + "load_more_reviews": "Загрузить больше отзывов", + "you_seemed_to_enjoy_this_game": "Похоже, вам понравилась эта игра", + "would_you_recommend_this_game": "Хотели бы вы оставить отзыв об этой игре?", + "yes": "Да", + "maybe_later": "Может быть позже" }, "activation": { "title": "Активировать Hydra", @@ -475,7 +513,8 @@ "game_card": { "available_one": "Доступный", "available_other": "Доступный", - "no_downloads": "Нет доступных источников" + "no_downloads": "Нет доступных источников", + "calculating": "Вычисление" }, "binary_not_found_modal": { "title": "Программы не установлены", @@ -572,7 +611,12 @@ "show_achievements_on_profile": "Покажите свои достижения в профиле", "show_points_on_profile": "Показывать заработанные очки в своем профиле", "error_adding_friend": "Не удалось отправить запрос в друзья. Пожалуйста, проверьте код друга", - "friend_code_length_error": "Код друга должен содержать 8 символов" + "friend_code_length_error": "Код друга должен содержать 8 символов", + "game_removed_from_pinned": "Игра удалена из закрепленных", + "game_added_to_pinned": "Игра добавлена в закрепленные", + "karma": "Карма", + "karma_count": "карма", + "karma_description": "Заработано от положительных лайков на отзывах" }, "achievement": { "achievement_unlocked": "Достижение разблокировано", diff --git a/src/renderer/src/components/game-card/game-card.scss b/src/renderer/src/components/game-card/game-card.scss index ee4a22b1..99aa866e 100644 --- a/src/renderer/src/components/game-card/game-card.scss +++ b/src/renderer/src/components/game-card/game-card.scss @@ -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 { diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index 15b5439b..1aa58ba7 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -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) : "…"} - {stats?.averageScore && ( -
- - {stats.averageScore.toFixed(1)} -
- )} +
+ +
diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index 9970be42..89dccdbc 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -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"; diff --git a/src/renderer/src/components/star-rating/index.ts b/src/renderer/src/components/star-rating/index.ts new file mode 100644 index 00000000..0f153ca4 --- /dev/null +++ b/src/renderer/src/components/star-rating/index.ts @@ -0,0 +1 @@ +export * from "./star-rating"; \ No newline at end of file diff --git a/src/renderer/src/components/star-rating/star-rating.scss b/src/renderer/src/components/star-rating/star-rating.scss new file mode 100644 index 00000000..4fa7ba2a --- /dev/null +++ b/src/renderer/src/components/star-rating/star-rating.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/renderer/src/components/star-rating/star-rating.tsx b/src/renderer/src/components/star-rating/star-rating.tsx new file mode 100644 index 00000000..5aa2a5ee --- /dev/null +++ b/src/renderer/src/components/star-rating/star-rating.tsx @@ -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 ( +
+ + {calculatingText} +
+ ); + } + + if (rating === null || rating === undefined) { + return ( +
+ + +
+ ); + } + + const filledStars = Math.floor(rating); + const hasHalfStar = rating % 1 >= 0.5; + const emptyStars = maxStars - filledStars - (hasHalfStar ? 1 : 0); + + return ( +
+ + {Array.from({ length: filledStars }, (_, index) => ( + + ))} + + + {hasHalfStar && ( +
+ + +
+ )} + + + {Array.from({ length: emptyStars }, (_, index) => ( + + ))} + + {rating.toFixed(1)} +
+ ); +} \ No newline at end of file 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 9da59d4c..2b6de1d8 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -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(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 && (
- - +
+ {[1, 2, 3, 4, 5].map((starValue) => ( + + ))} +
)} {review.isBlocked && diff --git a/src/renderer/src/pages/game-details/game-details.scss b/src/renderer/src/pages/game-details/game-details.scss index 83758524..b0726655 100644 --- a/src/renderer/src/pages/game-details/game-details.scss +++ b/src/renderer/src/pages/game-details/game-details.scss @@ -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; } } diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.scss b/src/renderer/src/pages/game-details/review-prompt-banner.scss index b8f7557b..f9358e52 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.scss +++ b/src/renderer/src/pages/game-details/review-prompt-banner.scss @@ -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 { diff --git a/src/renderer/src/pages/game-details/review-prompt-banner.tsx b/src/renderer/src/pages/game-details/review-prompt-banner.tsx index aeddaaad..01fdd075 100644 --- a/src/renderer/src/pages/game-details/review-prompt-banner.tsx +++ b/src/renderer/src/pages/game-details/review-prompt-banner.tsx @@ -18,7 +18,7 @@ export function ReviewPromptBanner({
- You've seemed to enjoy this game + {t("you_seemed_to_enjoy_this_game")} {t("would_you_recommend_this_game")} diff --git a/src/renderer/src/pages/game-details/review-sort-options.tsx b/src/renderer/src/pages/game-details/review-sort-options.tsx index 75ec0f39..0944e58e 100644 --- a/src/renderer/src/pages/game-details/review-sort-options.tsx +++ b/src/renderer/src/pages/game-details/review-sort-options.tsx @@ -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"; diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index d8aa2128..febb6a8b 100755 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -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() {

{numberFormatter.format(stats?.playerCount)}

- {stats?.averageScore && ( -
-

- - {t("rating_count")} -

-

{stats.averageScore.toFixed(1)}/10

-
- )} +
+

+ + {t("rating_count")} +

+ +
)} diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx index 8c85217d..d2232276 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx @@ -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 (
@@ -24,7 +27,7 @@ export function UserKarmaBox() {

- {numberFormatter.format(userDetails.karma)}{" "} + {numberFormatter.format(karma)}{" "} {t("karma_count")}

diff --git a/src/types/index.ts b/src/types/index.ts index 17ed08cc..0c6af89b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -203,6 +203,7 @@ export interface UserProfile { currentGame: UserProfileCurrentGame | null; bio: string; hasActiveSubscription: boolean; + karma: number; quirks: { backupsPerGameLimit: number; };