Fix login form
This commit is contained in:
parent
02f83c58f5
commit
8c20158e58
6 changed files with 106 additions and 64 deletions
|
@ -1,29 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MessageBox from '@/components/MessageBox';
|
import MessageBox from '@/components/MessageBox';
|
||||||
import { State, useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
byKey?: string;
|
byKey?: string;
|
||||||
|
className?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default ({ byKey }: Props) => {
|
const FlashMessageRender = ({ byKey, className }: Props) => {
|
||||||
const flashes = useStoreState((state: State<ApplicationStore>) => state.flashes.items);
|
const flashes = useStoreState(state => state.flashes.items.filter(
|
||||||
|
flash => byKey ? flash.key === byKey : true,
|
||||||
let filtered = flashes;
|
));
|
||||||
if (byKey) {
|
|
||||||
filtered = flashes.filter(flash => flash.key === byKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
{
|
{
|
||||||
filtered.map((flash, index) => (
|
flashes.map((flash, index) => (
|
||||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||||
{index > 0 && <div css={tw`mt-2`}></div>}
|
{index > 0 && <div css={tw`mt-2`}></div>}
|
||||||
<MessageBox type={flash.type} title={flash.title}>
|
<MessageBox type={flash.type} title={flash.title}>
|
||||||
|
@ -35,3 +28,5 @@ export default ({ byKey }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default FlashMessageRender;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import tw, { TwStyle } from 'twin.macro';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
export type FlashMessageType = 'success' | 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
|
@ -8,11 +10,60 @@ interface Props {
|
||||||
type?: FlashMessageType;
|
type?: FlashMessageType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ title, children, type }: Props) => (
|
const styling = (type?: FlashMessageType): TwStyle | string => {
|
||||||
<div className={`lg:inline-flex alert ${type}`} role={'alert'}>
|
switch (type) {
|
||||||
{title && <span className={'title'}>{title}</span>}
|
case 'error':
|
||||||
<span className={'message'}>
|
return tw`bg-red-600 border-red-800`;
|
||||||
|
case 'info':
|
||||||
|
return tw`bg-primary-600 border-primary-800`;
|
||||||
|
case 'success':
|
||||||
|
return tw`bg-green-600 border-green-800`;
|
||||||
|
case 'warning':
|
||||||
|
return tw`bg-yellow-600 border-yellow-800`;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBackground = (type?: FlashMessageType): TwStyle | string => {
|
||||||
|
switch (type) {
|
||||||
|
case 'error':
|
||||||
|
return tw`bg-red-500`;
|
||||||
|
case 'info':
|
||||||
|
return tw`bg-primary-500`;
|
||||||
|
case 'success':
|
||||||
|
return tw`bg-green-500`;
|
||||||
|
case 'warning':
|
||||||
|
return tw`bg-yellow-500`;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled.div<{ $type?: FlashMessageType }>`
|
||||||
|
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
|
||||||
|
${props => styling(props.$type)};
|
||||||
|
`;
|
||||||
|
Container.displayName = 'MessageBox.Container';
|
||||||
|
|
||||||
|
const MessageBox = ({ title, children, type }: Props) => (
|
||||||
|
<Container css={tw`lg:inline-flex`} $type={type} role={'alert'}>
|
||||||
|
{title &&
|
||||||
|
<span
|
||||||
|
className={'title'}
|
||||||
|
css={[
|
||||||
|
tw`flex rounded-full uppercase px-2 py-1 text-xs font-bold mr-3 leading-none`,
|
||||||
|
getBackground(type),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<span css={tw`mr-2 text-left flex-auto`}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
|
MessageBox.displayName = 'MessageBox';
|
||||||
|
|
||||||
|
export default MessageBox;
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { ApplicationStore } from '@/state';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import { Formik, FormikHelpers } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
import { object, string } from 'yup';
|
import { object, string } from 'yup';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -43,33 +45,30 @@ export default () => {
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
title={'Request Password Reset'}
|
title={'Request Password Reset'}
|
||||||
className={'w-full flex'}
|
css={tw`w-full flex`}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
light={true}
|
light
|
||||||
label={'Email'}
|
label={'Email'}
|
||||||
description={'Enter your account email address to receive instructions on resetting your password.'}
|
description={'Enter your account email address to receive instructions on resetting your password.'}
|
||||||
name={'email'}
|
name={'email'}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button
|
||||||
type={'submit'}
|
type={'submit'}
|
||||||
className={'btn btn-primary btn-jumbo flex justify-center'}
|
size={'xlarge'}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ?
|
Send Email
|
||||||
<div className={'spinner-circle spinner-sm spinner-white'}></div>
|
</Button>
|
||||||
:
|
|
||||||
'Send Email'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
type={'button'}
|
type={'button'}
|
||||||
to={'/auth/login'}
|
to={'/auth/login'}
|
||||||
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
|
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||||
>
|
>
|
||||||
Return to Login
|
Return to Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { httpErrorToHuman } from '@/api/http';
|
||||||
import { FlashMessage } from '@/state/flashes';
|
import { FlashMessage } from '@/state/flashes';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps & {
|
type OwnProps = RouteComponentProps & {
|
||||||
clearFlashes: ActionCreator<void>;
|
clearFlashes: ActionCreator<void>;
|
||||||
|
@ -36,36 +38,29 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
{ref.current && ref.current.render()}
|
{ref.current && ref.current.render()}
|
||||||
<LoginFormContainer
|
<LoginFormContainer
|
||||||
title={'Login to Continue'}
|
title={'Login to Continue'}
|
||||||
className={'w-full flex'}
|
css={tw`w-full flex`}
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
>
|
>
|
||||||
<label htmlFor={'username'}>Username or Email</label>
|
|
||||||
<Field
|
<Field
|
||||||
type={'text'}
|
type={'text'}
|
||||||
|
label={'Username or Email'}
|
||||||
id={'username'}
|
id={'username'}
|
||||||
name={'username'}
|
name={'username'}
|
||||||
className={'input'}
|
light
|
||||||
/>
|
/>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<label htmlFor={'password'}>Password</label>
|
|
||||||
<Field
|
<Field
|
||||||
type={'password'}
|
type={'password'}
|
||||||
|
label={'Password'}
|
||||||
id={'password'}
|
id={'password'}
|
||||||
name={'password'}
|
name={'password'}
|
||||||
className={'input'}
|
light
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'mt-6'}>
|
<div css={tw`mt-6`}>
|
||||||
<button
|
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting}>
|
||||||
type={'submit'}
|
Login
|
||||||
className={'btn btn-primary btn-jumbo'}
|
</Button>
|
||||||
>
|
|
||||||
{isSubmitting ?
|
|
||||||
<Spinner size={'small'} className={'mx-auto'}/>
|
|
||||||
:
|
|
||||||
'Login'
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{recaptchaEnabled &&
|
{recaptchaEnabled &&
|
||||||
<ReCAPTCHA
|
<ReCAPTCHA
|
||||||
|
@ -80,10 +75,10 @@ const LoginContainer = ({ isSubmitting, setFieldValue, values, submitForm, handl
|
||||||
onExpired={() => setFieldValue('recaptchaData', null)}
|
onExpired={() => setFieldValue('recaptchaData', null)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div className={'mt-6 text-center'}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/password'}
|
to={'/auth/password'}
|
||||||
className={'text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600'}
|
css={tw`text-xs text-neutral-500 tracking-wide no-underline uppercase hover:text-neutral-600`}
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { Form } from 'formik';
|
import { Form } from 'formik';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
import { breakpoint } from 'styled-components-breakpoint';
|
import { breakpoint } from '@/theme';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
|
@ -30,27 +30,29 @@ const Container = styled.div`
|
||||||
|
|
||||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
||||||
<Container>
|
<Container>
|
||||||
{title && <h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
{title &&
|
||||||
|
<h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>
|
||||||
{title}
|
{title}
|
||||||
</h2>}
|
</h2>
|
||||||
<FlashMessageRender className={'mb-2 px-1'}/>
|
}
|
||||||
|
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
||||||
<Form {...props} ref={ref}>
|
<Form {...props} ref={ref}>
|
||||||
<div className={'md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1'}>
|
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
||||||
<div className={'flex-none select-none mb-6 md:mb-0 self-center'}>
|
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
||||||
<img src={'/assets/svgs/pterodactyl.svg'} className={'block w-48 md:w-64 mx-auto'}/>
|
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1'}>
|
<div css={tw`flex-1`}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<p className={'text-center text-neutral-500 text-xs mt-4'}>
|
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
||||||
© 2015 - 2020
|
© 2015 - 2020
|
||||||
<a
|
<a
|
||||||
rel={'noopener nofollow'}
|
rel={'noopener nofollow noreferrer'}
|
||||||
href={'https://pterodactyl.io'}
|
href={'https://pterodactyl.io'}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
className={'no-underline text-neutral-500 hover:text-neutral-300'}
|
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
|
||||||
>
|
>
|
||||||
Pterodactyl Software
|
Pterodactyl Software
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -22,7 +22,6 @@ const inputStyle = css<Props>`
|
||||||
${tw`p-3 border rounded text-sm transition-all duration-150`};
|
${tw`p-3 border rounded text-sm transition-all duration-150`};
|
||||||
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none`};
|
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none`};
|
||||||
|
|
||||||
${props => props.hasError && tw`text-red-600 border-red-500 hover:border-red-600`};
|
|
||||||
& + .input-help {
|
& + .input-help {
|
||||||
${tw`mt-1 text-xs`};
|
${tw`mt-1 text-xs`};
|
||||||
${props => props.hasError ? tw`text-red-400` : tw`text-neutral-400`};
|
${props => props.hasError ? tw`text-red-400` : tw`text-neutral-400`};
|
||||||
|
@ -41,6 +40,7 @@ const inputStyle = css<Props>`
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.isLight && light};
|
${props => props.isLight && light};
|
||||||
|
${props => props.hasError && tw`text-red-600 border-red-500 hover:border-red-600`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Input = styled.input<Props>`${inputStyle}`;
|
const Input = styled.input<Props>`${inputStyle}`;
|
||||||
|
|
Loading…
Reference in a new issue