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

View File

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