added badge stacking in user popover
This commit is contained in:
parent
dffe0685bb
commit
a88ce39a48
@ -19,6 +19,7 @@
|
|||||||
"badges.label.granted_by": "Granted by: {username}",
|
"badges.label.granted_by": "Granted by: {username}",
|
||||||
"badges.label.granted_at": "Granted at: {date}",
|
"badges.label.granted_at": "Granted at: {date}",
|
||||||
"badges.label.reason": "Why? {reason}",
|
"badges.label.reason": "Why? {reason}",
|
||||||
|
"badges.label.count": "Count: {count}",
|
||||||
|
|
||||||
"badges.granted.not_yet": "Not yet granted.",
|
"badges.granted.not_yet": "Not yet granted.",
|
||||||
"badges.granted.multiple": "Granted {times, plural, one {# time} other {# times}} to {users, plural, one {# user} other {# users}}.",
|
"badges.granted.multiple": "Granted {times, plural, one {# time} other {# times}} to {users, plural, one {# user} other {# users}}.",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"badges.label.granted_by": "Выдал: {username}",
|
"badges.label.granted_by": "Выдал: {username}",
|
||||||
"badges.label.granted_at": "Выдан: {date}",
|
"badges.label.granted_at": "Выдан: {date}",
|
||||||
"badges.label.reason": "Причина: {reason}",
|
"badges.label.reason": "Причина: {reason}",
|
||||||
|
"badges.label.count": "Количество: {count}",
|
||||||
|
|
||||||
"badges.granted.not_yet": "Ещё не выдан.",
|
"badges.granted.not_yet": "Ещё не выдан.",
|
||||||
"badges.granted.multiple": "Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
"badges.granted.multiple": "Выдан {times, plural, one {# раз} few {# раза} many {# раз} other {# раз}} {users, plural, one {# пользователю} few {# пользователям} many {# пользователям} other {# пользователям}}.",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-content: flex-end;
|
align-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#showMoreButton {
|
#showMoreButton {
|
||||||
@ -23,6 +24,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-stacked {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-stack-count {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
right: -4px;
|
||||||
|
background: var(--button-bg, #166de0);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 1px 3px;
|
||||||
|
border-radius: 6px;
|
||||||
|
min-width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
#grantBadgeButton {
|
#grantBadgeButton {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|||||||
@ -32,6 +32,11 @@ type Props = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BadgeGroup = {
|
||||||
|
badge: UserBadge;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
badges?: UserBadge[];
|
badges?: UserBadge[];
|
||||||
loaded?: boolean;
|
loaded?: boolean;
|
||||||
@ -55,12 +60,12 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
if (this.state.badges !== prevState.badges) {
|
if (this.state.badges !== prevState.badges && this.state.badges) {
|
||||||
const nBadges = this.state.badges?.length || 0;
|
const groups = this.groupBadges(this.state.badges);
|
||||||
const toShow = nBadges < MAX_BADGES ? nBadges : MAX_BADGES;
|
const toShow = Math.min(groups.length, MAX_BADGES);
|
||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
for (let i = 0; i < toShow; i++) {
|
for (let i = 0; i < toShow; i++) {
|
||||||
const badge = this.state.badges![i];
|
const {badge} = groups[i];
|
||||||
if (badge.image_type === IMAGE_TYPE_EMOJI) {
|
if (badge.image_type === IMAGE_TYPE_EMOJI) {
|
||||||
names.push(badge.image);
|
names.push(badge.image);
|
||||||
}
|
}
|
||||||
@ -70,6 +75,19 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupBadges = (badges: UserBadge[]): BadgeGroup[] => {
|
||||||
|
const map = new Map<BadgeID, BadgeGroup>();
|
||||||
|
for (const badge of badges) {
|
||||||
|
const existing = map.get(badge.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.count++;
|
||||||
|
} else {
|
||||||
|
map.set(badge.id, {badge, count: 1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
onMoreClick = () => {
|
onMoreClick = () => {
|
||||||
if (!this.props.openRHS) {
|
if (!this.props.openRHS) {
|
||||||
return;
|
return;
|
||||||
@ -78,6 +96,7 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
if (this.props.currentUserID === this.props.user.id) {
|
if (this.props.currentUserID === this.props.user.id) {
|
||||||
this.props.actions.setRHSView(RHS_STATE_MY);
|
this.props.actions.setRHSView(RHS_STATE_MY);
|
||||||
this.props.openRHS();
|
this.props.openRHS();
|
||||||
|
this.props.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,19 +124,34 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {intl} = this.props;
|
const {intl} = this.props;
|
||||||
const nBadges = this.state.badges?.length || 0;
|
const groups = this.state.badges ? this.groupBadges(this.state.badges) : [];
|
||||||
const toShow = nBadges < MAX_BADGES ? nBadges : MAX_BADGES;
|
const nGroups = groups.length;
|
||||||
|
const toShow = Math.min(nGroups, MAX_BADGES);
|
||||||
|
|
||||||
const content: React.ReactNode[] = [];
|
|
||||||
for (let i = 0; i < toShow; i++) {
|
|
||||||
const badge = this.state.badges![i];
|
|
||||||
const time = new Date(badge.time);
|
|
||||||
const nameLabel = intl.formatMessage(
|
const nameLabel = intl.formatMessage(
|
||||||
{id: 'badges.label.name', defaultMessage: 'Название:'},
|
{id: 'badges.label.name', defaultMessage: 'Название:'},
|
||||||
);
|
);
|
||||||
const descLabel = intl.formatMessage(
|
const descLabel = intl.formatMessage(
|
||||||
{id: 'badges.label.description', defaultMessage: 'Описание:'},
|
{id: 'badges.label.description', defaultMessage: 'Описание:'},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const content: React.ReactNode[] = [];
|
||||||
|
for (let i = 0; i < toShow; i++) {
|
||||||
|
const {badge, count} = groups[i];
|
||||||
|
|
||||||
|
let tooltipLines: string;
|
||||||
|
if (count > 1) {
|
||||||
|
const countLabel = intl.formatMessage(
|
||||||
|
{id: 'badges.label.count', defaultMessage: 'Количество: {count}'},
|
||||||
|
{count},
|
||||||
|
);
|
||||||
|
tooltipLines = [
|
||||||
|
nameLabel + ' ' + badge.name,
|
||||||
|
descLabel + ' ' + badge.description,
|
||||||
|
countLabel,
|
||||||
|
].join('\n');
|
||||||
|
} else {
|
||||||
|
const time = new Date(badge.time);
|
||||||
let reason: string | null = null;
|
let reason: string | null = null;
|
||||||
if (badge.reason) {
|
if (badge.reason) {
|
||||||
reason = intl.formatMessage(
|
reason = intl.formatMessage(
|
||||||
@ -133,30 +167,40 @@ class BadgeList extends React.PureComponent<Props, State> {
|
|||||||
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
||||||
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
|
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
|
||||||
);
|
);
|
||||||
const tooltipLines = [
|
tooltipLines = [
|
||||||
nameLabel + ' ' + badge.name,
|
nameLabel + ' ' + badge.name,
|
||||||
descLabel + ' ' + badge.description,
|
descLabel + ' ' + badge.description,
|
||||||
reason,
|
reason,
|
||||||
grantedBy,
|
grantedBy,
|
||||||
grantedAt,
|
grantedAt,
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
const badgeComponent = (
|
const badgeComponent = (
|
||||||
<TooltipWrapper tooltipContent={tooltipLines}>
|
<TooltipWrapper tooltipContent={tooltipLines}>
|
||||||
<a onClick={() => this.onBadgeClick(badge)}>
|
<a onClick={() => this.onBadgeClick(badge)}>
|
||||||
|
<span className='badge-stacked'>
|
||||||
<BadgeImage
|
<BadgeImage
|
||||||
badge={badge}
|
badge={badge}
|
||||||
size={BADGE_SIZE}
|
size={BADGE_SIZE}
|
||||||
/>
|
/>
|
||||||
|
{count > 1 && (
|
||||||
|
<span className='badge-stack-count'>
|
||||||
|
{'×'}{count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
);
|
);
|
||||||
content.push(badgeComponent);
|
content.push(badgeComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
let andMore: React.ReactNode = null;
|
let andMore: React.ReactNode = null;
|
||||||
if (nBadges > MAX_BADGES) {
|
if (nGroups > MAX_BADGES) {
|
||||||
const andMoreText = intl.formatMessage(
|
const andMoreText = intl.formatMessage(
|
||||||
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
|
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
|
||||||
{count: nBadges - MAX_BADGES},
|
{count: nGroups - MAX_BADGES},
|
||||||
);
|
);
|
||||||
andMore = (
|
andMore = (
|
||||||
<TooltipWrapper tooltipContent={andMoreText}>
|
<TooltipWrapper tooltipContent={andMoreText}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user