Work on forms (#76)

* Work on connecting forms
This commit is contained in:
Fede Álvarez 2022-06-15 19:03:44 +02:00 committed by GitHub
parent da0130db58
commit 08c938de76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 568 additions and 249 deletions

View File

@ -22,6 +22,7 @@
"@graphql-codegen/typescript-graphql-request": "^4.4.5",
"@graphql-codegen/typescript-operations": "^2.3.5",
"@juggle/resize-observer": "^3.3.1",
"@notionhq/client": "^1.0.4",
"@radix-ui/react-polymorphic": "^0.0.14",
"@reach/dialog": "^0.17.0",
"@types/lodash": "^4.14.182",
@ -52,7 +53,8 @@
"react-use-measure": "^2.1.1",
"react-youtube": "^9.0.2",
"sharp": "^0.30.4",
"tiny-json-http": "^7.4.2"
"tiny-json-http": "^7.4.2",
"zod": "^3.17.3"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.6.2",

View File

@ -5,6 +5,7 @@
margin-top: 0;
display: flex;
place-content: center;
padding-bottom: tovw(80px, 'default', 80px);
}
position: relative;
@ -49,87 +50,113 @@
}
}
form {
display: flex;
flex-direction: column;
.form {
position: relative;
height: fit-content;
> div:first-child {
> div {
@include respond-to('mobile') {
display: flex;
flex-direction: column;
left: 0;
bottom: tovw(-75px, 'mobile', -165px);
font-size: tovw(8px, 'mobile', 12px);
width: 90%;
text-align: center;
margin: auto;
}
display: grid;
margin-top: tovw(60px, 'default', 40px);
grid-template-columns: repeat(2, 1fr);
gap: tovw(40px, 'default', 25px);
bottom: 0;
right: 0;
text-align: end;
position: absolute;
width: tovw(245px, 'default', 80px);
color: var(--color-white);
font-size: tovw(14px, 'default', 9px);
text-transform: uppercase;
font-family: var(--font-dm-mono);
}
label {
form {
display: flex;
flex-direction: column;
font-family: var(--font-tt-hoves);
font-size: tovw(30px, 'default', 18px);
input,
textarea,
select {
appearance: none;
width: 100%;
font-size: tovw(24px, 'default', 18px);
padding: tovw(16px, 'default', 12px) tovw(12px, 'default', 10px);
margin-top: tovw(20px, 'default', 16px);
background: rgb(142 142 142 / 0.1);
border: tovw(1px, 'default', 1px) solid var(--color-grey);
border-radius: tovw(8px, 'default', 8px);
&:focus {
border: tovw(1px, 'default', 1px) solid var(--color-accent);
background: rgb(0 0 244 / 0.1);
transition: all 250ms;
> div:first-child {
@include respond-to('mobile') {
display: flex;
flex-direction: column;
}
&::placeholder {
color: var(--color-grey-light);
display: grid;
margin-top: tovw(60px, 'default', 40px);
grid-template-columns: repeat(2, 1fr);
gap: tovw(40px, 'default', 25px);
}
label {
display: flex;
flex-direction: column;
font-family: var(--font-tt-hoves);
font-size: tovw(30px, 'default', 18px);
input,
textarea,
select {
appearance: none;
width: 100%;
font-size: tovw(24px, 'default', 18px);
padding: tovw(16px, 'default', 12px) tovw(12px, 'default', 10px);
margin-top: tovw(20px, 'default', 16px);
background: rgb(142 142 142 / 0.1);
border: tovw(1px, 'default', 1px) solid var(--color-grey);
border-radius: tovw(8px, 'default', 8px);
&:focus {
border: tovw(1px, 'default', 1px) solid var(--color-accent);
background: rgb(0 0 244 / 0.1);
transition: all 250ms;
}
&::placeholder {
color: var(--color-grey-light);
}
option {
background-color: black;
opacity: 50%;
border: none;
color: var(--color-white);
}
}
option {
background-color: black;
opacity: 50%;
border: none;
color: var(--color-white);
select {
background: url('/images/dropdown.svg') no-repeat 95% 50%;
background-color: rgb(142 142 142 / 0.1);
background-size: tovw(20px, 'default', 16px);
&:invalid {
color: var(--color-grey-light);
}
}
textarea {
resize: none;
}
}
select {
background: url('/images/dropdown.svg') no-repeat 95% 50%;
background-color: rgb(142 142 142 / 0.1);
background-size: tovw(20px, 'default', 16px);
> label:nth-child(2) {
margin-top: tovw(60px, 'default', 35px);
}
&:invalid {
color: var(--color-grey-light);
button {
@include respond-to('mobile') {
width: 100%;
font-size: tovw(18px, 'default', 18px);
}
width: fit-content;
align-self: center;
font-size: tovw(18px, 'default', 14px);
margin-top: tovw(50px, 'default', 35px);
}
textarea {
resize: none;
}
}
> label:nth-child(2) {
margin-top: tovw(60px, 'default', 35px);
}
button {
@include respond-to('mobile') {
width: 100%;
font-size: tovw(18px, 'default', 18px);
}
width: fit-content;
align-self: center;
font-size: tovw(18px, 'default', 14px);
margin-top: tovw(50px, 'default', 35px);
}
}

View File

@ -6,9 +6,10 @@ import Section from '~/components/layout/section'
import { Button } from '~/components/primitives/button'
import Heading from '~/components/primitives/heading'
// import { notion } from '~/lib/notion'
import s from './form.module.scss'
interface Props {
interface DataProps {
data: {
formHeading: string
formWarning: string
@ -26,9 +27,111 @@ interface Props {
}
}
const Form = ({ data }: Props) => {
const [selectedOption, setSelectedOption] = useState<any>(null)
type FormProps = {
data: DataProps['data']
style?: React.CSSProperties
}
const CustomForm = ({ data }: FormProps) => {
const [selectedOption, setSelectedOption] = useState<any>(null)
const [email, setEmail] = useState('')
const [text, setText] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const [isFormSent, setIsFormSent] = useState(false)
const [isSending, setIsSending] = useState(false)
const handleSubmit = async (e: any) => {
e.preventDefault()
try {
setErrorMsg('')
if (!selectedOption?.value) {
setErrorMsg('Inquiry is required')
return
}
setIsSending(true)
const data = {
email: email,
message: text,
inquiry: selectedOption.value
}
await fetch('/api/contact', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
setIsSending(false)
setIsFormSent(true)
} catch (error) {
// console.log(error)
}
}
return (
<div className={s.form}>
{isSending && <div>sending...</div>}
{errorMsg !== '' && (
<div dangerouslySetInnerHTML={{ __html: errorMsg }} />
)}
{isFormSent && errorMsg === '' && (
<div>Thanks for reaching out! We'll contact you shortly.</div>
)}
<form action="/" onSubmit={(e) => handleSubmit(e)}>
<div>
<label htmlFor="partnership">
{data?.formLabelPartner}
<Select
instanceId={'Contact'}
className="select"
classNamePrefix="select"
placeholder={data?.formPlaceholderPartner}
value={selectedOption}
onChange={setSelectedOption}
options={data?.formSelectOptions}
/>
</label>
<label htmlFor="email">
{data?.formLabelEmail}
<input
placeholder={data?.formPlaceholderEmail}
type="email"
name="email"
id="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
</div>
<label htmlFor="message">
{data?.formLabelMsg}
<textarea
placeholder={data?.formPlaceholderMsg}
name="message"
id="message"
rows={5}
required
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
</label>
<Button size="medium" variant="primary">
{data?.formLabelButton}
</Button>
</form>
</div>
)
}
const Form = ({ data }: DataProps) => {
return (
<Section className={s['section']} id="contactform">
<Container className={s['container']}>
@ -38,45 +141,7 @@ const Form = ({ data }: Props) => {
</Heading>
<span>{data?.formWarning}</span>
</div>
<form action="/">
<div>
<label htmlFor="partnership">
{data?.formLabelPartner}
<Select
instanceId={'Contact'}
className="select"
classNamePrefix="select"
placeholder={data?.formPlaceholderPartner}
value={selectedOption}
onChange={setSelectedOption}
options={data?.formSelectOptions}
/>
</label>
<label htmlFor="email">
{data?.formLabelEmail}
<input
placeholder={data?.formPlaceholderEmail}
type="email"
name="email"
id="email"
required
/>
</label>
</div>
<label htmlFor="message">
{data?.formLabelMsg}
<textarea
placeholder={data?.formPlaceholderMsg}
name="message"
id="message"
rows={5}
required
></textarea>
</label>
<Button size="medium" variant="primary">
{data?.formLabelButton}
</Button>
</form>
<CustomForm data={data} />
<div className={s['gradient']} />
</Container>
</Section>

View File

@ -111,88 +111,114 @@
}
}
form {
display: flex;
flex-direction: column;
.form {
position: relative;
height: fit-content;
> div:first-child {
> div {
@include respond-to('mobile') {
display: flex;
flex-direction: column;
left: 0;
bottom: tovw(-75px, 'mobile', -165px);
font-size: tovw(8px, 'mobile', 12px);
width: 90%;
text-align: center;
margin: auto;
}
display: grid;
margin-top: tovw(60px, 'default', 40px);
grid-template-columns: repeat(2, 1fr);
gap: tovw(40px, 'default', 25px);
bottom: 0;
right: 0;
text-align: end;
position: absolute;
width: tovw(250px, 'default', 80px);
color: var(--color-white);
font-size: tovw(14px, 'default', 9px);
text-transform: uppercase;
font-family: var(--font-dm-mono);
}
label {
form {
display: flex;
flex-direction: column;
justify-content: space-between;
font-family: var(--font-tt-hoves);
font-size: tovw(30px, 'default', 18px);
input,
textarea,
select {
appearance: none;
width: 100%;
font-size: tovw(24px, 'default', 18px);
padding: tovw(16px, 'default', 12px) tovw(12px, 'default', 10px);
margin-top: tovw(20px, 'default', 16px);
background: rgb(142 142 142 / 0.1);
border: tovw(1px, 'default', 1px) solid var(--color-grey);
border-radius: tovw(8px, 'default', 8px);
&:focus {
border: tovw(1px, 'default', 1px) solid var(--color-accent);
background: rgb(0 0 244 / 0.1);
transition: all 250ms;
> div:first-child {
@include respond-to('mobile') {
display: flex;
flex-direction: column;
}
&::placeholder {
color: var(--color-grey-light);
display: grid;
margin-top: tovw(60px, 'default', 40px);
grid-template-columns: repeat(2, 1fr);
gap: tovw(40px, 'default', 25px);
}
label {
display: flex;
flex-direction: column;
justify-content: space-between;
font-family: var(--font-tt-hoves);
font-size: tovw(30px, 'default', 18px);
input,
textarea,
select {
appearance: none;
width: 100%;
font-size: tovw(24px, 'default', 18px);
padding: tovw(16px, 'default', 12px) tovw(12px, 'default', 10px);
margin-top: tovw(20px, 'default', 16px);
background: rgb(142 142 142 / 0.1);
border: tovw(1px, 'default', 1px) solid var(--color-grey);
border-radius: tovw(8px, 'default', 8px);
&:focus {
border: tovw(1px, 'default', 1px) solid var(--color-accent);
background: rgb(0 0 244 / 0.1);
transition: all 250ms;
}
&::placeholder {
color: var(--color-grey-light);
}
option {
background-color: black;
opacity: 50%;
border: none;
color: var(--color-white);
}
}
option {
background-color: black;
opacity: 50%;
border: none;
color: var(--color-white);
select {
background: url('/images/dropdown.svg') no-repeat 95% 52%;
background-color: rgb(142 142 142 / 0.1);
background-size: tovw(20px, 'default', 16px);
&:invalid {
color: var(--color-grey-light);
}
}
textarea {
resize: none;
}
}
select {
background: url('/images/dropdown.svg') no-repeat 95% 52%;
background-color: rgb(142 142 142 / 0.1);
background-size: tovw(20px, 'default', 16px);
> label:nth-child(2) {
margin-top: tovw(60px, 'default', 35px);
}
&:invalid {
color: var(--color-grey-light);
button {
@include respond-to('mobile') {
width: 100%;
font-size: tovw(18px, 'default', 18px);
}
width: fit-content;
align-self: center;
font-size: tovw(18px, 'default', 14px);
margin-top: tovw(50px, 'default', 35px);
}
textarea {
resize: none;
}
}
> label:nth-child(2) {
margin-top: tovw(60px, 'default', 35px);
}
button {
@include respond-to('mobile') {
width: 100%;
font-size: tovw(18px, 'default', 18px);
}
width: fit-content;
align-self: center;
font-size: tovw(18px, 'default', 14px);
margin-top: tovw(50px, 'default', 35px);
}
}

View File

@ -9,7 +9,7 @@ import Heading from '~/components/primitives/heading'
import s from './contact.module.scss'
interface Props {
interface DataProps {
data: {
contactHeading: string
contactDescription: string
@ -37,9 +37,168 @@ interface Props {
}
}
const Contact = ({ data }: Props) => {
type FormProps = {
data: DataProps['data']
}
const CustomForm = ({ data }: FormProps) => {
const [selectedOption, setSelectedOption] = useState<any>(null)
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [role, setRole] = useState('')
const [company, setCompany] = useState('')
const [jurisdiction, setJurisdiction] = useState('')
const [text, setText] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const [isFormSent, setIsFormSent] = useState(false)
const [isSending, setIsSending] = useState(false)
const handleSubmit = async (e: any) => {
e.preventDefault()
try {
setErrorMsg('')
if (!selectedOption?.value) {
setErrorMsg('Inquiry is required')
return
}
setIsSending(true)
const data = {
name: name,
email: email,
message: text,
inquiry: selectedOption.value,
jurisdiction: jurisdiction,
company: company,
role: role
}
await fetch('/api/inquiry', {
method: 'POST',
mode: 'no-cors',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
})
setIsSending(false)
setIsFormSent(true)
} catch (error) {
// console.log(error)
}
}
return (
<div className={s.form}>
{isSending && <div>sending...</div>}
{errorMsg !== '' && (
<div dangerouslySetInnerHTML={{ __html: errorMsg }} />
)}
{isFormSent && errorMsg === '' && (
<div>Thanks for reaching out! We'll contact you shortly.</div>
)}
<form action="/" onSubmit={(e) => handleSubmit(e)}>
<div>
<label htmlFor="name">
{data?.contactNameLabel}
<input
placeholder={data?.contactNamePlaceholder}
type="text"
name="name"
id="name"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label htmlFor="email">
{data?.contactEmailLabel}
<input
placeholder={data?.contactEmailPlaceholder}
type="email"
name="email"
id="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
<label htmlFor="role">
{data?.contactRoleLabel}
<input
placeholder={data?.contactRolePlaceholder}
type="text"
name="role"
id="role"
required
value={role}
onChange={(e) => setRole(e.target.value)}
/>
</label>
<label htmlFor="company">
{data?.contactCompanyLabel}
<input
placeholder={data?.contactCompanyPlaceholder}
type="text"
name="company"
id="company"
required
value={company}
onChange={(e) => setCompany(e.target.value)}
/>
</label>
<label htmlFor="jurisdiction">
{data?.contactLegalLabel}
<input
placeholder={data?.contactLegalPlaceholder}
type="text"
name="jurisdiction"
id="jurisdiction"
required
value={jurisdiction}
onChange={(e) => setJurisdiction(e.target.value)}
/>
</label>
<label htmlFor="partnership">
{data?.contactInquiryLabel}
<Select
instanceId={'Partners'}
className="select"
classNamePrefix="select"
placeholder={data?.contactInquiryPlaceholder}
value={selectedOption}
onChange={setSelectedOption}
options={data?.contactInquiryOptions}
/>
</label>
</div>
<label htmlFor="message">
{data?.contactMsgLabel}
<textarea
placeholder={data?.contactMsgPlaceholder}
name="message"
id="message"
rows={5}
required
value={text}
onChange={(e) => setText(e.target.value)}
></textarea>
</label>
<Button size="medium" variant="primary">
{data?.contactButtonLabel}
</Button>
</form>
</div>
)
}
const Contact = ({ data }: DataProps) => {
return (
<Section className={s['section']}>
<Container className={s['container']}>
@ -57,85 +216,7 @@ const Contact = ({ data }: Props) => {
</Heading>
<span>{data?.contactFormWarning}</span>
</div>
<form action="/">
<div>
<label htmlFor="name">
{data?.contactNameLabel}
<input
placeholder={data?.contactNamePlaceholder}
type="text"
name="name"
id="name"
required
/>
</label>
<label htmlFor="email">
{data?.contactEmailLabel}
<input
placeholder={data?.contactEmailPlaceholder}
type="email"
name="email"
id="email"
required
/>
</label>
<label htmlFor="role">
{data?.contactRoleLabel}
<input
placeholder={data?.contactRolePlaceholder}
type="text"
name="role"
id="role"
required
/>
</label>
<label htmlFor="company">
{data?.contactCompanyLabel}
<input
placeholder={data?.contactCompanyPlaceholder}
type="text"
name="company"
id="company"
required
/>
</label>
<label htmlFor="jurisdiction">
{data?.contactLegalLabel}
<input
placeholder={data?.contactLegalPlaceholder}
type="text"
name="jurisdiction"
id="jurisdiction"
required
/>
</label>
<label htmlFor="partnership">
{data?.contactInquiryLabel}
<Select
instanceId={'Partners'}
className="select"
classNamePrefix="select"
placeholder={data?.contactInquiryPlaceholder}
value={selectedOption}
onChange={setSelectedOption}
options={data?.contactInquiryOptions}
/>
</label>
</div>
<label htmlFor="message">
{data?.contactMsgLabel}
<textarea
placeholder={data?.contactMsgPlaceholder}
name="message"
id="message"
rows={5}
required
></textarea>
</label>
<Button size="medium" variant="primary">
{data?.contactButtonLabel}
</Button>
</form>
<CustomForm data={data} />
</div>
<div className={s['gradient']} />
</Container>

9
src/lib/notion/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { Client } from '@notionhq/client'
export const notion = new Client({
auth: process.env.NOTION_TOKEN
})
export const notionAlt = new Client({
auth: process.env.NOTION_TOKEN_ALT
})

33
src/pages/api/contact.ts Normal file
View File

@ -0,0 +1,33 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { internalServerError, success } from '../../lib/api-responses'
import { notion } from '../../lib/notion'
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { email, message, inquiry } = JSON.parse(req.body)
await notion.pages.create({
parent: {
database_id: '03c225f0469d436db60bd1b225deffef' as string
},
properties: {
Email: {
email,
type: 'email'
},
Message: {
type: 'rich_text',
rich_text: [{ text: { content: message }, type: 'text' }]
},
'Type of inquiry': {
type: 'select',
select: { name: inquiry }
}
}
})
success(res)
} catch (error) {
internalServerError(res, error)
}
}

55
src/pages/api/inquiry.ts Normal file
View File

@ -0,0 +1,55 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { internalServerError, success } from '../../lib/api-responses'
import { notionAlt } from '../../lib/notion'
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { name, email, message, inquiry, role, company, jurisdiction } =
JSON.parse(req.body)
await notionAlt.pages.create({
parent: {
database_id: '90a7d71e342f4b91b7ca263a14d839ea' as string
},
properties: {
Name: {
title: [
{
text: {
content: name
}
}
]
},
Email: {
email,
type: 'email'
},
Message: {
type: 'rich_text',
rich_text: [{ text: { content: message }, type: 'text' }]
},
Role: {
type: 'rich_text',
rich_text: [{ text: { content: role }, type: 'text' }]
},
Company: {
type: 'rich_text',
rich_text: [{ text: { content: company }, type: 'text' }]
},
'Legal jurisdiction': {
type: 'rich_text',
rich_text: [{ text: { content: jurisdiction }, type: 'text' }]
},
Inquiry: {
type: 'select',
select: { name: inquiry }
}
}
})
success(res)
} catch (error) {
internalServerError(res, error)
}
}

View File

@ -1101,6 +1101,14 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@notionhq/client@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-1.0.4.tgz#405e9468576baf81019db4d791f4b52d091f4a57"
integrity sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==
dependencies:
"@types/node-fetch" "^2.5.10"
node-fetch "^2.6.1"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@ -1216,6 +1224,14 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node-fetch@^2.5.10":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "17.0.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
@ -6674,3 +6690,8 @@ youtube-player@5.5.2:
debug "^2.6.6"
load-script "^1.0.0"
sister "^3.0.0"
zod@^3.17.3:
version "3.17.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba"
integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg==