Add max count to sidebar icons

This commit is contained in:
Chewbacca 2022-11-03 12:13:45 -04:00
parent d0960de07c
commit be136fe6cf
8 changed files with 34 additions and 11 deletions

View file

@ -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>

View file

@ -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>
<HStack space={2} alignItems='center'>
<Text weight='semibold' theme='inherit'>{text}</Text> <Text weight='semibold' theme='inherit'>{text}</Text>
{count ? (
<Counter count={count} countMax={countMax} />
) : null}
</HStack>
</NavLink> </NavLink>
); );
}); });

View file

@ -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' />}
/> />
); );

View file

@ -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

View file

@ -24,6 +24,7 @@ const ThumbNavigation: React.FC = (): JSX.Element => {
to='/chats' to='/chats'
exact exact
count={unreadChatsCount} count={unreadChatsCount}
countMax={20}
/> />
); );
} }

View file

@ -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>
); );
}; };

View file

@ -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');

View file

@ -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