Skip to content

Commit 1a5465d

Browse files
committed
Update react, add some V2 components for V1 usage
1 parent 921da09 commit 1a5465d

File tree

21 files changed

+564
-43
lines changed

21 files changed

+564
-43
lines changed

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
"@fortawesome/fontawesome-svg-core": "^1.2.32",
55
"@fortawesome/free-solid-svg-icons": "^5.15.1",
66
"@fortawesome/react-fontawesome": "^0.1.11",
7+
"@headlessui/react": "^1.6.4",
8+
"@heroicons/react": "^1.0.6",
9+
"@hot-loader/react-dom": "^16.14.0",
710
"@tailwindcss/forms": "^0.5.2",
811
"axios": "^0.21.1",
912
"chart.js": "^2.8.0",
13+
"classnames": "^2.3.1",
1014
"codemirror": "^5.57.0",
1115
"date-fns": "^2.16.1",
1216
"debounce": "^1.2.0",
@@ -20,7 +24,7 @@
2024
"i18next-xhr-backend": "^3.2.2",
2125
"qrcode.react": "^1.0.1",
2226
"query-string": "^6.7.0",
23-
"react": "^16.13.1",
27+
"react": "^16.14.0",
2428
"react-copy-to-clipboard": "^5.0.2",
2529
"react-dom": "npm:@hot-loader/react-dom",
2630
"react-fast-compare": "^3.2.0",
@@ -64,9 +68,9 @@
6468
"@types/node": "^14.11.10",
6569
"@types/qrcode.react": "^1.0.1",
6670
"@types/query-string": "^6.3.0",
67-
"@types/react": "^16.9.41",
71+
"@types/react": "^16.14.0",
6872
"@types/react-copy-to-clipboard": "^4.3.0",
69-
"@types/react-dom": "^16.9.8",
73+
"@types/react-dom": "^16.9.16",
7074
"@types/react-helmet": "^6.0.0",
7175
"@types/react-redux": "^7.1.1",
7276
"@types/react-router": "^5.1.3",
@@ -103,7 +107,7 @@
103107
"terser-webpack-plugin": "^4.2.3",
104108
"ts-essentials": "^9.1.2",
105109
"twin.macro": "^2.8.2",
106-
"typescript": "^4.2.4",
110+
"typescript": "^4.7.3",
107111
"webpack": "^4.43.0",
108112
"webpack-assets-manifest": "^3.1.1",
109113
"webpack-bundle-analyzer": "^3.8.0",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { forwardRef } from 'react';
2+
import classNames from 'classnames';
3+
import styles from './style.module.css';
4+
5+
export type ButtonProps = JSX.IntrinsicElements['button'] & {
6+
square?: boolean;
7+
small?: boolean;
8+
}
9+
10+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
11+
({ children, square, small, className, ...rest }, ref) => {
12+
return (
13+
<button
14+
ref={ref}
15+
className={classNames(styles.button, { [styles.square]: square, [styles.small]: small }, className)}
16+
{...rest}
17+
>
18+
{children}
19+
</button>
20+
);
21+
},
22+
);
23+
24+
const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
25+
// @ts-expect-error
26+
<Button ref={ref} className={classNames(styles.text, className)} {...props} />
27+
));
28+
29+
const DangerButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
30+
// @ts-expect-error
31+
<Button ref={ref} className={classNames(styles.danger, className)} {...props} />
32+
));
33+
34+
const _Button = Object.assign(Button, { Text: TextButton, Danger: DangerButton });
35+
36+
export default _Button;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Button } from './Button';
2+
export { default as styles } from './style.module.css';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.button {
2+
@apply px-4 py-2 inline-flex items-center justify-center;
3+
@apply bg-blue-600 rounded text-base font-semibold text-blue-50 transition-all duration-100;
4+
@apply hover:bg-blue-500 active:bg-blue-500;
5+
6+
&.square {
7+
@apply p-2;
8+
}
9+
10+
&:focus {
11+
@apply ring-[3px] ring-blue-500 ring-offset-2 ring-offset-neutral-700;
12+
}
13+
14+
/* Sizing Controls */
15+
&.small {
16+
@apply px-3 py-1 font-normal focus:ring-2;
17+
18+
&.square {
19+
@apply p-1;
20+
}
21+
}
22+
}
23+
24+
.text {
25+
@apply bg-transparent focus:ring-neutral-300 focus:ring-opacity-50 hover:bg-neutral-500 active:bg-neutral-500;
26+
}
27+
28+
.danger {
29+
@apply bg-red-600 hover:bg-red-500 active:bg-red-500 focus:ring-red-500 text-red-50;
30+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { Fragment } from 'react';
2+
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
3+
import { Button } from '@/components/elements/button/index';
4+
import styles from './style.module.css';
5+
import { XIcon } from '@heroicons/react/solid';
6+
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
7+
import classNames from 'classnames';
8+
9+
interface Props {
10+
visible: boolean;
11+
onDismissed: () => void;
12+
title?: string;
13+
children?: React.ReactNode;
14+
}
15+
16+
interface DialogIconProps {
17+
type: 'danger' | 'info' | 'success' | 'warning';
18+
className?: string;
19+
}
20+
21+
const DialogIcon = ({ type, className }: DialogIconProps) => {
22+
const [ Component, styles ] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
23+
switch (type) {
24+
case 'danger':
25+
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
26+
case 'warning':
27+
return [ ExclamationIcon, 'bg-yellow-600 text-yellow-50' ];
28+
case 'success':
29+
return [ CheckIcon, 'bg-green-600 text-green-50' ];
30+
case 'info':
31+
return [ InformationCircleIcon, 'bg-primary-500 text-primary-50' ];
32+
}
33+
})();
34+
35+
return (
36+
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
37+
<Component className={'w-6 h-6'} />
38+
</div>
39+
);
40+
};
41+
42+
const DialogButtons = ({ children }: { children: React.ReactNode }) => (
43+
<>{children}</>
44+
);
45+
46+
const Dialog = ({ visible, title, onDismissed, children }: Props) => {
47+
const items = React.Children.toArray(children || []);
48+
const [ buttons, icon, content ] = [
49+
// @ts-expect-error
50+
items.find(child => child.type === DialogButtons),
51+
// @ts-expect-error
52+
items.find(child => child.type === DialogIcon),
53+
// @ts-expect-error
54+
items.filter(child => ![ DialogIcon, DialogButtons ].includes(child.type)),
55+
];
56+
57+
return (
58+
<Transition show={visible} as={Fragment}>
59+
<HeadlessDialog onClose={() => onDismissed()} className={styles.wrapper}>
60+
<div className={'flex items-center justify-center min-h-screen'}>
61+
<Transition.Child
62+
as={Fragment}
63+
enter={'ease-out duration-200'}
64+
enterFrom={'opacity-0'}
65+
enterTo={'opacity-100'}
66+
leave={'ease-in duration-100'}
67+
leaveFrom={'opacity-100'}
68+
leaveTo={'opacity-0'}
69+
>
70+
<HeadlessDialog.Overlay className={styles.overlay}/>
71+
</Transition.Child>
72+
<Transition.Child
73+
as={Fragment}
74+
enter={'ease-out duration-200'}
75+
enterFrom={'opacity-0 scale-95'}
76+
enterTo={'opacity-100 scale-100'}
77+
leave={'ease-in duration-100'}
78+
leaveFrom={'opacity-100 scale-100'}
79+
leaveTo={'opacity-0 scale-95'}
80+
>
81+
<div className={styles.container}>
82+
<div className={'flex p-6'}>
83+
{icon && <div className={'mr-4'}>{icon}</div>}
84+
<div className={'flex-1'}>
85+
{title &&
86+
<HeadlessDialog.Title className={styles.title}>
87+
{title}
88+
</HeadlessDialog.Title>
89+
}
90+
<HeadlessDialog.Description className={'pr-4'}>
91+
{content}
92+
</HeadlessDialog.Description>
93+
</div>
94+
</div>
95+
{buttons && <div className={styles.button_bar}>{buttons}</div>}
96+
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
97+
<div className={'absolute right-0 top-0 m-4'}>
98+
<Button.Text square small onClick={() => onDismissed()} className={'hover:rotate-90'}>
99+
<XIcon className={'w-5 h-5'}/>
100+
</Button.Text>
101+
</div>
102+
</div>
103+
</Transition.Child>
104+
</div>
105+
</HeadlessDialog>
106+
</Transition>
107+
);
108+
};
109+
110+
const _Dialog = Object.assign(Dialog, { Buttons: DialogButtons, Icon: DialogIcon });
111+
112+
export default _Dialog;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as Dialog } from './Dialog';
2+
export { default as styles } from './style.module.css';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.wrapper {
2+
@apply fixed z-10 inset-0 overflow-y-auto;
3+
}
4+
5+
.overlay {
6+
@apply fixed inset-0 bg-gray-900 opacity-50;
7+
}
8+
9+
.container {
10+
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg;
11+
@apply ring-4 ring-gray-800 ring-opacity-80;
12+
13+
& .title {
14+
@apply font-header text-xl font-medium mb-2 text-white pr-4;
15+
}
16+
17+
& > .button_bar {
18+
@apply px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b;
19+
}
20+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { ElementType, forwardRef, useMemo } from 'react';
2+
import { Menu, Transition } from '@headlessui/react';
3+
import styles from './style.module.css';
4+
import classNames from 'classnames';
5+
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
6+
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
7+
8+
interface Props {
9+
as?: ElementType;
10+
children: React.ReactNode;
11+
}
12+
13+
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
14+
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })}/>
15+
);
16+
17+
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
18+
type?: JSX.Element;
19+
}
20+
21+
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
22+
const [ Button, items ] = useMemo(() => {
23+
const list = React.Children.toArray(children) as unknown as TypedChild[];
24+
25+
return [
26+
list.filter(child => child.type === DropdownButton),
27+
list.filter(child => child.type !== DropdownButton),
28+
];
29+
}, [ children ]);
30+
31+
if (!Button) {
32+
throw new Error('Cannot mount <Dropdown /> component without a child <Dropdown.Button />.');
33+
}
34+
35+
return (
36+
<Menu as={as || 'div'} className={styles.menu} ref={ref}>
37+
{Button}
38+
<Transition
39+
enter={'transition duration-100 ease-out'}
40+
enterFrom={'transition scale-95 opacity-0'}
41+
enterTo={'transform scale-100 opacity-100'}
42+
leave={'transition duration-75 ease-out'}
43+
leaveFrom={'transform scale-100 opacity-100'}
44+
leaveTo={'transform scale-95 opacity-0'}
45+
>
46+
<Menu.Items className={classNames(styles.items_container, 'w-56')}>
47+
<div className={'px-1 py-1'}>
48+
{items}
49+
</div>
50+
</Menu.Items>
51+
</Transition>
52+
</Menu>
53+
);
54+
});
55+
56+
const _Dropdown = Object.assign(Dropdown, {
57+
Button: DropdownButton,
58+
Item: DropdownItem,
59+
Gap: DropdownGap,
60+
});
61+
62+
export { _Dropdown as default };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import classNames from 'classnames';
2+
import styles from '@/components/elements/dropdown/style.module.css';
3+
import { ChevronDownIcon } from '@heroicons/react/solid';
4+
import { Menu } from '@headlessui/react';
5+
import React from 'react';
6+
7+
interface Props {
8+
className?: string;
9+
animate?: boolean;
10+
children: React.ReactNode;
11+
}
12+
13+
export default ({ className, animate = true, children }: Props) => (
14+
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
15+
{typeof children === 'string' ?
16+
<>
17+
<span className={'mr-2'}>{children}</span>
18+
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()}/>
19+
</>
20+
:
21+
children
22+
}
23+
</Menu.Button>
24+
);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { forwardRef } from 'react';
2+
import { Menu } from '@headlessui/react';
3+
import styles from './style.module.css';
4+
import classNames from 'classnames';
5+
6+
interface Props {
7+
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
8+
danger?: boolean;
9+
disabled?: boolean;
10+
className?: string;
11+
icon?: JSX.Element;
12+
onClick?: (e: React.MouseEvent) => void;
13+
}
14+
15+
const DropdownItem = forwardRef<HTMLAnchorElement, Props>(({
16+
disabled,
17+
danger,
18+
className,
19+
onClick,
20+
children,
21+
icon: IconComponent,
22+
}, ref) => {
23+
return (
24+
<Menu.Item disabled={disabled}>
25+
{({ disabled, active }) => (
26+
<a
27+
ref={ref}
28+
href={'#'}
29+
className={classNames(styles.menu_item, {
30+
[styles.danger]: danger,
31+
[styles.disabled]: disabled,
32+
}, className)}
33+
onClick={onClick}
34+
>
35+
{IconComponent}
36+
{typeof children === 'function' ? children({ disabled, active }) : children}
37+
</a>
38+
)}
39+
</Menu.Item>
40+
);
41+
});
42+
43+
export default DropdownItem;

0 commit comments

Comments
 (0)