Add max count to sidebar icons
This commit is contained in:
parent
d0960de07c
commit
be136fe6cf
8 changed files with 34 additions and 11 deletions
|
@ -5,18 +5,19 @@ import { Counter } from 'soapbox/components/ui';
|
||||||
|
|
||||||
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
count: number,
|
count: number,
|
||||||
|
countMax?: number
|
||||||
icon?: string;
|
icon?: string;
|
||||||
src?: string;
|
src?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, ...rest }) => {
|
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, countMax, ...rest }) => {
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<Icon id={icon} {...rest as IIcon} />
|
<Icon id={icon} {...rest as IIcon} />
|
||||||
|
|
||||||
{count > 0 && (
|
{count > 0 && (
|
||||||
<i className='absolute -top-2 -right-2'>
|
<i className='absolute -top-2 -right-2'>
|
||||||
<Counter count={count} />
|
<Counter count={count} countMax={countMax} />
|
||||||
</i>
|
</i>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,13 @@ import classNames from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon, Text } from './ui';
|
import { Counter, HStack, Icon, Text } from './ui';
|
||||||
|
|
||||||
interface ISidebarNavigationLink {
|
interface ISidebarNavigationLink {
|
||||||
/** Notification count, if any. */
|
/** Notification count, if any. */
|
||||||
count?: number,
|
count?: number,
|
||||||
|
/** Optional max to cap count (ie: N+) */
|
||||||
|
countMax?: number
|
||||||
/** URL to an SVG icon. */
|
/** URL to an SVG icon. */
|
||||||
icon: string,
|
icon: string,
|
||||||
/** Link label. */
|
/** Link label. */
|
||||||
|
@ -19,7 +21,7 @@ interface ISidebarNavigationLink {
|
||||||
|
|
||||||
/** Desktop sidebar navigation link. */
|
/** Desktop sidebar navigation link. */
|
||||||
const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef<HTMLAnchorElement>): JSX.Element => {
|
const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, ref: React.ForwardedRef<HTMLAnchorElement>): JSX.Element => {
|
||||||
const { icon, text, to = '', count, onClick } = props;
|
const { icon, text, to = '', count, countMax, onClick } = props;
|
||||||
const isActive = location.pathname === to;
|
const isActive = location.pathname === to;
|
||||||
|
|
||||||
const handleClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
|
@ -44,14 +46,19 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
||||||
<span className='relative'>
|
<span className='relative'>
|
||||||
<Icon
|
<Icon
|
||||||
src={icon}
|
src={icon}
|
||||||
count={count}
|
|
||||||
className={classNames('h-5 w-5 group-hover:text-primary-500', {
|
className={classNames('h-5 w-5 group-hover:text-primary-500', {
|
||||||
'text-primary-500': isActive,
|
'text-primary-500': isActive,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Text weight='semibold' theme='inherit'>{text}</Text>
|
<HStack space={2} alignItems='center'>
|
||||||
|
<Text weight='semibold' theme='inherit'>{text}</Text>
|
||||||
|
|
||||||
|
{count ? (
|
||||||
|
<Counter count={count} countMax={countMax} />
|
||||||
|
) : null}
|
||||||
|
</HStack>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -116,6 +116,7 @@ const SidebarNavigation = () => {
|
||||||
to='/chats'
|
to='/chats'
|
||||||
icon={require('@tabler/icons/mail.svg')}
|
icon={require('@tabler/icons/mail.svg')}
|
||||||
count={unreadChatsCount}
|
count={unreadChatsCount}
|
||||||
|
countMax={20}
|
||||||
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Icon, Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
interface IThumbNavigationLink {
|
interface IThumbNavigationLink {
|
||||||
count?: number,
|
count?: number,
|
||||||
|
countMax?: number,
|
||||||
src: string,
|
src: string,
|
||||||
text: string | React.ReactElement,
|
text: string | React.ReactElement,
|
||||||
to: string,
|
to: string,
|
||||||
|
@ -14,7 +15,7 @@ interface IThumbNavigationLink {
|
||||||
paths?: Array<string>,
|
paths?: Array<string>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, src, text, to, exact, paths }): JSX.Element => {
|
const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax, src, text, to, exact, paths }): JSX.Element => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const isActive = (): boolean => {
|
const isActive = (): boolean => {
|
||||||
|
@ -38,6 +39,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, src, text,
|
||||||
'text-primary-500': active,
|
'text-primary-500': active,
|
||||||
})}
|
})}
|
||||||
count={count}
|
count={count}
|
||||||
|
countMax={countMax}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
|
|
|
@ -24,6 +24,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||||
to='/chats'
|
to='/chats'
|
||||||
exact
|
exact
|
||||||
count={unreadChatsCount}
|
count={unreadChatsCount}
|
||||||
|
countMax={20}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,15 @@ import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
interface ICounter {
|
interface ICounter {
|
||||||
/** Number this counter should display. */
|
/** Number this counter should display. */
|
||||||
count: number,
|
count: number,
|
||||||
|
/** Optional max number (ie: N+) */
|
||||||
|
countMax?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A simple counter for notifications, etc. */
|
/** A simple counter for notifications, etc. */
|
||||||
const Counter: React.FC<ICounter> = ({ count }) => {
|
const Counter: React.FC<ICounter> = ({ count, countMax }) => {
|
||||||
return (
|
return (
|
||||||
<span className='block px-1.5 py-0.5 bg-secondary-500 text-xs text-white rounded-full ring-2 ring-white dark:ring-gray-800'>
|
<span className='block px-1.5 py-0.5 bg-secondary-500 text-xs font-semibold text-white rounded-full ring-2 ring-white dark:ring-gray-800'>
|
||||||
{shortNumberFormat(count)}
|
{shortNumberFormat(count, countMax)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,11 @@ describe('shortNumberFormat', () => {
|
||||||
expect(screen.getByTestId('num')).toHaveTextContent('•');
|
expect(screen.getByTestId('num')).toHaveTextContent('•');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handles max argument', () => {
|
||||||
|
render(<div data-testid='num'>{shortNumberFormat(25, 20)}</div>, undefined, null);
|
||||||
|
expect(screen.getByTestId('num')).toHaveTextContent('20+');
|
||||||
|
});
|
||||||
|
|
||||||
test('formats numbers under 1,000', () => {
|
test('formats numbers under 1,000', () => {
|
||||||
render(<div data-testid='num'>{shortNumberFormat(555)}</div>, undefined, null);
|
render(<div data-testid='num'>{shortNumberFormat(555)}</div>, undefined, null);
|
||||||
expect(screen.getByTestId('num')).toHaveTextContent('555');
|
expect(screen.getByTestId('num')).toHaveTextContent('555');
|
||||||
|
|
|
@ -16,7 +16,7 @@ const roundDown = (num: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Display a number nicely for the UI, eg 1000 becomes 1K. */
|
/** Display a number nicely for the UI, eg 1000 becomes 1K. */
|
||||||
export const shortNumberFormat = (number: any): React.ReactNode => {
|
export const shortNumberFormat = (number: any, max?: number): React.ReactNode => {
|
||||||
if (!isNumber(number)) return '•';
|
if (!isNumber(number)) return '•';
|
||||||
|
|
||||||
let value = number;
|
let value = number;
|
||||||
|
@ -29,6 +29,10 @@ export const shortNumberFormat = (number: any): React.ReactNode => {
|
||||||
value = roundDown(value / 1000000);
|
value = roundDown(value / 1000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (max && value > max) {
|
||||||
|
return <span>{max}+</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
|
|
Loading…
Reference in a new issue