diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-09 19:02:25 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-09 19:02:25 +0800 |
commit | ea14913b412e78ff458bdfba47182f7363e776e5 (patch) | |
tree | 3ee220bfbbd9923b5e1adc36ee51f9b5d39ad640 /packages/website/ts/components/modals | |
parent | 5868c91cfb54cfa9177572b201d88d1168bf5b06 (diff) | |
parent | 5dd55491b86bf8577405e37d0f2d668aa1273b10 (diff) | |
download | dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.gz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.bz2 dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.lz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.xz dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.tar.zst dexon-sol-tools-ea14913b412e78ff458bdfba47182f7363e776e5.zip |
Merge development
Diffstat (limited to 'packages/website/ts/components/modals')
-rw-r--r-- | packages/website/ts/components/modals/input.tsx | 95 | ||||
-rw-r--r-- | packages/website/ts/components/modals/modal_contact.tsx | 395 |
2 files changed, 490 insertions, 0 deletions
diff --git a/packages/website/ts/components/modals/input.tsx b/packages/website/ts/components/modals/input.tsx new file mode 100644 index 000000000..8cfcc9763 --- /dev/null +++ b/packages/website/ts/components/modals/input.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +export enum InputWidth { + Half, + Full, +} + +interface InputProps { + name: string; + width?: InputWidth; + label: string; + type?: string; + errors?: ErrorProps; + isErrors?: boolean; + required?: boolean; +} + +interface ErrorProps { + [key: string]: string; +} + +export const Input = React.forwardRef((props: InputProps, ref?: React.Ref<HTMLInputElement>) => { + const { name, label, type, errors } = props; + const id = `input-${name}`; + const componentType = type === 'textarea' ? 'textarea' : 'input'; + const isErrors = errors.hasOwnProperty(name) && errors[name] !== null; + const errorMessage = isErrors ? errors[name] : null; + + return ( + <InputWrapper {...props}> + <Label htmlFor={id}>{label}</Label> + <StyledInput as={componentType} ref={ref} id={id} isErrors={isErrors} {...props} /> + {isErrors && <Error>{errorMessage}</Error>} + </InputWrapper> + ); +}); + +Input.defaultProps = { + width: InputWidth.Full, + errors: {}, +}; + +const StyledInput = styled.input` + appearance: none; + background-color: #fff; + border: 1px solid #d5d5d5; + color: #000; + font-size: 1.294117647rem; + padding: 16px 15px 14px; + outline: none; + width: 100%; + min-height: ${props => props.type === 'textarea' && `120px`}; + + background-color: ${(props: InputProps) => props.isErrors && `#FDEDED`}; + border-color: ${(props: InputProps) => props.isErrors && `#FD0000`}; + + &::placeholder { + color: #c3c3c3; + } +`; + +const InputWrapper = + styled.div < + InputProps > + ` + position: relative; + flex-grow: ${props => props.width === InputWidth.Full && 1}; + width: ${props => props.width === InputWidth.Half && `calc(50% - 15px)`}; + + @media (max-width: 768px) { + width: 100%; + margin-bottom: 30px; + } +`; + +const Label = styled.label` + color: #000; + font-size: 1.111111111rem; + line-height: 1.4em; + margin-bottom: 10px; + display: inline-block; +`; + +const Error = styled.span` + color: #fd0000; + font-size: 0.833333333rem; + line-height: 1em; + display: inline-block; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + transform: translateY(24px); +`; diff --git a/packages/website/ts/components/modals/modal_contact.tsx b/packages/website/ts/components/modals/modal_contact.tsx new file mode 100644 index 000000000..a3a1f13f5 --- /dev/null +++ b/packages/website/ts/components/modals/modal_contact.tsx @@ -0,0 +1,395 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { colors } from 'ts/style/colors'; + +import { DialogContent, DialogOverlay } from '@reach/dialog'; +import '@reach/dialog/styles.css'; + +import { Button } from 'ts/components/button'; +import { Icon } from 'ts/components/icon'; +import { Input, InputWidth } from 'ts/components/modals/input'; +import { Heading, Paragraph } from 'ts/components/text'; +import { GlobalStyle } from 'ts/constants/globalStyle'; + +export enum ModalContactType { + General = 'GENERAL', + MarketMaker = 'MARKET_MAKER', +} + +interface Props { + theme?: GlobalStyle; + isOpen?: boolean; + onDismiss?: () => void; + modalContactType: ModalContactType; +} + +interface FormProps { + isSuccessful?: boolean; + isSubmitting?: boolean; +} + +interface ErrorResponseProps { + param: string; + location: string; + msg: string; +} + +interface ErrorResponse { + errors: ErrorResponseProps[]; +} + +interface ErrorProps { + [key: string]: string; +} + +export class ModalContact extends React.Component<Props> { + public static defaultProps = { + modalContactType: ModalContactType.General, + }; + public state = { + isSubmitting: false, + isSuccessful: false, + errors: {}, + }; + // shared fields + public nameRef: React.RefObject<HTMLInputElement> = React.createRef(); + public emailRef: React.RefObject<HTMLInputElement> = React.createRef(); + public companyProjectRef: React.RefObject<HTMLInputElement> = React.createRef(); + public commentsRef: React.RefObject<HTMLInputElement> = React.createRef(); + // general lead fields + public linkRef: React.RefObject<HTMLInputElement> = React.createRef(); + // market maker lead fields + public countryRef: React.RefObject<HTMLInputElement> = React.createRef(); + public fundSizeRef: React.RefObject<HTMLInputElement> = React.createRef(); + public constructor(props: Props) { + super(props); + } + public render(): React.ReactNode { + const { isOpen, onDismiss } = this.props; + const { isSuccessful, errors } = this.state; + return ( + <> + <DialogOverlay + style={{ background: 'rgba(0, 0, 0, 0.75)', zIndex: 30 }} + isOpen={isOpen} + onDismiss={onDismiss} + > + <StyledDialogContent> + <Form onSubmit={this._onSubmitAsync.bind(this)} isSuccessful={isSuccessful}> + <Heading color={colors.textDarkPrimary} size={34} asElement="h2"> + Contact the 0x Core Team + </Heading> + {this._renderFormContent(errors)} + <ButtonRow> + <Button + color="#5C5C5C" + isNoBorder={true} + isTransparent={true} + type="button" + onClick={this.props.onDismiss} + > + Back + </Button> + <Button>Submit</Button> + </ButtonRow> + </Form> + <Confirmation isSuccessful={isSuccessful}> + <Icon name="rocketship" size="large" margin={[0, 0, 'default', 0]} /> + <Heading color={colors.textDarkPrimary} size={34} asElement="h2"> + Thanks for contacting us. + </Heading> + <Paragraph isMuted={true} color={colors.textDarkPrimary}> + We'll get back to you soon. If you need quick support in the meantime, reach out to the + 0x team on Discord. + </Paragraph> + <Button onClick={this.props.onDismiss}>Done</Button> + </Confirmation> + </StyledDialogContent> + </DialogOverlay> + </> + ); + } + public _renderFormContent(errors: ErrorProps): React.ReactNode { + switch (this.props.modalContactType) { + case ModalContactType.MarketMaker: + return this._renderMarketMakerFormContent(errors); + case ModalContactType.General: + default: + return this._renderGeneralFormContent(errors); + } + } + private _renderMarketMakerFormContent(errors: ErrorProps): React.ReactNode { + return ( + <> + <Paragraph isMuted={true} color={colors.textDarkPrimary}> + If you’re considering market making on 0x, we’re happy to answer your questions. Fill out the form + so we can connect you with the right person to help you get started. + </Paragraph> + <InputRow> + <Input + name="name" + label="Your name" + type="text" + width={InputWidth.Half} + ref={this.nameRef} + required={true} + errors={errors} + /> + <Input + name="email" + label="Your email" + type="email" + ref={this.emailRef} + required={true} + errors={errors} + width={InputWidth.Half} + /> + </InputRow> + <InputRow> + <Input + name="country" + label="Country of Location" + type="text" + ref={this.countryRef} + required={true} + errors={errors} + /> + </InputRow> + <InputRow> + <Input + name="fundSize" + label="Fund Size" + type="text" + ref={this.fundSizeRef} + required={true} + errors={errors} + /> + </InputRow> + <InputRow> + <Input + name="companyOrProject" + label="Name of your project / company" + type="text" + ref={this.companyProjectRef} + required={false} + errors={errors} + /> + </InputRow> + <InputRow> + <Input + name="comments" + label="What is prompting you to reach out?" + type="textarea" + ref={this.commentsRef} + required={false} + errors={errors} + /> + </InputRow> + </> + ); + } + private _renderGeneralFormContent(errors: ErrorProps): React.ReactNode { + return ( + <> + <Paragraph isMuted={true} color={colors.textDarkPrimary}> + If you're considering building on 0x, we're happy to answer your questions. Fill out the form so we + can connect you with the right person to help you get started. + </Paragraph> + <InputRow> + <Input + name="name" + label="Your name" + type="text" + width={InputWidth.Half} + ref={this.nameRef} + required={true} + errors={errors} + /> + <Input + name="email" + label="Your email" + type="email" + ref={this.emailRef} + required={true} + errors={errors} + width={InputWidth.Half} + /> + </InputRow> + <InputRow> + <Input + name="companyOrProject" + label="Name of your project / company" + type="text" + ref={this.companyProjectRef} + required={true} + errors={errors} + /> + </InputRow> + <InputRow> + <Input + name="link" + label="Do you have any documentation or a website?" + type="text" + ref={this.linkRef} + errors={errors} + /> + </InputRow> + <InputRow> + <Input + name="comments" + label="Anything else?" + type="textarea" + ref={this.commentsRef} + errors={errors} + /> + </InputRow> + </> + ); + } + private async _onSubmitAsync(e: Event): Promise<void> { + e.preventDefault(); + + let jsonBody; + if (this.props.modalContactType === ModalContactType.MarketMaker) { + jsonBody = { + name: this.nameRef.current.value, + email: this.emailRef.current.value, + country: this.countryRef.current.value, + fundSize: this.fundSizeRef.current.value, + projectOrCompany: this.companyProjectRef.current.value, + comments: this.commentsRef.current.value, + }; + } else { + jsonBody = { + name: this.nameRef.current.value, + email: this.emailRef.current.value, + projectOrCompany: this.companyProjectRef.current.value, + link: this.linkRef.current.value, + comments: this.commentsRef.current.value, + }; + } + + this.setState({ ...this.state, errors: [], isSubmitting: true }); + + const endpoint = + this.props.modalContactType === ModalContactType.MarketMaker ? '/market_maker_leads' : '/leads'; + + try { + // Disabling no-unbound method b/c no reason for _.isEmpty to be bound + // tslint:disable:no-unbound-method + const response = await fetch(`https://website-api.0xproject.com${endpoint}`, { + method: 'post', + mode: 'cors', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(_.omitBy(jsonBody, _.isEmpty)), + }); + + if (!response.ok) { + const errorResponse: ErrorResponse = await response.json(); + const errors = this._parseErrors(errorResponse.errors); + this.setState({ ...this.state, isSubmitting: false, errors }); + + throw new Error('Request failed'); + } + + this.setState({ ...this.state, isSuccessful: true }); + } catch (e) { + // Empty block + } + } + private _parseErrors(errors: ErrorResponseProps[]): ErrorProps { + const initialValue: {} = {}; + return _.reduce( + errors, + (hash: ErrorProps, error: ErrorResponseProps) => { + const { param, msg } = error; + const key = param; + hash[key] = msg; + + return hash; + }, + initialValue, + ); + } +} + +// Handle errors: {"errors":[{"location":"body","param":"name","msg":"Invalid value"},{"location":"body","param":"email","msg":"Invalid value"}]} + +const InputRow = styled.div` + width: 100%; + flex: 0 0 auto; + + @media (min-width: 768px) { + display: flex; + justify-content: space-between; + margin-bottom: 30px; + } +`; + +const ButtonRow = styled(InputRow)` + @media (max-width: 768px) { + display: flex; + flex-direction: column; + + button:nth-child(1) { + order: 2; + } + + button:nth-child(2) { + order: 1; + margin-bottom: 10px; + } + } +`; + +const StyledDialogContent = styled(DialogContent)` + position: relative; + max-width: 800px; + background-color: #f6f6f6 !important; + padding: 60px 60px !important; + + @media (max-width: 768px) { + width: calc(100vw - 40px) !important; + margin: 40px auto !important; + padding: 30px 30px !important; + } +`; + +const Form = + styled.form < + FormProps > + ` + position: relative; + transition: opacity 0.30s ease-in-out, visibility 0.30s ease-in-out; + + opacity: ${props => props.isSuccessful && `0`}; + visibility: ${props => props.isSuccessful && `hidden`}; +`; + +const Confirmation = + styled.div < + FormProps > + ` + position: absolute; + top: 50%; + text-align: center; + width: 100%; + left: 0; + transition: opacity 0.30s ease-in-out, visibility 0.30s ease-in-out; + transition-delay: 0.40s; + padding: 60px 60px; + transform: translateY(-50%); + opacity: ${props => (props.isSuccessful ? `1` : `0`)}; + visibility: ${props => (props.isSuccessful ? 'visible' : `hidden`)}; + + p { + max-width: 492px; + margin-left: auto; + margin-right: auto; + } +`; |