mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 01:03:57 +00:00
feat: add all badges modal and enhance badges display in profile
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
|
||||
|
||||
export interface Badge {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
badge: {
|
||||
url: string;
|
||||
|
||||
Reference in New Issue
Block a user