Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jeremy/eng 997b current upcoming #222

Merged
merged 24 commits into from
Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
40d039a
building card WIP
jerminatorhits Feb 27, 2019
de517e9
get basic card layout to show
jerminatorhits Feb 28, 2019
e29479f
get card spacing to be even
jerminatorhits Feb 28, 2019
198d8ba
modal WIP
jerminatorhits Feb 28, 2019
2e65a28
cancel booking working
jerminatorhits Mar 1, 2019
1e95308
finish contact host modal
jerminatorhits Mar 1, 2019
4e30cac
cancel booking modal flow working
jerminatorhits Mar 1, 2019
eec8176
merge
jerminatorhits Mar 1, 2019
58cd299
condense code
jerminatorhits Mar 1, 2019
21144be
remove extra templating
jerminatorhits Mar 1, 2019
8b558bb
remove unnecessary arrow func
jerminatorhits Mar 1, 2019
18296c1
set currency before/same time as isSubmitting Loading renders
jerminatorhits Mar 1, 2019
5992a51
remove unnecessary check
jerminatorhits Mar 1, 2019
ec1a363
remove unneeded state
jerminatorhits Mar 1, 2019
d223c69
Merge branch 'master' into jeremy/ENG-997b-current-upcoming
jerminatorhits Mar 1, 2019
2d83b97
onModalAction naming
jerminatorhits Mar 1, 2019
f3c054b
format address ternary
jerminatorhits Mar 1, 2019
97114a6
Merge branch 'master' into jeremy/ENG-997b-current-upcoming
jerminatorhits Mar 1, 2019
c0caa2b
remove button, add classNames to a tag
jerminatorhits Mar 2, 2019
62c2eb9
add c-pointer class
jerminatorhits Mar 2, 2019
06ee2ae
use explicit functions for modal
jerminatorhits Mar 4, 2019
ee49f08
swap pointer class with a tag + prevent default
jerminatorhits Mar 4, 2019
a822732
add click anywhere to close modal
jerminatorhits Mar 4, 2019
1eb140d
localize event.preventDefault and openModal methods
jerminatorhits Mar 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/shared/loading/Loading/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { Spinner } from 'reactstrap';

type Props = Partial<{
color: string; // 'primary'
className?: string;
height: string; // '8rem'
type: string; // 'grow' or 'border'
width: string; // '8rem'
}>;

const Loading = (props: Props) => {
const { color, height, type, width } = props;
const { className, color, height, type, width } = props;
const style = {
height: height || '3rem',
width: width || '3rem',
};

return <Spinner color={color || 'primary'} style={style} type={type || 'grow'} />;
return <Spinner className={`${className || ''}`.trim()} color={color || 'primary'} style={style} type={type || 'grow'} />;
};

export default Loading;
89 changes: 89 additions & 0 deletions src/components/work/CancelBookingModal.tsx/CancelBookingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import { ModalHeader, Modal, ModalBody, ModalFooter, Button } from 'reactstrap';
import { GUEST_CANCEL_BOOKING, Booking, Currency, GET_GUEST_SORTED_BOOKINGS } from 'networking/bookings';
import { graphql, compose } from 'react-apollo';
import { differenceInDays } from 'date-fns';
import { cancel, loadWeb3 } from 'utils/web3';
import { AlertProperties } from 'components/work/Alert/Alert';
import LoadingPortal from 'components/work/LoadingPortal';
import { getFriendlyErrorMessage } from 'utils/validators';

interface Props {
booking: Booking;
cancelBooking: (booking: Booking) => Promise<Booking>;
onModalAction: () => void;
setAlert: (alert: AlertProperties) => void;
}

function CancelBookingModal({ booking, cancelBooking, onModalAction, setAlert }: Props) {
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);

if (isSubmitting) {
return <LoadingPortal currency={booking.currency} />;
}

return (
<Modal isOpen toggle={() => !isSubmitting && onModalAction}>
<ModalHeader>Cancel Booking</ModalHeader>
<ModalBody>
<h6>Are you sure you want to cancel this booking?</h6>
<h6>Booking: {booking.id}</h6>
</ModalBody>
<ModalFooter>
<Button color="secondary" disabled={isSubmitting} onClick={onModalAction}>Back</Button>{' '}
<Button color="danger" disabled={isSubmitting} onClick={handleCancelBooking}>Yes, Cancel Booking</Button>
</ModalFooter>
</Modal>
);

function handleCancelBooking() {
setSubmitting(true);
cancelBooking(booking)
.then(() => {
setAlert({
color: 'success',
msg: 'Your booking has been cancelled',
show: true,
});
})
.catch((error: Error) => {
setAlert({
color: 'danger',
msg: `There was an error processing your request. ${getFriendlyErrorMessage(error)}`,
show: true,
});
})
.finally(() => {
setSubmitting(false)
onModalAction();
});
};
}

export default compose(
graphql(GUEST_CANCEL_BOOKING, {
props: ({ mutate }: any) => ({
cancelBooking: async (booking: Booking) => {
const { id, currency, checkInDate, status } = booking;
const days = differenceInDays(checkInDate, Date.now());
if (currency === Currency.BEE && days >= 7 && status === 'guest_paid') {
const web3 = loadWeb3();
await cancel(web3.eth, id);
}
return mutate({
variables: { id },
refetchQueries: [{ query: GET_GUEST_SORTED_BOOKINGS }],
update: (store: any, { data: guestCancelBooking }: any) => {
if (!store.data.data.ROOT_QUERY || !store.data.data.ROOT_QUERY.allBookings) {
return;
}
const { allBookings } = store.readQuery({ query: GET_GUEST_SORTED_BOOKINGS });
const index = allBookings.findIndex((booking: Booking) => booking.id === id);
allBookings[index].status = guestCancelBooking.status;
store.writeQuery({ query: GET_GUEST_SORTED_BOOKINGS, data: allBookings });
},
});
},
}),
})
)(CancelBookingModal);
1 change: 1 addition & 0 deletions src/components/work/CancelBookingModal.tsx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CancelBookingModal';
162 changes: 162 additions & 0 deletions src/components/work/ContactHostFormModal/ContactHostFormModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as React from 'react';
import { Formik, Field, Form as FormikForm } from 'formik';
import * as Yup from 'yup';
import { compose, graphql } from 'react-apollo';

import { Booking } from 'networking/bookings';
import { CONTACT_USER, ContactUserField, User } from 'networking/users';
import { Button, Form, FormGroup, Label, FormFeedback, Input, ModalFooter, ModalBody, Modal, ModalHeader } from 'reactstrap';
import Textarea from 'components/shared/Textarea';
import { TextareaEvent } from 'components/shared/Textarea/Textarea';
import Loading from 'components/shared/loading/Loading';

interface Props {
contactUser: (input: ContactUserInput) => Promise<EmailResponse>;
booking: Booking;
onModalAction: () => void;
}

interface FormValues {
[name: string]: string;
}

interface ContactUserInput {
bookingId?: string;
listingId?: string;
message: string;
recipientId: string;
subject: string;
}

interface EmailResponse {
bookingId: string;
listingId: string;
message: string;
recipient: User;
subject: string;
}

const defaultValues: FormValues = {
[ContactUserField.SUBJECT]: '',
[ContactUserField.MESSAGE]: '',
};

const ContactHostSchema = Yup.object({
subject: Yup.string().required('Please fill out the subject field.'),
message: Yup.string().required('Please fill out the message field.'),
});

function ContactHostForm({ booking, contactUser, onModalAction }: Props) {
const [successMessage, setSuccessMessage] = React.useState<string>('');
const [errorMessage, setErrorMessage] = React.useState<string>('');
const { host, listingId, id } = booking;

if (successMessage) {
return (
<Modal isOpen toggle={onModalAction}>
<ModalHeader>Message Successfully Sent</ModalHeader>
<ModalBody>
<p>{successMessage}</p>
</ModalBody>
<ModalFooter>
<Button color="success" onClick={onModalAction}>
Okay
</Button>
</ModalFooter>
</Modal>
);
}
return (
<Formik
initialValues={defaultValues}
isInitialValid
validationSchema={ContactHostSchema}
onSubmit={({ message, subject }, actions) => {
const input = {
bookingId: id,
listingId,
message,
recipientId: host.id,
subject,
};
return contactUser(input)
.then((response: any) => {
const emailResponse: EmailResponse = response.data.contactUser;
const { subject, recipient } = emailResponse || { subject: '', recipient: { firstName: 'the host' } };
const success = `Your message ${subject ? `"${subject}" ` : ' '}was sent to ${recipient.firstName}.`;
setSuccessMessage(success);
})
.catch((error: Error) => {
console.error(error);
setErrorMessage(`${error.message}. If this continues to occur, please contact us at support@beenest.com`);
})
.finally(() => actions.setSubmitting(false));
}}
>
{({ errors, isSubmitting, setFieldTouched, setFieldValue, touched, values }) => (
<Modal isOpen toggle={onModalAction}>
<Form tag={FormikForm}>
<ModalHeader>Contact {host.firstName || 'Host'}</ModalHeader>
<ModalBody>
<FormGroup>
<Label for={ContactUserField.SUBJECT}>Subject</Label>
<Input
id={ContactUserField.SUBJECT}
invalid={!!errors.subject && !!touched.subject}
name={ContactUserField.SUBJECT}
placeholder="First name"
tag={Field}
type="text"
/>
<FormFeedback>{errors.subject}</FormFeedback>
</FormGroup>

<FormGroup>
<Label for={ContactUserField.MESSAGE}>Message</Label>
<Textarea
className={`form-control${errors.message && touched.message ? ' is-invalid' : ''}`}
html
name={ContactUserField.MESSAGE}
onBlur={() => setFieldTouched(ContactUserField.MESSAGE, true)}
onChange={(event: TextareaEvent) => {
setFieldValue(ContactUserField.MESSAGE, event.target.value);
}}
placeholder="Type in your message here"
textareaHeight="164px"
value={values.message}
/>
<FormFeedback>{errors.message}</FormFeedback>
</FormGroup>
</ModalBody>

<ModalFooter>
{errorMessage && <FormFeedback className="d-block">{errorMessage.slice(0, 150)}</FormFeedback>}
<Button
color="secondary"
className="d-flex align-items-center justify-content-center"
disabled={isSubmitting}
style={{ width: '180px' }} // TODO: Make button full width for mobile
type="submit">
{isSubmitting
? <Loading height="1.5rem" width="1.5rem" />
: 'Send Message'}
</Button>
</ModalFooter>
</Form>
</Modal>
)}
</Formik>
);
}

export default compose(
graphql(CONTACT_USER, {
props: ({ mutate }: any) => ({
contactUser: (input: ContactUserInput) => {
return mutate({
variables: { input },
});
},
}),
})
)(ContactHostForm);
1 change: 1 addition & 0 deletions src/components/work/ContactHostFormModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ContactHostFormModal';
40 changes: 40 additions & 0 deletions src/components/work/LoadingPortal/LoadingPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';

import { Currency } from 'networking/bookings';
import Loading from 'components/shared/loading/Loading';
import { Container, Modal } from 'reactstrap';
import { VIEWPORT_CENTER_LAYOUT } from 'styled/sharedClasses/layout';

interface Props {
currency?: Currency | null;
message?: string;
}

const SUPPORT_EMAIL = 'support@beenest.com';
const ContactSupport = (): JSX.Element => <p>If you have any issues, please contact support at {SUPPORT_EMAIL}.</p>;

export default ({ currency, message }: Props) => (
<Modal isOpen className={VIEWPORT_CENTER_LAYOUT}>
<Container className="text-center p-6">
<Loading className="mb-4" height="6rem" width="6rem" />
{currency === (Currency.USD || Currency.BTC)
?
<>
<h2>Processing request...</h2>
{!!message && <p className="mb-0">{message}</p>}
<p className="mb-0">Please wait while we process your request.</p>
<p className="mb-0">This may take up to 30 seconds to complete.</p>
<ContactSupport />
</>
:
<>
<h2>Processing transaction...</h2>
{!!message && <p className="mb-0">{message}</p>}
<p className="mb-0">Please Confirm this transaction with your wallet (e.g. MetaMask).</p>
<p className="mb-0">Crypto transactions may take up to 30 seconds to complete.</p>
<ContactSupport />
</>
}
</Container>
</Modal>
);
1 change: 1 addition & 0 deletions src/components/work/LoadingPortal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './LoadingPortal';
Loading