aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/modals
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts/components/modals')
-rw-r--r--packages/website/ts/components/modals/input.tsx95
-rw-r--r--packages/website/ts/components/modals/modal_contact.tsx278
2 files changed, 373 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..b97baf5e7
--- /dev/null
+++ b/packages/website/ts/components/modals/modal_contact.tsx
@@ -0,0 +1,278 @@
+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/@next/components/button';
+import { Icon } from 'ts/@next/components/icon';
+import { Input, InputWidth } from 'ts/@next/components/modals/input';
+import { Heading, Paragraph } from 'ts/@next/components/text';
+import { GlobalStyle } from 'ts/@next/constants/globalStyle';
+
+interface Props {
+ theme?: GlobalStyle;
+ isOpen?: boolean;
+ onDismiss?: () => void;
+}
+
+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 state = {
+ isSubmitting: false,
+ isSuccessful: false,
+ errors: {},
+ };
+ public nameRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public emailRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public companyProjectRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public linkRef: React.RefObject<HTMLInputElement> = React.createRef();
+ public commentsRef: 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>
+ <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>
+ <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>
+ </>
+ );
+ }
+ private async _onSubmitAsync(e: Event): Promise<void> {
+ e.preventDefault();
+
+ const name = this.nameRef.current.value;
+ const email = this.emailRef.current.value;
+ const projectOrCompany = this.companyProjectRef.current.value;
+ const link = this.linkRef.current.value;
+ const comments = this.commentsRef.current.value;
+
+ this.setState({ ...this.state, errors: [], isSubmitting: true });
+
+ 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/leads', {
+ method: 'post',
+ mode: 'cors',
+ credentials: 'same-origin',
+ headers: {
+ 'content-type': 'application/json; charset=utf-8',
+ },
+ body: JSON.stringify(_.omitBy({ name, email, projectOrCompany, link, comments }, _.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;
+ }
+`;