mirror of
https://github.com/System-End/site.git
synced 2026-04-19 22:05:11 +00:00
Redesign Fiscal Sponsorship Apply page (#1089)
Co-Authored-By: Dylan Wahbe <153225786+dwahbe@users.noreply.github.com>
This commit is contained in:
parent
dd7b544030
commit
c07684bb96
22 changed files with 553 additions and 1112 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 you’re 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 you’re looking to set up a for-profit entity, consider{' '}
|
||||
<Link href="https://stripe.com/atlas" target="_blank">
|
||||
Stripe Atlas
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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="2–4 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
65
components/fiscal-sponsorship/apply/submit.js
Normal file
65
components/fiscal-sponsorship/apply/submit.js
Normal 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
|
||||
}
|
||||
|
|
@ -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)'
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
Let’s 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 />
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
We’ll 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
BIN
public/fiscal-sponsorship/hcb-icon-small.png
Normal file
BIN
public/fiscal-sponsorship/hcb-icon-small.png
Normal file
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 |
98
yarn.lock
98
yarn.lock
|
|
@ -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==
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue