Apply new eslint rules; default to prettier for styling
This commit is contained in:
parent
f22cce8881
commit
dc84af9937
218 changed files with 3876 additions and 3564 deletions
|
@ -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 } }} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 "{text}" to clipboard.</p>
|
||||
</div>
|
||||
</Toast>
|
||||
:
|
||||
) : (
|
||||
<></>
|
||||
}
|
||||
)}
|
||||
</Fade>
|
||||
</SwitchTransition>
|
||||
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }} css={tw`cursor-pointer`}>
|
||||
|
|
|
@ -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 });
|
||||
}}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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`};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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®
|
||||
</a>
|
||||
© 2015 - {(new Date()).getFullYear()}
|
||||
© 2015 - {new Date().getFullYear()}
|
||||
</p>
|
||||
</ContentContainer>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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'>>) => (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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'}> | </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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -20,4 +20,4 @@ export type ButtonProps = JSX.IntrinsicElements['button'] & {
|
|||
shape?: Shape;
|
||||
size?: Size;
|
||||
variant?: Variant;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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} />
|
||||
));
|
||||
|
|
|
@ -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
|
||||
<span className={'font-semibold text-neutral-400'}>
|
||||
{Math.max(start, Math.min(pagination.total, 1))}
|
||||
</span> to
|
||||
</span>
|
||||
to
|
||||
<span className={'font-semibold text-neutral-400'}>{end}</span> of
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue