Some useful packages I have put together to streamline development.
A collection of handy-dandy UI components for enhancing the functionality of some existing UI libraries out there (react-bootstrap, @headlessui, etc) without having to write all of the extra logic. One would argue that many of these enhancements should be baked into these default libraries - but keep in mind it's also a good idea to keep libraries lightweight - and customizable. This is a selection that I've either written from scratch, or wrapped components from libraries to make them more usable. Any required dependencies are noted.
Better React-Boostrap alert with icon support, internal dismissing, fixed functionality.
Requires: react-bootstrap, classnames
1import classNames from 'classnames';
2import React, { CSSProperties, memo, ReactElement, ReactNode, useState } from 'react';
3import { Alert, AlertProps } from 'react-bootstrap';
4
5export interface RPCAlertProps extends AlertProps {
6 icon?: ReactNode;
7 isFixed?: boolean;
8 offsetTop?: number;
9}
10
11const AlertComponent = (props: RPCAlertProps): ReactElement => {
12 const {
13 className,
14 children,
15 dismissible,
16 icon,
17 isFixed,
18 offsetTop,
19 onClose,
20 show,
21 variant,
22 } = props;
23
24 const [isAlertShowing, setIsAlertShowing] = useState(show);
25
26 const alertClassNames = classNames({
27 'rpc-alert border-0 rounded-0': true,
28 'rpc-alert--fixed mb-0 w-100 fixed-top': isFixed,
29 [`${className}`]: className,
30 });
31
32 const alertStyles: CSSProperties | undefined = offsetTop
33 ? {
34 top: `${offsetTop}px`,
35 }
36 : undefined;
37
38 return (
39 <Alert
40 className={alertClassNames}
41 dismissible={dismissible}
42 onClose={() => {
43 setIsAlertShowing(false);
44 if (onClose) onClose;
45 }}
46 show={dismissible ? isAlertShowing : show}
47 style={alertStyles}
48 variant={variant}>
49 <div className="rpc-alert__content d-flex">
50 {icon ? (
51 <div className="rpc-alert__icon me-3 lh-0">{icon}</div>
52 ) : undefined}
53 <div className="rpc-alert__message">{children}</div>
54 </div>
55 </Alert>
56 );
57};
58
59export const RPCAlert = memo<RPCAlertProps>(AlertComponent);
60
Better React-Boostrap button with icon support, tooltip functionality, badge support, and more.
Requires: react-bootstrap, classnames
1import React, { memo, ReactElement, ReactNode } from 'react';
2import { Button, ButtonProps } from 'react-bootstrap';
3import { Color, Placement, Variant } from 'react-bootstrap/esm/types';
4import classNames from 'classnames';
5
6// NOTE: Requires other components from this page
7import { RPCBadge } from '../RPCBadge/RPCBadge';
8import { RPCTooltip } from '../RPCTooltip/RPCTooltip';
9
10export interface RPCButtonProps extends ButtonProps {
11 badgeLabel?: string;
12 badgeTextColor?: Color;
13 badgeVariant?: Variant;
14 circular?: boolean;
15 fluid?: boolean;
16 hasTooltip?: boolean;
17 icon?: ReactNode;
18 iconPosition?: 'left' | 'right';
19 label?: string;
20 tooltipContent?: string;
21 tooltipPlacement?: Placement;
22 type?: 'button' | 'submit';
23}
24
25const RPCButtonComponent = (props: RPCButtonProps): ReactElement => {
26 const {
27 active,
28 badgeLabel,
29 badgeTextColor = 'dark',
30 badgeVariant = 'warning',
31 children,
32 circular,
33 className,
34 disabled,
35 fluid,
36 hasTooltip,
37 icon,
38 iconPosition = 'left',
39 label,
40 onClick,
41 size,
42 style,
43 title,
44 tooltipContent,
45 tooltipPlacement,
46 type,
47 variant,
48 } = props;
49
50 const buttonClassNames = classNames({
51 'has-badge': badgeLabel,
52 'has-icon': icon,
53 [`icon--${iconPosition}`]: iconPosition,
54 circular: circular,
55 'w-100': fluid,
56 [`${className}`]: className,
57 });
58
59 const renderButton = (): ReactElement => {
60 return (
61 <Button
62 active={active}
63 className={buttonClassNames}
64 disabled={disabled}
65 onClick={onClick}
66 size={size}
67 style={style}
68 title={title}
69 type={type}
70 variant={variant}>
71 {label || icon || badgeLabel ? (
72 <>
73 {icon && iconPosition === 'left' ? icon : undefined}
74 {label ? <span className="btn__label">{label}</span> : undefined}
75 {badgeLabel ? (
76 <RPCBadge bg={badgeVariant} text={badgeTextColor}>
77 {badgeLabel}
78 </RPCBadge>
79 ) : undefined}
80 {icon && iconPosition === 'right' ? icon : undefined}
81 </>
82 ) : (
83 <>{children}</>
84 )}
85 </Button>
86 );
87 };
88
89 return hasTooltip ? (
90 <RPCTooltip tooltipPopper={renderButton()} placement={tooltipPlacement}>
91 {tooltipContent}
92 </RPCTooltip>
93 ) : (
94 renderButton()
95 );
96};
97
98export const RPCButton = memo<RPCButtonProps>(RPCButtonComponent);
99
Simple confirm dialog using the default Modal component.
Requires: react-bootstrap, classnames
1import React, { memo, ReactElement, ReactNode } from 'react';
2import { Variant } from 'react-bootstrap/esm/types';
3import { ModalProps } from 'react-bootstrap';
4
5// NOTE: Requires other components from this page
6import { RPCModal } from '../RPCModal/RPCModal';
7import { RPCButton } from '../RPCButton/RPCButton';
8
9// NOTE: This exists in RPCModal - remove and import from there
10export interface RPCModalProps extends ModalProps {
11 dismissible?: boolean;
12 footer?: ReactNode;
13 title?: string | ReactNode;
14 variant?: Variant;
15}
16
17export interface RPCConfirmationModalProps extends RPCModalProps {
18 cancelLabel?: string;
19 children?: ReactNode;
20 confirmLabel?: string;
21 confirmVariant?: Variant;
22 onCancel?: () => void;
23 onConfirm?: () => void;
24}
25
26const RPCConfirmationModalComponent = (
27 props: RPCConfirmationModalProps,
28): ReactElement => {
29 const {
30 backdrop,
31 cancelLabel = 'Cancel',
32 children,
33 confirmLabel = 'Confirm',
34 confirmVariant = 'danger',
35 keyboard,
36 onCancel,
37 onConfirm,
38 title = 'Are you sure?',
39 show,
40 } = props;
41
42 return (
43 <RPCModal
44 backdrop={backdrop}
45 className="confirmation-modal"
46 variant="danger"
47 footer={
48 <div className="d-flex justify-content-center align-items-center w-100">
49 <RPCButton
50 className="me-2 w-100"
51 label={confirmLabel}
52 onClick={onConfirm}
53 variant={confirmVariant}
54 />
55 <RPCButton className="w-100" label={cancelLabel} onClick={onCancel} />
56 </div>
57 }
58 keyboard={keyboard}
59 size="md"
60 title={title}
61 show={show}
62 onHide={onCancel}>
63 {children}
64 </RPCModal>
65 );
66};
67
68export const RPCConfirmationModal = memo<RPCConfirmationModalProps>(
69 RPCConfirmationModalComponent,
70);
71
Better React-Bootstrap Modal with variants.
Requires: react-bootstrap, classnames
1import classNames from 'classnames';
2import React, { memo, ReactElement, ReactNode } from 'react';
3import { Modal, ModalProps } from 'react-bootstrap';
4import { Variant } from 'react-bootstrap/esm/types';
5
6// types
7export interface RPCModalProps extends ModalProps {
8 dismissible?: boolean;
9 footer?: ReactNode;
10 title?: string | ReactNode;
11 variant?: Variant;
12}
13
14// util
15const isLightText = (variant: Variant): boolean => {
16 const textLightVariants = ['black', 'dark', 'danger', 'primary', 'info'];
17 return textLightVariants.includes(variant);
18};
19
20const ModalComponent = (props: RPCModalProps): ReactElement => {
21 const {
22 backdrop,
23 children,
24 centered = true,
25 className,
26 closeVariant,
27 dismissible,
28 footer,
29 fullscreen,
30 keyboard,
31 onHide,
32 onShow,
33 scrollable,
34 show,
35 size = 'lg',
36 title,
37 variant,
38 } = props;
39
40 const modalClassNames = classNames({
41 'rpc-modal': true,
42 [`${className}`]: className,
43 });
44
45 const modalHeaderClassNames = classNames({
46 [`bg-${variant}`]: variant,
47 [`text-light`]: variant && isLightText(variant),
48 });
49
50 return (
51 <Modal
52 backdrop={backdrop}
53 centered={centered}
54 className={modalClassNames}
55 fullscreen={fullscreen}
56 keyboard={keyboard}
57 onHide={onHide}
58 onShow={onShow}
59 scrollable={scrollable}
60 show={show}
61 size={size}>
62 {dismissible || title ? (
63 <Modal.Header
64 className={modalHeaderClassNames}
65 closeVariant={
66 closeVariant || isLightText(variant) ? 'white' : undefined
67 }
68 closeButton={dismissible}>
69 {title ? <Modal.Title>{title}</Modal.Title> : undefined}
70 </Modal.Header>
71 ) : undefined}
72 <Modal.Body>{children}</Modal.Body>
73 {footer ? <Modal.Footer>{footer}</Modal.Footer> : undefined}
74 </Modal>
75 );
76};
77
78export const RPCModal = memo<RPCModalProps>(ModalComponent);
79
A React-Bootstrap pagination component with thresholds, variant, etc.
Requires: react-bootstrap, classnames
1import classNames from 'classnames';
2import React, { memo, ReactElement } from 'react';
3import Pagination, { PaginationProps } from 'react-bootstrap/Pagination';
4import { Variant } from 'react-bootstrap/esm/types';
5
6// types
7export interface RPCPaginationProps extends PaginationProps {
8 activePage?: number;
9 ellipsisThreshold?: number;
10 isNextDisabled?: boolean;
11 isPreviousDisabled?: boolean;
12 onEllipsisClick?: () => void;
13 onFirstClick?: () => void;
14 onLastClick?: () => void;
15 onNextClick?: () => void;
16 onPageClick?: (pageNumber: number) => void;
17 onPreviousClick?: () => void;
18 pageCount: number;
19 variant?: Variant;
20}
21
22const RPCPaginationComponent = (props: RPCPaginationProps): ReactElement => {
23 const {
24 activePage = 1,
25 ellipsisThreshold = 5,
26 isNextDisabled,
27 isPreviousDisabled,
28 onEllipsisClick,
29 onFirstClick,
30 onLastClick,
31 onNextClick,
32 onPageClick,
33 onPreviousClick,
34 pageCount,
35 size,
36 variant,
37 } = props;
38
39 const paginationClassNames = classNames({
40 'rpc-pagination': true,
41 [`rpc-pagination--${variant}`]: variant,
42 });
43
44 let isPageNumberOutOfRange: boolean;
45
46 const paginationItems = [...new Array(pageCount)].map(
47 (_, index: number): ReactElement => {
48 const pageNumber = index + 1;
49 const isPageNumberFirst = pageNumber === 1;
50 const isPageNumberLast = pageNumber === pageCount;
51 const isCurrentPageWithinTwoPageNumbers =
52 Math.abs(pageNumber - activePage) <= 2;
53
54 const showPaginationItem =
55 pageCount <= ellipsisThreshold
56 ? true
57 : isPageNumberFirst ||
58 isPageNumberLast ||
59 isCurrentPageWithinTwoPageNumbers;
60
61 if (showPaginationItem) {
62 isPageNumberOutOfRange = false;
63 return (
64 <Pagination.Item
65 key={pageNumber}
66 onClick={() => onPageClick(pageNumber)}
67 active={pageNumber === activePage}
68 >
69 {pageNumber}
70 </Pagination.Item>
71 );
72 }
73
74 if (!isPageNumberOutOfRange) {
75 isPageNumberOutOfRange = true;
76 return (
77 <Pagination.Ellipsis
78 key={pageNumber}
79 className="muted"
80 onClick={onEllipsisClick}
81 />
82 );
83 }
84
85 return null;
86 },
87 );
88
89 return (
90 <Pagination size={size} className={paginationClassNames}>
91 {onFirstClick ? <Pagination.First onClick={onFirstClick} /> : undefined}
92 <Pagination.Prev
93 onClick={onPreviousClick}
94 disabled={isPreviousDisabled}
95 />
96 {paginationItems}
97 <Pagination.Next onClick={onNextClick} disabled={isNextDisabled} />
98 {onLastClick ? <Pagination.Last onClick={onLastClick} /> : undefined}
99 </Pagination>
100 );
101};
102
103export const RPCPagination = memo<RPCPaginationProps>(RPCPaginationComponent);
104
A React-Bootstrap tooltip with popper, rootClose functionality.
Requires: react-bootstrap, classnames
1import classNames from 'classnames';
2import React, { memo, ReactElement, ReactNode } from 'react';
3import { OverlayTrigger, Tooltip, OverlayTriggerProps, TooltipProps } from 'react-bootstrap';
4
5// types
6export interface RPCTooltipProps
7 extends TooltipProps,
8 Omit<OverlayTriggerProps, 'children' | 'overlay'> {
9 tooltipPopper?: ReactNode;
10 transparent?: boolean;
11}
12
13const RPCTooltipComponent = (props: RPCTooltipProps): ReactElement => {
14 const {
15 children,
16 id,
17 placement = 'right',
18 tooltipPopper,
19 show,
20 transparent,
21 } = props;
22
23 const tooltipClassNames = classNames({
24 transparent: transparent,
25 });
26
27 return (
28 <OverlayTrigger
29 placement={placement}
30 // delay={{show: 250, hide: 400}}
31 overlay={
32 <Tooltip
33 id={id || `tooltip-${placement}`}
34 show={show}
35 className={tooltipClassNames}>
36 {children}
37 </Tooltip>
38 }
39 rootClose>
40 <div className="tooltip-popper">{tooltipPopper}</div>
41 </OverlayTrigger>
42 );
43};
44
45export const RPCTooltip = memo<RPCTooltipProps>(RPCTooltipComponent);
46