feat: add all badges modal and enhance badges display in profile

This commit is contained in:
Moyasee
2025-12-23 15:43:52 +02:00
parent 1d1bbd2de5
commit 3c296fe721
6 changed files with 263 additions and 47 deletions

View File

@@ -30,7 +30,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 = false;
private static readonly ADD_LOG_INTERCEPTOR = true;
private static secondsToMilliseconds(seconds: number) {
return seconds * 1000;

View File

@@ -0,0 +1,87 @@
@use "../../../scss/globals.scss";
.all-badges-modal {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
max-height: 400px;
margin-top: calc(globals.$spacing-unit * -1);
&__title {
display: flex;
align-items: center;
gap: globals.$spacing-unit;
}
&__count {
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;
}
&__list {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
overflow-y: auto;
padding-right: globals.$spacing-unit;
}
&__item {
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 1.5);
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
transition: background-color ease 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
&__item-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: globals.$background-color;
img {
width: 32px;
height: 32px;
object-fit: contain;
}
}
&__item-content {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 0.5);
flex: 1;
min-width: 0;
}
&__item-title {
font-size: globals.$body-font-size;
font-weight: 600;
color: globals.$body-color;
margin: 0;
}
&__item-description {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
margin: 0;
}
}

View File

@@ -0,0 +1,58 @@
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { Modal } from "@renderer/components";
import { userProfileContext } from "@renderer/context";
import "./all-badges-modal.scss";
interface AllBadgesModalProps {
visible: boolean;
onClose: () => void;
}
export function AllBadgesModal({
visible,
onClose,
}: Readonly<AllBadgesModalProps>) {
const { t } = useTranslation("user_profile");
const { userProfile, badges } = useContext(userProfileContext);
const userBadges = userProfile?.badges
.map((badgeName) => badges.find((b) => b.name === badgeName))
.filter((badge) => badge !== undefined);
const modalTitle = (
<div className="all-badges-modal__title">
{t("badges")}
{userBadges && userBadges.length > 0 && (
<span className="all-badges-modal__count">{userBadges.length}</span>
)}
</div>
);
return (
<Modal visible={visible} title={modalTitle} onClose={onClose}>
<div className="all-badges-modal">
<div className="all-badges-modal__list">
{userBadges?.map((badge) => (
<div key={badge.name} className="all-badges-modal__item">
<div className="all-badges-modal__item-icon">
<img
src={badge.badge.url}
alt={badge.name}
width={32}
height={32}
/>
</div>
<div className="all-badges-modal__item-content">
<h3 className="all-badges-modal__item-title">{badge.title}</h3>
<p className="all-badges-modal__item-description">
{badge.description}
</p>
</div>
</div>
))}
</div>
</div>
</Modal>
);
}

View File

@@ -17,28 +17,76 @@
&__list {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
}
&__item {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
gap: calc(globals.$spacing-unit * 2);
width: 100%;
padding: calc(globals.$spacing-unit * 1.5);
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
cursor: pointer;
transition: all ease 0.2s;
transition: background-color ease 0.2s;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
transform: scale(1.05);
}
}
&__item-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: globals.$background-color;
img {
border-radius: 4px;
width: 32px;
height: 32px;
object-fit: contain;
}
}
&__item-content {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 0.5);
flex: 1;
min-width: 0;
}
&__item-title {
font-size: globals.$body-font-size;
font-weight: 600;
color: globals.$body-color;
margin: 0;
}
&__item-description {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
margin: 0;
}
&__view-all {
background: none;
border: none;
color: globals.$body-color;
font-size: globals.$small-font-size;
cursor: pointer;
text-decoration: underline;
padding: 0;
transition: color ease 0.2s;
&:hover {
color: globals.$muted-color;
}
}
}

View File

@@ -1,56 +1,78 @@
import { userProfileContext } from "@renderer/context";
import { useFormat } from "@renderer/hooks";
import { useContext } from "react";
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "react-tooltip";
import { AllBadgesModal } from "./all-badges-modal";
import "./badges-box.scss";
const MAX_VISIBLE_BADGES = 4;
export function BadgesBox() {
const { userProfile, badges } = useContext(userProfileContext);
const { t } = useTranslation("user_profile");
const { numberFormatter } = useFormat();
const [showAllBadgesModal, setShowAllBadgesModal] = useState(false);
if (!userProfile?.badges.length) return null;
const visibleBadges = userProfile.badges.slice(0, MAX_VISIBLE_BADGES);
const hasMoreBadges = userProfile.badges.length > MAX_VISIBLE_BADGES;
return (
<div>
<div className="badges-box__section-header">
<div className="profile-content__section-title-group">
<h2>{t("badges")}</h2>
<span className="profile-content__section-badge">
{numberFormatter.format(userProfile.badges.length)}
</span>
<>
<div>
<div className="badges-box__section-header">
<div className="profile-content__section-title-group">
<h2>{t("badges")}</h2>
<span className="profile-content__section-badge">
{numberFormatter.format(userProfile.badges.length)}
</span>
</div>
{hasMoreBadges && (
<button
type="button"
className="badges-box__view-all"
onClick={() => setShowAllBadgesModal(true)}
>
{t("view_all")}
</button>
)}
</div>
<div className="badges-box__box">
<div className="badges-box__list">
{visibleBadges.map((badgeName) => {
const badge = badges.find((b) => b.name === badgeName);
if (!badge) return null;
return (
<div key={badge.name} className="badges-box__item">
<div className="badges-box__item-icon">
<img
src={badge.badge.url}
alt={badge.name}
width={32}
height={32}
/>
</div>
<div className="badges-box__item-content">
<h3 className="badges-box__item-title">{badge.title}</h3>
<p className="badges-box__item-description">
{badge.description}
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="badges-box__box">
<div className="badges-box__list">
{userProfile.badges.map((badgeName) => {
const badge = badges.find((b) => b.name === badgeName);
if (!badge) return null;
return (
<div
key={badge.name}
className="badges-box__item"
data-tooltip-place="top"
data-tooltip-content={badge.description}
data-tooltip-id="badges-box-tooltip"
>
<img
src={badge.badge.url}
alt={badge.name}
width={32}
height={32}
/>
</div>
);
})}
</div>
<Tooltip id="badges-box-tooltip" />
</div>
</div>
<AllBadgesModal
visible={showAllBadgesModal}
onClose={() => setShowAllBadgesModal(false)}
/>
</>
);
}

View File

@@ -170,6 +170,7 @@ export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
export interface Badge {
name: string;
title: string;
description: string;
badge: {
url: string;