Apply new eslint rules; default to prettier for styling

This commit is contained in:
DaneEveritt 2022-06-26 15:13:52 -04:00
parent f22cce8881
commit dc84af9937
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
218 changed files with 3876 additions and 3564 deletions

View file

@ -3,14 +3,14 @@ import { Redirect, Route, RouteProps } from 'react-router';
import { useStoreState } from '@/state/hooks';
export default ({ children, ...props }: Omit<RouteProps, 'render'>) => {
const isAuthenticated = useStoreState(state => !!state.user.data?.uuid);
const isAuthenticated = useStoreState((state) => !!state.user.data?.uuid);
return (
<Route
{...props}
render={({ location }) => (
isAuthenticated ? children : <Redirect to={{ pathname: '/auth/login', state: { from: location } }}/>
)}
render={({ location }) =>
isAuthenticated ? children : <Redirect to={{ pathname: '/auth/login', state: { from: location } }} />
}
/>
);
};

View file

@ -12,88 +12,103 @@ interface Props {
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
${props => ((!props.isSecondary && !props.color) || props.color === 'primary') && css<Props>`
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
&:hover:not(:disabled) {
${tw`bg-primary-600 border-primary-700`};
}
`};
${props => props.color === 'grey' && css`
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
&:hover:not(:disabled) {
${tw`bg-neutral-600 border-neutral-700`};
}
`};
${props => props.color === 'green' && css<Props>`
${tw`border-green-600 bg-green-500 text-green-50`};
&:hover:not(:disabled) {
${tw`bg-green-600 border-green-700`};
}
${props => props.isSecondary && css`
&:active:not(:disabled) {
${(props) =>
((!props.isSecondary && !props.color) || props.color === 'primary') &&
css<Props>`
${(props) => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
&:hover:not(:disabled) {
${tw`bg-primary-600 border-primary-700`};
}
`};
${(props) =>
props.color === 'grey' &&
css`
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
&:hover:not(:disabled) {
${tw`bg-neutral-600 border-neutral-700`};
}
`};
${(props) =>
props.color === 'green' &&
css<Props>`
${tw`border-green-600 bg-green-500 text-green-50`};
&:hover:not(:disabled) {
${tw`bg-green-600 border-green-700`};
}
${(props) =>
props.isSecondary &&
css`
&:active:not(:disabled) {
${tw`bg-green-600 border-green-700`};
}
`};
`};
`};
${props => props.color === 'red' && css<Props>`
${tw`border-red-600 bg-red-500 text-red-50`};
&:hover:not(:disabled) {
${tw`bg-red-600 border-red-700`};
}
${props => props.isSecondary && css`
&:active:not(:disabled) {
${(props) =>
props.color === 'red' &&
css<Props>`
${tw`border-red-600 bg-red-500 text-red-50`};
&:hover:not(:disabled) {
${tw`bg-red-600 border-red-700`};
}
${(props) =>
props.isSecondary &&
css`
&:active:not(:disabled) {
${tw`bg-red-600 border-red-700`};
}
`};
`};
`};
${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
${props => props.size === 'large' && tw`p-4 text-sm`};
${props => props.size === 'xlarge' && tw`p-4 w-full`};
${props => props.isSecondary && css<Props>`
${tw`border-neutral-600 bg-transparent text-neutral-200`};
&:hover:not(:disabled) {
${tw`border-neutral-500 text-neutral-100`};
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
${props => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
}
`};
&:disabled { opacity: 0.55; cursor: default }
${(props) => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
${(props) => (!props.size || props.size === 'small') && tw`px-4 py-2`};
${(props) => props.size === 'large' && tw`p-4 text-sm`};
${(props) => props.size === 'xlarge' && tw`p-4 w-full`};
${(props) =>
props.isSecondary &&
css<Props>`
${tw`border-neutral-600 bg-transparent text-neutral-200`};
&:hover:not(:disabled) {
${tw`border-neutral-500 text-neutral-100`};
${(props) => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
${(props) => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
${(props) => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
}
`};
&:disabled {
opacity: 0.55;
cursor: default;
}
`;
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) => (
<ButtonStyle {...props}>
{isLoading &&
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
<Spinner size={'small'}/>
</div>
}
<span css={isLoading ? tw`text-transparent` : undefined}>
{children}
</span>
{isLoading && (
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
<Spinner size={'small'} />
</div>
)}
<span css={isLoading ? tw`text-transparent` : undefined}>{children}</span>
</ButtonStyle>
);
type LinkProps = Omit<JSX.IntrinsicElements['a'], 'ref' | keyof Props> & Props;
const LinkButton: React.FC<LinkProps> = props => <ButtonStyle as={'a'} {...props}/>;
const LinkButton: React.FC<LinkProps> = (props) => <ButtonStyle as={'a'} {...props} />;
export { LinkButton, ButtonStyle };
export default Button;

View file

@ -14,12 +14,9 @@ const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
return (
<>
{
((matchAny && can.filter(p => p).length > 0) || (!matchAny && can.every(p => p))) ?
children
:
renderOnError
}
{(matchAny && can.filter((p) => p).length > 0) || (!matchAny && can.every((p) => p))
? children
: renderOnError}
</>
);
};

View file

@ -29,7 +29,7 @@ const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => (
type={'checkbox'}
checked={(field.value || []).includes(value)}
onClick={() => form.setFieldTouched(field.name, true)}
onChange={e => {
onChange={(e) => {
const set = new Set(field.value);
set.has(value) ? set.delete(value) : set.add(value);

View file

@ -98,7 +98,7 @@ const EditorContainer = styled.div`
}
.CodeMirror-foldmarker {
color: #CBCCC6;
color: #cbccc6;
text-shadow: none;
margin-left: 0.25rem;
margin-right: 0.25rem;
@ -144,7 +144,7 @@ const findModeByFilename = (filename: string) => {
};
export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => {
const [ editor, setEditor ] = useState<CodeMirror.Editor>();
const [editor, setEditor] = useState<CodeMirror.Editor>();
const ref = useCallback((node) => {
if (!node) return;
@ -173,7 +173,7 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
// @ts-ignore
autoCloseBrackets: true,
matchBrackets: true,
gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
});
setEditor(e);
@ -185,15 +185,15 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
}
onModeChanged(findModeByFilename(filename)?.mime || 'text/plain');
}, [ filename ]);
}, [filename]);
useEffect(() => {
editor && editor.setOption('mode', mode);
}, [ editor, mode ]);
}, [editor, mode]);
useEffect(() => {
editor && editor.setValue(initialContent || '');
}, [ editor, initialContent ]);
}, [editor, initialContent]);
useEffect(() => {
if (!editor) {
@ -207,11 +207,11 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
});
fetchContent(() => Promise.resolve(editor.getValue()));
}, [ editor, fetchContent, onContentSaved ]);
}, [editor, fetchContent, onContentSaved]);
return (
<EditorContainer style={style}>
<textarea ref={ref}/>
<textarea ref={ref} />
</EditorContainer>
);
};

View file

@ -17,9 +17,7 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
return (
<>
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
<div css={tw`text-neutral-300`}>
{children}
</div>
<div css={tw`text-neutral-300`}>{children}</div>
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
Cancel
@ -34,6 +32,6 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
ConfirmationModal.displayName = 'ConfirmationModal';
export default asModal<Props>(props => ({
export default asModal<Props>((props) => ({
showSpinnerOverlay: props.showSpinnerOverlay,
}))(ConfirmationModal);

View file

@ -3,29 +3,23 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import tw from 'twin.macro';
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
title?: string;
borderColor?: string;
showFlashes?: string | boolean;
showLoadingOverlay?: boolean;
}>;
type Props = Readonly<
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
title?: string;
borderColor?: string;
showFlashes?: string | boolean;
showLoadingOverlay?: boolean;
}
>;
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
<div {...props}>
{title && <h2 css={tw`text-neutral-300 mb-4 px-4 text-2xl`}>{title}</h2>}
{showFlashes &&
<FlashMessageRender
byKey={typeof showFlashes === 'string' ? showFlashes : undefined}
css={tw`mb-4`}
/>
}
<div
css={[
tw`bg-neutral-700 p-4 rounded shadow-lg relative`,
!!borderColor && tw`border-t-4`,
]}
>
<SpinnerOverlay visible={showLoadingOverlay || false}/>
{showFlashes && (
<FlashMessageRender byKey={typeof showFlashes === 'string' ? showFlashes : undefined} css={tw`mb-4`} />
)}
<div css={[tw`bg-neutral-700 p-4 rounded shadow-lg relative`, !!borderColor && tw`border-t-4`]}>
<SpinnerOverlay visible={showLoadingOverlay || false} />
{children}
</div>
</div>

View file

@ -20,7 +20,7 @@ const Toast = styled.div`
`;
const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
const [ copied, setCopied ] = useState(false);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (!copied) return;
@ -32,7 +32,7 @@ const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
return () => {
clearTimeout(timeout);
};
}, [ copied ]);
}, [copied]);
const onCopy = useCallback(() => {
setCopied(true);
@ -42,15 +42,15 @@ const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
<>
<SwitchTransition>
<Fade timeout={250} key={copied ? 'visible' : 'invisible'}>
{copied ?
{copied ? (
<Toast>
<div>
<p>Copied &quot;{text}&quot; to clipboard.</p>
</div>
</Toast>
:
) : (
<></>
}
)}
</Fade>
</SwitchTransition>
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }} css={tw`cursor-pointer`}>

View file

@ -13,7 +13,7 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
transition: 150ms all ease;
&:hover {
${props => props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`};
${(props) => (props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`)};
}
`;
@ -30,11 +30,11 @@ class DropdownMenu extends React.PureComponent<Props, State> {
visible: false,
};
componentWillUnmount () {
componentWillUnmount() {
this.removeListeners();
}
componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
const menu = this.menu.current;
if (this.state.visible && !prevState.visible && menu) {
@ -76,19 +76,20 @@ class DropdownMenu extends React.PureComponent<Props, State> {
}
};
triggerMenu = (posX: number) => this.setState(s => ({
posX: !s.visible ? posX : s.posX,
visible: !s.visible,
}));
triggerMenu = (posX: number) =>
this.setState((s) => ({
posX: !s.visible ? posX : s.posX,
visible: !s.visible,
}));
render () {
render() {
return (
<div>
{this.props.renderToggle(this.onClickHandler)}
<Fade timeout={150} in={this.state.visible} unmountOnExit>
<div
ref={this.menu}
onClick={e => {
onClick={(e) => {
e.stopPropagation();
this.setState({ visible: false });
}}

View file

@ -13,26 +13,27 @@ class ErrorBoundary extends React.Component<{}, State> {
hasError: false,
};
static getDerivedStateFromError () {
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch (error: Error) {
componentDidCatch(error: Error) {
console.error(error);
}
render () {
return this.state.hasError ?
render() {
return this.state.hasError ? (
<div css={tw`flex items-center justify-center w-full my-4`}>
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`} />
<p css={tw`text-sm text-neutral-100`}>
An error was encountered by the application while rendering this view. Try refreshing the page.
</p>
</div>
</div>
:
this.props.children;
) : (
this.props.children
);
}
}

View file

@ -8,25 +8,29 @@ interface Props extends Omit<CSSTransitionProps, 'timeout' | 'classNames'> {
}
const Container = styled.div<{ timeout: number }>`
.fade-enter, .fade-exit, .fade-appear {
.fade-enter,
.fade-exit,
.fade-appear {
will-change: opacity;
}
.fade-enter, .fade-appear {
.fade-enter,
.fade-appear {
${tw`opacity-0`};
&.fade-enter-active, &.fade-appear-active {
&.fade-enter-active,
&.fade-appear-active {
${tw`opacity-100 transition-opacity ease-in`};
transition-duration: ${props => props.timeout}ms;
transition-duration: ${(props) => props.timeout}ms;
}
}
.fade-exit {
${tw`opacity-100`};
&.fade-exit-active {
${tw`opacity-0 transition-opacity ease-in`};
transition-duration: ${props => props.timeout}ms;
transition-duration: ${(props) => props.timeout}ms;
}
}
`;

View file

@ -13,14 +13,16 @@ interface OwnProps {
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, ...props }, ref) => (
<FormikField innerRef={ref} name={name} validate={validate}>
{
({ field, form: { errors, touched } }: FieldProps) => (
const Field = forwardRef<HTMLInputElement, Props>(
({ id, name, light = false, label, description, validate, ...props }, ref) => (
<FormikField innerRef={ref} name={name} validate={validate}>
{({ field, form: { errors, touched } }: FieldProps) => (
<div>
{label &&
<Label htmlFor={id} isLight={light}>{label}</Label>
}
{label && (
<Label htmlFor={id} isLight={light}>
{label}
</Label>
)}
<Input
id={id}
{...field}
@ -28,18 +30,19 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
isLight={light}
hasError={!!(touched[field.name] && errors[field.name])}
/>
{touched[field.name] && errors[field.name] ?
{touched[field.name] && errors[field.name] ? (
<p className={'input-help error'}>
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
{(errors[field.name] as string).charAt(0).toUpperCase() +
(errors[field.name] as string).slice(1)}
</p>
:
description ? <p className={'input-help'}>{description}</p> : null
}
) : description ? (
<p className={'input-help'}>{description}</p>
) : null}
</div>
)
}
</FormikField>
));
)}
</FormikField>
)
);
Field.displayName = 'Field';
export default Field;

View file

@ -15,17 +15,15 @@ interface Props {
const FormikFieldWrapper = ({ id, name, label, className, description, validate, children }: Props) => (
<Field name={name} validate={validate}>
{
({ field, form: { errors, touched } }: FieldProps) => (
<div className={`${className} ${(touched[field.name] && errors[field.name]) ? 'has-error' : undefined}`}>
{label && <Label htmlFor={id}>{label}</Label>}
{children}
<InputError errors={errors} touched={touched} name={field.name}>
{description || null}
</InputError>
</div>
)
}
{({ field, form: { errors, touched } }: FieldProps) => (
<div className={`${className} ${touched[field.name] && errors[field.name] ? 'has-error' : undefined}`}>
{label && <Label htmlFor={id}>{label}</Label>}
{children}
<InputError errors={errors} touched={touched} name={field.name}>
{description || null}
</InputError>
</div>
)}
</Field>
);

View file

@ -3,8 +3,8 @@ import tw from 'twin.macro';
export default styled.div<{ $hoverable?: boolean }>`
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
${(props) => props.$hoverable !== false && tw`hover:border-neutral-500`};
& .icon {
${tw`rounded-full bg-neutral-500 p-3`};

View file

@ -9,9 +9,9 @@ interface Props {
}
const Icon = ({ icon, className, style }: Props) => {
let [ width, height, , , paths ] = icon.icon;
let [width, height, , , paths] = icon.icon;
paths = Array.isArray(paths) ? paths : [ paths ];
paths = Array.isArray(paths) ? paths : [paths];
return (
<svg
@ -22,7 +22,7 @@ const Icon = ({ icon, className, style }: Props) => {
style={style}
>
{paths.map((path, index) => (
<path key={`svg_path_${index}`} d={path}/>
<path key={`svg_path_${index}`} d={path} />
))}
</svg>
);

View file

@ -7,9 +7,11 @@ export interface Props {
}
const light = css<Props>`
${tw`bg-white border-neutral-200 text-neutral-800`};
&:focus { ${tw`border-primary-400`} }
${tw`bg-white border-neutral-200 text-neutral-800`};
&:focus {
${tw`border-primary-400`}
}
&:disabled {
${tw`bg-neutral-100 border-neutral-200`};
}
@ -40,43 +42,47 @@ const inputStyle = css<Props>`
${tw`appearance-none outline-none w-full min-w-0`};
${tw`p-3 border-2 rounded text-sm transition-all duration-150`};
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none focus:ring-0`};
& + .input-help {
${tw`mt-1 text-xs`};
${props => props.hasError ? tw`text-red-200` : tw`text-neutral-200`};
${(props) => (props.hasError ? tw`text-red-200` : tw`text-neutral-200`)};
}
&:required, &:invalid {
&:required,
&:invalid {
${tw`shadow-none`};
}
&:not(:disabled):not(:read-only):focus {
${tw`shadow-md border-primary-300 ring-2 ring-primary-400 ring-opacity-50`};
${props => props.hasError && tw`border-red-300 ring-red-200`};
${(props) => props.hasError && tw`border-red-300 ring-red-200`};
}
&:disabled {
${tw`opacity-75`};
}
${props => props.isLight && light};
${props => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
${(props) => props.isLight && light};
${(props) => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
`;
const Input = styled.input<Props>`
&:not([type="checkbox"]):not([type="radio"]) {
&:not([type='checkbox']):not([type='radio']) {
${inputStyle};
}
&[type="checkbox"], &[type="radio"] {
&[type='checkbox'],
&[type='radio'] {
${checkboxStyle};
&[type="radio"] {
&[type='radio'] {
${tw`rounded-full`};
}
}
`;
const Textarea = styled.textarea<Props>`${inputStyle}`;
const Textarea = styled.textarea<Props>`
${inputStyle}
`;
export { Textarea };
export default Input;

View file

@ -10,19 +10,15 @@ interface Props {
children?: string | number | null | undefined;
}
const InputError = ({ errors, touched, name, children }: Props) => (
touched[name] && errors[name] ?
const InputError = ({ errors, touched, name, children }: Props) =>
touched[name] && errors[name] ? (
<p css={tw`text-xs text-red-400 pt-2`}>
{typeof errors[name] === 'string' ?
capitalize(errors[name] as string)
:
capitalize((errors[name] as unknown as string[])[0])
}
{typeof errors[name] === 'string'
? capitalize(errors[name] as string)
: capitalize((errors[name] as unknown as string[])[0])}
</p>
:
<>
{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}
</>
);
) : (
<>{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}</>
);
export default InputError;

View file

@ -7,19 +7,21 @@ import Select from '@/components/elements/Select';
const Container = styled.div<{ visible?: boolean }>`
${tw`relative`};
${props => props.visible && css`
& ${Select} {
background-image: none;
}
`};
${(props) =>
props.visible &&
css`
& ${Select} {
background-image: none;
}
`};
`;
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
const InputSpinner = ({ visible, children }: { visible: boolean; children: React.ReactNode }) => (
<Container visible={visible}>
<Fade appear unmountOnExit in={visible} timeout={150}>
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
<Spinner size={'small'}/>
<Spinner size={'small'} />
</div>
</Fade>
{children}

View file

@ -3,7 +3,7 @@ import tw from 'twin.macro';
const Label = styled.label<{ isLight?: boolean }>`
${tw`block text-xs uppercase text-neutral-200 mb-1 sm:mb-2`};
${props => props.isLight && tw`text-neutral-700`};
${(props) => props.isLight && tw`text-neutral-700`};
`;
export default Label;

View file

@ -22,41 +22,55 @@ export interface ModalProps extends RequiredModalProps {
export const ModalMask = styled.div`
${tw`fixed z-50 overflow-auto flex w-full inset-0`};
background: rgba(0, 0, 0, 0.70);
background: rgba(0, 0, 0, 0.7);
`;
const ModalContainer = styled.div<{ alignTop?: boolean }>`
const ModalContainer = styled.div<{ alignTop?: boolean }>`
max-width: 95%;
max-height: calc(100vh - 8rem);
${breakpoint('md')`max-width: 75%`};
${breakpoint('lg')`max-width: 50%`};
${tw`relative flex flex-col w-full m-auto`};
${props => props.alignTop && css`
margin-top: 20%;
${breakpoint('md')`margin-top: 10%`};
`};
${(props) =>
props.alignTop &&
css`
margin-top: 20%;
${breakpoint('md')`margin-top: 10%`};
`};
margin-bottom: auto;
& > .close-icon {
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
top: -2.5rem;
&:hover {${tw`transform rotate-90`}}
&:hover {
${tw`transform rotate-90`}
}
& > svg {
${tw`w-6 h-6`};
}
}
`;
const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => {
const [ render, setRender ] = useState(visible);
const Modal: React.FC<ModalProps> = ({
visible,
appear,
dismissable,
showSpinnerOverlay,
top = true,
closeOnBackground = true,
closeOnEscape = true,
onDismissed,
children,
}) => {
const [render, setRender] = useState(visible);
const isDismissable = useMemo(() => {
return (dismissable || true) && !(showSpinnerOverlay || false);
}, [ dismissable, showSpinnerOverlay ]);
}, [dismissable, showSpinnerOverlay]);
useEffect(() => {
if (!isDismissable || !closeOnEscape) return;
@ -69,22 +83,16 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
return () => {
window.removeEventListener('keydown', handler);
};
}, [ isDismissable, closeOnEscape, render ]);
}, [isDismissable, closeOnEscape, render]);
useEffect(() => setRender(visible), [ visible ]);
useEffect(() => setRender(visible), [visible]);
return (
<Fade
in={render}
timeout={150}
appear={appear || true}
unmountOnExit
onExited={() => onDismissed()}
>
<Fade in={render} timeout={150} appear={appear || true} unmountOnExit onExited={() => onDismissed()}>
<ModalMask
onClick={e => e.stopPropagation()}
onContextMenu={e => e.stopPropagation()}
onMouseDown={e => {
onClick={(e) => e.stopPropagation()}
onContextMenu={(e) => e.stopPropagation()}
onMouseDown={(e) => {
if (isDismissable && closeOnBackground) {
e.stopPropagation();
if (e.target === e.currentTarget) {
@ -94,29 +102,36 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
}}
>
<ModalContainer alignTop={top}>
{isDismissable &&
<div className={'close-icon'} onClick={() => setRender(false)}>
<svg xmlns={'http://www.w3.org/2000/svg'} fill={'none'} viewBox={'0 0 24 24'} stroke={'currentColor'}>
<path
strokeLinecap={'round'}
strokeLinejoin={'round'}
strokeWidth={'2'}
d={'M6 18L18 6M6 6l12 12'}
/>
</svg>
</div>
}
{showSpinnerOverlay &&
<Fade timeout={150} appear in>
<div
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
>
<Spinner/>
{isDismissable && (
<div className={'close-icon'} onClick={() => setRender(false)}>
<svg
xmlns={'http://www.w3.org/2000/svg'}
fill={'none'}
viewBox={'0 0 24 24'}
stroke={'currentColor'}
>
<path
strokeLinecap={'round'}
strokeLinejoin={'round'}
strokeWidth={'2'}
d={'M6 18L18 6M6 6l12 12'}
/>
</svg>
</div>
</Fade>
}
<div css={tw`bg-neutral-800 p-3 sm:p-4 md:p-6 rounded shadow-md overflow-y-scroll transition-all duration-150`}>
)}
{showSpinnerOverlay && (
<Fade timeout={150} appear in>
<div
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
>
<Spinner />
</div>
</Fade>
)}
<div
css={tw`bg-neutral-800 p-3 sm:p-4 md:p-6 rounded shadow-md overflow-y-scroll transition-all duration-150`}
>
{children}
</div>
</ModalContainer>

View file

@ -15,15 +15,13 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
if (title) {
document.title = title;
}
}, [ title ]);
}, [title]);
return (
<CSSTransition timeout={150} classNames={'fade'} appear in>
<>
<ContentContainer css={tw`my-4 sm:my-10`} className={className}>
{showFlashKey &&
<FlashMessageRender byKey={showFlashKey} css={tw`mb-4`}/>
}
{showFlashKey && <FlashMessageRender byKey={showFlashKey} css={tw`mb-4`} />}
{children}
</ContentContainer>
<ContentContainer css={tw`mb-4`}>
@ -36,7 +34,7 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
>
Pterodactyl&reg;
</a>
&nbsp;&copy; 2015 - {(new Date()).getFullYear()}
&nbsp;&copy; 2015 - {new Date().getFullYear()}
</p>
</ContentContainer>
</>

View file

@ -22,13 +22,13 @@ interface Props<T> {
const Block = styled(Button)`
${tw`p-0 w-10 h-10`}
&:not(:last-of-type) {
${tw`mr-2`};
}
`;
function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
function Pagination<T>({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
const isFirstPage = pagination.currentPage === 1;
const isLastPage = pagination.currentPage >= pagination.totalPages;
@ -46,19 +46,14 @@ function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }:
return (
<>
{children({ items, isFirstPage, isLastPage })}
{(pages.length > 1) &&
<div css={tw`mt-4 flex justify-center`}>
{(pages[0] > 1 && !isFirstPage) &&
<Block
isSecondary
color={'primary'}
onClick={() => onPageSelect(1)}
>
<FontAwesomeIcon icon={faAngleDoubleLeft}/>
</Block>
}
{
pages.map(i => (
{pages.length > 1 && (
<div css={tw`mt-4 flex justify-center`}>
{pages[0] > 1 && !isFirstPage && (
<Block isSecondary color={'primary'} onClick={() => onPageSelect(1)}>
<FontAwesomeIcon icon={faAngleDoubleLeft} />
</Block>
)}
{pages.map((i) => (
<Block
isSecondary={pagination.currentPage !== i}
color={'primary'}
@ -67,19 +62,14 @@ function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }:
>
{i}
</Block>
))
}
{(pages[4] < pagination.totalPages && !isLastPage) &&
<Block
isSecondary
color={'primary'}
onClick={() => onPageSelect(pagination.totalPages)}
>
<FontAwesomeIcon icon={faAngleDoubleRight}/>
</Block>
}
</div>
}
))}
{pages[4] < pagination.totalPages && !isLastPage && (
<Block isSecondary color={'primary'} onClick={() => onPageSelect(pagination.totalPages)}>
<FontAwesomeIcon icon={faAngleDoubleRight} />
</Block>
)}
</div>
)}
</>
);
}

View file

@ -11,20 +11,17 @@ interface Props extends Omit<RouteProps, 'path'> {
export default ({ permission, children, ...props }: Props) => (
<Route {...props}>
{!permission ?
{!permission ? (
children
:
) : (
<Can
action={permission}
renderOnError={
<ServerError
title={'Access Denied'}
message={'You do not have permission to access this page.'}
/>
<ServerError title={'Access Denied'} message={'You do not have permission to access this page.'} />
}
>
{children}
</Can>
}
)}
</Route>
);

View file

@ -14,10 +14,10 @@ const BarFill = styled.div`
export default () => {
const interval = useRef<number>(null);
const timeout = useRef<number>(null);
const [ visible, setVisible ] = useState(false);
const progress = useStoreState(state => state.progress.progress);
const continuous = useStoreState(state => state.progress.continuous);
const setProgress = useStoreActions(actions => actions.progress.setProgress);
const [visible, setVisible] = useState(false);
const progress = useStoreState((state) => state.progress.progress);
const continuous = useStoreState((state) => state.progress.continuous);
const setProgress = useStoreActions((actions) => actions.progress.setProgress);
useEffect(() => {
return () => {
@ -33,7 +33,7 @@ export default () => {
// @ts-ignore
timeout.current = setTimeout(() => setProgress(undefined), 500);
}
}, [ progress ]);
}, [progress]);
useEffect(() => {
if (!continuous) {
@ -44,7 +44,7 @@ export default () => {
if (!progress || progress === 0) {
setProgress(randomInt(20, 30));
}
}, [ continuous ]);
}, [continuous]);
useEffect(() => {
if (continuous) {
@ -56,18 +56,12 @@ export default () => {
interval.current = setTimeout(() => setProgress(progress + randomInt(1, 5)), 500);
}
}
}, [ progress, continuous ]);
}, [progress, continuous]);
return (
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
<CSSTransition
timeout={150}
appear
in={visible}
unmountOnExit
classNames={'fade'}
>
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }}/>
<CSSTransition timeout={150} appear in={visible} unmountOnExit classNames={'fade'}>
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }} />
</CSSTransition>
</div>
);

View file

@ -43,22 +43,22 @@ const ActionButton = styled(Button)`
const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProps) => (
<PageContentBlock>
<div css={tw`flex justify-center`}>
<div css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}>
{(typeof onBack === 'function' || typeof onRetry === 'function') &&
<div css={tw`absolute left-0 top-0 ml-4 mt-4`}>
<ActionButton
onClick={() => onRetry ? onRetry() : (onBack ? onBack() : null)}
className={onRetry ? 'hover:spin' : undefined}
>
<FontAwesomeIcon icon={onRetry ? faSyncAlt : faArrowLeft}/>
</ActionButton>
</div>
}
<img src={image} css={tw`w-2/3 h-auto select-none mx-auto`}/>
<div
css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}
>
{(typeof onBack === 'function' || typeof onRetry === 'function') && (
<div css={tw`absolute left-0 top-0 ml-4 mt-4`}>
<ActionButton
onClick={() => (onRetry ? onRetry() : onBack ? onBack() : null)}
className={onRetry ? 'hover:spin' : undefined}
>
<FontAwesomeIcon icon={onRetry ? faSyncAlt : faArrowLeft} />
</ActionButton>
</div>
)}
<img src={image} css={tw`w-2/3 h-auto select-none mx-auto`} />
<h2 css={tw`mt-10 text-neutral-900 font-bold text-4xl`}>{title}</h2>
<p css={tw`text-sm text-neutral-700 mt-2`}>
{message}
</p>
<p css={tw`text-sm text-neutral-700 mt-2`}>{message}</p>
</div>
</div>
</PageContentBlock>
@ -66,10 +66,10 @@ const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProp
type ServerErrorProps = (Omit<PropsWithBack, 'image' | 'title'> | Omit<PropsWithRetry, 'image' | 'title'>) & {
title?: string;
}
};
const ServerError = ({ title, ...props }: ServerErrorProps) => (
<ScreenBlock title={title || 'Something went wrong'} image={ServerErrorSvg} {...props}/>
<ScreenBlock title={title || 'Something went wrong'} image={ServerErrorSvg} {...props} />
);
const NotFound = ({ title, message, onBack }: Partial<Pick<ScreenBlockProps, 'title' | 'message' | 'onBack'>>) => (

View file

@ -8,7 +8,9 @@ interface Props {
const Select = styled.select<Props>`
${tw`shadow-none block p-3 pr-8 rounded border w-full text-sm transition-colors duration-150 ease-linear`};
&, &:hover:not(:disabled), &:focus {
&,
&:hover:not(:disabled),
&:focus {
${tw`outline-none`};
}
@ -22,15 +24,18 @@ const Select = styled.select<Props>`
&::-ms-expand {
display: none;
}
${props => !props.hideDropdownArrow && css`
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
&:hover:not(:disabled), &:focus {
${tw`border-neutral-400`};
}
`};
${(props) =>
!props.hideDropdownArrow &&
css`
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
&:hover:not(:disabled),
&:focus {
${tw`border-neutral-400`};
}
`};
`;
export default Select;

View file

@ -7,7 +7,7 @@ interface Props extends PageContentBlockProps {
}
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
const name = ServerContext.useStoreState(state => state.server.data!.name);
const name = ServerContext.useStoreState((state) => state.server.data!.name);
return (
<PageContentBlock title={`${name} | ${title}`} {...props}>

View file

@ -25,30 +25,30 @@ const SpinnerComponent = styled.div<Props>`
${tw`w-8 h-8`};
border-width: 3px;
border-radius: 50%;
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite;
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.7) infinite;
${props => props.size === 'small' ? tw`w-4 h-4 border-2` : (props.size === 'large' ? css`
${tw`w-16 h-16`};
border-width: 6px;
` : null)};
${(props) =>
props.size === 'small'
? tw`w-4 h-4 border-2`
: props.size === 'large'
? css`
${tw`w-16 h-16`};
border-width: 6px;
`
: null};
border-color: ${props => !props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)'};
border-top-color: ${props => !props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)'};
border-color: ${(props) => (!props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)')};
border-top-color: ${(props) => (!props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)')};
`;
const Spinner: Spinner = ({ centered, ...props }) => (
centered ?
<div
css={[
tw`flex justify-center items-center`,
props.size === 'large' ? tw`m-20` : tw`m-6`,
]}
>
<SpinnerComponent {...props}/>
const Spinner: Spinner = ({ centered, ...props }) =>
centered ? (
<div css={[tw`flex justify-center items-center`, props.size === 'large' ? tw`m-20` : tw`m-6`]}>
<SpinnerComponent {...props} />
</div>
:
<SpinnerComponent {...props}/>
);
) : (
<SpinnerComponent {...props} />
);
Spinner.displayName = 'Spinner';
Spinner.Size = {
@ -58,10 +58,8 @@ Spinner.Size = {
};
Spinner.Suspense = ({ children, centered = true, size = Spinner.Size.LARGE, ...props }) => (
<Suspense fallback={<Spinner centered={centered} size={size} {...props}/>}>
<ErrorBoundary>
{children}
</ErrorBoundary>
<Suspense fallback={<Spinner centered={centered} size={size} {...props} />}>
<ErrorBoundary>{children}</ErrorBoundary>
</Suspense>
);
Spinner.Suspense.displayName = 'Spinner.Suspense';

View file

@ -19,7 +19,7 @@ const SpinnerOverlay: React.FC<Props> = ({ size, fixed, visible, backgroundOpaci
]}
style={{ background: `rgba(0, 0, 0, ${backgroundOpacity || 0.45})` }}
>
<Spinner size={size}/>
<Spinner size={size} />
{children && (typeof children === 'string' ? <p css={tw`mt-4 text-neutral-400`}>{children}</p> : children)}
</div>
</Fade>

View file

@ -8,7 +8,8 @@ const SubNavigation = styled.div`
${tw`flex items-center text-sm mx-auto px-2`};
max-width: 1200px;
& > a, & > div {
& > a,
& > div {
${tw`inline-block py-3 px-4 text-neutral-300 no-underline whitespace-nowrap transition-all duration-150`};
&:not(:first-of-type) {
@ -19,7 +20,8 @@ const SubNavigation = styled.div`
${tw`text-neutral-100`};
}
&:active, &.active {
&:active,
&.active {
${tw`text-neutral-100`};
box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()};
}

View file

@ -8,7 +8,7 @@ import Input from '@/components/elements/Input';
const ToggleContainer = styled.div`
${tw`relative select-none w-12 leading-normal`};
& > input[type="checkbox"] {
& > input[type='checkbox'] {
${tw`hidden`};
&:checked + label {
@ -30,7 +30,7 @@ const ToggleContainer = styled.div`
right: calc(50% + 0.125rem);
//width: 1.25rem;
//height: 1.25rem;
content: "";
content: '';
transition: all 75ms ease-in;
}
}
@ -52,35 +52,28 @@ const Switch = ({ name, label, description, defaultChecked, readOnly, onChange,
return (
<div css={tw`flex items-center`}>
<ToggleContainer css={tw`flex-none`}>
{children
|| <Input
id={uuid}
name={name}
type={'checkbox'}
onChange={e => onChange && onChange(e)}
defaultChecked={defaultChecked}
disabled={readOnly}
/>
}
<Label htmlFor={uuid}/>
{children || (
<Input
id={uuid}
name={name}
type={'checkbox'}
onChange={(e) => onChange && onChange(e)}
defaultChecked={defaultChecked}
disabled={readOnly}
/>
)}
<Label htmlFor={uuid} />
</ToggleContainer>
{(label || description) &&
<div css={tw`ml-4 w-full`}>
{label &&
<Label
css={[ tw`cursor-pointer`, !!description && tw`mb-0` ]}
htmlFor={uuid}
>
{label}
</Label>
}
{description &&
<p css={tw`text-neutral-400 text-sm mt-2`}>
{description}
</p>
}
</div>
}
{(label || description) && (
<div css={tw`ml-4 w-full`}>
{label && (
<Label css={[tw`cursor-pointer`, !!description && tw`mb-0`]} htmlFor={uuid}>
{label}
</Label>
)}
{description && <p css={tw`text-neutral-400 text-sm mt-2`}>{description}</p>}
</div>
)}
</div>
);
};

View file

@ -14,17 +14,16 @@ interface Props {
const TitledGreyBox = ({ icon, title, children, className }: Props) => (
<div css={tw`rounded shadow-md bg-neutral-700`} className={className}>
<div css={tw`bg-neutral-900 rounded-t p-3 border-b border-black`}>
{typeof title === 'string' ?
{typeof title === 'string' ? (
<p css={tw`text-sm uppercase`}>
{icon && <FontAwesomeIcon icon={icon} css={tw`mr-2 text-neutral-300`}/>}{title}
{icon && <FontAwesomeIcon icon={icon} css={tw`mr-2 text-neutral-300`} />}
{title}
</p>
:
) : (
title
}
</div>
<div css={tw`p-3`}>
{children}
)}
</div>
<div css={tw`p-3`}>{children}</div>
</div>
);

View file

@ -25,9 +25,12 @@ const formatProperties = (properties: Record<string, unknown>): Record<string, u
return {
...obj,
[key]: isCount || typeof value !== 'string'
? (isObject(value) ? formatProperties(value) : value)
: `<strong>${value}</strong>`,
[key]:
isCount || typeof value !== 'string'
? isObject(value)
? formatProperties(value)
: value
: `<strong>${value}</strong>`,
};
}, {});
};
@ -58,16 +61,18 @@ export default ({ activity, children }: Props) => {
{activity.event}
</Link>
<div className={classNames(style.icons, 'group-hover:text-gray-300')}>
{activity.isApi &&
{activity.isApi && (
<Tooltip placement={'top'} content={'Performed using API Key'}>
<span><TerminalIcon/></span>
<span>
<TerminalIcon />
</span>
</Tooltip>
}
)}
{children}
</div>
</div>
<p className={style.description}>
<Translate ns={'activity'} values={properties} i18nKey={activity.event.replace(':', '.')}/>
<Translate ns={'activity'} values={properties} i18nKey={activity.event.replace(':', '.')} />
</p>
<div className={'mt-1 flex items-center text-sm'}>
<Link
@ -77,17 +82,12 @@ export default ({ activity, children }: Props) => {
{activity.ip}
</Link>
<span className={'text-gray-400'}>&nbsp;|&nbsp;</span>
<Tooltip
placement={'right'}
content={format(activity.timestamp, 'MMM do, yyyy H:mm:ss')}
>
<span>
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
</span>
<Tooltip placement={'right'} content={format(activity.timestamp, 'MMM do, yyyy H:mm:ss')}>
<span>{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}</span>
</Tooltip>
</div>
</div>
{activity.hasAdditionalMetadata && <ActivityLogMetaButton meta={activity.properties}/>}
{activity.hasAdditionalMetadata && <ActivityLogMetaButton meta={activity.properties} />}
</div>
</div>
);

View file

@ -4,16 +4,11 @@ import { Dialog } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button/index';
export default ({ meta }: { meta: Record<string, unknown> }) => {
const [ open, setOpen ] = useState(false);
const [open, setOpen] = useState(false);
return (
<div className={'self-center md:px-4'}>
<Dialog
open={open}
onClose={() => setOpen(false)}
hideCloseIcon
title={'Metadata'}
>
<Dialog open={open} onClose={() => setOpen(false)} hideCloseIcon title={'Metadata'}>
<pre className={'bg-gray-900 rounded p-2 overflow-x-scroll font-mono text-sm leading-relaxed'}>
{JSON.stringify(meta, null, 2)}
</pre>
@ -23,10 +18,12 @@ export default ({ meta }: { meta: Record<string, unknown> }) => {
</Dialog>
<button
aria-describedby={'View additional event metadata'}
className={'p-2 transition-colors duration-100 text-gray-400 group-hover:text-gray-300 group-hover:hover:text-gray-50'}
className={
'p-2 transition-colors duration-100 text-gray-400 group-hover:text-gray-300 group-hover:hover:text-gray-50'
}
onClick={() => setOpen(true)}
>
<ClipboardListIcon className={'w-5 h-5'}/>
<ClipboardListIcon className={'w-5 h-5'} />
</button>
</div>
);

View file

@ -17,14 +17,14 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
[styles.small]: size === Options.Size.Small,
[styles.large]: size === Options.Size.Large,
},
className,
className
)}
{...rest}
>
{children}
</button>
);
},
}
);
const TextButton = forwardRef<HTMLButtonElement, ButtonProps>(({ className, ...props }, ref) => (

View file

@ -20,4 +20,4 @@ export type ButtonProps = JSX.IntrinsicElements['button'] & {
shape?: Shape;
size?: Size;
variant?: Variant;
}
};

View file

@ -7,7 +7,7 @@ type ConfirmationProps = Omit<DialogProps, 'description' | 'children'> & {
children: React.ReactNode;
confirm?: string | undefined;
onConfirmed: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
};
export default ({ confirm = 'Okay', children, onConfirmed, ...props }: ConfirmationProps) => {
return (

View file

@ -16,19 +16,17 @@ export interface DialogProps {
children?: React.ReactNode;
}
const DialogButtons = ({ children }: { children: React.ReactNode }) => (
<>{children}</>
);
const DialogButtons = ({ children }: { children: React.ReactNode }) => <>{children}</>;
const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }: DialogProps) => {
const items = React.Children.toArray(children || []);
const [ buttons, icon, content ] = [
const [buttons, icon, content] = [
// @ts-expect-error
items.find(child => child.type === DialogButtons),
items.find((child) => child.type === DialogButtons),
// @ts-expect-error
items.find(child => child.type === DialogIcon),
items.find((child) => child.type === DialogIcon),
// @ts-expect-error
items.filter(child => ![ DialogIcon, DialogButtons ].includes(child.type)),
items.filter((child) => ![DialogIcon, DialogButtons].includes(child.type)),
];
return (
@ -44,7 +42,7 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
open={open}
onClose={onClose}
>
<div className={'fixed inset-0 bg-gray-900/50 z-40'}/>
<div className={'fixed inset-0 bg-gray-900/50 z-40'} />
<div className={'fixed inset-0 overflow-y-auto z-50'}>
<div className={'flex min-h-full items-center justify-center p-4 text-center'}>
<HDialog.Panel
@ -61,22 +59,28 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
<div className={'flex p-6 overflow-y-auto'}>
{icon && <div className={'mr-4'}>{icon}</div>}
<div className={'flex-1 max-h-[70vh]'}>
{title &&
<HDialog.Title className={'font-header text-xl font-medium mb-2 text-gray-50 pr-4'}>
{title && (
<HDialog.Title
className={'font-header text-xl font-medium mb-2 text-gray-50 pr-4'}
>
{title}
</HDialog.Title>
}
)}
{description && <HDialog.Description>{description}</HDialog.Description>}
{content}
</div>
</div>
{buttons &&
<div className={'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'}>
{buttons && (
<div
className={
'px-6 py-3 bg-gray-700 flex items-center justify-end space-x-3 rounded-b'
}
>
{buttons}
</div>
}
)}
{/* Keep this below the other buttons so that it isn't the default focus if they're present. */}
{!hideCloseIcon &&
{!hideCloseIcon && (
<div className={'absolute right-0 top-0 m-4'}>
<Button.Text
size={Button.Sizes.Small}
@ -84,10 +88,10 @@ const Dialog = ({ open, title, description, onClose, hideCloseIcon, children }:
onClick={onClose}
className={'hover:rotate-90'}
>
<XIcon className={'w-5 h-5'}/>
<XIcon className={'w-5 h-5'} />
</Button.Text>
</div>
}
)}
</HDialog.Panel>
</div>
</div>

View file

@ -8,22 +8,22 @@ interface Props {
}
export default ({ type, className }: Props) => {
const [ Component, styles ] = (function (): [ (props: React.ComponentProps<'svg'>) => JSX.Element, string ] {
const [Component, styles] = (function (): [(props: React.ComponentProps<'svg'>) => JSX.Element, string] {
switch (type) {
case 'danger':
return [ ShieldExclamationIcon, 'bg-red-500 text-red-50' ];
return [ShieldExclamationIcon, 'bg-red-500 text-red-50'];
case 'warning':
return [ ExclamationIcon, 'bg-yellow-600 text-yellow-50' ];
return [ExclamationIcon, 'bg-yellow-600 text-yellow-50'];
case 'success':
return [ CheckIcon, 'bg-green-600 text-green-50' ];
return [CheckIcon, 'bg-green-600 text-green-50'];
case 'info':
return [ InformationCircleIcon, 'bg-primary-500 text-primary-50' ];
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'}/>
<Component className={'w-6 h-6'} />
</div>
);
};

View file

@ -11,22 +11,22 @@ interface Props {
}
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })}/>
<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 [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),
list.filter((child) => child.type === DropdownButton),
list.filter((child) => child.type !== DropdownButton),
];
}, [ children ]);
}, [children]);
if (!Button) {
throw new Error('Cannot mount <Dropdown /> component without a child <Dropdown.Button />.');
@ -44,9 +44,7 @@ const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
leaveTo={'transform scale-95 opacity-0'}
>
<Menu.Items className={classNames(styles.items_container, 'w-56')}>
<div className={'px-1 py-1'}>
{items}
</div>
<div className={'px-1 py-1'}>{items}</div>
</Menu.Items>
</Transition>
</Menu>

View file

@ -12,13 +12,13 @@ interface Props {
export default ({ className, animate = true, children }: Props) => (
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
{typeof children === 'string' ?
{typeof children === 'string' ? (
<>
<span className={'mr-2'}>{children}</span>
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()}/>
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()} />
</>
:
) : (
children
}
)}
</Menu.Button>
);

View file

@ -12,32 +12,31 @@ interface Props {
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>
);
});
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;

View file

@ -3,9 +3,5 @@ import classNames from 'classnames';
import styles from './styles.module.css';
export default forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(({ className, ...props }, ref) => (
<input
ref={ref}
className={classNames('form-input', styles.text_input, className)}
{...props}
/>
<input ref={ref} className={classNames('form-input', styles.text_input, className)} {...props} />
));

View file

@ -3,9 +3,5 @@ import classNames from 'classnames';
import styles from './styles.module.css';
export default forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(({ className, ...props }, ref) => (
<input
ref={ref}
className={classNames('form-input', styles.text_input, className)}
{...props}
/>
<input ref={ref} className={classNames('form-input', styles.text_input, className)} {...props} />
));

View file

@ -12,7 +12,7 @@ interface Props {
const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
const start = (pagination.currentPage - 1) * pagination.perPage;
const end = ((pagination.currentPage - 1) * pagination.perPage) + pagination.count;
const end = (pagination.currentPage - 1) * pagination.perPage + pagination.count;
const { currentPage: current, totalPages: total } = pagination;
@ -43,31 +43,34 @@ const PaginationFooter = ({ pagination, className, onPageSelect }: Props) => {
Showing&nbsp;
<span className={'font-semibold text-neutral-400'}>
{Math.max(start, Math.min(pagination.total, 1))}
</span>&nbsp;to&nbsp;
</span>
&nbsp;to&nbsp;
<span className={'font-semibold text-neutral-400'}>{end}</span> of&nbsp;
<span className={'font-semibold text-neutral-400'}>{pagination.total}</span> results.
</p>
{pagination.totalPages > 1 &&
{pagination.totalPages > 1 && (
<div className={'flex space-x-1'}>
<Button.Text {...buttonProps(1)} disabled={pages.previous.length !== 2}>
<ChevronDoubleLeftIcon className={'w-3 h-3'}/>
<ChevronDoubleLeftIcon className={'w-3 h-3'} />
</Button.Text>
{pages.previous.reverse().map((value) => (
<Button.Text key={`previous-${value}`} {...buttonProps(value)}>
{value}
</Button.Text>
))}
<Button size={Button.Sizes.Small} shape={Button.Shapes.IconSquare}>{current}</Button>
<Button size={Button.Sizes.Small} shape={Button.Shapes.IconSquare}>
{current}
</Button>
{pages.next.map((value) => (
<Button.Text key={`next-${value}`} {...buttonProps(value)}>
{value}
</Button.Text>
))}
<Button.Text {...buttonProps(total)} disabled={pages.next.length !== 2}>
<ChevronDoubleRightIcon className={'w-3 h-3'}/>
<ChevronDoubleRightIcon className={'w-3 h-3'} />
</Button.Text>
</div>
}
)}
</div>
);
};

View file

@ -38,17 +38,9 @@ const arrowSides: Record<Side, string> = {
left: 'top-0 right-[-6px]',
};
export default ({
content,
children,
disabled = false,
alwaysOpen = false,
delay = 0,
rest = 30,
...props
}: Props) => {
export default ({ content, children, disabled = false, alwaysOpen = false, delay = 0, rest = 30, ...props }: Props) => {
const arrowEl = useRef<HTMLDivElement>(null);
const [ open, setOpen ] = useState(alwaysOpen || false);
const [open, setOpen] = useState(alwaysOpen || false);
const { x, y, reference, floating, middlewareData, strategy, context } = useFloating({
open,
@ -82,7 +74,7 @@ export default ({
<>
{cloneElement(children, getReferenceProps({ ref: reference, ...children.props }))}
<AnimatePresence>
{open &&
{open && (
<motion.div
initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }}
@ -90,7 +82,8 @@ export default ({
transition={{ type: 'spring', damping: 20, stiffness: 300, duration: 0.075 }}
{...getFloatingProps({
ref: floating,
className: 'absolute top-0 left-0 bg-gray-900 text-sm text-gray-200 px-3 py-2 rounded pointer-events-none max-w-[20rem] z-[9999]',
className:
'absolute top-0 left-0 bg-gray-900 text-sm text-gray-200 px-3 py-2 rounded pointer-events-none max-w-[20rem] z-[9999]',
style: {
position: strategy,
top: `${y || 0}px`,
@ -99,17 +92,19 @@ export default ({
})}
>
{content}
{props.arrow &&
{props.arrow && (
<div
ref={arrowEl}
style={{
transform: `translate(${Math.round(ax || 0)}px, ${Math.round(ay || 0)}px) rotate(45deg)`,
transform: `translate(${Math.round(ax || 0)}px, ${Math.round(
ay || 0
)}px) rotate(45deg)`,
}}
className={classNames('absolute bg-gray-900 w-3 h-3', side)}
/>
}
)}
</motion.div>
}
)}
</AnimatePresence>
</>
);

View file

@ -5,15 +5,17 @@ type Duration = `duration-${number}`;
interface Props {
as?: React.ElementType;
duration?: Duration | [ Duration, Duration ];
duration?: Duration | [Duration, Duration];
show: boolean;
children: React.ReactNode;
}
export default ({ children, duration, ...props }: Props) => {
const [ enterDuration, exitDuration ] = Array.isArray(duration)
const [enterDuration, exitDuration] = Array.isArray(duration)
? duration
: (!duration ? [ 'duration-200', 'duration-100' ] : [ duration, duration ]);
: !duration
? ['duration-200', 'duration-100']
: [duration, duration];
return (
<Transition