Merge branch 'dane/fiddle-with-new-tables' into v2
This commit is contained in:
commit
341fa45cbd
|
@ -42,10 +42,12 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@hot-loader/react-dom": "^16.14.0",
|
||||
"axios": "^0.21.4",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.3.1",
|
||||
"date-fns": "^2.25.0",
|
||||
"debounce": "^1.2.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import http from '@/api/http';
|
||||
import { User } from '@/api/admin/user';
|
||||
import { AdminTransformers } from '@/api/admin/transformers';
|
||||
import { Dropdown } from '@/components/elements/dropdown';
|
||||
import {
|
||||
BanIcon,
|
||||
DotsVerticalIcon,
|
||||
LockOpenIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
SupportIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import { Checkbox, InputField } from '@/components/elements/inputs';
|
||||
|
||||
const UsersContainerV2 = () => {
|
||||
const [ users, setUsers ] = useState<User[]>([]);
|
||||
useEffect(() => {
|
||||
document.title = 'Admin | Users';
|
||||
}, []);
|
||||
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
http.get('/api/application/users')
|
||||
.then(({ data }) => {
|
||||
setUsers(data.data.map(AdminTransformers.toUser));
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={'flex justify-end mb-4'}>
|
||||
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}>
|
||||
Add User <PlusIcon className={'ml-2 w-5 h-5'}/>
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog title={'Delete account'} visible={visible} onDismissed={() => setVisible(false)}>
|
||||
<Dialog.Icon type={'danger'}/>
|
||||
This account will be permanently deleted.
|
||||
<Dialog.Buttons>
|
||||
<Button.Text
|
||||
onClick={() => setVisible(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button.Text>
|
||||
<Button.Danger>Delete</Button.Danger>
|
||||
</Dialog.Buttons>
|
||||
</Dialog>
|
||||
<div className={'relative flex items-center rounded-t bg-neutral-700 px-4 py-2'}>
|
||||
<div className={'mr-6'}>
|
||||
<Checkbox />
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
<InputField type={'text'} name={'filter'} placeholder={'Begin typing to filter...'} className={'w-56 focus:w-96'} />
|
||||
</div>
|
||||
<div className={'absolute rounded-t bg-neutral-700 w-full h-full top-0 left-0 flex items-center justify-end space-x-4 px-4'}>
|
||||
<div className={'flex-1'}>
|
||||
<Checkbox indeterminate />
|
||||
</div>
|
||||
<Button.Text square>
|
||||
<SupportIcon className={'w-4 h-4'} />
|
||||
</Button.Text>
|
||||
<Button.Text square>
|
||||
<LockOpenIcon className={'w-4 h-4'} />
|
||||
</Button.Text>
|
||||
<Button.Text square>
|
||||
<TrashIcon className={'w-4 h-4'} />
|
||||
</Button.Text>
|
||||
</div>
|
||||
</div>
|
||||
<table className={'min-w-full rounded bg-neutral-700'}>
|
||||
<thead className={'bg-neutral-900'}>
|
||||
<tr>
|
||||
<th scope={'col'} className={'w-8'}/>
|
||||
<th scope={'col'} className={'text-left px-6 py-2 w-full'}>Email</th>
|
||||
<th scope={'col'}/>
|
||||
<th scope={'col'}/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map(user => (
|
||||
<tr key={user.uuid}>
|
||||
<td className={'whitespace-nowrap'}>
|
||||
<div className={'flex justify-end items-center w-8'}>
|
||||
<Checkbox/>
|
||||
</div>
|
||||
</td>
|
||||
<td className={'pl-6 py-4 whitespace-nowrap'}>
|
||||
<div className={'flex items-center'}>
|
||||
<div className={'w-10 h-10'}>
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
className={'w-10 h-10 rounded-full'}
|
||||
alt={'User avatar'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'ml-4'}>
|
||||
<p className={'font-medium'}>
|
||||
{user.email}
|
||||
</p>
|
||||
<p className={'text-sm text-neutral-400'}>
|
||||
{user.uuid}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className={'pl-2 py-4 whitespace-nowrap'}>
|
||||
{user.isUsingTwoFactor &&
|
||||
<span className={'bg-green-100 uppercase text-green-600 font-semibold text-xs px-2 py-0.5 rounded'}>
|
||||
2-FA Enabled
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td className={'px-6 py-4 whitespace-nowrap'}>
|
||||
<Dropdown>
|
||||
<Dropdown.Button className={'px-2'}>
|
||||
<DotsVerticalIcon/>
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Item icon={<PencilIcon/>}>Edit</Dropdown.Item>
|
||||
<Dropdown.Item icon={<SupportIcon/>}>Reset Password</Dropdown.Item>
|
||||
<Dropdown.Item icon={<LockOpenIcon/>} disabled={!user.isUsingTwoFactor}>
|
||||
Disable 2-FA
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={<BanIcon/>}>Suspend</Dropdown.Item>
|
||||
<Dropdown.Gap/>
|
||||
<Dropdown.Item icon={<TrashIcon/>} onClick={() => setVisible(true)} danger>Delete
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
</Dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersContainerV2;
|
|
@ -0,0 +1,36 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './button.module.css';
|
||||
|
||||
export type ButtonProps = JSX.IntrinsicElements['button'] & {
|
||||
square?: boolean;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, square, small, className, ...rest }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames(styles.button, { [styles.square]: square, [styles.small]: small }, className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
|
||||
// @ts-expect-error
|
||||
<Button ref={ref} className={classNames(styles.text, className)} {...props} />
|
||||
));
|
||||
|
||||
const DangerButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (
|
||||
// @ts-expect-error
|
||||
<Button ref={ref} className={classNames(styles.danger, className)} {...props} />
|
||||
));
|
||||
|
||||
const _Button = Object.assign(Button, { Text: TextButton, Danger: DangerButton });
|
||||
|
||||
export default _Button;
|
|
@ -0,0 +1,30 @@
|
|||
.button {
|
||||
@apply px-4 py-2 inline-flex items-center justify-center;
|
||||
@apply bg-blue-600 rounded text-base font-semibold text-blue-50 transition-all duration-100;
|
||||
@apply hover:bg-blue-500 active:bg-blue-500;
|
||||
|
||||
&.square {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply ring-[3px] ring-blue-500 ring-offset-2 ring-offset-neutral-700;
|
||||
}
|
||||
|
||||
/* Sizing Controls */
|
||||
&.small {
|
||||
@apply px-3 py-1 font-normal focus:ring-2;
|
||||
|
||||
&.square {
|
||||
@apply p-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
@apply bg-transparent focus:ring-neutral-300 focus:ring-opacity-50 hover:bg-neutral-500 active:bg-neutral-500;
|
||||
}
|
||||
|
||||
.danger {
|
||||
@apply bg-red-600 hover:bg-red-500 active:bg-red-500 focus:ring-red-500 text-red-50;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Button } from './Button';
|
||||
export { default as styles } from './button.module.css';
|
|
@ -0,0 +1,112 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import { Dialog as HeadlessDialog, Transition } from '@headlessui/react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import styles from './dialog.module.css';
|
||||
import { XIcon } from '@heroicons/react/solid';
|
||||
import { CheckIcon, ExclamationIcon, InformationCircleIcon, ShieldExclamationIcon } from '@heroicons/react/outline';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
onDismissed: () => void;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface DialogIconProps {
|
||||
type: 'danger' | 'info' | 'success' | 'warning';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DialogIcon = ({ type, className }: DialogIconProps) => {
|
||||
const [ Component, styles ] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
|
||||
switch (type) {
|
||||
case 'danger':
|
||||
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
|
||||
case 'warning':
|
||||
return [ ExclamationIcon, 'bg-yellow-600 text-yellow-50' ];
|
||||
case 'success':
|
||||
return [ CheckIcon, 'bg-green-600 text-green-50' ];
|
||||
case 'info':
|
||||
return [ InformationCircleIcon, 'bg-primary-500 text-primary-50' ];
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={classNames('flex items-center justify-center w-10 h-10 rounded-full', styles, className)}>
|
||||
<Component className={'w-6 h-6'} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DialogButtons = ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const Dialog = ({ visible, title, onDismissed, children }: Props) => {
|
||||
const items = React.Children.toArray(children || []);
|
||||
const [ buttons, icon, content ] = [
|
||||
// @ts-expect-error
|
||||
items.find(child => child.type === DialogButtons),
|
||||
// @ts-expect-error
|
||||
items.find(child => child.type === DialogIcon),
|
||||
// @ts-expect-error
|
||||
items.filter(child => ![ DialogIcon, DialogButtons ].includes(child.type)),
|
||||
];
|
||||
|
||||
return (
|
||||
<Transition show={visible} as={Fragment}>
|
||||
<HeadlessDialog onClose={() => onDismissed()} className={styles.wrapper}>
|
||||
<div className={'flex items-center justify-center min-h-screen'}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter={'ease-out duration-200'}
|
||||
enterFrom={'opacity-0'}
|
||||
enterTo={'opacity-100'}
|
||||
leave={'ease-in duration-100'}
|
||||
leaveFrom={'opacity-100'}
|
||||
leaveTo={'opacity-0'}
|
||||
>
|
||||
<HeadlessDialog.Overlay className={styles.overlay}/>
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter={'ease-out duration-200'}
|
||||
enterFrom={'opacity-0 scale-95'}
|
||||
enterTo={'opacity-100 scale-100'}
|
||||
leave={'ease-in duration-100'}
|
||||
leaveFrom={'opacity-100 scale-100'}
|
||||
leaveTo={'opacity-0 scale-95'}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={'flex p-6'}>
|
||||
{icon && <div className={'mr-4'}>{icon}</div>}
|
||||
<div className={'flex-1'}>
|
||||
{title &&
|
||||
<HeadlessDialog.Title className={styles.title}>
|
||||
{title}
|
||||
</HeadlessDialog.Title>
|
||||
}
|
||||
<HeadlessDialog.Description className={'pr-4'}>
|
||||
{content}
|
||||
</HeadlessDialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
{buttons && <div className={styles.button_bar}>{buttons}</div>}
|
||||
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
|
||||
<div className={'absolute right-0 top-0 m-4'}>
|
||||
<Button.Text square small onClick={() => onDismissed()} className={'hover:rotate-90'}>
|
||||
<XIcon className={'w-5 h-5'}/>
|
||||
</Button.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</HeadlessDialog>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
const _Dialog = Object.assign(Dialog, { Buttons: DialogButtons, Icon: DialogIcon });
|
||||
|
||||
export default _Dialog;
|
|
@ -0,0 +1,20 @@
|
|||
.wrapper {
|
||||
@apply fixed z-10 inset-0 overflow-y-auto;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@apply fixed inset-0 bg-gray-900 opacity-50;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply relative bg-gray-600 rounded max-w-xl w-full mx-auto shadow-lg;
|
||||
@apply ring-4 ring-gray-800 ring-opacity-80;
|
||||
|
||||
& .title {
|
||||
@apply font-header text-xl font-medium mb-2 text-white pr-4;
|
||||
}
|
||||
|
||||
& > .button_bar {
|
||||
@apply px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Dialog } from './Dialog';
|
||||
export { default as styles } from './dialog.module.css';
|
|
@ -0,0 +1,62 @@
|
|||
import React, { ElementType, forwardRef, useMemo } from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import styles from './dropdown.module.css';
|
||||
import classNames from 'classnames';
|
||||
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
|
||||
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
|
||||
|
||||
interface Props {
|
||||
as?: ElementType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
|
||||
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })}/>
|
||||
);
|
||||
|
||||
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
|
||||
type?: JSX.Element;
|
||||
}
|
||||
|
||||
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
|
||||
const [ Button, items ] = useMemo(() => {
|
||||
const list = React.Children.toArray(children) as unknown as TypedChild[];
|
||||
|
||||
return [
|
||||
list.filter(child => child.type === DropdownButton),
|
||||
list.filter(child => child.type !== DropdownButton),
|
||||
];
|
||||
}, [ children ]);
|
||||
|
||||
if (!Button) {
|
||||
throw new Error('Cannot mount <Dropdown /> component without a child <Dropdown.Button />.');
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu as={as || 'div'} className={styles.menu} ref={ref}>
|
||||
{Button}
|
||||
<Transition
|
||||
enter={'transition duration-100 ease-out'}
|
||||
enterFrom={'transition scale-95 opacity-0'}
|
||||
enterTo={'transform scale-100 opacity-100'}
|
||||
leave={'transition duration-75 ease-out'}
|
||||
leaveFrom={'transform scale-100 opacity-100'}
|
||||
leaveTo={'transform scale-95 opacity-0'}
|
||||
>
|
||||
<Menu.Items className={classNames(styles.items_container, 'w-56')}>
|
||||
<div className={'px-1 py-1'}>
|
||||
{items}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
|
||||
const _Dropdown = Object.assign(Dropdown, {
|
||||
Button: DropdownButton,
|
||||
Item: DropdownItem,
|
||||
Gap: DropdownGap,
|
||||
});
|
||||
|
||||
export { _Dropdown as default };
|
|
@ -0,0 +1,24 @@
|
|||
import classNames from 'classnames';
|
||||
import styles from '@/components/elements/dropdown/dropdown.module.css';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default ({ className, animate = true, children }: Props) => (
|
||||
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
|
||||
{typeof children === 'string' ?
|
||||
<>
|
||||
<span className={'mr-2'}>{children}</span>
|
||||
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()}/>
|
||||
</>
|
||||
:
|
||||
children
|
||||
}
|
||||
</Menu.Button>
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import styles from './dropdown.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
icon?: JSX.Element;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const DropdownItem = forwardRef<HTMLAnchorElement, Props>(({
|
||||
disabled,
|
||||
danger,
|
||||
className,
|
||||
onClick,
|
||||
children,
|
||||
icon: IconComponent,
|
||||
}, ref) => {
|
||||
return (
|
||||
<Menu.Item disabled={disabled}>
|
||||
{({ disabled, active }) => (
|
||||
<a
|
||||
ref={ref}
|
||||
href={'#'}
|
||||
className={classNames(styles.menu_item, {
|
||||
[styles.danger]: danger,
|
||||
[styles.disabled]: disabled,
|
||||
}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{IconComponent}
|
||||
{typeof children === 'function' ? children({ disabled, active }) : children}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
export default DropdownItem;
|
|
@ -0,0 +1,58 @@
|
|||
.menu {
|
||||
@apply relative inline-block text-left;
|
||||
|
||||
& .button {
|
||||
@apply inline-flex justify-center items-center w-full py-2 text-neutral-100 rounded-md;
|
||||
@apply transition-all duration-100;
|
||||
|
||||
&:hover, &[aria-expanded="true"] {
|
||||
@apply bg-neutral-600 text-white;
|
||||
}
|
||||
|
||||
&:focus, &:focus-within, &:active {
|
||||
@apply ring-2 ring-opacity-50 ring-neutral-300 text-white;
|
||||
}
|
||||
|
||||
& svg {
|
||||
@apply w-5 h-5 transition-transform duration-75;
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] svg[data-animated="true"] {
|
||||
@apply rotate-180;
|
||||
}
|
||||
}
|
||||
|
||||
& .items_container {
|
||||
@apply absolute right-0 mt-2 origin-top-right bg-neutral-900 rounded z-10;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_item {
|
||||
@apply flex items-center rounded w-full px-2 py-2;
|
||||
|
||||
& svg {
|
||||
@apply w-4 h-4 mr-4 text-neutral-300;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
@apply bg-blue-500 text-blue-50;
|
||||
|
||||
& svg {
|
||||
@apply text-blue-50;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
&:hover, &:focus {
|
||||
@apply bg-red-500 text-red-50;
|
||||
|
||||
& svg {
|
||||
@apply text-red-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-not-allowed hover:bg-neutral-800 opacity-30 focus:bg-transparent focus:hover:bg-neutral-800;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Dropdown } from './Dropdown';
|
||||
export * as styles from './dropdown.module.css';
|
|
@ -0,0 +1,19 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import styles from './inputs.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = Omit<React.ComponentProps<'input'>, 'type'> & {
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLInputElement, Props>(({ className, indeterminate, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type={'checkbox'}
|
||||
className={classNames('form-checkbox', {
|
||||
[styles.checkbox]: true,
|
||||
[styles.indeterminate]: indeterminate,
|
||||
}, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,11 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './inputs.module.css';
|
||||
|
||||
export default forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(({ className, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className={classNames('form-input', styles.text_input, className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,3 @@
|
|||
export { default as Checkbox } from './Checkbox';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as styles } from './inputs.module.css';
|
|
@ -0,0 +1,21 @@
|
|||
.checkbox {
|
||||
@apply w-4 h-4 rounded-sm border-neutral-500 bg-neutral-600 text-primary-500;
|
||||
|
||||
&:focus, &:active {
|
||||
@apply ring-2 ring-primary-500 ring-offset-2 ring-offset-neutral-700;
|
||||
}
|
||||
|
||||
&.indeterminate:checked {
|
||||
@apply text-primary-500/50 border border-primary-500;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='white'%3E%3Cpath fill-rule='evenodd' d='M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z' clip-rule='evenodd' /%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
.text_input {
|
||||
@apply transition-all duration-75;
|
||||
@apply bg-neutral-800 border-neutral-600 rounded px-4 py-2 outline-none;
|
||||
|
||||
&:focus {
|
||||
@apply border-blue-600 ring-2 ring-blue-500;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
declare module '*.jpg';
|
||||
declare module '*.png';
|
||||
declare module '*.svg';
|
||||
declare module '*.css';
|
||||
|
|
|
@ -15,7 +15,6 @@ import LocationEditContainer from '@/components/admin/locations/LocationEditCont
|
|||
import ServersContainer from '@/components/admin/servers/ServersContainer';
|
||||
import NewServerContainer from '@/components/admin/servers/NewServerContainer';
|
||||
import ServerRouter from '@/components/admin/servers/ServerRouter';
|
||||
import UsersContainer from '@/components/admin/users/UsersContainer';
|
||||
import NewUserContainer from '@/components/admin/users/NewUserContainer';
|
||||
import UserRouter from '@/components/admin/users/UserRouter';
|
||||
import RolesContainer from '@/components/admin/roles/RolesContainer';
|
||||
|
@ -46,6 +45,7 @@ import {
|
|||
import CollapsedIcon from '@/assets/images/pterodactyl.svg';
|
||||
import Sidebar from '@/components/admin/Sidebar';
|
||||
import useUserPersistedState from '@/plugins/useUserPersistedState';
|
||||
import UsersContainerV2 from '@/components/admin/users/UsersContainerV2';
|
||||
|
||||
const AdminRouter = ({ location, match }: RouteComponentProps) => {
|
||||
const email = useStoreState((state: State<ApplicationStore>) => state.user.data!.email);
|
||||
|
@ -132,7 +132,7 @@ const AdminRouter = ({ location, match }: RouteComponentProps) => {
|
|||
<Route path={`${match.path}/servers`} component={ServersContainer} exact/>
|
||||
<Route path={`${match.path}/servers/new`} component={NewServerContainer} exact/>
|
||||
<Route path={`${match.path}/servers/:id`} component={ServerRouter}/>
|
||||
<Route path={`${match.path}/users`} component={UsersContainer} exact/>
|
||||
<Route path={`${match.path}/users`} component={UsersContainerV2} exact/>
|
||||
<Route path={`${match.path}/users/new`} component={NewUserContainer} exact/>
|
||||
<Route path={`${match.path}/users/:id`} component={UserRouter}/>
|
||||
<Route path={`${match.path}/roles`} component={RolesContainer} exact/>
|
||||
|
|
|
@ -5,38 +5,16 @@ module.exports = {
|
|||
'./resources/scripts/**/*.{js,ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: [ 'Rubik', '-apple-system', 'BlinkMacSystemFont', '"Helvetica Neue"', '"Roboto"', 'system-ui', 'sans-serif' ],
|
||||
header: [ '"IBM Plex Sans"', '"Roboto"', 'system-ui', 'sans-serif' ],
|
||||
mono: [ '"IBM Plex Mono"', '"Source Code Pro"', 'SourceCodePro', 'Menlo', 'Monaco', 'Consolas', 'monospace' ],
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
header: [ '"IBM Plex Sans"', '"Roboto"', 'system-ui', 'sans-serif' ],
|
||||
},
|
||||
colors: {
|
||||
black: '#131a20',
|
||||
primary: {
|
||||
50: '#e6f6ff',
|
||||
100: '#b8e2ff',
|
||||
200: '#7ac3fa',
|
||||
300: '#49a4f3',
|
||||
400: '#2487eb',
|
||||
500: '#0967d3',
|
||||
600: '#0550b3',
|
||||
700: '#0345a0',
|
||||
800: '#01337e',
|
||||
900: '#002057',
|
||||
},
|
||||
neutral: {
|
||||
50: '#f5f7fa',
|
||||
100: '#e5e8eb',
|
||||
200: '#cad1d8',
|
||||
300: '#9aa5b1',
|
||||
400: '#7b8793',
|
||||
500: '#606d7b',
|
||||
600: '#515f6c',
|
||||
700: '#3f4d5a',
|
||||
800: '#33404d',
|
||||
900: '#1f2933',
|
||||
},
|
||||
// Deprecated, prefer "blue"...
|
||||
primary: colors.blue,
|
||||
// Deprecate, prefer "gray"...
|
||||
neutral: colors.gray,
|
||||
cyan: colors.cyan,
|
||||
},
|
||||
fontSize: {
|
||||
|
@ -51,6 +29,8 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/forms')({
|
||||
strategy: 'class',
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
|
|
@ -36,6 +36,11 @@ module.exports = {
|
|||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: {
|
||||
auto: true,
|
||||
localIdentName: isProduction ? '[name]_[hash:base64:8]' : '[path][name]__[local]',
|
||||
localIdentContext: path.join(__dirname, "resources/scripts/components"),
|
||||
},
|
||||
sourceMap: !isProduction,
|
||||
importLoaders: 1,
|
||||
},
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -2474,6 +2474,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@headlessui/react@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@headlessui/react@npm:1.5.0"
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
checksum: e3373dfb73936950d659a87718fb9fdedc599b344b30cb0d8b1ef0a2ec13d6f405c653fa51284236788658840ece82c6a8538a5a5931e595274e9590b2020079
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@heroicons/react@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "@heroicons/react@npm:1.0.5"
|
||||
|
@ -4640,6 +4650,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"classnames@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "classnames@npm:2.3.1"
|
||||
checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"clean-set@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "clean-set@npm:1.1.2"
|
||||
|
@ -10933,6 +10950,7 @@ fsevents@^1.2.7:
|
|||
"@fortawesome/free-regular-svg-icons": ^5.15.4
|
||||
"@fortawesome/free-solid-svg-icons": ^5.15.4
|
||||
"@fortawesome/react-fontawesome": ^0.1.16
|
||||
"@headlessui/react": ^1.5.0
|
||||
"@heroicons/react": ^1.0.5
|
||||
"@hot-loader/react-dom": ^16.14.0
|
||||
"@tailwindcss/forms": ^0.4.0
|
||||
|
@ -10965,6 +10983,7 @@ fsevents@^1.2.7:
|
|||
babel-plugin-styled-components: ^2.0.3
|
||||
browserslist: ^4.17.6
|
||||
chart.js: ^2.9.4
|
||||
classnames: ^2.3.1
|
||||
cross-env: ^7.0.3
|
||||
css-loader: ^5.2.7
|
||||
date-fns: ^2.25.0
|
||||
|
|
Loading…
Reference in New Issue