Merge branch 'main' into chore/bump-electron-version

This commit is contained in:
Zamitto
2025-10-24 21:05:00 -03:00
16 changed files with 1208 additions and 1116 deletions

View File

@@ -70,6 +70,24 @@
"edit_game_modal_icon_resolution": "Resolución recomendada: 256x256px",
"edit_game_modal_logo_resolution": "Resolución recomendada: 640x360px",
"edit_game_modal_hero_resolution": "Resolución recomendada: 1920x620px",
"cancel": "Cancelar",
"confirm": "Confirmar",
"decky_plugin_installation_error": "Error instalando plugin Decky: {{error}}",
"decky_plugin_installation_failed": "Falló instalar plugin Decky: {{error}}",
"decky_plugin_installed": "Plugin Decky v{{version}} instalanda exitosamente",
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
"edit_game_modal_drop_hero_image_here": "Soltá la imagen hero acá",
"edit_game_modal_drop_icon_image_here": "Soltá la imagen de ícono hero acá",
"edit_game_modal_drop_logo_image_here": "Soltá la imagen de logo hero acá",
"edit_game_modal_drop_to_replace_hero": "Soltá para reemplazar hero",
"edit_game_modal_drop_to_replace_icon": "Soltá para reemplazar el ícono",
"edit_game_modal_drop_to_replace_logo": "Soltá para reemplazar el logo",
"install_decky_plugin": "Instalar plugin Decky",
"install_decky_plugin_message": "Esto va a descargar e instalar el plugin de Decky Loader para Hydra. Esto quizás requierea permisos elevados, ¿querés continuar?",
"install_decky_plugin_title": "Instarlar el plugin Decky Hydra",
"update_decky_plugin": "Actualizar plugin Decky",
"update_decky_plugin_message": "Una nueva versión del plugin Decky para Hydra está disponible. ¿Querés actualizarlo ahora?",
"update_decky_plugin_title": "Actualizar plugin Decky para Hydra",
"edit_game_modal_assets": "Recursos"
},
"header": {
@@ -285,6 +303,62 @@
"keyshop_price": "Precio de tiendas de terceros",
"historical_retail": "Precio de tiendas",
"historical_keyshop": "Precio de tiendas de terceros",
"add_to_favorites": "Añadir a favoritos",
"be_first_to_review": "¡Sé la primera persona en compartir lo que pensas de este juego!",
"create_shortcut_simple": "Crear atajo",
"delete_review": "Eliminar reseña",
"delete_review_modal_cancel_button": "Cancelar",
"delete_review_modal_delete_button": "Eliminar",
"delete_review_modal_description": "Esta acción no se puede deshacer.",
"delete_review_modal_title": "¿De verdad querés eliminar esta reseña?",
"failed_remove_files": "Error al eliminar los archivos",
"failed_remove_from_library": "Error al eliminar de la librería",
"failed_update_favorites": "Error al actualizar favoritos",
"files_removed_success": "Archivos eliminados correctamente",
"filter_by_source": "Filtrar por fuente",
"game_removed_from_library": "Juego eliminado de la librería",
"hide_original": "Ocultar original",
"leave_a_review": "Crear una reseña",
"load_more_reviews": "Cargar más reseñas",
"loading_more_reviews": "Cargando más reseñas...",
"loading_reviews": "Cargando reseñas...",
"maybe_later": "Tal vez después",
"no_repacks_found": "Sin fuentes encontradas para este juego",
"no_reviews_yet": "Sin reseñas aún",
"properties": "Propiedades",
"rating": "Calificación",
"rating_count": "Calificación",
"rating_negative": "Negativa",
"rating_neutral": "Neutral",
"rating_positive": "Positiva",
"rating_stats": "Calificación",
"rating_very_negative": "Muy Negativa",
"rating_very_positive": "Muy Positiva",
"remove_from_favorites": "Eliminar de favoritos",
"remove_review": "Eliminar reseña",
"review_cannot_be_empty": "El campo de la reseña no puede estar vacío.",
"review_deleted_successfully": "Reseña eliminada exitosamente.",
"review_deletion_failed": "Error al eliminar reseña. Por favor intentá de nuevo.",
"review_submission_failed": "Error al subir reseña. Por favor intentá de nuevo.",
"review_submitted_successfully": "¡Reseña eliminada exitosamente!",
"reviews": "Reseñas",
"show_less": "Ver menos",
"show_more": "Ver más",
"show_original": "Ver original",
"show_original_translated_from": "Ver original (traducido del {{language}})",
"show_translation": "Ver traducción",
"sort_highest_score": "Puntuación más alta",
"sort_lowest_score": "Puntuación más baja",
"sort_most_voted": "Más votads",
"sort_newest": "Más nuevos",
"sort_oldest": "Más viejos",
"submit_review": "Enviar",
"submitting": "Subiendo...",
"vote_failed": "Error al registrar tu voto. Por favor intentá de nuevo.",
"would_you_recommend_this_game": "¿Querés escribir una reseña para este juego?",
"write_review_placeholder": "Compartí tus pensamientos sobre este juego...",
"yes": "Si",
"you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego",
"language": "Idioma",
"caption": "Subtítulo",
"audio": "Audio"
@@ -345,7 +419,7 @@
"enable_real_debrid": "Habilitar Real-Debrid",
"real_debrid_description": "Real-Debrid es un descargador que te permite descargar archivos más rápidos, solo límitado por la velocidad de tu internet.",
"debrid_invalid_token": "Token API inválido",
"debrid_api_token_hint": "Podés obtener la el token de tu API <0>acá</0>",
"debrid_api_token_hint": "Podés obtener el token de tu API <0>acá</0>",
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratis. Por favor suscribíte a Real-Debrid",
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
"save_changes": "Guardar cambios",
@@ -357,7 +431,7 @@
"download_count_zero": "Sin opciones de descarga",
"download_count_one": "{{countFormatted}} opción de descarga",
"download_count_other": "{{countFormatted}} opciones de descarga",
"download_source_url": "Descargar fuente URL",
"download_source_url": "Añadir URL de una fuente",
"add_download_source_description": "Introducí la URL del archivo .json",
"download_source_up_to_date": "Actualizado",
"download_source_errored": "Error",
@@ -409,7 +483,7 @@
"subscription_renew_cancelled": "Renovación automática desactivada",
"subscription_renews_on": "Tu suscripción se renueva el {{date}}",
"bill_sent_until": "Tu próxima factura se enviará este día",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupés, presiona acá para hacer tu primera obra maestra.",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para hacer tu primera obra maestra.",
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
@@ -443,7 +517,7 @@
"enable_friend_request_notifications": "Cuando recibís una solicitud de amistad",
"enable_auto_install": "Descargar actualizaciones automáticamente",
"common_redist": "Common redistributables",
"common_redist_description": "Common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.",
"common_redist_description": "Los common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.",
"install_common_redist": "Instalar",
"installing_common_redist": "Instalando…",
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
@@ -465,6 +539,8 @@
"hidden": "Oculto",
"test_notification": "Probar notificación",
"notification_preview": "Probar notificación de logro",
"debrid": "Debrid",
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego"
},
"notifications": {
@@ -492,6 +568,7 @@
"game_card": {
"available_one": "Disponible",
"available_other": "Disponibles",
"calculating": "Calculando",
"no_downloads": "Sin descargas disponibles"
},
"binary_not_found_modal": {
@@ -593,6 +670,12 @@
"error_adding_friend": "No se pudo enviar la solicitud de amistad. Por favor revisá el código",
"friend_code_length_error": "El código de amistad debe tener mínimo 8 caracteres",
"game_removed_from_pinned": "Juego removido de fijados",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Conseguido por me gustas positivos en reseñas",
"sort_by": "Filtrar por:",
"game_added_to_pinned": "Juego añadido a fijados"
},
"achievement": {

View File

@@ -29,7 +29,7 @@ export class HydraApi {
private static instance: AxiosInstance;
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
private static readonly ADD_LOG_INTERCEPTOR = true;
private static readonly ADD_LOG_INTERCEPTOR = false;
private static secondsToMilliseconds(seconds: number) {
return seconds * 1000;

View File

@@ -106,7 +106,7 @@ export class PythonRPC {
"main.py"
);
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
const childProcess = cp.spawn("python", [scriptPath, ...commonArgs], {
stdio: ["inherit", "inherit"],
});

View File

@@ -11,7 +11,6 @@
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-bottom: calc(globals.$spacing-unit * 1.5);
&__info {
display: flex;

View File

@@ -2,7 +2,7 @@
.gallery-slider {
&__container {
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 1);
padding: calc(globals.$spacing-unit * 1.5) 0;
width: 100%;
display: flex;
flex-direction: column;

View File

@@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import { PencilIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
@@ -17,6 +17,7 @@ import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
import { useUserDetails, useLibrary } from "@renderer/hooks";
import { useSubscription } from "@renderer/hooks/use-subscription";
import "./game-details.scss";
import "./hero.scss";
const processMediaElements = (document: Document) => {
const $images = Array.from(document.querySelectorAll("img"));
@@ -53,8 +54,6 @@ const getImageWithCustomPriority = (
};
export function GameDetailsContent() {
const heroRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation("game_details");
const {
@@ -152,19 +151,11 @@ export function GameDetailsContent() {
className={`game-details__wrapper ${hasNSFWContentBlocked ? "game-details__wrapper--blurred" : ""}`}
>
<section className="game-details__container">
<div ref={heroRef} className="game-details__hero">
<div className="game-details__hero-image-wrapper">
<img
src={heroImage}
className="game-details__hero-image"
alt={game?.title}
/>
</div>
<div
className="game-details__hero-backdrop"
style={{
flex: 1,
}}
<div className="game-details__hero">
<img
src={heroImage}
className="game-details__hero-image"
alt={game?.title}
/>
<div
@@ -204,11 +195,13 @@ export function GameDetailsContent() {
)}
</div>
</div>
<div className="game-details__hero-panel">
<HeroPanel />
</div>
</div>
</div>
<HeroPanel />
<div className="game-details__description-container">
<div className="game-details__description-content">
<DescriptionHeader />

View File

@@ -1,63 +1,170 @@
import Skeleton from "react-loading-skeleton";
import { Button } from "@renderer/components";
import { useTranslation } from "react-i18next";
import "./game-details.scss";
import "react-loading-skeleton/dist/skeleton.css";
export function GameDetailsSkeleton() {
const { t } = useTranslation("game_details");
return (
<div className="game-details__container">
<div className="game-details__hero">
<Skeleton className="game-details__hero-image-skeleton" />
</div>
<div className="game-details__hero-panel-skeleton">
<section className="description-header__info">
<Skeleton width={155} />
<Skeleton width={135} />
</section>
</div>
<div className="game-details__description-container">
<div className="game-details__description-content">
<div className="description-header">
<section className="description-header__info">
<Skeleton width={145} />
<Skeleton width={150} />
</section>
<div className="game-details__wrapper game-details__skeleton">
<section className="game-details__container">
<div className="game-details__hero">
<Skeleton
height={350}
style={{
borderRadius: "0px 0px 8px 8px",
position: "absolute",
width: "100%",
zIndex: 0,
}}
/>
<div className="game-details__hero-logo-backdrop">
<div className="game-details__hero-content">
<div className="game-details__game-logo" />
<div className="game-details__hero-buttons game-details__hero-buttons--right" />
</div>
<div className="game-details__hero-panel">
<div className="hero-panel__container">
<div className="hero-panel">
<div className="hero-panel__content">
<Skeleton height={16} width={150} />
<Skeleton height={16} width={120} />
</div>
<div className="hero-panel__actions" style={{ gap: "16px" }}>
<Skeleton
height={36}
width={36}
style={{ borderRadius: "6px" }}
/>
<Skeleton
height={36}
width={36}
style={{ borderRadius: "6px" }}
/>
<Skeleton
height={36}
width={100}
style={{ borderRadius: "6px" }}
/>
</div>
</div>
</div>
</div>
</div>
<div className="game-details__description-skeleton">
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} />
))}
<Skeleton className="game-details__hero-image-skeleton" />
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton key={index} />
))}
<Skeleton className="game-details__hero-image-skeleton" />
<Skeleton />
</div>
<div className="game-details__description-container">
<div className="game-details__description-content">
<div className="description-header">
<section className="description-header__info">
<Skeleton height={16} width={200} />
<Skeleton height={16} width={150} />
</section>
</div>
<div style={{ marginBottom: "24px" }}>
<Skeleton
height={200}
width="100%"
style={{ borderRadius: "8px" }}
/>
</div>
<div className="game-details__description">
<Skeleton count={8} height={22} style={{ marginBottom: "8px" }} />
<Skeleton height={22} width="60%" />
</div>
<Skeleton
width={120}
height={36}
className="game-details__description-toggle"
width={100}
style={{
borderRadius: "4px",
marginTop: "24px",
alignSelf: "center",
}}
/>
<div style={{ marginTop: "48px" }} />
</div>
<aside className="content-sidebar">
<div className="sidebar-section">
<div
className="sidebar-section__button"
style={{ pointerEvents: "none" }}
>
<Skeleton height={16} width={16} />
<Skeleton height={16} width={60} />
</div>
<div className="sidebar-section__content">
<div className="stats__section">
<div className="stats__category">
<div className="stats__category-title">
<Skeleton height={14} width={14} />
<Skeleton height={14} width={80} />
</div>
<Skeleton height={14} width={40} />
</div>
<div className="stats__category">
<div className="stats__category-title">
<Skeleton height={14} width={14} />
<Skeleton height={14} width={70} />
</div>
<Skeleton height={14} width={35} />
</div>
<div className="stats__category">
<div className="stats__category-title">
<Skeleton height={14} width={14} />
<Skeleton height={14} width={60} />
</div>
<Skeleton height={14} width={30} />
</div>
</div>
</div>
</div>
<div className="sidebar-section">
<div
className="sidebar-section__button"
style={{ pointerEvents: "none" }}
>
<Skeleton height={16} width={16} />
<Skeleton height={16} width={120} />
</div>
<div className="sidebar-section__content">
<ul className="list">
{Array.from({ length: 4 }).map((_, index) => (
<li key={index}>
<div
className="list__item"
style={{ pointerEvents: "none" }}
>
<Skeleton
height={54}
width={54}
style={{ borderRadius: "4px" }}
/>
<div>
<Skeleton
height={14}
width={120}
style={{ marginBottom: "4px" }}
/>
<Skeleton height={12} width={80} />
</div>
</div>
</li>
))}
</ul>
</div>
</div>
</aside>
</div>
<div className="content-sidebar">
<div className="requirement__button-container">
<Button className="requirement__button" theme="primary" disabled>
{t("minimum")}
</Button>
<Button className="requirement__button" theme="outline" disabled>
{t("recommended")}
</Button>
</div>
<div className="requirement__details-skeleton">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} height={20} />
))}
</div>
</div>
</div>
</section>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ import { Downloader, getDownloadersForUri } from "@shared";
import { CloudSyncModal } from "./cloud-sync-modal/cloud-sync-modal";
import { CloudSyncFilesModal } from "./cloud-sync-files-modal/cloud-sync-files-modal";
import "./game-details.scss";
import "./hero.scss";
export default function GameDetails() {
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);

View File

@@ -0,0 +1,116 @@
@use "../../scss/globals.scss";
.game-details {
&__reviews-section {
margin-top: calc(globals.$spacing-unit * 3);
padding-top: calc(globals.$spacing-unit * 3);
border-top: 1px solid rgba(255, 255, 255, 0.1);
width: 100%;
margin-left: auto;
margin-right: auto;
@media (min-width: 1280px) {
width: 60%;
}
@media (min-width: 1536px) {
width: 50%;
}
}
&__reviews-title {
font-size: 1.25rem;
font-weight: 600;
color: globals.$muted-color;
margin: 0;
}
&__reviews-title-group {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
flex: 1;
}
&__reviews-badge {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.7);
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
min-width: 24px;
text-align: center;
flex-shrink: 0;
}
&__reviews-container {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 4);
}
&__reviews-separator {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: calc(globals.$spacing-unit * 3) 0;
width: 100%;
}
&__reviews-list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: calc(globals.$spacing-unit * 1);
}
&__reviews-empty {
text-align: center;
padding: calc(globals.$spacing-unit * 4) calc(globals.$spacing-unit * 2);
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__reviews-empty-icon {
font-size: 48px;
margin-bottom: calc(globals.$spacing-unit * 2);
color: rgba(255, 255, 255, 0.6);
}
&__reviews-empty-title {
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
margin: 0 0 calc(globals.$spacing-unit * 1) 0;
}
&__reviews-empty-message {
color: rgba(255, 255, 255, 0.6);
font-size: globals.$small-font-size;
margin: 0;
line-height: 1.4;
}
&__reviews-loading {
text-align: center;
color: rgba(255, 255, 255, 0.6);
padding: calc(globals.$spacing-unit * 2);
}
&__load-more-reviews {
background: rgba(255, 255, 255, 0.05);
border: 1px solid globals.$border-color;
color: globals.$body-color;
padding: calc(globals.$spacing-unit * 1) calc(globals.$spacing-unit * 2);
border-radius: 4px;
cursor: pointer;
font-size: globals.$body-font-size;
font-family: inherit;
transition: all 0.2s ease;
width: 100%;
margin-top: calc(globals.$spacing-unit * 2);
&:hover {
background-color: rgba(255, 255, 255, 0.1);
border-color: globals.$brand-teal;
}
}
}

View File

@@ -9,6 +9,7 @@ import { ReviewForm } from "./review-form";
import { ReviewItem } from "./review-item";
import { ReviewSortOptions } from "./review-sort-options";
import { ReviewPromptBanner } from "./review-prompt-banner";
import "./game-reviews.scss";
import { useToast } from "@renderer/hooks";
type ReviewSortOption =
@@ -465,84 +466,82 @@ export function GameReviews({
</>
)}
<div className="game-details__reviews-list">
<div className="game-details__reviews-list-header">
<div className="game-details__reviews-title-group">
<h3 className="game-details__reviews-title">{t("reviews")}</h3>
<span className="game-details__reviews-badge">
{totalReviewCount}
</span>
</div>
<div className="game-details__reviews-list-header">
<div className="game-details__reviews-title-group">
<h3 className="game-details__reviews-title">{t("reviews")}</h3>
<span className="game-details__reviews-badge">
{totalReviewCount}
</span>
</div>
<ReviewSortOptions
sortBy={reviewsSortBy}
onSortChange={handleSortChange}
/>
{reviewsLoading && reviews.length === 0 && (
<div className="game-details__reviews-loading">
{t("loading_reviews")}
</div>
)}
{!reviewsLoading && reviews.length === 0 && (
<div className="game-details__reviews-empty">
<div className="game-details__reviews-empty-icon">
<NoteIcon size={48} />
</div>
<h4 className="game-details__reviews-empty-title">
{t("no_reviews_yet")}
</h4>
<p className="game-details__reviews-empty-message">
{t("be_first_to_review")}
</p>
</div>
)}
<div
className="game-details__reviews-container"
style={{
opacity: reviewsLoading && reviews.length > 0 ? 0.5 : 1,
transition: "opacity 0.2s ease",
}}
>
{reviews.map((review) => (
<ReviewItem
key={review.id}
review={review}
userDetailsId={userDetailsId}
isBlocked={review.isBlocked}
isVisible={visibleBlockedReviews.has(review.id)}
isVoting={votingReviews.has(review.id)}
previousVotes={
previousVotesRef.current.get(review.id) || {
upvotes: 0,
downvotes: 0,
}
}
onVote={handleVoteReview}
onDelete={handleDeleteReview}
onToggleVisibility={toggleBlockedReview}
onAnimationComplete={handleVoteAnimationComplete}
/>
))}
</div>
{hasMoreReviews && !reviewsLoading && (
<button
className="game-details__load-more-reviews"
onClick={loadMoreReviews}
>
{t("load_more_reviews")}
</button>
)}
{reviewsLoading && reviews.length > 0 && (
<div className="game-details__reviews-loading">
{t("loading_more_reviews")}
</div>
)}
</div>
<ReviewSortOptions
sortBy={reviewsSortBy}
onSortChange={handleSortChange}
/>
{reviewsLoading && reviews.length === 0 && (
<div className="game-details__reviews-loading">
{t("loading_reviews")}
</div>
)}
{!reviewsLoading && reviews.length === 0 && (
<div className="game-details__reviews-empty">
<div className="game-details__reviews-empty-icon">
<NoteIcon size={48} />
</div>
<h4 className="game-details__reviews-empty-title">
{t("no_reviews_yet")}
</h4>
<p className="game-details__reviews-empty-message">
{t("be_first_to_review")}
</p>
</div>
)}
<div
className="game-details__reviews-container"
style={{
opacity: reviewsLoading && reviews.length > 0 ? 0.5 : 1,
transition: "opacity 0.2s ease",
}}
>
{reviews.map((review) => (
<ReviewItem
key={review.id}
review={review}
userDetailsId={userDetailsId}
isBlocked={review.isBlocked}
isVisible={visibleBlockedReviews.has(review.id)}
isVoting={votingReviews.has(review.id)}
previousVotes={
previousVotesRef.current.get(review.id) || {
upvotes: 0,
downvotes: 0,
}
}
onVote={handleVoteReview}
onDelete={handleDeleteReview}
onToggleVisibility={toggleBlockedReview}
onAnimationComplete={handleVoteAnimationComplete}
/>
))}
</div>
{hasMoreReviews && !reviewsLoading && (
<button
className="game-details__load-more-reviews"
onClick={loadMoreReviews}
>
{t("load_more_reviews")}
</button>
)}
{reviewsLoading && reviews.length > 0 && (
<div className="game-details__reviews-loading">
{t("loading_more_reviews")}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,271 @@
@use "../../scss/globals.scss";
$hero-height: 350px;
@keyframes slide-in {
0% {
transform: translateY(calc(40px + globals.$spacing-unit * 2));
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.game-details {
&__hero-panel {
padding: globals.$spacing-unit;
}
&__hero {
width: 100%;
height: $hero-height;
min-height: $hero-height;
display: flex;
flex-direction: column;
position: relative;
transition: all ease 0.2s;
@media (min-width: 1250px) {
height: 350px;
min-height: 350px;
}
}
&__hero-content {
padding: calc(globals.$spacing-unit * 1.5);
height: 100%;
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-end;
@media (min-width: 768px) {
padding: calc(globals.$spacing-unit * 2);
}
}
&__hero-buttons {
display: flex;
gap: globals.$spacing-unit;
align-items: center;
&--right {
margin-left: auto;
}
}
&__edit-custom-game-button {
padding: calc(globals.$spacing-unit * 1.5);
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 8px;
transition: all ease 0.2s;
cursor: pointer;
min-height: 40px;
min-width: 40px;
display: flex;
align-items: center;
justify-content: center;
color: globals.$muted-color;
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
&:active {
opacity: 0.9;
}
&:hover {
background-color: rgba(0, 0, 0, 0.5);
color: globals.$body-color;
}
}
&__hero-logo-backdrop {
width: 100%;
height: 100%;
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
}
&__hero-image-wrapper {
position: absolute;
width: 100%;
height: 384px;
max-height: 384px;
overflow: hidden;
border-radius: 0px 0px 8px 8px;
z-index: 0;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.3) 60%,
transparent 100%
);
z-index: 1;
pointer-events: none;
border-radius: inherit;
}
@media (min-width: 1250px) {
height: calc(350px + 82px);
min-height: calc(350px + 84px);
}
}
&__hero-image {
width: 100%;
height: $hero-height;
min-height: $hero-height;
object-fit: cover;
object-position: top;
transition: all ease 0.2s;
position: absolute;
z-index: 0;
border-radius: 0px 0px 8px 8px;
@media (min-width: 1250px) {
object-position: center;
height: $hero-height;
min-height: $hero-height;
}
}
&__game-logo {
width: 200px;
align-self: flex-end;
@media (min-width: 768px) {
width: 250px;
}
@media (min-width: 1024px) {
width: 300px;
}
}
&__game-logo-text {
width: 200px;
align-self: flex-end;
font-size: 1.8rem;
font-weight: bold;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
text-align: left;
line-height: 1.2;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
@media (min-width: 768px) {
width: 250px;
font-size: 2.2rem;
}
@media (min-width: 1024px) {
width: 300px;
font-size: 2.5rem;
}
}
&__cloud-sync-button {
padding: calc(globals.$spacing-unit * 1.5) calc(globals.$spacing-unit * 2);
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 8px;
transition: all ease 0.2s;
cursor: pointer;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: globals.$spacing-unit;
color: globals.$muted-color;
font-size: globals.$small-font-size;
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
&:active {
opacity: 0.9;
}
&:disabled {
opacity: globals.$disabled-opacity;
cursor: not-allowed;
}
&:hover {
background-color: rgba(0, 0, 0, 0.5);
}
}
&__cloud-icon-container {
width: 20px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
&__cloud-icon {
width: 26px;
position: absolute;
top: -3px;
}
&__randomizer-button {
padding: calc(globals.$spacing-unit * 1.5);
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20px);
border-radius: 8px;
transition: all ease 0.2s;
cursor: pointer;
min-height: 40px;
min-width: 40px;
display: flex;
align-items: center;
justify-content: center;
color: globals.$muted-color;
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
animation: slide-in 0.3s cubic-bezier(0.33, 1, 0.68, 1);
&:active {
opacity: 0.9;
}
&:hover {
background-color: rgba(0, 0, 0, 0.5);
color: globals.$body-color;
}
}
&__stars-icon-container {
width: 20px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
&__stars-icon {
width: 26px;
position: absolute;
top: -3px;
}
}

View File

@@ -20,11 +20,6 @@
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
&__container {
padding: 0px 12px 12px;
margin: 0;
}
&--stuck {
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(12px);
@@ -35,7 +30,18 @@
&__content {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
gap: calc(globals.$spacing-unit * 0.5);
p {
font-size: globals.$small-font-size;
color: globals.$muted-color;
font-weight: 400;
margin: 0;
&:first-child {
font-weight: 600;
}
}
}
&__actions {

View File

@@ -0,0 +1,232 @@
@use "../../scss/globals.scss";
.game-details {
&__reviews-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: calc(globals.$spacing-unit * 2);
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
gap: calc(globals.$spacing-unit * 1.5);
}
}
&__reviews-title {
font-size: 1.25rem;
font-weight: 600;
color: globals.$muted-color;
margin: 0;
}
&__review-form {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
&__review-form-bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
&__review-score-container {
display: flex;
align-items: center;
gap: 4px;
}
&__review-score-select {
background-color: #2a2a2a;
border: 1px solid #3a3a3a;
border-radius: 4px;
color: #ffffff;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition:
border-color 0.2s ease,
background-color 0.2s ease;
&:focus {
outline: none;
}
&--red {
border-color: #e74c3c;
background-color: rgba(231, 76, 60, 0.1);
}
&--yellow {
border-color: #f39c12;
background-color: rgba(243, 156, 18, 0.1);
}
&--green {
border-color: #27ae60;
background-color: rgba(39, 174, 96, 0.1);
}
option {
background-color: #2a2a2a;
color: #ffffff;
}
}
&__star-rating {
display: flex;
align-items: center;
gap: 2px;
}
&__star {
background: none;
border: none;
color: #666666;
cursor: pointer;
padding: 2px;
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;
}
}
&__review-input-container {
display: flex;
flex-direction: column;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background-color: globals.$dark-background-color;
overflow: hidden;
}
&__review-input-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: globals.$background-color;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
&__review-editor-toolbar {
display: flex;
gap: 4px;
}
&__editor-button {
background: none;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
color: #ffffff;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
}
&.is-active {
background-color: globals.$brand-blue;
border-color: globals.$brand-blue;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&__review-char-counter {
font-size: 12px;
color: #888888;
.over-limit {
color: #ff6b6b;
}
}
&__review-input {
min-height: 120px;
padding: 12px;
cursor: text;
.ProseMirror {
outline: none;
color: #ffffff;
font-size: 14px;
line-height: 1.5;
min-height: 96px; // 120px - 24px padding
width: 100%;
cursor: text;
&:focus {
outline: none;
}
p {
margin: 0 0 8px 0;
&:last-child {
margin-bottom: 0;
}
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
u {
text-decoration: underline;
}
}
}
}

View File

@@ -2,6 +2,7 @@ import { Star } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EditorContent, Editor } from "@tiptap/react";
import { Button } from "@renderer/components";
import "./review-form.scss";
interface ReviewFormProps {
editor: Editor | null;

View File

@@ -1,6 +1,213 @@
@use "../../scss/globals.scss";
.game-details {
&__review-item {
overflow: hidden;
word-wrap: break-word;
}
&__review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: calc(globals.$spacing-unit * 1.5);
}
&__review-user {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 1);
}
&__review-user-info {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 0.25);
}
&__review-display-name {
color: rgba(255, 255, 255, 0.9);
font-size: globals.$small-font-size;
font-weight: 600;
display: inline-flex;
&--clickable {
cursor: pointer;
transition: color 0.2s ease;
&:hover {
text-decoration: underline;
}
}
}
&__review-actions {
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
&__review-votes {
display: flex;
gap: 12px;
}
&__vote-button {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 6px 12px;
color: #ccc;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
&--active {
&.game-details__vote-button--upvote {
svg {
fill: white;
}
}
&.game-details__vote-button--downvote {
svg {
fill: white;
}
}
}
span {
font-weight: 500;
display: inline-block;
min-width: 1ch;
overflow: hidden;
}
}
&__delete-review-button {
display: flex;
align-items: center;
justify-content: center;
background: rgba(244, 67, 54, 0.1);
border: 1px solid rgba(244, 67, 54, 0.3);
border-radius: 6px;
padding: 6px;
color: #f44336;
cursor: pointer;
transition: all 0.2s ease;
gap: 6px;
&:hover {
background: rgba(244, 67, 54, 0.2);
border-color: #f44336;
color: #ff5722;
}
}
&__blocked-review-simple {
color: rgba(255, 255, 255, 0.6);
font-size: globals.$small-font-size;
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 0.5);
}
&__blocked-review-show-link {
background: none;
border: none;
color: #ffc107;
font-size: globals.$small-font-size;
cursor: pointer;
text-decoration: underline;
padding: 0;
transition: color 0.2s ease;
&:hover {
color: #ffeb3b;
}
}
&__blocked-review-hide-link {
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
font-size: globals.$small-font-size;
cursor: pointer;
text-decoration: underline;
padding: 0;
transition: color 0.2s ease;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
&__review-score-stars {
display: flex;
align-items: center;
gap: 2px;
}
&__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;
}
}
&--empty {
color: #666666;
}
svg {
fill: currentColor;
}
}
&__review-date {
display: flex;
align-items: center;
gap: 4px;
color: rgba(255, 255, 255, 0.6);
font-size: globals.$small-font-size;
}
&__review-content {
color: globals.$body-color;
line-height: 1.5;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
max-width: 100%;
}
&__review-translation-toggle {
display: inline-flex;
align-items: center;