mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-03 00:33:49 +03:00
refactor: migrate achievement list styles from VE to SCSS + BEM
This commit is contained in:
parent
2d665b2266
commit
86d7ced0c0
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
262
src/renderer/src/pages/achievements/achievements.scss
Normal file
262
src/renderer/src/pages/achievements/achievements.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user