refactor: migrate achievement list styles from VE to SCSS + BEM

This commit is contained in:
Hachi-R 2025-01-18 23:43:56 -03:00
parent 2d665b2266
commit 86d7ced0c0
4 changed files with 300 additions and 88 deletions

View File

@ -1,11 +1,10 @@
import { useDate } from "@renderer/hooks";
import type { UserAchievement } from "@types";
import { useTranslation } from "react-i18next";
import * as styles from "./achievements.css";
import "./achievements.scss";
import { EyeClosedIcon } from "@primer/octicons-react";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
import { vars } from "@renderer/theme.css";
interface AchievementListProps {
achievements: UserAchievement[];
@ -17,27 +16,21 @@ export function AchievementList({ achievements }: AchievementListProps) {
const { formatDateTime } = useDate();
return (
<ul className={styles.list}>
<ul className="achievements__list">
{achievements.map((achievement) => (
<li
key={achievement.name}
className={styles.listItem}
style={{ display: "flex" }}
>
<li key={achievement.name} className="achievements__item">
<img
className={styles.listItemImage({
unlocked: achievement.unlocked,
})}
className={`achievements__item-image ${!achievement.unlocked ? "achievements__item-image--locked" : ""}`}
src={achievement.icon}
alt={achievement.displayName}
loading="lazy"
/>
<div style={{ flex: 1 }}>
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<div className="achievements__item-content">
<h4 className="achievements__item-title">
{achievement.hidden && (
<span
style={{ display: "flex" }}
className="achievements__item-hidden-icon"
title={t("hidden_achievement_tooltip")}
>
<EyeClosedIcon size={12} />
@ -47,41 +40,36 @@ export function AchievementList({ achievements }: AchievementListProps) {
</h4>
<p>{achievement.description}</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<div className="achievements__item-meta">
{achievement.points != undefined ? (
<div
style={{ display: "flex", alignItems: "center", gap: "4px" }}
className="achievements__item-points"
title={t("achievement_earn_points", {
points: achievement.points,
})}
>
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
<HydraIcon className="achievements__item-points-icon" />
<p className="achievements__item-points-value">
{achievement.points}
</p>
</div>
) : (
<button
onClick={() => showHydraCloudModal("achievements")}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
cursor: "pointer",
color: vars.color.warning,
}}
title={t("achievement_earn_points", {
points: "???",
})}
className="achievements__item-points achievements__item-points--locked"
title={t("achievement_earn_points", { points: "???" })}
>
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>???</p>
<HydraIcon className="achievements__item-points-icon" />
<p className="achievements__item-points-value">???</p>
</button>
)}
{achievement.unlockTime != null && (
<div
className="achievements__item-unlock-time"
title={t("unlocked_at", {
date: formatDateTime(achievement.unlockTime),
})}
style={{ whiteSpace: "nowrap", gap: "4px", display: "flex" }}
>
<small>{formatDateTime(achievement.unlockTime)}</small>
</div>

View File

@ -1,13 +1,13 @@
import Skeleton from "react-loading-skeleton";
import * as styles from "./achievements.css";
import "./achievements.scss";
export function AchievementsSkeleton() {
return (
<div className={styles.container}>
<div className={styles.hero}>
<Skeleton className={styles.heroImageSkeleton} />
<div className="achievements__container">
<div className="achievements__hero">
<Skeleton className="achievements__hero-image-skeleton" />
</div>
<div className={styles.heroPanelSkeleton}></div>
<div className="achievements__hero-panel-skeleton"></div>
</div>
);
}

View File

@ -0,0 +1,262 @@
@use "../../scss/globals.scss";
@use "sass:math";
$hero-height: 150px;
$logo-height: 100px;
$logo-max-width: 200px;
.achievements {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
transition: all ease 0.3s;
&__hero {
width: 100%;
height: $hero-height;
min-height: $hero-height;
display: flex;
flex-direction: column;
position: relative;
transition: all ease 0.2s;
&-content {
padding: globals.$spacing-unit * 2;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
&-logo-backdrop {
width: 100%;
height: 100%;
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
&-image-skeleton {
height: 150px;
}
}
&__game-logo {
width: $logo-max-width;
height: $logo-height;
object-fit: contain;
transition: all ease 0.2s;
&:hover {
transform: scale(1.05);
}
}
&__container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
z-index: 1;
}
&__table-header {
width: 100%;
background-color: var(--color-dark-background);
transition: all ease 0.2s;
border-bottom: solid 1px var(--color-border);
position: sticky;
top: 0;
z-index: 1;
&--stuck {
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
}
}
&__list {
list-style: none;
margin: 0;
display: flex;
flex-direction: column;
gap: globals.$spacing-unit * 2;
padding: globals.$spacing-unit * 2;
width: 100%;
background-color: var(--color-background);
}
&__item {
display: flex;
transition: all ease 0.1s;
color: var(--color-muted);
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: globals.$spacing-unit globals.$spacing-unit;
gap: globals.$spacing-unit * 2;
align-items: center;
text-align: left;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
}
&-image {
width: 54px;
height: 54px;
border-radius: 4px;
object-fit: cover;
&--locked {
filter: grayscale(100%);
}
}
&-content {
flex: 1;
}
&-title {
display: flex;
align-items: center;
gap: 4px;
}
&-hidden-icon {
display: flex;
color: var(--color-warning);
opacity: 0.8;
&:hover {
opacity: 1;
}
svg {
width: 12px;
height: 12px;
}
}
&-eye-closed {
width: 12px;
height: 12px;
color: globals.$warning-color;
scale: 4;
}
&-meta {
display: flex;
flex-direction: column;
gap: 8px;
}
&-points {
display: flex;
align-items: center;
gap: 4px;
margin-right: 4px;
font-weight: 600;
&--locked {
cursor: pointer;
color: var(--color-warning);
}
&-icon {
width: 18px;
height: 18px;
}
&-value {
font-size: 1.1em;
}
}
&-unlock-time {
white-space: nowrap;
gap: 4px;
display: flex;
}
&-compared {
display: grid;
grid-template-columns: 3fr 1fr 1fr;
&--no-owner {
grid-template-columns: 3fr 2fr;
}
}
&-main {
display: flex;
flex-direction: row;
align-items: center;
gap: globals.$spacing-unit;
}
&-status {
display: flex;
padding: globals.$spacing-unit;
justify-content: center;
&--unlocked {
white-space: nowrap;
flex-direction: row;
gap: globals.$spacing-unit;
padding: 0;
}
}
}
&__progress-bar {
width: 100%;
height: 8px;
transition: all ease 0.2s;
&::-webkit-progress-bar {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
&::-webkit-progress-value {
background-color: var(--color-muted);
border-radius: 4px;
}
}
&__profile-avatar {
height: 54px;
width: 54px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--color-background);
position: relative;
object-fit: cover;
&--small {
height: 32px;
width: 32px;
}
}
&__subscription-button {
text-decoration: none;
display: flex;
justify-content: center;
width: 100%;
gap: math.div(globals.$spacing-unit, 2);
color: var(--color-body);
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}

View File

@ -1,12 +1,11 @@
import type { ComparedAchievements } from "@types";
import * as styles from "./achievements.css";
import "./achievements.scss";
import {
CheckCircleIcon,
EyeClosedIcon,
LockIcon,
} from "@primer/octicons-react";
import { useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useTranslation } from "react-i18next";
export interface ComparedAchievementListProps {
@ -20,39 +19,26 @@ export function ComparedAchievementList({
const { formatDateTime } = useDate();
return (
<ul className={styles.list}>
<ul className="achievements__list">
{achievements.achievements.map((achievement, index) => (
<li
key={index}
className={styles.listItem}
style={{
display: "grid",
gridTemplateColumns: achievement.ownerStat
? "3fr 1fr 1fr"
: "3fr 2fr",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
}}
className={`achievements__item achievements__item-compared ${
!achievement.ownerStat && "achievements__item-compared--no-owner"
}`}
>
<div className="achievements__item-main">
<img
className={styles.listItemImage({
unlocked: true,
})}
className="achievements__item-image"
src={achievement.icon}
alt={achievement.displayName}
loading="lazy"
/>
<div>
<h4 style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<div className="achievements__item-content">
<h4 className="achievements__item-title">
{achievement.hidden && (
<span
style={{ display: "flex" }}
className="achievements__item-hidden-icon"
title={t("hidden_achievement_tooltip")}
>
<EyeClosedIcon size={12} />
@ -67,25 +53,13 @@ export function ComparedAchievementList({
{achievement.ownerStat ? (
achievement.ownerStat.unlocked ? (
<div
style={{
whiteSpace: "nowrap",
display: "flex",
flexDirection: "row",
gap: `${SPACING_UNIT}px`,
justifyContent: "center",
}}
className="achievements__item-status achievements__item-status--unlocked"
title={formatDateTime(achievement.ownerStat.unlockTime!)}
>
<CheckCircleIcon />
</div>
) : (
<div
style={{
display: "flex",
padding: `${SPACING_UNIT}px`,
justifyContent: "center",
}}
>
<div className="achievements__item-status">
<LockIcon />
</div>
)
@ -93,25 +67,13 @@ export function ComparedAchievementList({
{achievement.targetStat.unlocked ? (
<div
style={{
whiteSpace: "nowrap",
display: "flex",
flexDirection: "row",
gap: `${SPACING_UNIT}px`,
justifyContent: "center",
}}
className="achievements__item-status achievements__item-status--unlocked"
title={formatDateTime(achievement.targetStat.unlockTime!)}
>
<CheckCircleIcon />
</div>
) : (
<div
style={{
display: "flex",
padding: `${SPACING_UNIT}px`,
justifyContent: "center",
}}
>
<div className="achievements__item-status">
<LockIcon />
</div>
)}