-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
998401f
commit 5a4e0ef
Showing
7 changed files
with
225 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,6 @@ import { classNames } from '@oacore/design/lib/utils' | |
import styles from './styles.module.css' | ||
|
||
import { Button, TextField } from 'design' | ||
import { requestPremium } from 'texts/letters' | ||
import { withGlobalStore } from 'store' | ||
import Markdown from 'components/markdown' | ||
|
||
// TODO: Should be moved to @oacore/design | ||
const Textarea = ({ | ||
|
@@ -23,12 +20,20 @@ const Textarea = ({ | |
) | ||
|
||
const Form = ({ | ||
userName, | ||
userEmail, | ||
organisationName, | ||
organisationId, | ||
className, | ||
// data props | ||
fromEmail, | ||
fromName, | ||
subject, | ||
body, | ||
|
||
// event handlers | ||
onChange, | ||
onSubmit, | ||
onCancel, | ||
|
||
// html props | ||
children, | ||
className, | ||
...formProps | ||
}) => { | ||
// Showing name only when email is edited de | ||
|
@@ -37,26 +42,20 @@ const Form = ({ | |
// Showing message only when user decided to edit it | ||
const [showMessage, toggleMessage] = useState(false) | ||
|
||
const message = requestPremium.render({ | ||
userName, | ||
organisationName, | ||
organisationId, | ||
}) | ||
|
||
const handleMessageToggle = useCallback(() => toggleMessage(true), []) | ||
|
||
return ( | ||
<form | ||
className={classNames | ||
.use(styles.container, styles.backdrop) | ||
.join(className)} | ||
className={classNames.use(styles.container).join(className)} | ||
onSubmit={onSubmit} | ||
onChange={onChange} | ||
{...formProps} | ||
> | ||
<div className={styles.personal}> | ||
<TextField | ||
type="email" | ||
name="email" | ||
defaultValue={userEmail} | ||
defaultValue={fromEmail} | ||
autoComplete="email" | ||
label="Email" | ||
helper="We will reach you back using this email address" | ||
|
@@ -66,26 +65,22 @@ const Form = ({ | |
{showName ? ( | ||
<TextField | ||
name="name" | ||
defaultValue={userName} | ||
defaultValue={fromName} | ||
autoComplete="name" | ||
label="Name" | ||
tag="p" | ||
/> | ||
) : ( | ||
<p> | ||
<input type="hidden" name="name" value={userName} /> | ||
<input type="hidden" name="name" value={fromName} /> | ||
</p> | ||
)} | ||
</div> | ||
{showMessage ? ( | ||
<Textarea | ||
id="message" | ||
name="message" | ||
label="Message" | ||
defaultValue={message} | ||
/> | ||
<Textarea id="body" name="body" label="Message" defaultValue={body} /> | ||
) : ( | ||
<p> | ||
<input type="hidden" name="body" defaultValue={body} /> | ||
<Button | ||
variant="outlined" | ||
type="button" | ||
|
@@ -96,6 +91,8 @@ const Form = ({ | |
</p> | ||
)} | ||
|
||
{children} | ||
|
||
<Button variant="contained">Send</Button> | ||
{onCancel && ( | ||
<Button type="button" onClick={onCancel}> | ||
|
@@ -106,70 +103,4 @@ const Form = ({ | |
) | ||
} | ||
|
||
const FormController = withGlobalStore(({ store, ...formProps }) => { | ||
// editing, sending, success, failure | ||
const [status, changeStatus] = useState('editing') | ||
|
||
const handleSubmit = useCallback( | ||
(event) => { | ||
event.preventDefault() | ||
const formData = new FormData(event.target) | ||
const data = Object.fromEntries(formData.entries()) | ||
|
||
changeStatus('sending') | ||
store.sendContactRequest(data).then( | ||
(success) => { | ||
changeStatus(success ? 'success' : 'failure') | ||
}, | ||
() => { | ||
changeStatus('failure') | ||
} | ||
) | ||
}, | ||
[store] | ||
) | ||
|
||
return ( | ||
<Form | ||
action={new URL(store.contactUrl, process.env.API_URL)} | ||
method="post" | ||
actionStatus={status} | ||
userName={store.user.name} | ||
userEmail={store.user.email} | ||
organisationName={store.organisation.name} | ||
organisationId={store.organisation.id} | ||
onSubmit={handleSubmit} | ||
{...formProps} | ||
/> | ||
) | ||
}) | ||
|
||
const RequestPremiumForm = ({ variant = 'button', template }) => { | ||
const [formShown, toggleForm] = useState(variant === 'form') | ||
const handleToggle = useCallback(() => toggleForm(!formShown), [formShown]) | ||
|
||
return ( | ||
<> | ||
{template && ( | ||
<Markdown> | ||
{template.render({ | ||
email: '[email protected]', | ||
mailtoUrl: `mailto:[email protected]?subject=${encodeURIComponent( | ||
'CORE Dashboard Premium for {{ organisation }}' | ||
)}&body=${encodeURIComponent( | ||
'Hello,\n\nI would like to request CORE Dashboard Premium for {{ organisationName }}\n\nSincerely,\nMatteo' | ||
)}`, | ||
})} | ||
</Markdown> | ||
)} | ||
<p hidden={formShown} style={{ marginBottom: 0 }}> | ||
<Button variant="contained" type="button" onClick={handleToggle}> | ||
Request premium | ||
</Button> | ||
</p> | ||
<FormController hidden={!formShown} onCancel={handleToggle} /> | ||
</> | ||
) | ||
} | ||
|
||
export default RequestPremiumForm | ||
export default Form |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { useEffect } from 'react' | ||
import { autorun, observable } from 'mobx' | ||
import { useObserver } from 'mobx-react-lite' | ||
|
||
import { requestPremium as letter } from 'texts/letters' | ||
|
||
const globalContext = observable({ | ||
user: {}, | ||
organisation: {}, | ||
}) | ||
|
||
const userInput = observable({ | ||
update(patch) { | ||
Object.assign(this, patch) | ||
}, | ||
}) | ||
|
||
const emailContext = observable({ | ||
toEmail: 'enterp\u0072is\u0065\u0040cor\u0065\u002e\u0061\u0063\u002euk', | ||
|
||
// These 4 are mirrored from the global store | ||
// with and React's effect and MobX's autorun in useSync() | ||
organisationId: '', | ||
organisationName: '', | ||
|
||
get fromName() { | ||
return userInput.name ?? globalContext.user?.name | ||
}, | ||
|
||
get fromEmail() { | ||
return userInput.email ?? globalContext.user?.email | ||
}, | ||
|
||
get context() { | ||
return { | ||
userName: this.fromName, | ||
userEmail: this.fromEmail, | ||
organisationName: globalContext.organisation.name, | ||
organisationId: globalContext.organisation.id, | ||
} | ||
}, | ||
|
||
get subject() { | ||
return userInput.subject ?? letter.subject.render(this.context) | ||
}, | ||
|
||
get body() { | ||
return userInput.body ?? letter.body.render(this.context) | ||
}, | ||
|
||
get mailtoUrl() { | ||
const { email, subject, body } = this | ||
const params = new URLSearchParams({ | ||
subject, | ||
body, | ||
}) | ||
return `m\u0061il\u0074\u006f\u003a${email}?${params}` | ||
}, | ||
|
||
update(...args) { | ||
userInput.update(...args) | ||
}, | ||
}) | ||
|
||
const useSync = (store) => | ||
useEffect( | ||
() => | ||
autorun(() => { | ||
// Observing on root `user` and `organisation`` objects is intentional. | ||
// It should prevent potential data overriding. | ||
// | ||
// However, it does not cover the case when the user changes the name | ||
// and then makes the request. | ||
globalContext.user = store.user | ||
globalContext.organisation = store.organisation | ||
|
||
emailContext.actionUrl = store.contactUrl | ||
emailContext.send = store.sendContactRequest | ||
}), | ||
[store] | ||
) | ||
|
||
const useFormAction = () => | ||
useObserver(() => [emailContext.actionUrl, emailContext.send]) | ||
|
||
const useFormContext = () => | ||
useObserver(() => ({ | ||
subject: emailContext.subject, | ||
body: emailContext.body, | ||
fromName: emailContext.fromName, | ||
fromEmail: emailContext.fromEmail, | ||
})) | ||
|
||
const useNoteContext = () => | ||
useObserver(() => ({ | ||
email: emailContext.email, | ||
mailtoUrl: emailContext.mailtoUrl, | ||
})) | ||
|
||
export default emailContext | ||
export { useFormAction, useFormContext, useNoteContext, useSync } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React, { useState, useCallback } from 'react' | ||
|
||
import data, { | ||
useFormAction, | ||
useFormContext, | ||
useNoteContext, | ||
useSync, | ||
} from './data' | ||
|
||
import { Button } from 'design' | ||
import Markdown from 'components/markdown' | ||
import Form from 'components/forms/request-premium' | ||
import { withGlobalStore } from 'store' | ||
|
||
const FormController = (formProps) => { | ||
// editing, sending, success, failure | ||
const [status, changeStatus] = useState('editing') | ||
|
||
const context = useFormContext() | ||
const [action, send] = useFormAction() | ||
|
||
const handleSubmit = useCallback( | ||
(event) => { | ||
event.preventDefault() | ||
const rawDataInput = new FormData(event.target) | ||
const dataInput = Object.fromEntries(rawDataInput.entries()) | ||
|
||
changeStatus('sending') | ||
send(dataInput).then( | ||
(success) => { | ||
changeStatus(success ? 'success' : 'failure') | ||
}, | ||
() => { | ||
changeStatus('failure') | ||
} | ||
) | ||
}, | ||
[send] | ||
) | ||
|
||
// Populate changes on blur only to prevent live editing in two places | ||
const handleChange = useCallback((event) => { | ||
const { name, value } = event.target | ||
data.update({ [name]: value }) | ||
}) | ||
|
||
return ( | ||
<Form | ||
action={action} | ||
method="post" | ||
status={status} | ||
{...context} | ||
onSubmit={handleSubmit} | ||
onBlur={handleChange} | ||
{...formProps} | ||
/> | ||
) | ||
} | ||
|
||
const BillingRequiredNote = ({ | ||
store, | ||
children, | ||
template, | ||
variant = 'button', | ||
}) => { | ||
const [formShown, toggleForm] = useState(variant === 'form') | ||
const handleToggle = useCallback(() => toggleForm(!formShown), [formShown]) | ||
const context = useNoteContext() | ||
|
||
useSync(store) | ||
|
||
return ( | ||
<> | ||
{template && <Markdown>{template.render(context)}</Markdown>} | ||
{children} | ||
<p hidden={formShown} style={{ marginBottom: 0 }}> | ||
<Button variant="contained" type="button" onClick={handleToggle}> | ||
Request premium | ||
</Button> | ||
</p> | ||
<FormController hidden={!formShown} onCancel={handleToggle} /> | ||
</> | ||
) | ||
} | ||
|
||
export default withGlobalStore(BillingRequiredNote) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,17 @@ | ||
import Template from '../template' | ||
import requestPremiumSrc from './request-premium.txt' | ||
|
||
const requestPremium = new Template( | ||
const subjectSrc = requestPremiumSrc.attributes.subject | ||
const bodySrc = requestPremiumSrc.body | ||
|
||
const requestPremium = { | ||
...requestPremiumSrc.attributes, | ||
|
||
subject: new Template(subjectSrc), | ||
|
||
// Removing '\n' from the end | ||
requestPremiumSrc.slice(0, requestPremiumSrc.length - 1) | ||
) | ||
body: new Template(bodySrc.slice(0, bodySrc.length - 1)), | ||
} | ||
|
||
export default { requestPremium } | ||
export { requestPremium } |
Oops, something went wrong.