Redesign Fiscal Sponsorship Apply page (#1089)

Co-Authored-By: Dylan Wahbe <153225786+dwahbe@users.noreply.github.com>
This commit is contained in:
Lachlan Campbell 2024-03-05 01:55:58 -05:00 committed by GitHub
parent dd7b544030
commit c07684bb96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 553 additions and 1112 deletions

View file

@ -1,179 +0,0 @@
import { useEffect, useRef, useState, useCallback } from 'react'
import { Box, 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 Icon from '../../icon'
const approvedCountries = [
'AT',
'FI',
'FR',
'DE',
'GR',
'ES',
'IT',
'SE',
'TR',
'GB',
'NO',
'UA',
'BR',
'CO',
'US',
'CA',
'MX',
'JP',
'PH',
'MY',
'SG'
]
export default function AutoComplete({ name, isPersonalAddressInput }) {
const input = useRef()
const [predictions, setPredictions] = useState(null)
const [countryCode, setCountryCode] = useState(null)
const optionClicked = async prediction => {
input.current.value = prediction.name
// Needs to match the shape of the event object because onInput takes an event object.
await onInput({ target: { value: prediction.name } })
setPredictions(null)
}
const clickOutside = e => {
if (input.current && !input.current.contains(e.target)) {
setPredictions(null)
}
}
const onInput = useCallback(
async e => {
if (!e.target.value) return
const value = e.target.value
setPredictions(value ? (await search(value)).results : null)
if (isPersonalAddressInput) return
geocode(value)
.then(res => {
const country = res?.results[0]?.country
const countryCode = res?.results[0]?.countryCode
setCountryCode(countryCode)
sessionStorage.setItem('bank-signup-eventCountry', country)
sessionStorage.setItem('bank-signup-eventCountryCode', countryCode)
})
.catch(err => console.error(err))
},
[isPersonalAddressInput]
)
//TODO: Close suggestions view when focus is lost via tabbing.
//TODO: Navigate suggestions with arrow keys.
useEffect(() => {
const inputEl = input.current
if (!inputEl) return
document.addEventListener('click', clickOutside)
inputEl.addEventListener('input', onInput)
inputEl.addEventListener('focus', onInput)
return () => {
document.removeEventListener('click', clickOutside)
inputEl.removeEventListener('input', onInput)
inputEl.removeEventListener('focus', onInput)
}
}, [onInput])
return (
<Box sx={{ position: 'relative', width: '100%' }}>
<FlexCol flexDirection="column" position="relative" width="100%" gap="2">
<Input
ref={input}
name={name}
id={name}
placeholder="Shelburne, VT"
autoComplete="off"
sx={{ ...AutofillColourFix }}
/>
<Box>
{/* {String(countryCode)} */}
{countryCode && !approvedCountries.includes(countryCode) && (
<Flex sx={{ alignItems: 'center' }}>
<Icon
glyph="sad"
size="2.5rem"
sx={{ color: 'red', mr: 1, flexShrink: 0 }}
/>
<Text
as="label"
htmlFor={name}
sx={{
color: 'red'
// fontWeight: 'medium',
}}
>
Currently, we only have first-class support for organizations in
select countries.
<br />
If you're somewhere else, you can still use HCB!
<br />
Please contact us at hcb@hackclub.com
</Text>
</Flex>
)}
</Box>
</FlexCol>
{predictions && predictions.length > 0 && (
<Box
sx={{
background: '#47454f',
border: '1px solid #696675',
width: '100%',
p: 3,
borderRadius: '4px',
position: 'absolute',
bottom: 'calc(100% + 0.5em)'
}}
>
<FlexCol gap={1}>
{predictions.map((prediction, idx) => (
<>
<Text
as="button"
onClick={() => optionClicked(prediction)}
sx={{
cursor: 'pointer',
border: 'none',
background: 'none',
color: '#d1cbe7',
'&:hover': {
color: 'white'
},
fontFamily: 'inherit',
fontSize: 'inherit',
textAlign: 'inherit'
}}
key={idx}
>
{prediction.name}
</Text>
{idx < predictions.length - 1 && (
<hr
style={{
width: '100%',
color: '#8492a6'
}}
/>
)}
</>
))}
</FlexCol>
</Box>
)}
</Box>
)
}

View file

@ -1,42 +0,0 @@
import { Box, Button, Flex, Text } from 'theme-ui'
import Icon from '../../icon'
export default function AlertModal({ formError, setFormError }) {
if (!formError) return null
const close = () => setFormError(null)
return (
<Box>
<Box
onClick={close}
sx={{
position: 'fixed',
inset: 0,
background: '#000000',
opacity: 0.5,
zIndex: 1000
}}
/>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center',
gap: 3,
background: '#252429',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 1001,
padding: 4,
borderRadius: 'default'
}}
>
<Text variant="title">Oops!</Text>
<Text variant="lead">{formError}</Text>
<Button onClick={close}>Dismiss</Button>
</Flex>
</Box>
)
}

View file

@ -1,10 +0,0 @@
//TODO: Move to main theme
const autofillColourFix = {
'&:-webkit-autofill': {
boxShadow: '0 0 0 100px #252429 inset !important',
WebkitTextFillColor: 'white'
}
}
export default autofillColourFix

View file

@ -1,37 +0,0 @@
import { useEffect, useState } from 'react'
import Icon from '../../icon'
import { useRouter } from 'next/router'
export default function Checkbox({ name, defaultChecked = false, size = 38 }) {
const [checked, setChecked] = useState(defaultChecked)
const toggle = () => setChecked(!checked)
const router = useRouter()
/* Fill in the field with the value from sessionStorage.
For other input elements, the value is set in the Field component,
but these checkboxes hold their state in useState rather than in the DOM. */
useEffect(() => {
const value = router.query[name] || sessionStorage.getItem('bank-signup-' + name)
if (value) {
const input = document.getElementById(name)
input && setChecked(!!value)
}
}, [router.query, name])
return (
<>
<input aria-hidden="true" type="hidden" value={checked} name={name} />
<Icon
glyph={checked ? 'checkmark' : 'checkbox'}
size={size}
id={name}
name={name}
aria-checked={checked}
role="checkbox"
tabindex="0"
onClick={() => toggle()}
onKeyDown={e => e.key === 'Enter' && toggle()}
/>
</>
)
}

View file

@ -1,7 +1,6 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { Box, Flex, Label, Text } from 'theme-ui'
import FlexCol from '../../flex-col'
import { Flex, Label, Text } from 'theme-ui'
export default function Field({
name,
@ -12,8 +11,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 +25,56 @@ export default function Field({
}, [router.query, name])
return (
<FlexCol gap={2} width={'100%'}>
<Flex
<Flex
aria-required={isRequired}
sx={{
flexDirection: col ? 'column' : 'row',
alignItems: col ? 'flex-start' : 'center',
gap: 1,
width: '100%',
// Wrapper around Select
'> div': {
width: '100%'
},
'input, select, textarea': {
border: '1px solid',
borderColor: 'smoke',
outlineColor: 'blue',
'&:-webkit-autofill': {
boxShadow: '0 0 0 100px white inset !important',
WebkitTextFillColor: 'black !important'
}
}
}}
>
<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,25 +1,35 @@
import { forwardRef } from 'react'
import { Box } from 'theme-ui'
import { Box, Container } from 'theme-ui'
const formContainer = forwardRef(({ children }, ref) => {
const formContainer = forwardRef(({ children, ...props }, ref) => {
return (
<Box
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',
'&.has-errors div[aria-required="true"] input:placeholder-shown': {
borderColor: 'primary'
}
}}
{...props}
>
{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,66 @@
import { Box, Flex, Link, Text } from 'theme-ui'
import { Box, Link, Heading } 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: { pl: [3, 0], color: 'muted', mb: 4 },
p: { color: 'muted', mb: 0 }
}}
>
<Heading variant="subheadline">
HCB is a{' '}
<Link
href="https://en.wikipedia.org/wiki/Fiscal_sponsorship"
target="_blank"
sx={{
display: 'inline-flex',
alignItems: 'flex-end',
gap: 1
}}
>
fiscal sponsor
<Icon glyph="external" size={24} aria-hidden />
</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.</Heading>
<ul>
<li>
We partner with{' '}
<Link href="https://column.com" target="_blank">
Column Bank
</Link>{' '}
to offer a bank account 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 youre looking to set up a for-profit entity, consider{' '}
<Link href="https://stripe.com/atlas" target="_blank">
Stripe Atlas
</Link>
.
</p>
</Box>
)
}

View file

@ -1,178 +0,0 @@
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Button, Flex, Text, Spinner } from 'theme-ui'
async function sendApplication() {
// Get the form data from sessionStorage
const data = {}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i)
if (key.startsWith('bank-signup-')) {
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
}
}
console.dir('Sending data:', data)
// Send the data
try {
const res = await fetch('/api/fiscal-sponsorship/apply', {
method: 'POST',
cors: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
} catch (error) {
console.error(error)
}
}
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,
setFormError
}) {
const router = useRouter()
const [spinner, setSpinner] = useState(false)
useEffect(() => {
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
we want all input values to be saved every time */
let wasError = false
const formData = new FormData(form.current)
// Save form data
formData.forEach((value, key) => {
sessionStorage.setItem('bank-signup-' + key, value)
// Check if there are empty required fields.
if (
(!isBack &&
(!value || value.trim() === '') &&
requiredFields[step - 1].includes(key)) ||
(!isBack &&
formData.get('contactOption') === 'slack' &&
!formData.get('slackUsername')) // I'm so sorry for this
) {
setFormError('Please fill all required fields')
wasError = true
setSpinner(false)
}
})
if (wasError) return
// 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)
}
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 && (
<Spinner
sx={{
height: '32px',
color: 'white',
position: 'absolute',
right: '-0.3rem',
margin: '0 !important'
}}
/>
)}
</Button>
)
}

View file

@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'
import { Input, Textarea } from 'theme-ui'
import Checkbox from './checkbox'
import AddressInput from './address-input'
import { Input, Select, Textarea } from 'theme-ui'
// import Checkbox from './checkbox'
import Field from './field'
import AutofillColourFix from './autofill-colour-fix'
// This is using country-list instead of country-list-js as it has a smaller bundle size
import { getNames } from 'country-list'
export default function OrganizationInfoForm({ requiredFields }) {
const [org, setOrg] = useState('Organization')
@ -23,7 +23,6 @@ export default function OrganizationInfoForm({ requiredFields }) {
name="eventName"
id="eventName"
placeholder="Shelburne School Hackathon"
sx={{ ...AutofillColourFix }}
/>
</Field>
<Field
@ -35,18 +34,25 @@ export default function OrganizationInfoForm({ requiredFields }) {
<Input
name="eventWebsite"
id="eventWebsite"
type="url"
inputMode="url"
placeholder="hackclub.com"
sx={{ ...AutofillColourFix }}
/>
</Field>
<Field
name="eventLocation"
label={`${org} location`}
description="If your organization runs online, put your own address."
label={`Primary country of operations`}
requiredFields={requiredFields}
>
<AddressInput isPersonalAddressInput={false} name="eventLocation" />
<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>
</Field>
{/* <Field
name="transparent"
@ -64,8 +70,8 @@ export default function OrganizationInfoForm({ requiredFields }) {
</Field> */}
<Field
name="eventDescription"
label={`Tell us about your ${org.toLowerCase()}!`}
description="1 or 2 sentences will suffice"
label={`Tell us about your ${org.toLowerCase()}`}
description="24 sentences will suffice."
requiredFields={requiredFields}
>
<Textarea
@ -73,9 +79,7 @@ export default function OrganizationInfoForm({ requiredFields }) {
id="eventDescription"
rows={3}
sx={{
resize: 'vertical',
width: '100%',
...AutofillColourFix
resize: 'vertical'
}}
/>
</Field>

View file

@ -1,18 +1,9 @@
import { Input, Flex, Label, Radio } from 'theme-ui'
import Checkbox from './checkbox'
import AddressInput from './address-input'
import { Input, Flex, Label, Radio, Grid, Select } from 'theme-ui'
import Field from './field'
import AutofillColourFix from './autofill-colour-fix'
import { useState } from 'react'
export default function PersonalInfoForm({
setValidationResult,
requiredFields
}) {
export default function PersonalInfoForm({ 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.
return (
<>
@ -22,24 +13,14 @@ export default function PersonalInfoForm({
label="First name"
requiredFields={requiredFields}
>
<Input
name="firstName"
id="firstName"
placeholder="Fiona"
sx={{ ...AutofillColourFix }}
/>
<Input name="firstName" id="firstName" placeholder="Fiona" />
</Field>
<Field
name="lastName"
label="Last name"
requiredFields={requiredFields}
>
<Input
name="lastName"
id="lastName"
placeholder="Hacksworth"
sx={{ ...AutofillColourFix }}
/>
<Input name="lastName" id="lastName" placeholder="Hacksworth" />
</Field>
</Flex>
<Field name="userEmail" label="Email" requiredFields={requiredFields}>
@ -48,10 +29,78 @@ export default function PersonalInfoForm({
id="userEmail"
type="email"
placeholder="fiona@hackclub.com"
onInput={e => setEmail(e.target.value)}
sx={{ ...AutofillColourFix }}
/>
</Field>
<Field
name="contactOption"
label="Preferred contact channel"
requiredFields={requiredFields}
>
<Grid
columns={[null, 2]}
sx={{
rowGap: 2,
columnGap: 4,
width: '100%'
}}
>
<Label
sx={{
display: 'flex',
flexDirection: 'row'
}}
>
<Radio
name="contactOption"
value="Email"
defaultChecked={true}
onInput={() => setSelectedContactOption('Email')}
/>
Email
</Label>
<Grid
sx={{
columnGap: 0,
rowGap: 2,
gridTemplateColumns: 'auto 1fr'
}}
>
<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"
placeholder="FionaH"
autocomplete="off"
data-1p-ignore
autoFocus
/>
</Field>
</>
) : null}
</Grid>
</Grid>
</Field>
<Field
name="userPhone"
label="Phone"
@ -62,23 +111,31 @@ export default function PersonalInfoForm({
name="userPhone"
id="userPhone"
type="tel"
placeholder="(123) 456-7890"
sx={{ ...AutofillColourFix }}
placeholder="1-855-625-HACK"
/>
</Field>
<Field
name="userBirthday"
label="Birthday"
label="Birth year"
requiredFields={requiredFields}
>
<Input
name="userBirthday"
id="userBirthday"
type="date"
sx={{ ...AutofillColourFix }}
/>
<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>
</Field>
<Field
{/* <Field
name="referredBy"
label="Who were you referred by?"
requiredFields={requiredFields}
@ -87,7 +144,6 @@ export default function PersonalInfoForm({
name="referredBy"
id="referredBy"
placeholder="Max"
sx={{ ...AutofillColourFix }}
/>
</Field>
<Field
@ -111,67 +167,17 @@ 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 }}>
<Label
sx={{
display: 'flex',
flexDirection: 'row'
}}
>
<Radio
name="contactOption"
value="Email"
defaultChecked={true}
onInput={() => setSelectedContactOption('Email')}
/>
Email
</Label>
<Label
sx={{
display: 'flex',
flexDirection: 'row'
}}
>
<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}
</Field>
*/}
<Field
name="accommodations"
label="Accessibility needs"
description="Please specify any accommodations, accessibility needs, or other important information so we can support you during onboarding and while using HCB"
description="Please specify any accommodations, accessibility needs, or other important information so we can support you during onboarding and while using HCB."
requiredFields={requiredFields}
>
<Input
name="accommodations"
id="accommodations"
placeholder="I use a screen reader/I need increased text size during onboarding"
sx={{ ...AutofillColourFix }}
/>
</Field>
</>

View file

@ -1,85 +0,0 @@
import { useRouter } from 'next/router'
import { Box, Flex, Text } from 'theme-ui'
import FlexCol from '../../flex-col'
function StepIcon({ completed, number }) {
let strokeColour = completed ? '#33d6a6' : '#8492a6'
let fillColour = completed ? '#33d6a6' : 'none'
return (
<Box sx={{ position: 'relative' }}>
<svg
style={{ translate: '0 1px' }}
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 38C36.5 38 38 36.5 38 20C38 3.5 36.5 2 20 2C3.5 2 2 3.5 2 20C2 36.5 3.5 38 20 38Z"
stroke={strokeColour}
fill={fillColour}
stroke-width="3"
/>
</svg>
<Flex
sx={{
height: '100%',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
inset: '0'
}}
>
<Text
sx={{
color: 'white',
fontSize: 2
}}
>
{number}
</Text>
</Flex>
</Box>
)
}
function Step({ number, label, completed }) {
return (
<Flex sx={{ lineHeight: '1', alignItems: 'center', gap: '4' }}>
<StepIcon completed={completed} number={number + 1} />
<Text
sx={{
fontSize: '3',
display: ['none', null, null, 'block']
}}
>
{label}
</Text>
</Flex>
)
}
export default function Progress() {
const router = useRouter()
const step = parseInt(router.query.step)
const labels = ['Intro', 'Organization info', 'Personal info']
return (
<Flex
sx={{
gap: 3,
translate: [0, null, null, '-1rem 0'],
flexDirection: ['row', null, null, 'column']
}}
>
{labels.map((label, i) => (
<Step number={i} label={label} completed={step > i} key={i} />
))}
</Flex>
)
}

View file

@ -0,0 +1,65 @@
async function sendApplication() {
// Get the form data from sessionStorage
const data = {}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i)
if (key.startsWith('bank-signup-')) {
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
}
}
console.dir('Sending data:', data)
// Send the data
try {
return fetch('/api/fiscal-sponsorship/apply', {
method: 'POST',
cors: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
} catch (error) {
console.error(error)
}
}
export function onSubmit({
event,
router,
form,
requiredFields,
formError,
setFormError,
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)
// Save form data
formData.forEach((value, key) => {
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
) {
setFormError('Please fill out all required fields.')
wasError = true
}
})
if (wasError) return
if (!formError) {
setIsSubmitting(true)
sendApplication().then(() => {
router.push('/fiscal-sponsorship/apply/success')
})
}
return
}

View file

@ -13,8 +13,8 @@ export default function Watermark() {
if (!shineRef.current || !svgRef.current) return
const svgWidth = svgRef.current.clientWidth / 100
const svgFromTop = svgRef.current.getBoundingClientRect().top
const svgFromLeft = svgRef.current.getBoundingClientRect().left
// const svgFromTop = svgRef.current.getBoundingClientRect().top
// const svgFromLeft = svgRef.current.getBoundingClientRect().left
shineRef.current.style.top = `${clientY / svgWidth + 6.2}px`
shineRef.current.style.left = `${clientX / svgWidth + 9.2}px`
@ -58,7 +58,7 @@ export default function Watermark() {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: '#1d181f',
backgroundColor: 'snow',
clipPath: 'url(#my-clip-path)'
}}
>
@ -69,7 +69,7 @@ export default function Watermark() {
width: '2px',
height: '2px',
borderRadius: '50%',
backgroundColor: 'red',
backgroundColor: 'primary',
filter: 'blur(2px)'
}}
/>

View file

@ -25,6 +25,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"

View file

@ -1,83 +0,0 @@
async function getOrRefreshToken() {
// Get the token from localStorage
const token = JSON.parse(localStorage.getItem('mapkit-token'))
if (token) {
// If the token is still valid, return it
if (Date.now() < token.refreshBefore) return token.accessToken
} else {
// The token is invalid or doesn't exist, so get a new one
// This is a MapKit master token, restricted to https://hackclub.com
const master =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkNSQkg2S1VLTEIifQ.eyJpc3MiOiJQNlBWMlI5NDQzIiwiaWF0IjoxNjc5NjY3NjIyLCJleHAiOjE3MTEyMzg0MDB9.E6g9QPdbEWLgF6tdcL0YfX8NescYnwKhQpXdiyRitNm7-Oot-3VH0ze9xUd8xkOzuS_-7KeWy4bfYTD-2yX7Sg'
// Get a MapKit server token
const res = await fetch('https://maps-api.apple.com/v1/token', {
headers: { Authorization: `Bearer ${master}` }
})
const resJson = await res.json()
// Set the token's expiration time to 10 seconds before the actual expiration time
resJson.refreshBefore =
Date.now() + resJson.expiresInSeconds * 1_000 - 10_000
// Save the token to localStorage
localStorage.setItem('mapkit-token', JSON.stringify(resJson))
return resJson.accessToken
}
}
//TODO: Limit the number of retries
export async function search(query) {
if (!query || !query.trim()) return
const token = await getOrRefreshToken()
const res = await fetch(`https://maps-api.apple.com/v1/search?q=${query}`, {
headers: { Authorization: `Bearer ${token}` }
})
const resJson = await res.json()
if (resJson.error) {
if (resJson.error.message === 'Not Authorized') {
// The token is invalid, so remove it from localStorage
localStorage.removeItem('mapkit-token')
// Try again
console.warn('MapKit token expired, refreshing')
return search(query)
} else {
throw new Error(resJson.error.message)
}
}
return resJson
}
export async function geocode(query) {
if (!query || !query.trim()) return
const token = await getOrRefreshToken()
const res = await fetch(`https://maps-api.apple.com/v1/geocode?q=${query}`, {
headers: { Authorization: `Bearer ${token}` }
})
const resJson = await res.json()
if (resJson.error) {
if (resJson.error.message === 'Not Authorized') {
// The token is invalid, so remove it from localStorage
localStorage.removeItem('mapkit-token')
// Try again
console.warn('MapKit token expired, refreshing')
return geocode(query)
} else {
throw new Error(resJson.error.message)
}
}
return resJson
}

View file

@ -31,6 +31,7 @@
"animejs": "^3.2.2",
"axios": "^1.6.7",
"cookies-next": "^4.0.0",
"country-list": "^2.3.0",
"country-list-js": "^3.1.8",
"cursor-effects": "^1.0.15",
"date-fns": "^2.30.0",
@ -63,7 +64,7 @@
"react-use-websocket": "^4.7.0",
"react-wrap-balancer": "^1.1.0",
"recharts": "2.1.12",
"styled-components": "^6.1.1",
"styled-components": "^6.1.8",
"swr": "^2.2.4",
"theme-ui": "^0.14",
"tinytime": "^0.2.6",

View file

@ -1,140 +1,148 @@
import { useEffect, useState, useRef } from 'react'
import { useState, useRef } from 'react'
import { useRouter } from 'next/router'
import { Box, Flex, Text } from 'theme-ui'
import { Box, Text, Flex, Heading, Grid, Alert, Button } 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 { onSubmit } from '../../../components/fiscal-sponsorship/apply/submit'
import Watermark from '../../../components/fiscal-sponsorship/apply/watermark'
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 AlertModal from '../../../components/fiscal-sponsorship/apply/alert-modal'
import { geocode } from '../../../lib/fiscal-sponsorship/apply/address-validation'
const valiadateAddress = async step => {
// Validate the address
if (step === 3) {
// Get the raw personal address input
const userAddress = sessionStorage.getItem('bank-signup-userAddress')
if (!userAddress) return
const result = await geocode(userAddress)
const addrComp = type => result.results[0]?.structuredAddress[type] ?? ''
sessionStorage.setItem(
'bank-signup-addressLine1',
addrComp('fullThoroughfare')
)
sessionStorage.setItem('bank-signup-addressCity', addrComp('locality'))
sessionStorage.setItem(
'bank-signup-addressState',
addrComp('administrativeArea')
)
sessionStorage.setItem('bank-signup-addressZip', addrComp('postCode'))
sessionStorage.setItem(
'bank-signup-addressCountry',
result.results[0]?.country ?? ''
)
sessionStorage.setItem(
'bank-signup-addressCountryCode',
result.results[0]?.countryCode ?? ''
)
}
}
import Icon from '@hackclub/icons'
import Link from 'next/link'
export default function Apply() {
const router = useRouter()
const [step, setStep] = useState(1)
const formContainer = useRef()
const [formError, setFormError] = useState(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const requiredFields = [
[],
['eventName', 'eventLocation'],
['firstName', 'lastName', 'userEmail', 'userBirthday', 'contactOption']
'eventName',
'eventLocation',
'eventDescription',
'firstName',
'lastName',
'userEmail',
'userBirthday',
'slackUsername'
]
useEffect(() => {
console.error(`Form error: ${formError}`)
if (!router.isReady) return
setStep(parseInt(router.query.step))
// Set the query url parameter to 1 if it's not present
if (!step || step < 1) {
router.replace(
{
pathname: router.pathname,
query: { ...router.query, step: 1 }
},
undefined,
{}
)
}
}, [formError, router, step])
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: [4, 5],
height: [null, '100svh'],
position: [null, null, 'sticky'],
top: 0,
overflowY: [null, null, 'auto']
}}
>
<NavButton isBack={true} form={formContainer} />
<NavButton
isBack={false}
form={formContainer}
setFormError={setFormError}
requiredFields={requiredFields}
clickHandler={() => valiadateAddress(step)}
/>
{/* vertically align h1 to top of form */}
<Box as="header" sx={{ mt: [null, null, -24] }}>
<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'
}}
/>{' '}
HCB
</Flex>
</Heading>
</Box>
<HCBInfo />
</Flex>
</Box>
<AlertModal formError={formError} setFormError={setFormError} />
<FormContainer
ref={formContainer}
className={formError ? 'has-errors' : null}
onSubmit={event =>
onSubmit({
event,
router,
form: formContainer,
setFormError,
setIsSubmitting,
requiredFields
})
}
>
<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>
</FormContainer>
</Grid>
<Watermark />
</>
)

View file

@ -1,129 +1,75 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import {
Box,
Button,
Card,
Container,
Divider,
Text,
Link,
Flex,
Image
} from 'theme-ui'
import ForceTheme from '../../../components/force-theme'
import { Box, Container, Text, Link, Flex, Image } from 'theme-ui'
import JSConfetti from 'js-confetti'
import Icon from '../../../components/icon'
import FlexCol from '../../../components/flex-col'
import { Balancer } from 'react-wrap-balancer'
function Option({ icon, label, link }) {
const color =
icon === 'email' ? '#338eda' : icon === 'slack' ? '#a633d6' : '#ec3750'
return (
<Button
variant="outline"
as="a"
href={link}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 'fit-content',
color,
borderColor: color
}}
>
<Flex sx={{ alignItems: 'center', gap: [0, null, 1], px: 2 }}>
<Icon
glyph={icon}
sx={{ width: [32, null, 46], height: [32, null, 46] }}
/>
<Text sx={{ fontSize: [2, null, 3] }}>{label}</Text>
</Flex>
</Button>
)
function fireConfetti() {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti({
confettiColors: [
// Hack Club colours!
'#ec3750',
'#ff8c37',
'#f1c40f',
'#33d6a6',
'#5bc0de',
'#338eda',
'#a633d6'
]
})
}
export default function ApplicationSuccess() {
const router = useRouter()
useEffect(() => {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti({
confettiColors: [
// Hack Club colours!
'#ec3750',
'#ff8c37',
'#f1c40f',
'#33d6a6',
'#5bc0de',
'#338eda',
'#a633d6'
]
})
}, [router])
fireConfetti()
}, [])
return (
<Container variant="copy">
<ForceTheme theme="dark" />
<FlexCol
height="100vh"
textAlign="center"
alignItems="center"
justifyContent="space-between"
py={5}
gap={4}
>
<FlexCol gap={4} alignItems="center">
<Image
src="/fiscal-sponsorship/apply/party-orpheus.svg"
alt="Dinosaur partying"
sx={{ width: '40%' }}
/>
<FlexCol gap={2}>
<Text variant="title">Thanks for applying!</Text>
<Text variant="lead">
Head on over to HCB and explore the dashboard
</Text>
</FlexCol>
</FlexCol>
<Container
variant="narrow"
sx={{
height: '100svb',
textAlign: 'center',
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
py: 5,
pt: [null, null, 6],
gap: 4
}}
>
<header>
<Image
src="/fiscal-sponsorship/apply/party-orpheus.svg"
alt="Dinosaur partying"
onClick={fireConfetti}
sx={{ width: '40%' }}
/>
<Text as="h1" variant="title" mt={4}>
Thanks for applying!
</Text>
<Text as="p" variant="lead">
<Balancer>
Well review your application and get back to you within two
business days.
</Balancer>
</Text>
</header>
<FlexCol gap={4} width="100%">
<Text sx={{ fontSize: [3, null, 4] }}>
Questions about your application?
</Text>
<Flex
sx={{
flexDirection: ['column', null, 'row'],
justifyContent: 'space-evenly',
alignItems: 'center',
gap: [3, null, 0]
}}
>
<Option icon="email" label="Mail" link="mailto:hcb@hackclub.com">
hcb@hackclub.com
</Option>
<Option
icon="slack"
label="Slack"
link="https://hackclub.slack.com/channels/hcb"
>
#hcb
</Option>
<Option icon="help" label="FAQ" link="https://hcb.hackclub.com/faq">
FAQ
</Option>
</Flex>
</FlexCol>
<Button as="a" href="https://hcb.hackclub.com">
<Flex sx={{ alignItems: 'center', px: [2, null, 3], py: 2 }}>
<Icon glyph="bank-account" size={36} />
<Text sx={{ fontSize: 3 }}>Head to HCB!</Text>
</Flex>
</Button>
</FlexCol>
<footer>
<Text as="h2" variant="subheadline">
Questions about your application?
</Text>
<Text as="p" fontSize={2} color="muted">
You can always email us at{' '}
<Link href="mailto:hcb@hackclub.com" color="blue">
hcb@hackclub.com
</Link>
.
</Text>
</footer>
</Container>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1330,6 +1330,13 @@
resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz"
integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==
"@emotion/is-prop-valid@1.2.1", "@emotion/is-prop-valid@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc"
integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/is-prop-valid@^0.8.1":
version "0.8.8"
resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz"
@ -1337,13 +1344,6 @@
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/is-prop-valid@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc"
integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==
dependencies:
"@emotion/memoize" "^0.8.1"
"@emotion/memoize@0.7.4":
version "0.7.4"
resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
@ -1401,7 +1401,12 @@
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.1"
"@emotion/utils" "^1.2.1"
"@emotion/unitless@^0.8.0", "@emotion/unitless@^0.8.1":
"@emotion/unitless@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
"@emotion/unitless@^0.8.1":
version "0.8.1"
resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz"
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
@ -2233,7 +2238,7 @@
dependencies:
csstype "^3.0.2"
"@types/stylis@^4.0.2":
"@types/stylis@4.2.0":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.0.tgz#199a3f473f0c3a6f6e4e1b17cdbc967f274bdc6b"
integrity sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==
@ -2948,9 +2953,9 @@ camelcase@^5.3.1:
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelize@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
version "1.0.1"
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3"
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001282, caniuse-lite@^1.0.30001541:
version "1.0.30001554"
@ -3241,6 +3246,11 @@ country-list-js@^3.1.8:
dependencies:
micro "^9.3.3"
country-list@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/country-list/-/country-list-2.3.0.tgz#1e7ceaf9834c1d1210054301eabf4dc445ab978c"
integrity sha512-qZk66RlmQm7fQjMYWku1AyjlKPogjPEorAZJG88owPExoPV8EsyCcuFLvO2afTXHEhi9liVOoyd+5A6ZS5QwaA==
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@ -3300,10 +3310,10 @@ crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
css-color-keywords@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==
css-to-react-native@^3.2.0:
css-to-react-native@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32"
integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==
@ -3336,7 +3346,7 @@ cssnano-simple@2.0.0:
dependencies:
cssnano-preset-simple "^2.0.0"
csstype@^3.0.10, csstype@^3.0.2, csstype@^3.1.2:
csstype@3.1.2, csstype@^3.0.10, csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
@ -5789,7 +5799,7 @@ ms@^2.1.1:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.1.22, nanoid@^3.3.6:
nanoid@^3.1.22:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
@ -5799,6 +5809,11 @@ nanoid@^3.3.4:
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanoid@^3.3.6:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
native-url@0.3.4:
version "0.3.4"
resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.3.4.tgz#29c943172aed86c63cee62c8c04db7f5756661f8"
@ -6290,7 +6305,7 @@ postcss-value-parser@^3.3.0:
postcss-value-parser@^4.0.2:
version "4.2.0"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.2.13:
@ -6311,7 +6326,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.31:
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
@ -7139,9 +7154,9 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallowequal@^1.1.0:
shallowequal@1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
shebang-command@^2.0.0:
@ -7461,20 +7476,20 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
styled-components@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.1.tgz#a5414ada07fb1c17b96a26a05369daa4e2ad55e5"
integrity sha512-cpZZP5RrKRIClBW5Eby4JM1wElLVP4NQrJbJ0h10TidTyJf4SIIwa3zLXOoPb4gJi8MsJ8mjq5mu2IrEhZIAcQ==
styled-components@^6.1.8:
version "6.1.8"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.8.tgz#c109d36aeea52d8f049e12de2f3be39a6fc86201"
integrity sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==
dependencies:
"@emotion/is-prop-valid" "^1.2.1"
"@emotion/unitless" "^0.8.0"
"@types/stylis" "^4.0.2"
css-to-react-native "^3.2.0"
csstype "^3.1.2"
postcss "^8.4.31"
shallowequal "^1.1.0"
stylis "^4.3.0"
tslib "^2.5.0"
"@emotion/is-prop-valid" "1.2.1"
"@emotion/unitless" "0.8.0"
"@types/stylis" "4.2.0"
css-to-react-native "3.2.0"
csstype "3.1.2"
postcss "8.4.31"
shallowequal "1.1.0"
stylis "4.3.1"
tslib "2.5.0"
styled-jsx@3.3.2:
version "3.3.2"
@ -7529,10 +7544,10 @@ stylis@4.2.0:
resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz"
integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
stylis@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c"
integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==
stylis@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.1.tgz#ed8a9ebf9f76fe1e12d462f5cc3c4c980b23a7eb"
integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==
supports-color@^5.3.0:
version "5.5.0"
@ -7707,7 +7722,12 @@ tsconfig-paths@^3.14.2:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.5.0:
tslib@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==