Redesign Apply page

This commit is contained in:
Lachlan Campbell 2024-02-29 13:28:23 -05:00
parent 6dec10a43e
commit 4e1a96e84a
10 changed files with 234 additions and 315 deletions

View file

@ -1,8 +1,11 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { Box, Flex, Input, Text } from 'theme-ui'
import { Box, Card, Flex, Input, Text } from 'theme-ui'
import FlexCol from '../../flex-col'
import AutofillColourFix from './autofill-colour-fix'
import { geocode, search } from '../../../lib/fiscal-sponsorship/apply/address-validation'
import {
geocode,
search
} from '../../../lib/fiscal-sponsorship/apply/address-validation'
import Icon from '../../icon'
const approvedCountries = [
@ -127,13 +130,10 @@ export default function AutoComplete({ name, isPersonalAddressInput }) {
</Box>
</FlexCol>
{predictions && predictions.length > 0 && (
<Box
<Card
sx={{
background: '#47454f',
border: '1px solid #696675',
p: [3, 3],
width: '100%',
p: 3,
borderRadius: '4px',
position: 'absolute',
bottom: 'calc(100% + 0.5em)'
}}
@ -148,9 +148,8 @@ export default function AutoComplete({ name, isPersonalAddressInput }) {
cursor: 'pointer',
border: 'none',
background: 'none',
color: '#d1cbe7',
'&:hover': {
color: 'white'
color: 'blue'
},
fontFamily: 'inherit',
fontSize: 'inherit',
@ -172,7 +171,7 @@ export default function AutoComplete({ name, isPersonalAddressInput }) {
</>
))}
</FlexCol>
</Box>
</Card>
)}
</Box>
)

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { Box, Flex, Label, Text } from 'theme-ui'
import { Badge, Box, Flex, Label, Text } from 'theme-ui'
import FlexCol from '../../flex-col'
export default function Field({
@ -12,8 +12,7 @@ export default function Field({
children
}) {
const router = useRouter()
const isRequired =
requiredFields[parseInt(router.query.step) - 1].includes(name)
const isRequired = requiredFields.includes(name)
/* Fill in the field input element with the value from sessionStorage.
Note: the custom checkbox component does this in its own useEffect hook. */
@ -27,43 +26,47 @@ export default function Field({
}, [router.query, name])
return (
<FlexCol gap={2} width={'100%'}>
<Flex
<Flex
sx={{
flexDirection: col ? 'column' : 'row',
alignItems: col ? 'flex-start' : 'center',
gap: 1,
width: '100%',
'input, textarea': {
border: '1px solid',
borderColor: 'smoke',
outlineColor: 'blue'
}
}}
>
<Label
htmlFor={name}
sx={{
flexDirection: col ? 'column' : 'row',
alignItems: col ? 'flex-start' : 'center',
gap: 2
fontSize: 2,
flexDirection: 'row'
}}
>
<Flex sx={{ alignItems: 'center', gap: 2 }}>
<Label
htmlFor={name}
{label}
{isRequired && (
<Text
as="span"
sx={{
fontSize: 3,
width: 'fit-content'
color: 'red',
fontWeight: 'bold',
ml: 1
}}
title="Required"
>
{label}
</Label>
{isRequired && (
<Box
sx={{
backgroundColor: 'muted',
padding: '4px 6px',
borderRadius: '999px',
lineHeight: '1',
fontSize: 14
}}
>
Required
</Box>
)}
</Flex>
{children}
</Flex>
*
</Text>
)}
</Label>
{children}
{description && (
<Text sx={{ color: 'muted', fontSize: 1 }}>{description}</Text>
<Text as="p" variant="caption">
{description}
</Text>
)}
</FlexCol>
</Flex>
)
}

View file

@ -1,5 +1,5 @@
import { forwardRef } from 'react'
import { Box } from 'theme-ui'
import { Box, Container } from 'theme-ui'
const formContainer = forwardRef(({ children }, ref) => {
return (
@ -7,19 +7,25 @@ const formContainer = forwardRef(({ children }, ref) => {
ref={ref}
as="form"
sx={{
height: '100%',
width: ['100%', null, null, '50ch'],
flex: '1',
overflowY: ['none', null, null, 'auto'],
pr: [0, null, '2ch'],
pl: [0, null, 1],
pb: [0, null, 3],
display: 'flex',
flexDirection: 'column',
gap: 4
bg: 'snow',
px: [3, 5],
py: 5,
minHeight: '100dvb'
}}
>
{children}
<Container
variant="copy"
sx={{
ml: 0,
display: 'flex',
flexDirection: 'column',
columnGap: 4,
rowGap: 3,
px: 0
}}
>
{children}
</Container>
</Box>
)
})

View file

@ -1,89 +1,63 @@
import { Box, Flex, Link, Text } from 'theme-ui'
import { Box, Flex, Link, Text, Heading, Grid } from 'theme-ui'
import Icon from '../../icon'
import FlexCol from '../../flex-col'
export default function HCBInfo() {
return (
<Box>
<FlexCol gap={4}>
<FlexCol gap={4}>
<Text sx={{ fontSize: 36 }}>
What HCB <i>is</i>
</Text>
<FlexCol gap={3} ml={3}>
<FlexCol gap={2}>
<Flex sx={{ alignItems: 'center', gap: 2 }}>
<Link
color="white"
href="/fiscal-sponsorship/about"
target="_blank"
sx={{
fontSize: 3,
display: 'inline-flex',
alignItems: 'flex-end',
gap: 1
}}
>
A fiscal sponsor
<Icon glyph="external" />
</Link>
</Flex>
<Text sx={{ color: 'muted' }}>
<ul>
<li>Nonprofit status.</li>
<li>Tax-deductable donations.</li>
</ul>
</Text>
</FlexCol>
<FlexCol gap={2}>
<Text sx={{ fontSize: 3 }}>A financial platform</Text>
<Text sx={{ color: 'muted' }}>
<ul>
<li>A donations page and invoicing system.</li>
<li>Transfer money electronically.</li>
<li>Order cards for you and your team to make purchases.</li>
</ul>
</Text>
</FlexCol>
</FlexCol>
</FlexCol>
<FlexCol gap={4}>
<Text sx={{ fontSize: 36 }}>
What HCB <i>is not</i>
</Text>
<FlexCol gap={3} ml={3}>
<FlexCol gap={2}>
<Text sx={{ fontSize: 3 }}>
A bank!{' '}
<Text sx={{ color: 'muted', fontSize: 2 }}>(we're better)</Text>
</Text>
<Text sx={{ color: 'muted' }}>
<ul>
<li>
Rather than setting up a standard bank account, you'll get a
restricted fund within Hack Club accounts.
</li>
<li>
You can't deposit or withdraw cash. But you can receive any
kind of electronic payment!
</li>
</ul>
</Text>
</FlexCol>
<FlexCol gap={2}>
<Text sx={{ fontSize: 3 }}>For-profit</Text>
<Text sx={{ color: 'muted' }}>
<ul>
<li>
If youre a for-profit entity, then HCB is not for you.
Consider setting up a business.
</li>
</ul>
</Text>
</FlexCol>
</FlexCol>
</FlexCol>
</FlexCol>
<Box
sx={{
gridArea: 'info',
alignItems: 'start',
mark: { color: '#ec555c', bg: 'inherit' },
'ul, p': { pl: 0, color: 'muted', mb: 4 }
}}
>
<Heading variant="subheadline">
HCB is a{' '}
<Link
href="/fiscal-sponsorship/about"
target="_blank"
sx={{
display: 'inline-flex',
alignItems: 'flex-end',
gap: 1
}}
>
fiscal sponsor
<Icon glyph="external" size={24} />
</Link>
</Heading>
<ul>
<li>Nonprofit status.</li>
<li>Tax-deductable donations.</li>
</ul>
<Heading variant="subheadline">
HCB provides a financial platform.
</Heading>
<ul>
<li>A donations page and invoicing system.</li>
<li>Transfer money electronically.</li>
<li>Order cards for you and your team to make purchases.</li>
</ul>
<Heading variant="subheadline">
HCB is not a bank.{' '}
<Text sx={{ color: 'muted', fontWeight: 'body' }}>(were better)</Text>
</Heading>
<ul>
<li>
Rather than setting up a standard bank account, youll get a
restricted fund within Hack Club accounts.
</li>
<li>
You can't deposit or withdraw cash. But you can receive any kind of
electronic payment!
</li>
</ul>
<Heading variant="subheadline">HCB is not for for-profits.</Heading>
<p>
If youre a for-profit entity, HCB is not for you. Consider setting up a
business.
</p>
</Box>
)
}

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Button, Flex, Text, Spinner } from 'theme-ui'
import { Button, Spinner } from 'theme-ui'
async function sendApplication() {
// Get the form data from sessionStorage
@ -26,31 +26,7 @@ async function sendApplication() {
}
}
function NavIcon({ isBack }) {
const style = {
height: '1em',
fill: 'white',
margin: 0,
flexShrink: 0
}
return isBack ? (
<svg style={style} viewBox="10.73 7.72 9.27 16.53">
<g>
<path d="M19.768,23.89c0.354,-0.424 0.296,-1.055 -0.128,-1.408c-1.645,-1.377 -5.465,-4.762 -6.774,-6.482c1.331,-1.749 5.1,-5.085 6.774,-6.482c0.424,-0.353 0.482,-0.984 0.128,-1.408c-0.353,-0.425 -0.984,-0.482 -1.409,-0.128c-1.839,1.532 -5.799,4.993 -7.2,6.964c-0.219,0.312 -0.409,0.664 -0.409,1.054c0,0.39 0.19,0.742 0.409,1.053c1.373,1.932 5.399,5.462 7.2,6.964l0.001,0.001c0.424,0.354 1.055,0.296 1.408,-0.128Z"></path>
</g>
</svg>
) : (
<svg style={style} viewBox="12.75 7.72 9.25 16.53">
<g>
<path d="M12.982,23.89c-0.354,-0.424 -0.296,-1.055 0.128,-1.408c1.645,-1.377 5.465,-4.762 6.774,-6.482c-1.331,-1.749 -5.1,-5.085 -6.774,-6.482c-0.424,-0.353 -0.482,-0.984 -0.128,-1.408c0.353,-0.425 0.984,-0.482 1.409,-0.128c1.839,1.532 5.799,4.993 7.2,6.964c0.219,0.312 0.409,0.664 0.409,1.054c0,0.39 -0.19,0.742 -0.409,1.053c-1.373,1.932 -5.399,5.462 -7.2,6.964l-0.001,0.001c-0.424,0.354 -1.055,0.296 -1.408,-0.128Z"></path>
</g>
</svg>
)
}
export default function NavButton({
isBack,
form,
clickHandler,
requiredFields,
@ -63,37 +39,10 @@ export default function NavButton({
setSpinner(false)
}, [router.query.step])
const minStep = 1
const maxStep = 3
const click = async () => {
setSpinner(true)
let step = parseInt(router.query.step)
async function setStep(s) {
await router.push(
{
pathname: router.pathname,
query: { ...router.query, step: s }
},
undefined,
{}
)
}
if (!step) {
// Set the step query param to minStep if it's not there.
await setStep(minStep)
} else if (step === minStep && isBack) {
await router.push('/hcb')
return
} else if (step < minStep) {
// Set the step query param to minStep if it's lower than that.
await setStep(minStep)
}
/* Don't return from inside the loop since
/* Don't return from inside the loop since
we want all input values to be saved every time */
let wasError = false
@ -105,11 +54,9 @@ export default function NavButton({
// Check if there are empty required fields.
if (
(!isBack &&
(!value || value.trim() === '') &&
((!value || value.trim() === '') &&
requiredFields[step - 1].includes(key)) ||
(!isBack &&
formData.get('contactOption') === 'slack' &&
(formData.get('contactOption') === 'slack' &&
!formData.get('slackUsername')) // I'm so sorry for this
) {
setFormError('Please fill all required fields')
@ -122,47 +69,15 @@ export default function NavButton({
// Run the parent's click handler for this button.
if (clickHandler) await clickHandler()
if (step >= maxStep && !isBack) {
await sendApplication()
await router.push('/fiscal-sponsorship/apply/success')
return
} else {
step += isBack ? -1 : 1
}
await setStep(step)
await sendApplication()
await router.push('/fiscal-sponsorship/apply/success')
return
}
return (
<Button
variant={isBack ? 'outline' : 'ctaLg'}
sx={{
color: 'white',
width: '100%',
maxWidth: isBack ? '8rem' : '10rem',
position: 'relative'
}}
onClick={click}
>
<Flex
sx={{
flexDirection: isBack ? 'row' : 'row-reverse',
justifyContent: 'center',
placeItems: 'center',
fontSize: isBack ? 2 : 4,
gap: [2, null, null, 3]
}}
>
<NavIcon isBack={isBack} />
<Text
sx={{
textTransform: 'none',
fontWeight: 'bold'
}}
>
{isBack ? 'Back' : 'Next'}
</Text>
</Flex>
{!isBack && spinner && (
<Button variant="ctaLg" sx={{ width: 'fit-content' }} onClick={click}>
Submit
{spinner && (
<Spinner
sx={{
height: '32px',

View file

@ -71,7 +71,7 @@ export default function OrganizationInfoForm({ requiredFields }) {
<Textarea
name="eventDescription"
id="eventDescription"
rows={3}
rows={2}
sx={{
resize: 'vertical',
width: '100%',

View file

@ -1,18 +1,20 @@
import { Input, Flex, Label, Radio } from 'theme-ui'
import { Input, Flex, Label, Radio, Box, Grid } from 'theme-ui'
import Checkbox from './checkbox'
import AddressInput from './address-input'
import Field from './field'
import AutofillColourFix from './autofill-colour-fix'
import { useState } from 'react'
import { useEffect, useState } from 'react'
export default function PersonalInfoForm({
setValidationResult,
requiredFields
}) {
const [selectedContactOption, setSelectedContactOption] = useState('Email')
const [email, setEmail] = useState(
window.sessionStorage.getItem('bank-signup-userEmail')
) // For display only, is not used for data submission.
const [email, setEmail] = useState('') // For display only, is not used for data submission.
useEffect(() => {
setEmail(window.sessionStorage.getItem('bank-signup-userEmail'))
}, [])
return (
<>
@ -78,7 +80,7 @@ export default function PersonalInfoForm({
sx={{ ...AutofillColourFix }}
/>
</Field>
<Field
{/* <Field
name="referredBy"
label="Who were you referred by?"
requiredFields={requiredFields}
@ -111,13 +113,21 @@ export default function PersonalInfoForm({
/>
</Field>
*/}
<Field
name="contactOption"
label="Preferred contact channel"
description="So we know where to message you about your application!"
requiredFields={requiredFields}
>
<Flex sx={{ gap: 4 }}>
<Grid
columns={[null, 2]}
sx={{
rowGap: 2,
columnGap: 4,
width: '100%'
}}
>
<Label
sx={{
display: 'flex',
@ -132,34 +142,45 @@ export default function PersonalInfoForm({
/>
Email
</Label>
<Label
<Grid
sx={{
display: 'flex',
flexDirection: 'row'
columnGap: 1,
rowGap: 2,
gridTemplateColumns: 'auto 1fr'
}}
>
<Radio
name="contactOption"
value="Slack"
onInput={() => setSelectedContactOption('Slack')}
/>
Slack
</Label>
</Flex>
{selectedContactOption === 'Slack' ? (
<Field name="slackUsername" requiredFields={requiredFields}>
<Input
name="slackUsername"
id="slackUsername"
placeholder="Your name in the Hack Club Slack"
sx={{ ...AutofillColourFix }}
/>
</Field>
) : selectedContactOption === 'Email' ? (
<div>
We'll use {email ?? 'whatever you put for your email above!'}
</div>
) : null}
<Label
sx={{
display: 'contents',
'~ div > label': { fontSize: 1 }
}}
>
<Radio
name="contactOption"
value="Slack"
onInput={() => setSelectedContactOption('Slack')}
/>
Hack Club Slack
</Label>
{selectedContactOption === 'Slack' ? (
<>
<div />
<Field
label="Your Hack Club Slack username"
name="slackUsername"
requiredFields={requiredFields}
>
<Input
name="slackUsername"
id="slackUsername"
autoFocus
sx={{ ...AutofillColourFix }}
/>
</Field>
</>
) : null}
</Grid>
</Grid>
</Field>
<Field
name="accommodations"

View file

@ -58,7 +58,7 @@ export default function Watermark() {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: '#1d181f',
backgroundColor: 'snow',
clipPath: 'url(#my-clip-path)'
}}
>

View file

@ -23,6 +23,13 @@ export default function Features() {
name="Receive foundation grants"
body="with tax-deductible 501(c)(3) status."
/>
{/* Send money & reimburse via check, ACH, bank wire, PayPal, & more.
Operate globally with a US Entity.
Issue physical & virtual debit cards to your team.
Get 24 hour support on weekdays.
Pay team members with built-in payroll.
Embed a custom donation form on your website.
We file all your taxes automatically, including form 990. " */}
<Module
icon="card"
name="Issue physical & virtual debit cards"
@ -69,7 +76,6 @@ export default function Features() {
<Laptop
href="https://hcb.hackclub.com/reboot"
title="See Reboots finances in public"
sx={{}}
/>
</Container>
</Box>
@ -90,7 +96,6 @@ function Module({ icon, name, body }) {
color: 'slate',
lineHeight: '1.375',
fontSize: 20,
alignSelf: 'center',
m: 0
}}
>

View file

@ -1,11 +1,9 @@
import { useEffect, useState, useRef } from 'react'
import { useRouter } from 'next/router'
import { Box, Flex, Text } from 'theme-ui'
import { Box, Flex, Heading, Grid } from 'theme-ui'
import ForceTheme from '../../../components/force-theme'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import FlexCol from '../../../components/flex-col'
import Progress from '../../../components/fiscal-sponsorship/apply/progress'
import NavButton from '../../../components/fiscal-sponsorship/apply/nav-button'
import Watermark from '../../../components/fiscal-sponsorship/apply/watermark'
import FormContainer from '../../../components/fiscal-sponsorship/apply/form-container'
@ -15,7 +13,7 @@ import PersonalInfoForm from '../../../components/fiscal-sponsorship/apply/perso
import AlertModal from '../../../components/fiscal-sponsorship/apply/alert-modal'
import { geocode } from '../../../lib/fiscal-sponsorship/apply/address-validation'
const valiadateAddress = async step => {
const validateAddress = async step => {
// Validate the address
if (step === 3) {
// Get the raw personal address input
@ -55,9 +53,12 @@ export default function Apply() {
const [formError, setFormError] = useState(null)
const requiredFields = [
[],
['eventName', 'eventLocation'],
['firstName', 'lastName', 'userEmail', 'userBirthday', 'contactOption']
'eventName',
'eventLocation',
'firstName',
'lastName',
'userEmail',
'userBirthday'
]
useEffect(() => {
@ -81,59 +82,54 @@ export default function Apply() {
return (
<>
<Meta as={Head} title="Apply for HCB" />
<ForceTheme theme="dark" />
<ForceTheme theme="light" />
<Box
<Grid
columns={[null, null, 2]}
sx={{
display: 'grid',
gap: 5,
gridTemplateAreas: [
'"title" "form" "form" "nav"',
null,
null,
'"title form" "title form" "nav form"'
],
height: ['auto', null, null, '100vh'],
p: [4, 5]
gap: 0,
width: '100%',
minHeight: '100vh',
alignItems: 'start'
}}
>
<Box sx={{ gridArea: 'title' }}>
<FlexCol gap={[4, null, null, '20vh']}>
<Text variant="title">
Lets get you
<br />
set up on HCB.
</Text>
<Progress />
</FlexCol>
</Box>
<Box sx={{ gridArea: 'form', overflowY: 'auto' }}>
<FormContainer ref={formContainer}>
{step === 1 && <HCBInfo />}
{step === 2 && (
<OrganizationInfoForm requiredFields={requiredFields} />
)}
{step === 3 && <PersonalInfoForm requiredFields={requiredFields} />}
</FormContainer>
</Box>
<Flex
sx={{
gridArea: 'nav',
alignSelf: 'end',
alignItems: 'flex-end',
justifyContent: 'space-between'
flexDirection: 'column',
justifyContent: 'space-between',
px: [3, 5],
py: 5,
gap: [3, 5],
height: [null, '100dvh'],
position: 'sticky',
top: 0,
overflowY: 'auto'
}}
>
<NavButton isBack={true} form={formContainer} />
<Heading as="h1" variant="title">
Lets get you
<br />
set up on HCB.
</Heading>
<HCBInfo />
</Flex>
<FormContainer ref={formContainer}>
<Heading as="h2" variant="headline" sx={{ mb: -2 }}>
Your organization
</Heading>
<OrganizationInfoForm requiredFields={requiredFields} />
<Heading as="h2" variant="headline" sx={{ mb: -2 }}>
Personal details
</Heading>
<PersonalInfoForm requiredFields={requiredFields} />
<NavButton
isBack={false}
form={formContainer}
setFormError={setFormError}
requiredFields={requiredFields}
clickHandler={() => valiadateAddress(step)}
clickHandler={() => validateAddress(step)}
/>
</Flex>
</Box>
</FormContainer>
</Grid>
<AlertModal formError={formError} setFormError={setFormError} />
<Watermark />
</>