From question and copy edits with Mel

This commit is contained in:
Gary Tou 2025-03-07 16:09:33 -08:00
parent 4c79cf99e0
commit afa8fad66e
No known key found for this signature in database
GPG key ID: 1587ABD3593755C3
15 changed files with 357 additions and 217 deletions

View file

@ -1,11 +1,10 @@
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { Alert, Heading, Button } from 'theme-ui'
import { Alert, Button, Text } from 'theme-ui'
import FormContainer from './form-container'
import OrganizationInfoForm from './org-form'
import PersonalInfoForm from './personal-form'
import { onSubmit } from './submit'
import Callout from './callout'
import TeenagerOrAdultForm from './teenager-or-adult-form'
import MultiStepForm from './multi-step-form'
@ -18,18 +17,27 @@ export default function ApplicationForm() {
const requiredFields = {
// Key: form field name
// Value: humanize field name used in error message
eventName: 'organization name',
eventLocation: 'organization country',
eventPostalCode: 'organization zip/postal code',
eventDescription: 'organization description',
eventTeenagerLed: 'are you a teenager?',
eventPoliticalActivity: "organization's political activity",
eventAnnualBudget: 'organization annual budget',
eventName: 'project name',
eventPostalCode: 'project zip/postal code',
eventDescription: 'project description',
eventIsPolitical: "project's political activity",
eventPoliticalActivity: "project's political activity",
eventHasWebsite: 'project website',
eventWebsite: 'project website',
eventAnnualBudget: 'project annual budget',
firstName: 'first name',
lastName: 'last name',
userEmail: 'email',
userPhone: 'phone number',
userBirthday: 'birthday'
userBirthday: 'birthday',
userAddressLine1: 'address line 1',
userAddressCity: 'city',
userAddressProvince: 'state/province',
userAddressPostalCode: 'ZIP/postal code',
userAddressCountry: 'country',
referredBy: 'how did you hear about HCB?'
}
const submitButton = (
@ -66,14 +74,27 @@ export default function ApplicationForm() {
})
}
>
<MultiStepForm submitButton={submitButton}>
<MultiStepForm
submitButton={submitButton}
validationErrors={
formError && (
<Alert bg="primary" sx={{ mt: 4 }}>
{formError}
</Alert>
)
}
>
{/* Step 1 */}
<MultiStepForm.Step title="Let's get started">
<Callout />
<Text as="p" variant="caption" sx={{ marginBottom: '2rem' }}>
Fill out this quick application to run your project on HCB. If you
are a teenager, there is a very high likelihood we will accept your
project. We just need to collect a few pieces of information first.
</Text>
<TeenagerOrAdultForm requiredFields={requiredFields} />
</MultiStepForm.Step>
{/* Step 2 */}
<MultiStepForm.Step title="Your organization">
<MultiStepForm.Step>
<OrganizationInfoForm requiredFields={requiredFields} />
</MultiStepForm.Step>
{/* Step 3 */}
@ -81,7 +102,6 @@ export default function ApplicationForm() {
<PersonalInfoForm requiredFields={requiredFields} />
</MultiStepForm.Step>
</MultiStepForm>
{formError && <Alert bg="primary">{formError}</Alert>}
</FormContainer>
)
}

View file

@ -1,30 +0,0 @@
import { Box, Text } from 'theme-ui'
export default function Callout() {
return (
<Box
sx={{
borderLeft: '3px solid',
borderLeftColor: 'blue',
paddingLeft: 2,
color: 'blue',
fontSize: 1,
textWrap: 'pretty',
lineHeight: 1.375,
marginBottom: '2rem'
}}
>
<Text as="p" sx={{ fontWeight: 'bold' }}>
HCB is a fiscal sponsor primarily for teenage-led organizations
</Text>
<Text as="p" sx={{ mt: 1, textWrap: 'balance' }}>
Although we would love to be able to support organizations across all
ages and missions, we are currently prioritizing applications from
teenagers.
</Text>
<Text as="p" sx={{ mt: 1, textWrap: 'balance' }}>
We are accepting adult-led organizations on a case-by-case basis.
</Text>
</Box>
)
}

View file

@ -18,10 +18,17 @@ export default function Field({
useEffect(() => {
const value =
router.query[name] || sessionStorage.getItem('bank-signup-' + name)
if (value) {
const input = document.getElementById(name)
if (input) input.value = value
if (!value) return
let input = document.getElementById(name)
if (input) {
input.value = value
return
}
// Maybe it's radio buttons
input = document.querySelector(`input[name='${name}']`)
if (input) input.checked = true
}, [router.query, name])
return (

View file

@ -12,10 +12,14 @@ const formContainer = forwardRef(({ children, ...props }, ref) => {
bg: 'snow',
px: [3, 5],
py: [1, 5],
minHeight: '100dvb',
pb: 5,
minHeight: [null, null, '100dvb'],
'&.has-errors div[aria-required="true"] input:placeholder-shown': {
borderColor: 'primary'
},
'&.has-errors div[aria-required="true"] input[type="date"]': {
borderColor: 'primary'
},
'&.has-errors div[aria-required="true"] textarea:placeholder-shown': {
borderColor: 'primary'
}
@ -26,10 +30,6 @@ const formContainer = forwardRef(({ children, ...props }, ref) => {
variant="copy"
sx={{
ml: 0,
display: 'flex',
flexDirection: 'column',
columnGap: 4,
rowGap: 3,
px: 0
}}
>

View file

@ -1,4 +1,4 @@
import { Box, Link, Heading } from 'theme-ui'
import { Box, Link, Heading, Grid } from 'theme-ui'
import Icon from '../../icon'
import { useMultiStepContext } from './multi-step-context'
import { useEffect } from 'react'
@ -19,7 +19,7 @@ export default function HCBInfo() {
}}
>
<Heading variant="subheadline">
HCB is a{' '}
HCB is not a bank, we're a{' '}
<Link
href="https://en.wikipedia.org/wiki/Fiscal_sponsorship"
target="_blank"
@ -33,26 +33,23 @@ export default function HCBInfo() {
<Icon glyph="external" size={24} aria-hidden />
</Link>
</Heading>
{/* <Grid columns={2} bg="white" sx={{ color: 'muted', border: 'slate 1px' }}>
<Box>Gives your project nonprofit status.</Box>
</Grid> */}
<ul>
<li>Gives your project nonprofit status.</li>
<li>Enables tax-deductible donations.</li>
<li>
HCB is not a bank. We partner partner with{' '}
<Link href="https://column.com" target="_blank">
Column N.A.
</Link>{' '}
to offer restricted funds to fiscally-sponsored projects.
</li>
</ul>
<Heading variant="subheadline">
HCB provides a financial platform.
</Heading>
<ul>
<li>Accessed via a beautiful, modern interface.</li>
<li>Provides a donation page and invoicing system.</li>
<li>Transfer money electronically.</li>
<li>Order cards for you and your team to make purchases.</li>
<li>HCB gives your project nonprofit status.</li>
<li>Allows you to raise tax-deductible donations.</li>
<li>Provides a financial platform.</li>
<li>Allows you to order cards to make purchases.</li>
</ul>
<p>
HCB partners with{' '}
<Link href="https://column.com" target="_blank">
Column N.A.
</Link>{' '}
to provide restricted funds to fiscally-sponsored projects.
</p>
</Box>
)
}

View file

@ -2,7 +2,11 @@ import { Box, Button, Heading } from 'theme-ui'
import { useMultiStepContext } from './multi-step-context'
import { Children } from 'react'
export default function MultiStepForm({ children, submitButton }) {
export default function MultiStepForm({
children,
submitButton,
validationErrors
}) {
const { step, useStepper } = useMultiStepContext()
const steps = Children.toArray(children)
const { nextStep, previousStep } = useStepper(steps)
@ -14,17 +18,27 @@ export default function MultiStepForm({ children, submitButton }) {
with the form. So, we simple hide all non-current steps.
*/}
{steps.map((stepComponent, index) => (
<Box key={index} sx={step !== index ? { display: 'none' } : {}}>
<Box
key={index}
sx={{
display: 'flex',
flexDirection: 'column',
rowGap: 3,
...(step !== index ? { display: 'none' } : {})
}}
>
{stepComponent}
</Box>
))}
{validationErrors}
<Box
sx={{
display: 'flex',
gap: '1rem',
marginTop: '2rem',
marginLeft: 'auto'
justifyContent: 'flex-end'
}}
>
{step > 0 && (

View file

@ -11,23 +11,6 @@ export default function OrganizationAdultForm({ requiredFields }) {
<>
{teenagerLed !== 'true' && (
<>
<Field
name="eventPoliticalActivity"
label={`Please describe any political activity your ${org.toLowerCase()} is involved in, if any`}
description="This includes but is not limited to protests, public demonstrations, political education, and lobbying."
requiredFields={requiredFields}
>
<Textarea
name="eventPoliticalActivity"
id="eventPoliticalActivity"
placeholder="We are involved in..."
rows={3}
sx={{
resize: 'vertical'
}}
/>
</Field>
<Field
name="eventAnnualBudget"
label="What is your estimated annual budget (USD) for this year?"

View file

@ -1,15 +1,51 @@
import { useState, useEffect } from 'react'
import { Input, Select, Textarea } from 'theme-ui'
import { Input, Link, Select, Text, Textarea } from 'theme-ui'
import Checkbox from './checkbox'
import Field from './field'
// This is using country-list instead of country-list-js as it has a smaller bundle size
import { getNames } from 'country-list'
import useOrganizationI18n from '../organizationI18n'
import OrganizationAdultForm from './org-adult-form'
import { useTeenagerLedContext } from './teenager-led-context'
export default function OrganizationInfoForm({ requiredFields }) {
const org = useOrganizationI18n()
const { teenagerLed } = useTeenagerLedContext()
const [hasWebsite, setHasWebsite] = useState(true)
const onHasWebsiteChange = e => {
const newValue = e.target.value === 'true'
setHasWebsite(newValue)
}
const [isPolitical, setIsPolitical] = useState(null)
const onIsPoliticalChange = e => {
const newValue = e.target.value === 'true'
setIsPolitical(newValue)
}
const politicalActivityTextarea = admittedToActivity => (
<Field
name="eventPoliticalActivity"
label={`Please describe ${admittedToActivity ? 'the' : 'any'} political activity your ${org.toLowerCase()} is involved in${admittedToActivity ? '.' : ', if any.'}`}
requiredFields={requiredFields}
>
<Textarea
name="eventPoliticalActivity"
id="eventPoliticalActivity"
placeholder="We are involved in..."
rows={3}
sx={{
resize: 'vertical'
}}
/>
</Field>
)
const noPoliticalActivity = (
<input type="hidden" name="eventPoliticalActivity" value="None." />
)
return (
<>
<Field
@ -23,48 +59,102 @@ export default function OrganizationInfoForm({ requiredFields }) {
placeholder="Shelburne School Hackathon"
/>
</Field>
<Field
name="eventWebsite"
label={`${org} website`}
description="If you dont have one yet, you can leave this blank."
name="eventDescription"
label={`Tell us about your ${org.toLowerCase()}`}
requiredFields={requiredFields}
>
<Input
name="eventWebsite"
id="eventWebsite"
inputMode="url"
placeholder="hackclub.com"
<Text variant="caption">
Are you running a hackathon, robotics team, organizing a nonprofit,
building a project, etc.?
</Text>
<Textarea
name="eventDescription"
id="eventDescription"
rows={3}
sx={{
resize: 'vertical'
}}
/>
</Field>
<Field
name="eventLocation"
label={`Primary country of operations`}
label="Does your project have a website?"
name="eventHasWebsite"
requiredFields={requiredFields}
>
<Select name="eventLocation" id="eventLocation">
{getNames()
.sort()
.sort(item => (item === 'United States of America' ? -1 : 1))
.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
<Select
onChange={onHasWebsiteChange}
value={hasWebsite ? 'true' : 'false'}
>
{Object.entries({ Yes: 'true', No: 'false' }).map(([name, value]) => (
<option key={name} value={value}>
{name}
</option>
))}
</Select>
</Field>
<Field
name="eventPostalCode"
label={`ZIP code / postal code`}
description="If your organization runs online, please put your own postal code."
requiredFields={requiredFields}
>
<Input
name="eventPostalCode"
id="eventPostalCode"
placeholder="90069"
/>
</Field>
<Field
{
hasWebsite ? (
<Field
name="eventWebsite"
label={`${org} website`}
requiredFields={requiredFields}
>
<Input
name="eventWebsite"
id="eventWebsite"
inputMode="url"
placeholder="hackclub.com"
/>
</Field>
) : teenagerLed === 'true' ? (
<Text variant="caption">
A website is not required to apply for HCB. However, most successful
projects that raise money have a custom-build website. If you've
never built a website before, checkout{' '}
<Link href="https://boba.hackclub.com/">Boba Drops</Link>, a Hack
Club workshop on how to build a website.
</Text>
) : null /* don't show Boba Drops to adult-led orgs lol*/
}
{teenagerLed === 'true' ? (
<>
<Field
name="eventIsPolitical"
label="Is your project involved with political activity?"
requiredFields={requiredFields}
>
<Select
name="eventIsPolitical"
onChange={onIsPoliticalChange}
value={isPolitical ? 'true' : 'false'}
>
{Object.entries({ Yes: 'true', No: 'false' }).map(
([name, value]) => (
<option key={name} value={value}>
{name}
</option>
)
)}
</Select>
</Field>
{isPolitical ? politicalActivityTextarea(true) : noPoliticalActivity}
</>
) : (
// Adults always get the text area
politicalActivityTextarea(false)
)}
<Text variant="caption">
This includes but is not limited to protests, public demonstrations,
political education, and lobbying.
</Text>
{/* Move transparency mode prompt to HCB onboarding */}
{/* <Field
name="transparent"
label="Transparency mode"
col={true}
@ -77,22 +167,7 @@ export default function OrganizationInfoForm({ requiredFields }) {
requiredFields={requiredFields}
>
<Checkbox defaultChecked={true} name="transparent" />
</Field>
<Field
name="eventDescription"
label={`Tell us about your ${org.toLowerCase()}`}
description="24 sentences will suffice."
requiredFields={requiredFields}
>
<Textarea
name="eventDescription"
id="eventDescription"
rows={3}
sx={{
resize: 'vertical'
}}
/>
</Field>
</Field> */}
<OrganizationAdultForm requiredFields={requiredFields} />
</>

View file

@ -1,8 +1,9 @@
import { Input, Flex, Label, Radio, Grid, Select } from 'theme-ui'
import { Input, Flex, Label, Radio, Grid, Select, Box, Text } from 'theme-ui'
import Field from './field'
import Checkbox from './checkbox'
import { useEffect, useState } from 'react'
import { useTeenagerLedContext } from './teenager-led-context'
import { getNames } from 'country-list'
export default function PersonalInfoForm({ requiredFields }) {
const [selectedContactOption, setSelectedContactOption] = useState('Email')
@ -41,9 +42,9 @@ export default function PersonalInfoForm({ requiredFields }) {
label="Preferred contact channel"
requiredFields={requiredFields}
>
<Grid
columns={[null, 2]}
<Flex
sx={{
flexDirection: ['column', 'row'],
rowGap: 2,
columnGap: 4,
width: '100%'
@ -52,7 +53,8 @@ export default function PersonalInfoForm({ requiredFields }) {
<Label
sx={{
display: 'flex',
flexDirection: 'row'
flexDirection: 'row',
width: 'fit-content'
}}
>
<Radio
@ -67,7 +69,8 @@ export default function PersonalInfoForm({ requiredFields }) {
sx={{
columnGap: 0,
rowGap: 2,
gridTemplateColumns: 'auto 1fr'
gridTemplateColumns: 'auto 1fr',
flexGrow: 1
}}
>
<Label
@ -104,7 +107,7 @@ export default function PersonalInfoForm({ requiredFields }) {
</>
) : null}
</Grid>
</Grid>
</Flex>
</Field>
) : (
// When not teenage-led, default to "email" as preferred contact channel
@ -126,37 +129,77 @@ export default function PersonalInfoForm({ requiredFields }) {
</Field>
<Field
name="userBirthday"
label="Birth year"
label="Birthday"
requiredFields={requiredFields}
>
<Select name="userBirthday" id="userBirthday" defaultValue="">
<option value="" disabled>
Select a year
</option>
{/* show a century of years starting from 13 years ago */}
{Array.from({ length: 98 }, (_, i) => {
const year = new Date().getFullYear() - 13 - i
return (
<option key={year} value={year}>
{year}
</option>
)
})}
</Select>
<Input type="date" name="userBirthday" />
</Field>
{/* <Field
<Flex sx={{ flexDirection: 'column', gap: 1 }}>
<Field
name="userAddressLine1"
label={'Your personal address'}
requiredFields={requiredFields}
>
<Input
name="userAddressLine1"
placeholder="8605 Santa Monica Blvd, Suite 86294"
/>
</Field>
<Grid columns={2} gap={1}>
<Field
name="userAddressCity"
label={<Text sx={{ fontSize: 1 }}>City</Text>}
requiredFields={requiredFields}
>
<Input name="userAddressCity" placeholder="Santa Monica" />
</Field>
<Field
name="userAddressProvince"
label={<Text sx={{ fontSize: 1 }}>State / Province</Text>}
requiredFields={requiredFields}
>
<Input name="userAddressProvince" placeholder="California" />
</Field>
<Field
name="userAddressPostalCode"
label={<Text sx={{ fontSize: 1 }}>ZIP / Postal code</Text>}
requiredFields={requiredFields}
>
<Input name="userAddressPostalCode" placeholder="90069" />
</Field>
<Field
name="userAddressCountry"
label={<Text sx={{ fontSize: 1 }}>Country</Text>}
requiredFields={requiredFields}
>
<Select name="userAddressCountry" id="userAddressCountry">
{getNames()
.sort()
.sort(item => (item === 'United States of America' ? -1 : 1))
.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</Select>
</Field>
</Grid>
</Flex>
<Field
name="referredBy"
label="Who were you referred by?"
label="How did you hear about HCB?"
requiredFields={requiredFields}
>
<Input
name="referredBy"
id="referredBy"
placeholder="Max"
placeholder="Word of mouth, an event, etc. Be specific!"
/>
</Field>
*/}
<Field
name="returningUser"
label="Have you used HCB before?"
@ -165,19 +208,7 @@ export default function PersonalInfoForm({ requiredFields }) {
>
<Checkbox name="returningUser" />
</Field>
{/* <Field
name="userAddress"
label="Address"
description="This is so we can send you some swag and goodies if you ever request them!"
requiredFields={requiredFields}
>
<AddressInput
name="userAddress"
isPersonalAddressInput={true}
setValidationResult={setValidationResult}
/>
</Field>
*/}
<Field
name="accommodations"
label="Accessibility needs"

View file

@ -75,7 +75,7 @@ export function onSubmit({
const isAdult = formData.get('eventTeenagerLed') !== 'true'
const acceptanceEta = isAdult
? 'within two weeks'
: 'within two business days'
: 'within 24 hours on weekdays and 48 hours on weekends'
router.push(`/fiscal-sponsorship/apply/success?eta=${acceptanceEta}`)
})

View file

@ -4,7 +4,7 @@ const TeenagerLedContext = createContext()
const useTeenagerLedContext = () => useContext(TeenagerLedContext)
const TeenagerLedProvider = ({ children }) => {
const [teenagerLed, setTeenagerLed] = useState('false')
const [teenagerLed, setTeenagerLed] = useState('true')
return (
<TeenagerLedContext.Provider value={{ teenagerLed, setTeenagerLed }}>

View file

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { Select } from 'theme-ui'
import { Flex, Input, Label, Radio, Select } from 'theme-ui'
import Field from './field'
import { useTeenagerLedContext } from './teenager-led-context'
@ -24,37 +24,72 @@ export default function TeenagerOrAdultForm({ requiredFields }) {
// `teenagerLed` state may not be synced with the DOM input value. This code
// syncs `teenagerLed` with the DOM input value.
// NOTE: This depends on Field's useEffect hook to run first.
const eventTeenagerLedElm = document.getElementById('eventTeenagerLed')
setTeenagerLed(eventTeenagerLedElm.value)
const eventTeenagerLedElm = document.querySelector(
'input[name="eventTeenagerLed"]:checked'
)
if (eventTeenagerLedElm) setTeenagerLed(eventTeenagerLedElm.value)
})
return (
<>
<Field
name="eventTeenagerLed"
label={'Are you a teenager?'}
col={true}
description={'18 and under'}
requiredFields={requiredFields}
>
<Select
name="eventTeenagerLed"
id="eventTeenagerLed"
onChange={onTeenagerLedChange}
value={teenagerLed}
<Field
name="eventTeenagerLed"
label={'Are you a teenager?'}
col={true}
description={'18 and under'}
requiredFields={requiredFields}
>
<Flex columns={2} sx={{ gap: 3 }}>
<Label
sx={{
alignItems: 'center',
backgroundColor: ' rgba(91,192,222,.25)',
borderRadius: '.75rem',
color: '#338eda',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: '1.25',
padding: '1rem',
textAlign: 'center',
boxShadow:
teenagerLed === 'true' ? '0 0 0 1px #fff,0 0 0 3px #338eda' : null
}}
>
{Object.entries({ Yes: 'true', No: 'false' }).map(([name, value]) => (
<option key={name} value={value}>
{name}
</option>
))}
</Select>
</Field>
<p>
NOTE: this kinda ugly rn, i plan on making this teenager question look a
bit better by having two boxes side by side. one box/button for teen,
another for adult
</p>
</>
<Radio
name="eventTeenagerLed"
value="true"
onChange={onTeenagerLedChange}
sx={{ display: 'none!important' }}
/>
Yes, I'm a teenager
</Label>
<Label
sx={{
alignItems: 'center',
backgroundColor: ' rgba(91,192,222,.25)',
borderRadius: '.75rem',
color: '#338eda',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
lineHeight: '1.25',
padding: '1rem',
textAlign: 'center',
boxShadow:
teenagerLed === 'false'
? '0 0 0 1px #fff,0 0 0 3px #338eda'
: null
}}
>
<Radio
name="eventTeenagerLed"
value="false"
onChange={onTeenagerLedChange}
sx={{ display: 'none!important' }}
/>
I'm an adult
</Label>
</Flex>
</Field>
)
}

View file

@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'
export default function useOrganizationI18n() {
const [org, setOrg] = useState('Organization')
const [org, setOrg] = useState('Project')
useEffect(() => {
if (navigator.language === 'en-GB') setOrg('Organisation')
if (navigator.language === 'en-GB') setOrg('Project')
}, [])
return org

View file

@ -32,7 +32,7 @@ export default function Apply() {
px: [3, 5],
py: 4,
gap: 4,
height: [null, '100svh'],
height: [null, null, '100svh'],
position: [null, null, 'sticky'],
top: 0,
overflowY: [null, null, 'auto']
@ -62,10 +62,18 @@ export default function Apply() {
Back
</Text>
</Link>
<Heading as="h1" variant="title">
Apply to join
<Heading as="h1" variant="headline">
Turn your ideas into
<br />
<Flex sx={{ alignItems: 'center', gap: 3 }}>
reality with{' '}
<Flex
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 2,
verticalAlign: 'middle'
}}
>
<img
src="/fiscal-sponsorship/hcb-icon-small.png"
width={48}

View file

@ -213,7 +213,7 @@ export default function Page() {
}}
>
<Stat value="$30M+" label="processed transactions" reversed />
<Stat value="2000+" label="projects" reversed />
<Stat value="6500+" label="projects" reversed />
<Stat value="2018" label="serving nonprofits since" reversed />
</Flex>
<Grid columns={[1, 2]} gap={[3, 4]} sx={{ mt: 4 }}>