-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from 13 commits
40d039a
de517e9
e29479f
198d8ba
2e65a28
1e95308
4e30cac
eec8176
58cd299
21144be
8b558bb
18296c1
5992a51
ec1a363
d223c69
2d83b97
f3c054b
97114a6
c0caa2b
62c2eb9
06ee2ae
ee49f08
a822732
1eb140d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
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>; | ||
handleModalAction: () => void; | ||
setAlert: (alert: AlertProperties) => void; | ||
} | ||
|
||
function CancelBookingModal({ booking, cancelBooking, handleModalAction, setAlert }: Props) { | ||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false); | ||
const [currency, setCurrency] = React.useState<Currency | null>(Currency.USD); | ||
|
||
if (isSubmitting) { | ||
return <LoadingPortal currency={currency} />; | ||
} | ||
|
||
return ( | ||
<Modal isOpen toggle={() => !isSubmitting && handleModalAction}> | ||
<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={handleModalAction}>Back</Button>{' '} | ||
<Button color="danger" disabled={isSubmitting} onClick={handleCancelBooking}>Yes, Cancel Booking</Button> | ||
</ModalFooter> | ||
</Modal> | ||
); | ||
|
||
function handleCancelBooking() { | ||
setCurrency(booking.currency); | ||
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) | ||
handleModalAction(); | ||
}); | ||
}; | ||
} | ||
|
||
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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './CancelBookingModal'; |
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; | ||
handleModalAction: () => void; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above, I'd expect this to be named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 done |
||
} | ||
|
||
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, handleModalAction }: Props) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yay, this will come in handy for the Contact Host button on the listing page There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎉 |
||
const [successMessage, setSuccessMessage] = React.useState<string>(''); | ||
const [errorMessage, setErrorMessage] = React.useState<string>(''); | ||
const { host, listingId, id } = booking; | ||
|
||
if (successMessage) { | ||
return ( | ||
<Modal isOpen toggle={handleModalAction}> | ||
<ModalHeader>Message Successfully Sent</ModalHeader> | ||
<ModalBody> | ||
<p>{successMessage}</p> | ||
</ModalBody> | ||
<ModalFooter> | ||
<Button color="success" onClick={() => handleModalAction()}> | ||
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={handleModalAction}> | ||
<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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './ContactHostFormModal'; |
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> | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './LoadingPortal'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with React naming conventions, I'd expect this to be
onModalAction
at the interface level andhandleModalAction
at the implementation level, for usage like:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that was bugging me in the back of my head. I'll try to remember or refer back to this in the future. tyty!