mirror of
https://github.com/System-End/site.git
synced 2026-04-19 22:05:11 +00:00
Merge branch 'main' into garyhtou/security
This commit is contained in:
commit
502b3324a2
24 changed files with 929 additions and 406 deletions
|
|
@ -72,7 +72,7 @@ _Have questions? Join us in [#hackclub-site-dev](https://hackclub.slack.com/arch
|
|||
|
||||
---
|
||||
|
||||
Hack Club, 2024. MIT License.
|
||||
Hack Club, 2025. MIT License.
|
||||
|
||||
[next.js]: https://nextjs.org
|
||||
[mdx]: https://mdxjs.com
|
||||
|
|
|
|||
106
components/fiscal-sponsorship/apply/application-form.js
Normal file
106
components/fiscal-sponsorship/apply/application-form.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
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 TeenagerOrAdultForm from './teenager-or-adult-form'
|
||||
import MultiStepForm from './multi-step-form'
|
||||
|
||||
export default function ApplicationForm() {
|
||||
const router = useRouter()
|
||||
const formContainer = useRef()
|
||||
const [formError, setFormError] = useState(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const requiredFields = {
|
||||
// Key: form field name
|
||||
// Value: humanize field name used in error message
|
||||
eventTeenagerLed: 'are you a teenager?',
|
||||
eventName: 'project name',
|
||||
eventPostalCode: 'project zip/postal code',
|
||||
eventDescription: 'project description',
|
||||
eventIsPolitical: "project's political activity",
|
||||
eventPoliticalActivity: "project's political activity",
|
||||
eventHasWebsite: 'project website',
|
||||
eventAnnualBudget: 'project annual budget',
|
||||
firstName: 'first name',
|
||||
lastName: 'last name',
|
||||
userEmail: 'email',
|
||||
userPhone: 'phone number',
|
||||
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 = (
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
sx={{
|
||||
backgroundImage: theme => theme.util.gx('cyan', 'blue'),
|
||||
'&:disabled': {
|
||||
background: 'muted',
|
||||
cursor: 'not-allowed',
|
||||
transform: 'none !important'
|
||||
},
|
||||
width: 'fit-content'
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? 'Submitting…' : 'Submit'}
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<FormContainer
|
||||
ref={formContainer}
|
||||
className={formError ? 'has-errors' : null}
|
||||
onSubmit={event =>
|
||||
onSubmit({
|
||||
event,
|
||||
router,
|
||||
form: formContainer,
|
||||
setFormError,
|
||||
setIsSubmitting,
|
||||
requiredFields
|
||||
})
|
||||
}
|
||||
>
|
||||
<MultiStepForm
|
||||
submitButton={submitButton}
|
||||
validationErrors={
|
||||
formError && (
|
||||
<Alert bg="primary" sx={{ mt: 4 }}>
|
||||
{formError}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Step 1 */}
|
||||
<MultiStepForm.Step title="Let's get started">
|
||||
<Text as="p" variant="caption" sx={{ marginBottom: '1rem' }}>
|
||||
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>
|
||||
<OrganizationInfoForm requiredFields={requiredFields} />
|
||||
</MultiStepForm.Step>
|
||||
{/* Step 3 */}
|
||||
<MultiStepForm.Step title="Personal details">
|
||||
<PersonalInfoForm requiredFields={requiredFields} />
|
||||
</MultiStepForm.Step>
|
||||
</MultiStepForm>
|
||||
</FormContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,29 +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
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ export default function Checkbox({ name, defaultChecked = false, size = 38 }) {
|
|||
name={name}
|
||||
aria-checked={checked}
|
||||
role="checkbox"
|
||||
tabindex="0"
|
||||
onClick={() => toggle()}
|
||||
onKeyDown={e => e.key === 'Enter' && toggle()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -11,17 +11,24 @@ export default function Field({
|
|||
children
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const isRequired = requiredFields.includes(name)
|
||||
const isRequired = Object.keys(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. */
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Box, Container } from 'theme-ui'
|
||||
import { TeenagerLedProvider } from '../../../components/fiscal-sponsorship/apply/teenager-led-context'
|
||||
import { MultiStepProvider } from './multi-step-context'
|
||||
|
||||
const formContainer = forwardRef(({ children, ...props }, ref) => {
|
||||
return (
|
||||
|
|
@ -9,11 +11,15 @@ const formContainer = forwardRef(({ children, ...props }, ref) => {
|
|||
sx={{
|
||||
bg: 'snow',
|
||||
px: [3, 5],
|
||||
py: 5,
|
||||
minHeight: '100dvb',
|
||||
py: [1, 5],
|
||||
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'
|
||||
}
|
||||
|
|
@ -24,14 +30,10 @@ const formContainer = forwardRef(({ children, ...props }, ref) => {
|
|||
variant="copy"
|
||||
sx={{
|
||||
ml: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
columnGap: 4,
|
||||
rowGap: 3,
|
||||
px: 0
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<TeenagerLedProvider>{children}</TeenagerLedProvider>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
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'
|
||||
|
||||
export default function HCBInfo() {
|
||||
const { step } = useMultiStepContext()
|
||||
const firstStep = step === 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: firstStep ? 'block' : ['none', 'block'],
|
||||
gridArea: 'info',
|
||||
alignItems: 'start',
|
||||
mark: { color: '#ec555c', bg: 'inherit' },
|
||||
ul: { pl: [3, 0], color: 'muted', mb: 4 },
|
||||
ul: { pl: 3, color: 'muted', mb: 4 },
|
||||
p: { color: 'muted', mb: 0 }
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
|
|
@ -27,39 +33,22 @@ 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>Nonprofit status.</li>
|
||||
<li>Tax-deductible donations.</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>
|
||||
<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.</Heading>
|
||||
<ul>
|
||||
<li>
|
||||
We partner with{' '}
|
||||
<Link href="https://column.com" target="_blank">
|
||||
Column N.A.
|
||||
</Link>{' '}
|
||||
to offer restricted funds to fiscally-sponsored projects.
|
||||
</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 you’re looking to set up a for-profit entity, consider{' '}
|
||||
<Link href="https://stripe.com/atlas" target="_blank">
|
||||
Stripe Atlas
|
||||
</Link>
|
||||
.
|
||||
HCB partners with{' '}
|
||||
<Link href="https://column.com" target="_blank">
|
||||
Column N.A.
|
||||
</Link>{' '}
|
||||
to provide restricted funds to fiscally-sponsored projects.
|
||||
</p>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
37
components/fiscal-sponsorship/apply/multi-step-context.js
Normal file
37
components/fiscal-sponsorship/apply/multi-step-context.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
const MultiStepContext = createContext()
|
||||
const useMultiStepContext = () => useContext(MultiStepContext)
|
||||
|
||||
const MultiStepProvider = ({ children }) => {
|
||||
const [step, setStep] = useState(0)
|
||||
|
||||
const useStepper = steps => {
|
||||
const modifyStep = number => {
|
||||
const newStep = step + number
|
||||
|
||||
// Guard against invalid step numbers
|
||||
if (newStep < 0 || newStep > steps.length - 1) {
|
||||
console.error(
|
||||
`[MultiStepProvider] Invalid new step number: ${newStep}. Current step number: ${step}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setStep(newStep)
|
||||
}
|
||||
|
||||
return {
|
||||
nextStep: () => modifyStep(1),
|
||||
previousStep: () => modifyStep(-1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MultiStepContext.Provider value={{ step, useStepper }}>
|
||||
{children}
|
||||
</MultiStepContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { MultiStepProvider, useMultiStepContext }
|
||||
74
components/fiscal-sponsorship/apply/multi-step-form.js
Normal file
74
components/fiscal-sponsorship/apply/multi-step-form.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Box, Button, Heading } from 'theme-ui'
|
||||
import { useMultiStepContext } from './multi-step-context'
|
||||
import { Children } from 'react'
|
||||
|
||||
export default function MultiStepForm({
|
||||
children,
|
||||
submitButton,
|
||||
validationErrors
|
||||
}) {
|
||||
const { step, useStepper } = useMultiStepContext()
|
||||
const steps = Children.toArray(children)
|
||||
const { nextStep, previousStep } = useStepper(steps)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
We must render all form fields to DOM so that they can be submitted
|
||||
with the form. So, we simple hide all non-current steps.
|
||||
*/}
|
||||
{steps.map((stepComponent, index) => (
|
||||
<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',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
{step > 0 && (
|
||||
<Button type="button" variant="outline" onClick={previousStep}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{step < steps.length - 1 && (
|
||||
<Button type="button" variant="primary" onClick={nextStep}>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{step === steps.length - 1 && submitButton}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Step({ children, title }) {
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<Heading as="h2" variant="headline">
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
MultiStepForm.Step = Step
|
||||
|
|
@ -1,77 +1,16 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Input, Select, Textarea } from 'theme-ui'
|
||||
import { Input, Textarea } from 'theme-ui'
|
||||
import Field from './field'
|
||||
import useOrganizationI18n from '../organizationI18n'
|
||||
import { useTeenagerLedContext } from './teenager-led-context'
|
||||
|
||||
export default function OrganizationAdultForm({ requiredFields }) {
|
||||
const org = useOrganizationI18n()
|
||||
const { teenagerLed, setTeenagerLed } = useTeenagerLedContext()
|
||||
|
||||
const onTeenagerLedChange = e => {
|
||||
const newValue = e.target.value
|
||||
setTeenagerLed(newValue)
|
||||
|
||||
if (newValue === 'true') {
|
||||
// Clear cache of removed fields
|
||||
sessionStorage.removeItem('bank-signup-eventPoliticalActivity')
|
||||
sessionStorage.removeItem('bank-signup-eventAnnualBudget')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// [@garyhtou] welp... this exists because the Field component will cache
|
||||
// input values and set them on page load. It does it by directly setting
|
||||
// `input.value` with JavaScript; bypassing React. Because of that, the
|
||||
// `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 { teenagerLed } = useTeenagerLedContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
name="eventTeenagerLed"
|
||||
label={`Is your ${org.toLowerCase()} led by teenagers?`}
|
||||
col={true}
|
||||
description={`This means your ${org.toLowerCase()} was founded and is being led exclusively by teenagers.`}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Select
|
||||
name="eventTeenagerLed"
|
||||
id="eventTeenagerLed"
|
||||
onChange={onTeenagerLedChange}
|
||||
value={teenagerLed}
|
||||
>
|
||||
{Object.entries({ Yes: 'true', No: 'false' }).map(([name, value]) => (
|
||||
<option key={name} value={value}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
{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?"
|
||||
|
|
|
|||
|
|
@ -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,108 @@ export default function OrganizationInfoForm({ requiredFields }) {
|
|||
placeholder="Shelburne School Hackathon"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="eventWebsite"
|
||||
label={`${org} website`}
|
||||
description="If you don’t 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>
|
||||
) : (
|
||||
<>
|
||||
{/* No website */}
|
||||
<input type="hidden" name="eventWebsite" value="" />
|
||||
|
||||
{teenagerLed === 'true' && (
|
||||
/* don't show Boba Drops to adult-led orgs lol*/
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{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 +173,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="2–4 sentences will suffice."
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Textarea
|
||||
name="eventDescription"
|
||||
id="eventDescription"
|
||||
rows={3}
|
||||
sx={{
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</Field> */}
|
||||
|
||||
<OrganizationAdultForm requiredFields={requiredFields} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -88,7 +91,6 @@ export default function PersonalInfoForm({ requiredFields }) {
|
|||
<div />
|
||||
<Field
|
||||
label="Your Hack Club Slack username"
|
||||
description="For teenagers only!"
|
||||
name="slackUsername"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
|
|
@ -104,7 +106,7 @@ export default function PersonalInfoForm({ requiredFields }) {
|
|||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Field>
|
||||
) : (
|
||||
// When not teenage-led, default to "email" as preferred contact channel
|
||||
|
|
@ -126,37 +128,90 @@ 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" id="userBirthday" />
|
||||
</Field>
|
||||
|
||||
{/* <Field
|
||||
<Flex sx={{ flexDirection: 'column', gap: 1 }}>
|
||||
<Field
|
||||
name="userAddressLine1"
|
||||
label={'Your personal address'}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="userAddressLine1"
|
||||
id="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"
|
||||
id="userAddressCity"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name="userAddressProvince"
|
||||
label={<Text sx={{ fontSize: 1 }}>State / Province</Text>}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="userAddressProvince"
|
||||
placeholder="California"
|
||||
id="userAddressProvince"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name="userAddressPostalCode"
|
||||
label={<Text sx={{ fontSize: 1 }}>ZIP / Postal code</Text>}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="userAddressPostalCode"
|
||||
placeholder="90069"
|
||||
id="userAddressPostalCode"
|
||||
/>
|
||||
</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 +220,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"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
async function sendApplication() {
|
||||
// Get the form data from sessionStorage
|
||||
const data = {}
|
||||
|
|
@ -7,7 +9,7 @@ async function sendApplication() {
|
|||
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
|
||||
}
|
||||
}
|
||||
console.dir('Sending data:', data)
|
||||
console.log({ data })
|
||||
|
||||
// Send the data
|
||||
try {
|
||||
|
|
@ -22,6 +24,8 @@ async function sendApplication() {
|
|||
}
|
||||
}
|
||||
|
||||
const isBlank = string => !string || string.trim() === ''
|
||||
|
||||
export function onSubmit({
|
||||
event,
|
||||
router,
|
||||
|
|
@ -32,28 +36,34 @@ export function onSubmit({
|
|||
setIsSubmitting
|
||||
}) {
|
||||
event.preventDefault()
|
||||
/* Don't return from inside the loop since
|
||||
we want all input values to be saved every time */
|
||||
let wasError = false
|
||||
|
||||
const formData = new FormData(form.current)
|
||||
const missingFields = []
|
||||
|
||||
const conditionalRequiredFields = _.cloneDeep(requiredFields) // Deep clone to prevent modification from leaking
|
||||
if (formData.get('contactOption') === 'Slack') {
|
||||
// If contact option is Slack, they must provide a Slack username
|
||||
conditionalRequiredFields.slackUsername = 'slack username'
|
||||
}
|
||||
|
||||
// Save form data
|
||||
formData.forEach((value, key) => {
|
||||
// Save form data
|
||||
sessionStorage.setItem('bank-signup-' + key, value)
|
||||
|
||||
// Check if there are empty required fields.
|
||||
if (
|
||||
((!value || value.trim() === '') && requiredFields.includes(key)) ||
|
||||
(formData.get('contactOption') === 'slack' &&
|
||||
(!formData.get('slackUsername') != null ||
|
||||
formData.get('slackUsername') === '')) // I'm so sorry for this
|
||||
isBlank(value) &&
|
||||
Object.keys(conditionalRequiredFields).includes(key)
|
||||
) {
|
||||
setFormError('Please fill out all required fields.')
|
||||
wasError = true
|
||||
missingFields.push(conditionalRequiredFields[key])
|
||||
}
|
||||
})
|
||||
if (wasError) return
|
||||
|
||||
if (missingFields.length !== 0) {
|
||||
setFormError(
|
||||
`Please fill out all required fields: ${missingFields.join(', ')}`
|
||||
)
|
||||
return // Don't submit application
|
||||
}
|
||||
|
||||
if (!formError) {
|
||||
setIsSubmitting(true)
|
||||
|
|
@ -61,7 +71,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}`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import { useEffect } from 'react'
|
||||
import { Flex, Input, Label, Radio, Select } from 'theme-ui'
|
||||
import Field from './field'
|
||||
import { useTeenagerLedContext } from './teenager-led-context'
|
||||
|
||||
export default function TeenagerOrAdultForm({ requiredFields }) {
|
||||
const { teenagerLed, setTeenagerLed } = useTeenagerLedContext()
|
||||
|
||||
const onTeenagerLedChange = e => {
|
||||
const newValue = e.target.value
|
||||
setTeenagerLed(newValue)
|
||||
|
||||
if (newValue === 'true') {
|
||||
// Clear cache of removed fields
|
||||
sessionStorage.removeItem('bank-signup-eventPoliticalActivity')
|
||||
sessionStorage.removeItem('bank-signup-eventAnnualBudget')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// [@garyhtou] welp... this exists because the Field component will cache
|
||||
// input values and set them on page load. It does it by directly setting
|
||||
// `input.value` with JavaScript; bypassing React. Because of that, the
|
||||
// `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.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}
|
||||
>
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import Icon from '../icon'
|
||||
import { Flex, Link, Text } from 'theme-ui'
|
||||
import { useMultiStepContext } from './apply/multi-step-context'
|
||||
|
||||
const phoneNumber = '+1 (844) 237-2290'
|
||||
const phoneNumberUri = '+1-844-237-2290'
|
||||
const email = 'hcb@hackclub.com'
|
||||
|
||||
export default function ContactBanner({ sx }) {
|
||||
const stepContext = useMultiStepContext()
|
||||
const firstStep = stepContext?.step === 0
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
display: firstStep ? 'flex' : ['none', 'flex'],
|
||||
bg: 'sunken',
|
||||
color: 'slate',
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -43,9 +43,6 @@ function formatMoney(amount) {
|
|||
}
|
||||
|
||||
const Stats = ({ stats }) => {
|
||||
if (stats.transactions_volume === undefined) {
|
||||
return null
|
||||
}
|
||||
const [balance, setBalance] = useState(0) // A formatted balance string, split by decimal
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -67,6 +64,9 @@ const Stats = ({ stats }) => {
|
|||
|
||||
return () => observer.disconnect()
|
||||
}, [stats.transactions_volume])
|
||||
|
||||
if (stats.transactions_volume === undefined) return null
|
||||
|
||||
return (
|
||||
<Box id="parent">
|
||||
<Flex sx={{ flexDirection: 'column', alignItems: 'center' }}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
182
components/index/cards/scrapyard.js
Normal file
182
components/index/cards/scrapyard.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import CardModel from './card-model'
|
||||
import { Box, Flex, Grid, Image, Text, Heading } from 'theme-ui'
|
||||
import Buttons from './button'
|
||||
import { Global } from '@emotion/react'
|
||||
|
||||
/** @jsxImportSource theme-ui */
|
||||
|
||||
export default function Scrapyard() {
|
||||
return (
|
||||
|
||||
<CardModel
|
||||
color="white"
|
||||
sx={{
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#90A8E1',
|
||||
fontFamily: "p22stanyan",
|
||||
objectFit: "contain"
|
||||
}}
|
||||
position={[null, 'bottom', 'bottom']}
|
||||
|
||||
>
|
||||
<Global
|
||||
styles={`
|
||||
@font-face {
|
||||
font-family: 'p22stanyan';
|
||||
src: url('https://use.typekit.net/af/444506/00000000000000007735b3cd/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n7&v=3') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'moonblossom';
|
||||
src: url('https://use.typekit.net/af/bf03be/00000000000000007735fbe5/30/l?primer=7cdcb44be4a7db8877ffa5c0007b8dd865b3bbc383831fe2ea177f62257a9191&fvd=n4&v=3') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<Grid columns={[1, 1, 1]} sx={{ position: 'relative', zIndex: 2 }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="https://cloud-4fnsp2wse-hack-club-bot.vercel.app/0scrapyard.png"
|
||||
sx={{
|
||||
width: ['400px', '450px', '500px'],
|
||||
mt: ['30px', '40px', '45px'],
|
||||
mb: ['30px', '30px', '30px'],
|
||||
position: 'relative',
|
||||
zIndex: 3,
|
||||
ml: '0px',
|
||||
mr: '0px',
|
||||
fontSize: ['48px', 4, 5],
|
||||
color: 'white',
|
||||
// mx: 'auto'
|
||||
}}
|
||||
alt="Scrapyard"
|
||||
/>
|
||||
|
||||
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: ['row', 'row', 'column'],
|
||||
justifyContent: 'space-between'
|
||||
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
background: "url('https://scrapyard.hackclub.com/elements/ripped-paper.png')",
|
||||
backgroundSize: 'cover',
|
||||
display: 'block',
|
||||
width: 'min(500px, calc(100vw - 30px))',
|
||||
filter: 'drop-shadow(5px 5px 5px #000000AA)',
|
||||
position: 'relative',
|
||||
zIndex: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
as="h2"
|
||||
sx={{
|
||||
fontFamily: 'moonblossom',
|
||||
textAlign: 'center',
|
||||
margin: '8%',
|
||||
fontSize: '22px',
|
||||
color: '#1f2d3d'
|
||||
}}
|
||||
>
|
||||
Build stupid stuff, get stupid prizes.
|
||||
</Heading>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
width: 'max-content',
|
||||
backgroundImage: "url('https://scrapyard.hackclub.com/elements/yellow-strip@stretch.svg')",
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%',
|
||||
width: '75%',
|
||||
position: 'relative',
|
||||
zIndex: 30,
|
||||
top: '-15%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
// pt: '3%',
|
||||
filter: 'drop-shadow(5px 5px 5px #00000099)'
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
as="h2"
|
||||
sx={{
|
||||
fontFamily: 'p22stanyan',
|
||||
mx: '8%',
|
||||
my: '3%',
|
||||
p: 0,
|
||||
wordBreak: 'keep-all',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 'min-content',
|
||||
fontSize: ['1.2em', '1.4em'],
|
||||
color: '#1f2d3d'
|
||||
}}
|
||||
>
|
||||
100+ Cities worldwide – March 15-16
|
||||
</Heading>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Buttons
|
||||
href="https://scrapyard.hackclub.com/"
|
||||
target="_blank"
|
||||
|
||||
rel="noopener"
|
||||
primary="#fde778"
|
||||
icon = "door-enter"
|
||||
id="43"
|
||||
zIndex={999}
|
||||
sx = {{zIndex: 9999999, left: ["50%", "50%", "0%"], color: '#1f2d3d'}}
|
||||
>
|
||||
|
||||
Learn More
|
||||
</Buttons>
|
||||
|
||||
|
||||
</Grid>
|
||||
<Image
|
||||
src="https://cloud-hqnbfdg3v-hack-club-bot.vercel.app/0image__14_.png"
|
||||
sx={{
|
||||
width: ['100%', '100%', '100%'],
|
||||
mb: ['0px', '0px', '0px'],
|
||||
mr: ['0px', '0px', '0px'],
|
||||
ml: ['0px', '0px', '0px'],
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
fontSize: ['36px', 4, 5],
|
||||
color: 'white',
|
||||
objectFit: 'cover',
|
||||
height: '100%',
|
||||
mx: 0
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
</CardModel>
|
||||
)
|
||||
}
|
||||
|
|
@ -19,9 +19,9 @@ export default async function handler(req, res) {
|
|||
body: JSON.stringify({
|
||||
email: data.userEmail,
|
||||
name: data.eventName,
|
||||
country: getCode(data.eventLocation) || '',
|
||||
postal_code: data.eventPostalCode || '',
|
||||
transparent: data.transparent
|
||||
country: getCode(data.userAddressCountry) || '',
|
||||
postal_code: data.userAddressPostalCode || '',
|
||||
transparent: 'false'
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -39,16 +39,13 @@ export default async function handler(req, res) {
|
|||
'Date of Birth': data.userBirthday,
|
||||
'Event Name': data.eventName,
|
||||
'Event Website': data.eventWebsite,
|
||||
'Zip Code': data.eventPostalCode,
|
||||
'Zip Code': data.userAddressPostalCode,
|
||||
'Tell us about your event': data.eventDescription,
|
||||
'Mailing Address': data.userAddress,
|
||||
'Address Line 1': data.addressLine1,
|
||||
City: data.addressCity,
|
||||
State: data.addressState,
|
||||
'Address Country': data.addressCountry,
|
||||
'Address Country Code': data.addressCountryCode,
|
||||
'Event Location': data.eventLocation,
|
||||
'Event Country Code': data.eventCountryCode,
|
||||
'Address Line 1': data.userAddressLine1,
|
||||
City: data.userAddressCity,
|
||||
State: data.userAddressProvince,
|
||||
'Address Country': data.userAddressCountry,
|
||||
'Event Location': data.userAddressCountry,
|
||||
'Have you used HCB for any previous events?':
|
||||
data.returningUser === 'true'
|
||||
? 'Yes, I have used HCB before'
|
||||
|
|
@ -65,7 +62,7 @@ export default async function handler(req, res) {
|
|||
'(Adults) Annual Budget': parseInt(data.eventAnnualBudget),
|
||||
'HCB ID': r.id
|
||||
})
|
||||
res.status(200).end();
|
||||
res.status(200).end()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
|
|
|
|||
|
|
@ -1,162 +1,105 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Alert, Box, Button, Flex, Grid, Heading, Text } from 'theme-ui'
|
||||
import { Box, Flex, Grid, Heading, Text } from 'theme-ui'
|
||||
import Head from 'next/head'
|
||||
import Link from 'next/link'
|
||||
import Icon from '@hackclub/icons'
|
||||
import Meta from '@hackclub/meta'
|
||||
import ForceTheme from '../../../components/force-theme'
|
||||
import FormContainer from '../../../components/fiscal-sponsorship/apply/form-container'
|
||||
import HCBInfo from '../../../components/fiscal-sponsorship/apply/hcb-info'
|
||||
import OrganizationInfoForm from '../../../components/fiscal-sponsorship/apply/org-form'
|
||||
import PersonalInfoForm from '../../../components/fiscal-sponsorship/apply/personal-form'
|
||||
import { onSubmit } from '../../../components/fiscal-sponsorship/apply/submit'
|
||||
import Watermark from '../../../components/fiscal-sponsorship/apply/watermark'
|
||||
import ContactBanner from '../../../components/fiscal-sponsorship/contact'
|
||||
import Callout from '../../../components/fiscal-sponsorship/apply/callout'
|
||||
import { TeenagerLedProvider } from '../../../components/fiscal-sponsorship/apply/teenager-led-context'
|
||||
import ApplicationForm from '../../../components/fiscal-sponsorship/apply/application-form'
|
||||
import { MultiStepProvider } from '../../../components/fiscal-sponsorship/apply/multi-step-context'
|
||||
|
||||
export default function Apply() {
|
||||
const router = useRouter()
|
||||
const formContainer = useRef()
|
||||
const [formError, setFormError] = useState(null)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const requiredFields = [
|
||||
'eventName',
|
||||
'eventLocation',
|
||||
'eventPostalCode',
|
||||
'eventDescription',
|
||||
'eventTeenagerLed',
|
||||
'eventPoliticalActivity',
|
||||
'eventAnnualBudget',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'userEmail',
|
||||
'userPhone',
|
||||
'userBirthday',
|
||||
'slackUsername'
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta as={Head} title="Apply for HCB" />
|
||||
<ForceTheme theme="light" />
|
||||
|
||||
<Grid
|
||||
columns={[null, null, 2]}
|
||||
sx={{
|
||||
gap: 0,
|
||||
width: '100%',
|
||||
minHeight: '100vh',
|
||||
alignItems: 'start'
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
<MultiStepProvider>
|
||||
<Grid
|
||||
columns={[null, null, 2]}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
px: [3, 5],
|
||||
py: 4,
|
||||
gap: 4,
|
||||
height: [null, '100svh'],
|
||||
position: [null, null, 'sticky'],
|
||||
top: 0,
|
||||
overflowY: [null, null, 'auto']
|
||||
gap: 0,
|
||||
width: '100%',
|
||||
minHeight: '100vh',
|
||||
alignItems: 'start'
|
||||
}}
|
||||
>
|
||||
{/* vertically align h1 to top of form */}
|
||||
<Box as="header" sx={{ mt: [null, 3], mb: 'auto' }}>
|
||||
<Link href="/fiscal-sponsorship" passHref legacyBehavior>
|
||||
<Text
|
||||
as="a"
|
||||
variant="subheadline"
|
||||
sx={{
|
||||
mb: 3,
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'muted',
|
||||
textDecoration: 'none',
|
||||
':hover': { color: 'primary' }
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={24}
|
||||
glyph="inserter"
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
Back
|
||||
</Text>
|
||||
</Link>
|
||||
<Heading as="h1" variant="title">
|
||||
Apply to join
|
||||
<br />
|
||||
<Flex sx={{ alignItems: 'center', gap: 3 }}>
|
||||
<img
|
||||
src="/fiscal-sponsorship/hcb-icon-small.png"
|
||||
width={48}
|
||||
height={48}
|
||||
alt="HCB logo"
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
verticalAlign: 'baseline'
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
px: [3, 5],
|
||||
py: 4,
|
||||
gap: 4,
|
||||
height: [null, null, '100svh'],
|
||||
position: [null, null, 'sticky'],
|
||||
top: 0,
|
||||
overflowY: [null, null, 'auto']
|
||||
}}
|
||||
>
|
||||
{/* vertically align h1 to top of form */}
|
||||
<Box as="header" sx={{ mt: [null, 3], mb: 'auto' }}>
|
||||
<Link href="/fiscal-sponsorship" passHref legacyBehavior>
|
||||
<Text
|
||||
as="a"
|
||||
variant="subheadline"
|
||||
sx={{
|
||||
mb: 3,
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'muted',
|
||||
textDecoration: 'none',
|
||||
':hover': { color: 'primary' }
|
||||
}}
|
||||
/>{' '}
|
||||
HCB
|
||||
</Flex>
|
||||
</Heading>
|
||||
</Box>
|
||||
<HCBInfo />
|
||||
<ContactBanner
|
||||
sx={{ borderRadius: 'default', bg: 'snow', width: 'fit-content' }}
|
||||
/>
|
||||
</Flex>
|
||||
<FormContainer
|
||||
ref={formContainer}
|
||||
className={formError ? 'has-errors' : null}
|
||||
onSubmit={event =>
|
||||
onSubmit({
|
||||
event,
|
||||
router,
|
||||
form: formContainer,
|
||||
setFormError,
|
||||
setIsSubmitting,
|
||||
requiredFields
|
||||
})
|
||||
}
|
||||
>
|
||||
<TeenagerLedProvider>
|
||||
<Callout />
|
||||
|
||||
<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} />
|
||||
{formError && <Alert bg="primary">{formError}</Alert>}
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
sx={{
|
||||
backgroundImage: theme => theme.util.gx('cyan', 'blue'),
|
||||
'&:disabled': {
|
||||
background: 'muted',
|
||||
cursor: 'not-allowed',
|
||||
transform: 'none !important'
|
||||
},
|
||||
width: 'fit-content'
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? 'Submitting…' : 'Submit'}
|
||||
</Button>
|
||||
</TeenagerLedProvider>
|
||||
</FormContainer>
|
||||
</Grid>
|
||||
>
|
||||
<Icon
|
||||
size={24}
|
||||
glyph="inserter"
|
||||
style={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
Back
|
||||
</Text>
|
||||
</Link>
|
||||
<Heading as="h1" variant="headline">
|
||||
Turn your ideas into
|
||||
<br />
|
||||
reality with{' '}
|
||||
<Flex
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
verticalAlign: 'middle'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/fiscal-sponsorship/hcb-icon-small.png"
|
||||
width={48}
|
||||
height={48}
|
||||
alt="HCB logo"
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
verticalAlign: 'baseline'
|
||||
}}
|
||||
/>{' '}
|
||||
HCB
|
||||
</Flex>
|
||||
</Heading>
|
||||
<Text variant="caption">
|
||||
Use HCB to raise nonprofit dollars to fund your project.
|
||||
</Text>
|
||||
</Box>
|
||||
<HCBInfo />
|
||||
<ContactBanner
|
||||
sx={{ borderRadius: 'default', bg: 'snow', width: 'fit-content' }}
|
||||
/>
|
||||
</Flex>
|
||||
<ApplicationForm />
|
||||
</Grid>
|
||||
</MultiStepProvider>
|
||||
<Watermark />
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
@ -439,7 +439,7 @@ export default function Page() {
|
|||
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M2.04 4.326c.325 1.329 2.532 2.54 3.717 3.19.48.263.793.434.743.484q-.121.12-.242.234c-.416.396-.787.749-.758 1.266.035.634.618.824 1.214 1.017.577.188 1.168.38 1.286.983.082.417-.075.988-.22 1.52-.215.782-.406 1.48.22 1.48 1.5-.5 3.798-3.186 4-5 .138-1.243-2-2-3.5-2.5-.478-.16-.755.081-.99.284-.172.15-.322.279-.51.216-.445-.148-2.5-2-1.5-2.5.78-.39.952-.171 1.227.182.078.099.163.208.273.318.609.304.662-.132.723-.633.039-.322.081-.671.277-.867.434-.434 1.265-.791 2.028-1.12.712-.306 1.365-.587 1.579-.88A7 7 0 1 1 2.04 4.327Z" />
|
||||
</svg>
|
||||
<span>
|
||||
As part of our commitment to climate justice, funding for HCB’s
|
||||
As part of our commitment to the environment, funding for HCB’s
|
||||
operations and staff will never come from the{' '}
|
||||
<UILink
|
||||
href="https://www.ffisolutions.com/the-carbon-underground-200-500/"
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ import Comma from '../components/comma'
|
|||
import Haxidraw from '../components/index/cards/haxidraw'
|
||||
import Onboard from '../components/index/cards/onboard'
|
||||
import Trail from '../components/index/cards/trail'
|
||||
import HighSeas from '../components/index/cards/highseas'
|
||||
import Counterspell from '../components/index/cards/counterspell'
|
||||
import Scrapyard from '../components/index/cards/scrapyard'
|
||||
/** @jsxImportSource theme-ui */
|
||||
|
||||
function Page({
|
||||
|
|
@ -669,8 +668,7 @@ function Page({
|
|||
and make things together!
|
||||
</Text>
|
||||
</Box>
|
||||
<HighSeas />
|
||||
<Counterspell />
|
||||
<Scrapyard />
|
||||
<Trail />
|
||||
<Slack slackKey={slackKey} data={slackData} events={events} />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -685,6 +685,51 @@ const Philanthropy = ({ posts = [] }) => {
|
|||
firm and has audited financials through the current fiscal year.
|
||||
</span>
|
||||
</Fade>
|
||||
<Line />
|
||||
<Fade delay={100}>
|
||||
<Flex sx={{ justifyContent: 'space-between' }} mt={[3, 4]}>
|
||||
<Box>
|
||||
<Text as="h2">View Hack Club's Annual Reports</Text>
|
||||
<Text as="p">2024 Report will be shared when ready.</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
as="a"
|
||||
variant="outline"
|
||||
href="https://cloud-qfyq0eotn-hack-club-bot.vercel.app/02023_annual_report.pdf"
|
||||
target="_blank"
|
||||
mb={4}
|
||||
sx={{
|
||||
fontSize: '1em !important',
|
||||
width: 'fit-content',
|
||||
float: 'right',
|
||||
mt: 2
|
||||
}}
|
||||
>
|
||||
2023
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
variant="outline"
|
||||
href="https://cloud-8zfcx3ub5-hack-club-bot.vercel.app/0hack_club_annual_report_2022.pdf"
|
||||
target="_blank"
|
||||
mb={4}
|
||||
sx={{
|
||||
fontSize: '1em !important',
|
||||
width: 'fit-content',
|
||||
float: 'right',
|
||||
mt: 2,
|
||||
mr: 2
|
||||
}}
|
||||
>
|
||||
2022
|
||||
</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
<span>
|
||||
Explore Hack Club's annual reports from 2022 onward, showcasing each year's impact and key milestones.
|
||||
</span>
|
||||
</Fade>
|
||||
{/* <Fade delay={300}>
|
||||
<Text
|
||||
as="a"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue