LP-5613 #2
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import {Badge} from '../../types/badges';
|
||||
import RenderEmoji from '../utils/emoji';
|
||||
import RenderEmoji from '../emoji/emoji';
|
||||
import {IMAGE_TYPE_ABSOLUTE_URL, IMAGE_TYPE_EMOJI} from '../../constants';
|
||||
|
||||
type Props = {
|
||||
@ -6,6 +6,8 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
|
||||
import RenderEmoji from 'components/emoji/emoji';
|
||||
|
||||
import {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors';
|
||||
import {closeCreateBadgeModal, closeEditBadgeModal, setRHSView} from 'actions/actions';
|
||||
import {RHS_STATE_ALL} from '../../constants';
|
||||
@ -14,14 +16,14 @@ import Client from 'client/api';
|
||||
import {getServerErrorId} from 'utils/helpers';
|
||||
import CloseIcon from 'components/icons/close_icon';
|
||||
import EmojiIcon from 'components/icons/emoji_icon';
|
||||
import RenderEmoji from 'components/utils/emoji';
|
||||
|
||||
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
|
||||
|
||||
import EmojiPickerOverlay from './emoji_picker';
|
||||
import InlineTypeForm from './inline_type_form';
|
||||
import TypeSelect from './type_select';
|
||||
|
||||
import './badge_modal.scss';
|
||||
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
|
||||
|
||||
const NEW_TYPE_VALUE = '__new__';
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {AllBadgesBadge} from 'types/badges';
|
||||
import Client from 'client/api';
|
||||
import {getServerErrorId, getUserDisplayName} from 'utils/helpers';
|
||||
import CloseIcon from 'components/icons/close_icon';
|
||||
import RenderEmoji from 'components/utils/emoji';
|
||||
import RenderEmoji from 'components/emoji/emoji';
|
||||
|
||||
type GrantFormData = {
|
||||
badgeId: string;
|
||||
|
||||
@ -72,7 +72,7 @@ const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
|
||||
/>
|
||||
</span>
|
||||
{' '}
|
||||
{badge.description ? markdown(badge.description) : '—'}
|
||||
{badge.description ? markdown(badge.description) : '-'}
|
||||
</div>
|
||||
<div className='badge-meta'>
|
||||
<FormattedMessage
|
||||
|
||||
@ -9,7 +9,7 @@ import Client from '../../client/api';
|
||||
|
||||
import {RHSState} from '../../types/general';
|
||||
import {RHS_STATE_MY, RHS_STATE_OTHER, RHS_STATE_TYPES} from '../../constants';
|
||||
import BadgeImage from '../utils/badge_image';
|
||||
import BadgeImage from '../badge_image/badge_image';
|
||||
|
||||
import {markdown} from 'utils/markdown';
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import Client4 from 'mattermost-redux/client/client4';
|
||||
|
||||
import {UserBadge} from '../../types/badges';
|
||||
import BadgeImage from '../utils/badge_image';
|
||||
import BadgeImage from '../badge_image/badge_image';
|
||||
import {markdown} from 'utils/markdown';
|
||||
import Client from '../../client/api';
|
||||
import ConfirmDialog from '../confirm_dialog/confirm_dialog';
|
||||
|
||||
@ -1,256 +1,169 @@
|
||||
import {UserProfile} from 'mattermost-redux/types/users';
|
||||
import React from 'react';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
|
||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {systemEmojis} from 'mattermost-redux/actions/emojis';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
|
||||
import {BadgeID, UserBadge} from 'types/badges';
|
||||
import {systemEmojis, getCustomEmojisByName} from 'mattermost-redux/actions/emojis';
|
||||
|
||||
import {UserBadge} from 'types/badges';
|
||||
import Client from 'client/api';
|
||||
import BadgeImage from '../utils/badge_image';
|
||||
import TooltipWrapper from '../utils/tooltip_wrapper';
|
||||
import {RHSState} from 'types/general';
|
||||
import BadgeImage from '../badge_image/badge_image';
|
||||
import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants';
|
||||
import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions';
|
||||
import {getShowRHS} from 'selectors';
|
||||
import {groupBadges} from 'components/utils/badge_list_utils';
|
||||
|
||||
import BadgeTooltip from './badge_tooltip';
|
||||
import TooltipWrapper from './tooltip_wrapper';
|
||||
import './badge_list.scss';
|
||||
|
||||
type Props = {
|
||||
intl: IntlShape;
|
||||
debug: GlobalState;
|
||||
user: UserProfile;
|
||||
currentUserID: string;
|
||||
openRHS: (() => void) | null;
|
||||
hide: () => void;
|
||||
status?: string;
|
||||
actions: {
|
||||
setRHSView: (view: RHSState) => Promise<void>;
|
||||
setRHSBadge: (id: BadgeID | null) => Promise<void>;
|
||||
setRHSUser: (id: string | null) => Promise<void>;
|
||||
openGrant: (user?: string, badge?: string) => Promise<void>;
|
||||
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
type BadgeGroup = {
|
||||
badge: UserBadge;
|
||||
count: number;
|
||||
}
|
||||
|
||||
type State = {
|
||||
badges?: UserBadge[];
|
||||
loaded?: boolean;
|
||||
}
|
||||
|
||||
const MAX_BADGES = 6;
|
||||
const BADGE_SIZE = 24;
|
||||
|
||||
class BadgeList extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const BadgeList: React.FC<Props> = ({user, hide}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const currentUserID = useSelector(getCurrentUserId);
|
||||
const openRHS = useSelector(getShowRHS);
|
||||
const [badges, setBadges] = useState<UserBadge[]>();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
useEffect(() => {
|
||||
const c = new Client();
|
||||
c.getUserBadges(this.props.user.id).then((badges) => {
|
||||
this.setState({badges, loaded: true});
|
||||
c.getUserBadges(user.id).then((result) => {
|
||||
setBadges(result);
|
||||
|
|
||||
setLoaded(true);
|
||||
});
|
||||
}
|
||||
}, [user.id]);
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (this.state.badges !== prevState.badges && this.state.badges) {
|
||||
const groups = this.groupBadges(this.state.badges);
|
||||
const toShow = Math.min(groups.length, MAX_BADGES);
|
||||
const names: string[] = [];
|
||||
for (let i = 0; i < toShow; i++) {
|
||||
const {badge} = groups[i];
|
||||
if (badge.image_type === IMAGE_TYPE_EMOJI) {
|
||||
names.push(badge.image);
|
||||
}
|
||||
}
|
||||
const toLoad = names.filter((v) => !systemEmojis.has(v));
|
||||
this.props.actions.getCustomEmojisByName(toLoad);
|
||||
}
|
||||
}
|
||||
const groups = useMemo(
|
||||
() => (badges ? groupBadges(badges) : []),
|
||||
[badges],
|
||||
);
|
||||
|
||||
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 = () => {
|
||||
if (!this.props.openRHS) {
|
||||
useEffect(() => {
|
||||
if (!badges) {
|
||||
return;
|
||||
}
|
||||
const toShow = groups.slice(0, MAX_BADGES);
|
||||
const names = toShow.
|
||||
filter(({badge}) => badge.image_type === IMAGE_TYPE_EMOJI).
|
||||
map(({badge}) => badge.image).
|
||||
|
vladimir.khablak
commented
как будто filter.map.filter можно заменить на reduce как будто filter.map.filter можно заменить на reduce
|
||||
filter((v) => !systemEmojis.has(v));
|
||||
if (names.length > 0) {
|
||||
dispatch(getCustomEmojisByName(names));
|
||||
}
|
||||
}, [badges, groups, dispatch]);
|
||||
|
||||
if (this.props.currentUserID === this.props.user.id) {
|
||||
this.props.actions.setRHSView(RHS_STATE_MY);
|
||||
this.props.openRHS();
|
||||
this.props.hide();
|
||||
const handleMoreClick = useCallback(() => {
|
||||
if (!openRHS) {
|
||||
return;
|
||||
}
|
||||
if (currentUserID === user.id) {
|
||||
dispatch(setRHSView(RHS_STATE_MY));
|
||||
} else {
|
||||
dispatch(setRHSUser(user.id));
|
||||
dispatch(setRHSView(RHS_STATE_OTHER));
|
||||
}
|
||||
openRHS();
|
||||
hide();
|
||||
}, [openRHS, currentUserID, user.id, dispatch, hide]);
|
||||
|
||||
this.props.actions.setRHSUser(this.props.user.id);
|
||||
this.props.actions.setRHSView(RHS_STATE_OTHER);
|
||||
this.props.openRHS();
|
||||
this.props.hide();
|
||||
}
|
||||
|
||||
onBadgeClick = (badge: UserBadge) => {
|
||||
if (!this.props.openRHS) {
|
||||
const handleBadgeClick = useCallback((badge: UserBadge) => {
|
||||
if (!openRHS) {
|
||||
return;
|
||||
}
|
||||
dispatch(setRHSBadge(badge.id));
|
||||
dispatch(setRHSView(RHS_STATE_DETAIL));
|
||||
openRHS();
|
||||
hide();
|
||||
}, [openRHS, dispatch, hide]);
|
||||
|
||||
this.props.actions.setRHSBadge(badge.id);
|
||||
this.props.actions.setRHSView(RHS_STATE_DETAIL);
|
||||
this.props.openRHS();
|
||||
this.props.hide();
|
||||
}
|
||||
const handleGrantClick = useCallback(() => {
|
||||
dispatch(openGrant(user.username));
|
||||
hide();
|
||||
}, [dispatch, user.username, hide]);
|
||||
|
||||
onGrantClick = () => {
|
||||
this.props.actions.openGrant(this.props.user.username);
|
||||
this.props.hide();
|
||||
}
|
||||
const visibleGroups = groups.slice(0, MAX_BADGES);
|
||||
const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30;
|
||||
|
||||
render() {
|
||||
const {intl} = this.props;
|
||||
const groups = this.state.badges ? this.groupBadges(this.state.badges) : [];
|
||||
const nGroups = groups.length;
|
||||
const toShow = Math.min(nGroups, MAX_BADGES);
|
||||
|
||||
const nameLabel = intl.formatMessage(
|
||||
{id: 'badges.label.name', defaultMessage: 'Название:'},
|
||||
);
|
||||
const descLabel = intl.formatMessage(
|
||||
{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;
|
||||
if (badge.reason) {
|
||||
reason = intl.formatMessage(
|
||||
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
|
||||
{reason: badge.reason},
|
||||
);
|
||||
}
|
||||
const grantedBy = intl.formatMessage(
|
||||
{id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'},
|
||||
{username: badge.granted_by_name},
|
||||
);
|
||||
const grantedAt = intl.formatMessage(
|
||||
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
||||
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
|
||||
);
|
||||
tooltipLines = [
|
||||
nameLabel + ' ' + badge.name,
|
||||
descLabel + ' ' + badge.description,
|
||||
reason,
|
||||
grantedBy,
|
||||
grantedAt,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
const badgeComponent = (
|
||||
<TooltipWrapper tooltipContent={tooltipLines}>
|
||||
<a onClick={() => this.onBadgeClick(badge)}>
|
||||
<span className='badge-stacked'>
|
||||
<BadgeImage
|
||||
return (
|
||||
<div id='badgePlugin'>
|
||||
<div><b>
|
||||
<FormattedMessage
|
||||
id='badges.popover.title'
|
||||
defaultMessage='Достижения'
|
||||
/>
|
||||
</b></div>
|
||||
<div id='contentContainer'>
|
||||
|
vladimir.khablak
commented
немного поплыли стили немного поплыли стили
|
||||
{visibleGroups.map(({badge, count}) => (
|
||||
<TooltipWrapper
|
||||
key={badge.id}
|
||||
tooltipContent={
|
||||
<BadgeTooltip
|
||||
badge={badge}
|
||||
size={BADGE_SIZE}
|
||||
count={count}
|
||||
/>
|
||||
{count > 1 && (
|
||||
<span className='badge-stack-count'>
|
||||
{'×'}{count}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
content.push(badgeComponent);
|
||||
}
|
||||
|
||||
let andMore: React.ReactNode = null;
|
||||
if (nGroups > MAX_BADGES) {
|
||||
const andMoreText = intl.formatMessage(
|
||||
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
|
||||
{count: nGroups - MAX_BADGES},
|
||||
);
|
||||
andMore = (
|
||||
<TooltipWrapper tooltipContent={andMoreText}>
|
||||
<button
|
||||
id='showMoreButton'
|
||||
onClick={this.onMoreClick}
|
||||
}
|
||||
>
|
||||
<span className={'fa fa-angle-right'}/>
|
||||
</button>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30;
|
||||
let loading: React.ReactNode = null;
|
||||
if (!this.state.loaded) {
|
||||
loading = (
|
||||
|
||||
// Reserve enough height one row of badges and the "and more" button
|
||||
<a onClick={() => handleBadgeClick(badge)}>
|
||||
<span className='badge-stacked'>
|
||||
<BadgeImage
|
||||
badge={badge}
|
||||
size={BADGE_SIZE}
|
||||
/>
|
||||
{count > 1 && (
|
||||
<span className='badge-stack-count'>
|
||||
{'×'}{count}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</a>
|
||||
</TooltipWrapper>
|
||||
))}
|
||||
{groups.length > MAX_BADGES && (
|
||||
<TooltipWrapper
|
||||
tooltipContent={intl.formatMessage(
|
||||
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
|
||||
{count: groups.length - MAX_BADGES},
|
||||
)}
|
||||
>
|
||||
<button
|
||||
id='showMoreButton'
|
||||
onClick={handleMoreClick}
|
||||
>
|
||||
<span className={'fa fa-angle-right'}/>
|
||||
</button>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</div>
|
||||
{!loaded && (
|
||||
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
|
||||
<div className='spinner'/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div id='badgePlugin'>
|
||||
<div><b>
|
||||
<FormattedMessage
|
||||
id='badges.popover.title'
|
||||
defaultMessage='Достижения'
|
||||
/>
|
||||
</b></div>
|
||||
<div id='contentContainer' >
|
||||
{content}
|
||||
{andMore}
|
||||
</div>
|
||||
{loading}
|
||||
<button
|
||||
id='grantBadgeButton'
|
||||
onClick={this.onGrantClick}
|
||||
>
|
||||
<span className={'fa fa-plus-circle'}/>
|
||||
<FormattedMessage
|
||||
id='badges.grant_badge'
|
||||
defaultMessage='Выдать достижение'
|
||||
/>
|
||||
</button>
|
||||
<hr className='divider divider--expanded'/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
<button
|
||||
id='grantBadgeButton'
|
||||
onClick={handleGrantClick}
|
||||
>
|
||||
<span className={'fa fa-plus-circle'}/>
|
||||
<FormattedMessage
|
||||
id='badges.grant_badge'
|
||||
defaultMessage='Выдать достижение'
|
||||
/>
|
||||
</button>
|
||||
<hr className='divider divider--expanded'/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(BadgeList);
|
||||
export default BadgeList;
|
||||
|
||||
62
webapp/src/components/user_popover/badge_tooltip.tsx
Normal file
62
webapp/src/components/user_popover/badge_tooltip.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {UserBadge} from 'types/badges';
|
||||
|
||||
import {truncateText} from 'components/utils/badge_list_utils';
|
||||
|
||||
type Props = {
|
||||
badge: UserBadge;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const BadgeTooltip: React.FC<Props> = ({badge, count}) => {
|
||||
const intl = useIntl();
|
||||
const desc = badge.description ? truncateText(badge.description) : '—';
|
||||
|
||||
const nameRow = intl.formatMessage(
|
||||
{id: 'badges.label.name', defaultMessage: 'Название:'},
|
||||
) + ' ' + badge.name;
|
||||
|
||||
const descRow = intl.formatMessage(
|
||||
{id: 'badges.label.description', defaultMessage: 'Описание:'},
|
||||
) + ' ' + desc;
|
||||
|
||||
if (count > 1) {
|
||||
const countRow = intl.formatMessage(
|
||||
{id: 'badges.label.count', defaultMessage: 'Количество: {count}'},
|
||||
{count},
|
||||
);
|
||||
return <>{nameRow}{'\n'}{descRow}{'\n'}{countRow}</>;
|
||||
}
|
||||
|
||||
const time = new Date(badge.time);
|
||||
const grantedBy = intl.formatMessage(
|
||||
{id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'},
|
||||
{username: badge.granted_by_name},
|
||||
);
|
||||
const grantedAt = intl.formatMessage(
|
||||
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
|
||||
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{nameRow}{'\n'}
|
||||
|
vladimir.khablak
commented
такой конструкции я еще не видел, жестка такой конструкции я еще не видел, жестка
kirill.moos
commented
Что тут происходит?) Согласен с Владмиром Что тут происходит?) Согласен с Владмиром
|
||||
{descRow}{'\n'}
|
||||
{badge.reason && (
|
||||
<>
|
||||
{intl.formatMessage(
|
||||
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
|
||||
{reason: badge.reason},
|
||||
)}{'\n'}
|
||||
</>
|
||||
)}
|
||||
{grantedBy}{'\n'}
|
||||
{grantedAt}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeTooltip;
|
||||
@ -1,49 +1 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/actions/emojis';
|
||||
|
||||
import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions';
|
||||
|
||||
import {getShowRHS} from 'selectors';
|
||||
import {RHSState} from 'types/general';
|
||||
import {BadgeID} from 'types/badges';
|
||||
|
||||
import BadgeList from './badge_list';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
openRHS: getShowRHS(state),
|
||||
currentUserID: getCurrentUserId(state),
|
||||
debug: state,
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
setRHSView: (view: RHSState) => Promise<void>;
|
||||
setRHSBadge: (id: BadgeID | null) => Promise<void>;
|
||||
setRHSUser: (id: string | null) => Promise<void>;
|
||||
openGrant: (user?: string, badge?: string) => Promise<void>;
|
||||
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
|
||||
setRHSView,
|
||||
setRHSBadge,
|
||||
setRHSUser,
|
||||
openGrant,
|
||||
getCustomEmojisByName,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BadgeList);
|
||||
export {default} from './badge_list';
|
||||
|
||||
25
webapp/src/components/utils/badge_list_utils.ts
Normal file
25
webapp/src/components/utils/badge_list_utils.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {BadgeID, UserBadge} from 'types/badges';
|
||||
|
||||
export type BadgeGroup = {
|
||||
badge: UserBadge;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const MAX_DESC_LENGTH = 40;
|
||||
|
||||
export function 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());
|
||||
}
|
||||
|
||||
export function truncateText(text: string): string {
|
||||
return text.length > MAX_DESC_LENGTH ? text.slice(0, MAX_DESC_LENGTH) + '...' : text;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user
client