From 6667e00c9104737434361398ff4cf103be53b881 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 5 Oct 2025 20:32:41 +0300 Subject: [PATCH] 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 --- src/locales/en/translation.json | 18 +- src/locales/ru/translation.json | 50 +++- .../src/components/game-card/game-card.scss | 7 +- .../src/components/game-card/game-card.tsx | 17 +- src/renderer/src/components/index.ts | 1 + .../src/components/star-rating/index.ts | 1 + .../components/star-rating/star-rating.scss | 54 +++++ .../components/star-rating/star-rating.tsx | 64 +++++ .../game-details/game-details-content.tsx | 220 ++++++++++-------- .../src/pages/game-details/game-details.scss | 131 ++++++++--- .../game-details/review-prompt-banner.scss | 2 +- .../game-details/review-prompt-banner.tsx | 2 +- .../game-details/review-sort-options.tsx | 4 +- .../pages/game-details/sidebar/sidebar.tsx | 23 +- .../profile-content/user-karma-box.tsx | 11 +- src/types/index.ts | 1 + 16 files changed, 448 insertions(+), 158 deletions(-) create mode 100644 src/renderer/src/components/star-rating/index.ts create mode 100644 src/renderer/src/components/star-rating/star-rating.scss create mode 100644 src/renderer/src/components/star-rating/star-rating.tsx 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; };