Run prettier on Typescript files

Port 51b83ed195 to glitch-soc
pull/62/head
Claire 2023-05-09 23:41:18 +02:00
parent facc7ab03c
commit 96e99e2170
24 changed files with 373 additions and 195 deletions

View File

@ -98,9 +98,9 @@ export const decode83 = (str: string) => {
}; };
export const intToRGB = (int: number) => ({ export const intToRGB = (int: number) => ({
r: Math.max(0, (int >> 16)), r: Math.max(0, int >> 16),
g: Math.max(0, (int >> 8) & 255), g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, (int & 255)), b: Math.max(0, int & 255),
}); });
export const getAverageFromBlurhash = (blurhash: string) => { export const getAverageFromBlurhash = (blurhash: string) => {

View File

@ -16,11 +16,8 @@ const obfuscatedCount = (count: number) => {
type Props = { type Props = {
value: number; value: number;
obfuscate?: boolean; obfuscate?: boolean;
} };
export const AnimatedNumber: React.FC<Props> = ({ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
value,
obfuscate,
})=> {
const [previousValue, setPreviousValue] = useState(value); const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1 | -1>(1); const [direction, setDirection] = useState<1 | -1>(1);
@ -30,24 +27,45 @@ export const AnimatedNumber: React.FC<Props> = ({
} }
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback(() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), [direction]); const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
[direction]
);
if (reduceMotion) { if (reduceMotion) {
return obfuscate ? <>{obfuscatedCount(value)}</> : <ShortNumber value={value} />; return obfuscate ? (
<>{obfuscatedCount(value)}</>
) : (
<ShortNumber value={value} />
);
} }
const styles = [{ const styles = [
{
key: `${value}`, key: `${value}`,
data: value, data: value,
style: { y: spring(0, { damping: 35, stiffness: 400 }) }, style: { y: spring(0, { damping: 35, stiffness: 400 }) },
}]; },
];
return ( return (
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}> <TransitionMotion
{items => ( styles={styles}
willEnter={willEnter}
willLeave={willLeave}
>
{(items) => (
<span className='animated-number'> <span className='animated-number'>
{items.map(({ key, data, style }) => ( {items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span> <span
key={key}
style={{
position: direction * style.y > 0 ? 'absolute' : 'static',
transform: `translateY(${style.y * 100}%)`,
}}
>
{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
</span>
))} ))}
</span> </span>
)} )}

View File

@ -10,7 +10,7 @@ type Props = {
size: number; size: number;
style?: React.CSSProperties; style?: React.CSSProperties;
inline?: boolean; inline?: boolean;
} };
export const Avatar: React.FC<Props> = ({ export const Avatar: React.FC<Props> = ({
account, account,
@ -19,7 +19,8 @@ export const Avatar: React.FC<Props> = ({
inline = false, inline = false,
style: styleFromParent, style: styleFromParent,
}) => { }) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif); const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const style = { const style = {
...styleFromParent, ...styleFromParent,
@ -29,12 +30,18 @@ export const Avatar: React.FC<Props> = ({
}; };
if (account) { if (account) {
style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`; style.backgroundImage = `url(${account.get(
hovering ? 'avatar' : 'avatar_static'
)})`;
} }
return ( return (
<div <div
className={classNames('account__avatar', { 'account__avatar-inline': inline }, className)} className={classNames(
'account__avatar',
{ 'account__avatar-inline': inline },
className
)}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
style={style} style={style}

View File

@ -8,7 +8,7 @@ type Props = {
dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
children?: never; children?: never;
[key: string]: any; [key: string]: any;
} };
const Blurhash: React.FC<Props> = ({ const Blurhash: React.FC<Props> = ({
hash, hash,
width = 32, width = 32,

View File

@ -8,7 +8,7 @@ type Props = {
width: number; width: number;
height: number; height: number;
onClick?: () => void; onClick?: () => void;
} };
export const GIFV: React.FC<Props> = ({ export const GIFV: React.FC<Props> = ({
src, src,
@ -20,16 +20,20 @@ export const GIFV: React.FC<Props> = ({
}) => { }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> = useCallback(() => { const handleLoadedData: React.ReactEventHandler<HTMLVideoElement> =
useCallback(() => {
setLoading(false); setLoading(false);
}, [setLoading]); }, [setLoading]);
const handleClick: React.MouseEventHandler = useCallback((e) => { const handleClick: React.MouseEventHandler = useCallback(
(e) => {
if (onClick) { if (onClick) {
e.stopPropagation(); e.stopPropagation();
onClick(); onClick();
} }
}, [onClick]); },
[onClick]
);
return ( return (
<div className='gifv' style={{ position: 'relative' }}> <div className='gifv' style={{ position: 'relative' }}>

View File

@ -7,6 +7,15 @@ type Props = {
fixedWidth?: boolean; fixedWidth?: boolean;
children?: never; children?: never;
[key: string]: any; [key: string]: any;
} };
export const Icon: React.FC<Props> = ({ id, className, fixedWidth, ...other }) => export const Icon: React.FC<Props> = ({
<i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />; id,
className,
fixedWidth,
...other
}) => (
<i
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
{...other}
/>
);

View File

@ -26,13 +26,12 @@ type Props = {
obfuscateCount?: boolean; obfuscateCount?: boolean;
href?: string; href?: string;
ariaHidden: boolean; ariaHidden: boolean;
} };
type States = { type States = {
activate: boolean, activate: boolean;
deactivate: boolean, deactivate: boolean;
} };
export class IconButton extends React.PureComponent<Props, States> { export class IconButton extends React.PureComponent<Props, States> {
static defaultProps = { static defaultProps = {
size: 18, size: 18,
active: false, active: false,
@ -120,10 +119,7 @@ export class IconButton extends React.PureComponent<Props, States> {
ariaHidden, ariaHidden,
} = this.props; } = this.props;
const { const { activate, deactivate } = this.state;
activate,
deactivate,
} = this.state;
const classes = classNames(className, 'icon-button', { const classes = classNames(className, 'icon-button', {
active, active,
@ -141,7 +137,12 @@ export class IconButton extends React.PureComponent<Props, States> {
let contents = ( let contents = (
<React.Fragment> <React.Fragment>
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>} <Icon id={icon} fixedWidth aria-hidden='true' />{' '}
{typeof counter !== 'undefined' && (
<span className='icon-button__counter'>
<AnimatedNumber value={counter} obfuscate={obfuscateCount} />
</span>
)}
{this.props.label} {this.props.label}
</React.Fragment> </React.Fragment>
); );
@ -174,5 +175,4 @@ export class IconButton extends React.PureComponent<Props, States> {
</button> </button>
); );
} }
} }

View File

@ -1,18 +1,25 @@
import React from 'react'; import React from 'react';
import { Icon } from './icon'; import { Icon } from './icon';
const formatNumber = (num: number): number | string => num > 40 ? '40+' : num; const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num);
type Props = { type Props = {
id: string; id: string;
count: number; count: number;
issueBadge: boolean; issueBadge: boolean;
className: string; className: string;
} };
export const IconWithBadge: React.FC<Props> = ({ id, count, issueBadge, className }) => ( export const IconWithBadge: React.FC<Props> = ({
id,
count,
issueBadge,
className,
}) => (
<i className='icon-with-badge'> <i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} /> <Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>} {count > 0 && (
<i className='icon-with-badge__badge'>{formatNumber(count)}</i>
)}
{issueBadge && <i className='icon-with-badge__issue-badge' />} {issueBadge && <i className='icon-with-badge__issue-badge' />}
</i> </i>
); );

View File

@ -7,9 +7,14 @@ type Props = {
srcSet?: string; srcSet?: string;
blurhash?: string; blurhash?: string;
className?: string; className?: string;
} };
export const Image: React.FC<Props> = ({ src, srcSet, blurhash, className }) => { export const Image: React.FC<Props> = ({
src,
srcSet,
blurhash,
className,
}) => {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const handleLoad = useCallback(() => { const handleLoad = useCallback(() => {
@ -17,7 +22,10 @@ export const Image: React.FC<Props> = ({ src, srcSet, blurhash, className }) =>
}, [setLoaded]); }, [setLoaded]);
return ( return (
<div className={classNames('image', { loaded }, className)} role='presentation'> <div
className={classNames('image', { loaded }, className)}
role='presentation'
>
{blurhash && <Blurhash hash={blurhash} className='image__preview' />} {blurhash && <Blurhash hash={blurhash} className='image__preview' />}
<img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} /> <img src={src} srcSet={srcSet} alt='' onLoad={handleLoad} />
</div> </div>

View File

@ -4,7 +4,10 @@ import { FormattedMessage } from 'react-intl';
export const NotSignedInIndicator: React.FC = () => ( export const NotSignedInIndicator: React.FC = () => (
<div className='scrollable scrollable--flex'> <div className='scrollable scrollable--flex'>
<div className='empty-column-indicator'> <div className='empty-column-indicator'>
<FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' /> <FormattedMessage
id='not_signed_in_indicator.not_signed_in'
defaultMessage='You need to sign in to access this resource.'
/>
</div> </div>
</div> </div>
); );

View File

@ -9,7 +9,13 @@ type Props = {
label: React.ReactNode; label: React.ReactNode;
}; };
export const RadioButton: React.FC<Props> = ({ name, value, checked, onChange, label }) => { export const RadioButton: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return ( return (
<label className='radio-button'> <label className='radio-button'>
<input <input

View File

@ -4,20 +4,50 @@ import { injectIntl, defineMessages, InjectedIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' }, today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, just_now_full: {
id: 'relative_time.full.just_now',
defaultMessage: 'just now',
},
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, seconds_full: {
id: 'relative_time.full.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} ago',
},
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, minutes_full: {
id: 'relative_time.full.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago',
},
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, hours_full: {
id: 'relative_time.full.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} ago',
},
days: { id: 'relative_time.days', defaultMessage: '{number}d' }, days: { id: 'relative_time.days', defaultMessage: '{number}d' },
days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, days_full: {
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, id: 'relative_time.full.days',
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, defaultMessage: '{number, plural, one {# day} other {# days}} ago',
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, },
hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, moments_remaining: {
days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, id: 'time_remaining.moments',
defaultMessage: 'Moments remaining',
},
seconds_remaining: {
id: 'time_remaining.seconds',
defaultMessage: '{number, plural, one {# second} other {# seconds}} left',
},
minutes_remaining: {
id: 'time_remaining.minutes',
defaultMessage: '{number, plural, one {# minute} other {# minutes}} left',
},
hours_remaining: {
id: 'time_remaining.hours',
defaultMessage: '{number, plural, one {# hour} other {# hours}} left',
},
days_remaining: {
id: 'time_remaining.days',
defaultMessage: '{number, plural, one {# day} other {# days}} left',
},
}); });
const dateFormatOptions = { const dateFormatOptions = {
@ -70,7 +100,14 @@ const getUnitDelay = (units: string) => {
} }
}; };
export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year: number, timeGiven: boolean, short?: boolean) => { export const timeAgoString = (
intl: InjectedIntl,
date: Date,
now: number,
year: number,
timeGiven: boolean,
short?: boolean
) => {
const delta = now - date.getTime(); const delta = now - date.getTime();
let relativeTime; let relativeTime;
@ -78,27 +115,49 @@ export const timeAgoString = (intl: InjectedIntl, date: Date, now: number, year:
if (delta < DAY && !timeGiven) { if (delta < DAY && !timeGiven) {
relativeTime = intl.formatMessage(messages.today); relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) { } else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); relativeTime = intl.formatMessage(
short ? messages.just_now : messages.just_now_full
);
} else if (delta < 7 * DAY) { } else if (delta < 7 * DAY) {
if (delta < MINUTE) { if (delta < MINUTE) {
relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); relativeTime = intl.formatMessage(
short ? messages.seconds : messages.seconds_full,
{ number: Math.floor(delta / SECOND) }
);
} else if (delta < HOUR) { } else if (delta < HOUR) {
relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); relativeTime = intl.formatMessage(
short ? messages.minutes : messages.minutes_full,
{ number: Math.floor(delta / MINUTE) }
);
} else if (delta < DAY) { } else if (delta < DAY) {
relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); relativeTime = intl.formatMessage(
short ? messages.hours : messages.hours_full,
{ number: Math.floor(delta / HOUR) }
);
} else { } else {
relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); relativeTime = intl.formatMessage(
short ? messages.days : messages.days_full,
{ number: Math.floor(delta / DAY) }
);
} }
} else if (date.getFullYear() === year) { } else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions); relativeTime = intl.formatDate(date, shortDateFormatOptions);
} else { } else {
relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); relativeTime = intl.formatDate(date, {
...shortDateFormatOptions,
year: 'numeric',
});
} }
return relativeTime; return relativeTime;
}; };
const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGiven = true) => { const timeRemainingString = (
intl: InjectedIntl,
date: Date,
now: number,
timeGiven = true
) => {
const delta = date.getTime() - now; const delta = date.getTime() - now;
let relativeTime; let relativeTime;
@ -108,13 +167,21 @@ const timeRemainingString = (intl: InjectedIntl, date: Date, now: number, timeGi
} else if (delta < 10 * SECOND) { } else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.moments_remaining); relativeTime = intl.formatMessage(messages.moments_remaining);
} else if (delta < MINUTE) { } else if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); relativeTime = intl.formatMessage(messages.seconds_remaining, {
number: Math.floor(delta / SECOND),
});
} else if (delta < HOUR) { } else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); relativeTime = intl.formatMessage(messages.minutes_remaining, {
number: Math.floor(delta / MINUTE),
});
} else if (delta < DAY) { } else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); relativeTime = intl.formatMessage(messages.hours_remaining, {
number: Math.floor(delta / HOUR),
});
} else { } else {
relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); relativeTime = intl.formatMessage(messages.days_remaining, {
number: Math.floor(delta / DAY),
});
} }
return relativeTime; return relativeTime;
@ -126,18 +193,17 @@ type Props = {
year: number; year: number;
futureDate?: boolean; futureDate?: boolean;
short?: boolean; short?: boolean;
} };
type States = { type States = {
now: number; now: number;
} };
class RelativeTimestamp extends React.Component<Props, States> { class RelativeTimestamp extends React.Component<Props, States> {
state = { state = {
now: this.props.intl.now(), now: this.props.intl.now(),
}; };
static defaultProps = { static defaultProps = {
year: (new Date()).getFullYear(), year: new Date().getFullYear(),
short: true, short: true,
}; };
@ -146,9 +212,11 @@ class RelativeTimestamp extends React.Component<Props, States> {
shouldComponentUpdate(nextProps: Props, nextState: States) { shouldComponentUpdate(nextProps: Props, nextState: States) {
// As of right now the locale doesn't change without a new page load, // As of right now the locale doesn't change without a new page load,
// but we might as well check in case that ever changes. // but we might as well check in case that ever changes.
return this.props.timestamp !== nextProps.timestamp || return (
this.props.timestamp !== nextProps.timestamp ||
this.props.intl.locale !== nextProps.intl.locale || this.props.intl.locale !== nextProps.intl.locale ||
this.state.now !== nextState.now; this.state.now !== nextState.now
);
} }
UNSAFE_componentWillReceiveProps(nextProps: Props) { UNSAFE_componentWillReceiveProps(nextProps: Props) {
@ -173,11 +241,14 @@ class RelativeTimestamp extends React.Component<Props, States> {
window.clearTimeout(this._timer); window.clearTimeout(this._timer);
const { timestamp } = props; const { timestamp } = props;
const delta = (new Date(timestamp)).getTime() - state.now; const delta = new Date(timestamp).getTime() - state.now;
const unitDelay = getUnitDelay(selectUnits(delta)); const unitDelay = getUnitDelay(selectUnits(delta));
const unitRemainder = Math.abs(delta % unitDelay); const unitRemainder = Math.abs(delta % unitDelay);
const updateInterval = 1000 * 10; const updateInterval = 1000 * 10;
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); const delay =
delta < 0
? Math.max(updateInterval, unitDelay - unitRemainder)
: Math.max(updateInterval, unitRemainder);
this._timer = window.setTimeout(() => { this._timer = window.setTimeout(() => {
this.setState({ now: this.props.intl.now() }); this.setState({ now: this.props.intl.now() });
@ -189,15 +260,19 @@ class RelativeTimestamp extends React.Component<Props, States> {
const timeGiven = timestamp.includes('T'); const timeGiven = timestamp.includes('T');
const date = new Date(timestamp); const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); const relativeTime = futureDate
? timeRemainingString(intl, date, this.state.now, timeGiven)
: timeAgoString(intl, date, this.state.now, year, timeGiven, short);
return ( return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}> <time
dateTime={timestamp}
title={intl.formatDate(date, dateFormatOptions)}
>
{relativeTime} {relativeTime}
</time> </time>
); );
} }
} }
const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp); const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp);

View File

@ -1,5 +1,15 @@
const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; const easingOutQuint = (
const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => { x: number,
t: number,
b: number,
c: number,
d: number
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (
node: Element,
key: 'scrollTop' | 'scrollLeft',
target: number
) => {
const startTime = Date.now(); const startTime = Date.now();
const offset = node[key]; const offset = node[key];
const gap = target - offset; const gap = target - offset;
@ -25,7 +35,14 @@ const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number)
}; };
}; };
const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; const isScrollBehaviorSupported =
'scrollBehavior' in document.documentElement.style;
export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); export const scrollRight = (node: Element, position: number) =>
export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); isScrollBehaviorSupported
? node.scrollTo({ left: position, behavior: 'smooth' })
: scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) =>
isScrollBehaviorSupported
? node.scrollTo({ top: 0, behavior: 'smooth' })
: scroll(node, 'scrollTop', 0);

View File

@ -7,17 +7,21 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
export const store = configureStore({ export const store = configureStore({
reducer: rootReducer, reducer: rootReducer,
middleware: getDefaultMiddleware => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat( getDefaultMiddleware()
loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] })) .concat(
loadingBarMiddleware({
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
})
)
.concat(errorsMiddleware) .concat(errorsMiddleware)
.concat(soundsMiddleware()), .concat(soundsMiddleware()),
}); });
// Infer the `RootState` and `AppDispatch` types from the store itself // Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer> export type RootState = ReturnType<typeof rootReducer>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch export type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -5,7 +5,9 @@ import { RootState } from '..';
const defaultFailSuffix = 'FAIL'; const defaultFailSuffix = 'FAIL';
export const errorsMiddleware: Middleware<Record<string, never>, RootState> = export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
({ dispatch }) => next => action => { ({ dispatch }) =>
(next) =>
(action) => {
if (action.type && !action.skipAlert) { if (action.type && !action.skipAlert) {
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); const isFail = new RegExp(`${defaultFailSuffix}$`, 'g');

View File

@ -3,15 +3,23 @@ import { Middleware } from 'redux';
import { RootState } from '..'; import { RootState } from '..';
interface Config { interface Config {
promiseTypeSuffixes?: string[] promiseTypeSuffixes?: string[];
} }
const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = ['PENDING', 'FULFILLED', 'REJECTED']; const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
'PENDING',
'FULFILLED',
'REJECTED',
];
export const loadingBarMiddleware = (config: Config = {}): Middleware<Record<string, never>, RootState> => { export const loadingBarMiddleware = (
config: Config = {}
): Middleware<Record<string, never>, RootState> => {
const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes; const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
return ({ dispatch }) => next => (action) => { return ({ dispatch }) =>
(next) =>
(action) => {
if (action.type && !action.skipLoading) { if (action.type && !action.skipLoading) {
const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes; const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
@ -21,7 +29,10 @@ export const loadingBarMiddleware = (config: Config = {}): Middleware<Record<st
if (action.type.match(isPending)) { if (action.type.match(isPending)) {
dispatch(showLoading()); dispatch(showLoading());
} else if (action.type.match(isFulfilled) || action.type.match(isRejected)) { } else if (
action.type.match(isFulfilled) ||
action.type.match(isRejected)
) {
dispatch(hideLoading()); dispatch(hideLoading());
} }
} }

View File

@ -2,8 +2,8 @@ import { Middleware, AnyAction } from 'redux';
import { RootState } from '..'; import { RootState } from '..';
interface AudioSource { interface AudioSource {
src: string src: string;
type: string type: string;
} }
const createAudio = (sources: AudioSource[]) => { const createAudio = (sources: AudioSource[]) => {
@ -30,7 +30,10 @@ const play = (audio: HTMLAudioElement) => {
audio.play(); audio.play();
}; };
export const soundsMiddleware = (): Middleware<Record<string, never>, RootState> => { export const soundsMiddleware = (): Middleware<
Record<string, never>,
RootState
> => {
const soundCache: { [key: string]: HTMLAudioElement } = { const soundCache: { [key: string]: HTMLAudioElement } = {
boop: createAudio([ boop: createAudio([
{ {
@ -44,7 +47,7 @@ export const soundsMiddleware = (): Middleware<Record<string, never>, RootState
]), ]),
}; };
return () => next => (action: AnyAction) => { return () => (next) => (action: AnyAction) => {
const sound = action?.meta?.sound; const sound = action?.meta?.sound;
if (sound && soundCache[sound]) { if (sound && soundCache[sound]) {

View File

@ -21,7 +21,7 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
* shortNumber(5936); * shortNumber(5936);
* // => [5.936, 1000, 1] * // => [5.936, 1000, 1]
*/ */
export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits export type ShortNumber = [number, DecimalUnits, 0 | 1]; // Array of: shorten number, unit of shorten number and maximum fraction digits
export function toShortNumber(sourceNumber: number): ShortNumber { export function toShortNumber(sourceNumber: number): ShortNumber {
if (sourceNumber < DECIMAL_UNITS.THOUSAND) { if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
return [sourceNumber, DECIMAL_UNITS.ONE, 0]; return [sourceNumber, DECIMAL_UNITS.ONE, 0];
@ -38,11 +38,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
sourceNumber < TEN_MILLIONS ? 1 : 0, sourceNumber < TEN_MILLIONS ? 1 : 0,
]; ];
} else if (sourceNumber < DECIMAL_UNITS.TRILLION) { } else if (sourceNumber < DECIMAL_UNITS.TRILLION) {
return [ return [sourceNumber / DECIMAL_UNITS.BILLION, DECIMAL_UNITS.BILLION, 0];
sourceNumber / DECIMAL_UNITS.BILLION,
DECIMAL_UNITS.BILLION,
0,
];
} }
return [sourceNumber, DECIMAL_UNITS.ONE, 0]; return [sourceNumber, DECIMAL_UNITS.ONE, 0];
@ -56,7 +52,10 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
* pluralReady(1793, DECIMAL_UNITS.THOUSAND) * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
* // => 1790 * // => 1790
*/ */
export function pluralReady(sourceNumber: number, division: DecimalUnits): number { export function pluralReady(
sourceNumber: number,
division: DecimalUnits
): number {
if (division == null || division < DECIMAL_UNITS.HUNDRED) { if (division == null || division < DECIMAL_UNITS.HUNDRED) {
return sourceNumber; return sourceNumber;
} }

View File

@ -1,3 +1,8 @@
export function uuid(a?: string): string { export function uuid(a?: string): string {
return a ? ((a as any as number) ^ Math.random() * 16 >> (a as any as number) / 4).toString(16) : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid); return a
? (
(a as any as number) ^
((Math.random() * 16) >> ((a as any as number) / 4))
).toString(16)
: ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
} }