mirror of
https://github.com/System-End/site.git
synced 2026-04-19 15:18:18 +00:00
Redesign Bank signup flow (#728)
This commit is contained in:
parent
fd25fda1eb
commit
957bc1c0da
36 changed files with 1958 additions and 870 deletions
29
components/bank/apply-button.js
Normal file
29
components/bank/apply-button.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Button, Text, Image, Flex } from 'theme-ui'
|
||||
import Icon from '../icon'
|
||||
|
||||
export default function ApplyButton() {
|
||||
return (
|
||||
<Button
|
||||
variant='ctaLg'
|
||||
as='a'
|
||||
href='apply'
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '4.2rem',
|
||||
// borderRadius: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<Flex sx={{
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
mr: '-32px' // Man...
|
||||
}}>
|
||||
<Text
|
||||
color='white'
|
||||
sx={{ fontWeight: 'bold', fontSize: 4 }}
|
||||
>Apply now</Text>
|
||||
<Icon glyph='view-forward' size={46} color='white' />
|
||||
</Flex>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
160
components/bank/apply/address-input.js
Normal file
160
components/bank/apply/address-input.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Box, Flex, Input, Text } from 'theme-ui'
|
||||
import FlexCol from '../../flex-col'
|
||||
import AutofillColourFix from './autofill-colour-fix'
|
||||
import { geocode } from '../../../lib/bank/apply/address-validation'
|
||||
import Icon from '../../icon'
|
||||
|
||||
const approvedCountries = ['US', 'CA', 'MX'];
|
||||
|
||||
export default function AutoComplete({ name, isPersonalAddressInput }) {
|
||||
const input = useRef()
|
||||
const base = useRef()
|
||||
const [predictions, setPredictions] = useState(null)
|
||||
const [countryCode, setCountryCode] = useState(null)
|
||||
|
||||
const performGeocode = async (address) => {
|
||||
if (isPersonalAddressInput) return
|
||||
geocode(address)
|
||||
.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))
|
||||
}
|
||||
|
||||
const optionClicked = async (prediction) => {
|
||||
input.current.value = prediction.description
|
||||
performGeocode(prediction.description)
|
||||
setPredictions(null)
|
||||
}
|
||||
const clickOutside = (e) => {
|
||||
if (input.current && !input.current.contains(e.target)) {
|
||||
setPredictions(null)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Close suggestions view when focus is lost via tabbing.
|
||||
//TODO: Navigate suggestions with arrow keys.
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.google || !input.current) return
|
||||
|
||||
const service = new window.google.maps.places.AutocompleteService()
|
||||
|
||||
const onInput = async (e) => {
|
||||
if (!e.target.value) {
|
||||
setPredictions(null)
|
||||
} else {
|
||||
service.getPlacePredictions(
|
||||
{ input: e.target.value },
|
||||
(predictions, status) => {
|
||||
setPredictions(predictions)
|
||||
if (status !== window.google.maps.places.PlacesServiceStatus.OK) { //DEBUG
|
||||
setPredictions([])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', clickOutside)
|
||||
input.current.addEventListener('input', onInput)
|
||||
input.current.addEventListener('focus', onInput)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', clickOutside)
|
||||
input.current?.removeEventListener('input', onInput)
|
||||
input.current?.removeEventListener('focus', 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 }}
|
||||
onInput={async (e) => performGeocode(e.target.value)}
|
||||
/>
|
||||
<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 the United States, Canada, and Mexico.<br />
|
||||
If you're somewhere else, you can still use bank!<br />
|
||||
Please contact us at bank@hackclub.com
|
||||
</Text>
|
||||
</Flex>
|
||||
}
|
||||
</Box>
|
||||
</FlexCol>
|
||||
{ predictions &&
|
||||
<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={prediction.id}
|
||||
>
|
||||
{prediction.description}
|
||||
</Text>
|
||||
|
||||
{
|
||||
idx < predictions.length - 1 &&
|
||||
<hr
|
||||
style={{
|
||||
width: '100%',
|
||||
color: '#8492a6',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
))}
|
||||
</FlexCol>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
37
components/bank/apply/alert-modal.js
Normal file
37
components/bank/apply/alert-modal.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
7
components/bank/apply/autofill-colour-fix.js
Normal file
7
components/bank/apply/autofill-colour-fix.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'&:-webkit-autofill': {
|
||||
boxShadow: '0 0 0 100px #252429 inset !important',
|
||||
WebkitTextFillColor: 'white',
|
||||
},
|
||||
}
|
||||
//TODO: Move to main theme
|
||||
96
components/bank/apply/bank-info.js
Normal file
96
components/bank/apply/bank-info.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { Box, Flex, Link, Text } from 'theme-ui'
|
||||
import Icon from '../../icon'
|
||||
import FlexCol from '../../flex-col'
|
||||
|
||||
export default function BankInfo() {
|
||||
return (
|
||||
<Box>
|
||||
<FlexCol gap={4}>
|
||||
<FlexCol gap={4}>
|
||||
<Text sx={{ fontSize: 36 }}>
|
||||
What Hack Club Bank <i>is</i>
|
||||
</Text>
|
||||
<FlexCol gap={3} ml={3}>
|
||||
<FlexCol gap={2}>
|
||||
<Flex sx={{ alignItems: "center", gap: 2 }}>
|
||||
<Link
|
||||
color='white'
|
||||
href="/bank/fiscal-sponsorship"
|
||||
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 Hack Club Bank <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 money.
|
||||
</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 Bank is not for you.
|
||||
Consider setting up a business.
|
||||
</li>
|
||||
</ul>
|
||||
</Text>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
39
components/bank/apply/checkbox.js
Normal file
39
components/bank/apply/checkbox.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Icon from '../../icon'
|
||||
|
||||
export default function Checkbox({ name, defaultChecked=false, size=38 }) {
|
||||
const [checked, setChecked] = useState(defaultChecked)
|
||||
const toggle = () => setChecked(!checked)
|
||||
|
||||
/* 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 = sessionStorage.getItem('bank-signup-' + name)
|
||||
if (value) {
|
||||
const input = document.getElementById(name)
|
||||
input && setChecked(value === 'true')
|
||||
}
|
||||
}, [])
|
||||
|
||||
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()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
52
components/bank/apply/field.js
Normal file
52
components/bank/apply/field.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { Box, Flex, Label, Text } from 'theme-ui'
|
||||
import FlexCol from '../../flex-col'
|
||||
|
||||
export default function Field({ name, label, description, col = true, requiredFields, children }) {
|
||||
const router = useRouter()
|
||||
const isRequired = requiredFields[parseInt(router.query.step) - 1].includes(name)
|
||||
|
||||
/* Fill in the field input element with the value from sessionStorage.
|
||||
Note: the custom checkbox component does this in its own useEffect hook. */
|
||||
useEffect(() => {
|
||||
const value = sessionStorage.getItem('bank-signup-' + name)
|
||||
if (value) {
|
||||
const input = document.getElementById(name)
|
||||
if (input) input.value = value
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FlexCol gap={2} width={'100%'}>
|
||||
<Flex sx={{
|
||||
flexDirection: col ? 'column' : 'row',
|
||||
alignItems: col ? 'flex-start' : 'center',
|
||||
gap: 2,
|
||||
}}>
|
||||
<Flex sx={{ alignItems: 'center', gap: 2 }}>
|
||||
<Label htmlFor={name} sx={{
|
||||
textTransform: 'capitalize',
|
||||
fontSize: 3,
|
||||
width: 'fit-content',
|
||||
}}>
|
||||
{ label }
|
||||
</Label>
|
||||
{ isRequired && <Box sx={{
|
||||
backgroundColor: 'muted',
|
||||
padding: '4px 6px',
|
||||
borderRadius: '999px',
|
||||
lineHeight: '1',
|
||||
fontSize: 14,
|
||||
}}>
|
||||
Required
|
||||
</Box>}
|
||||
</Flex>
|
||||
{ children }
|
||||
</Flex>
|
||||
{ description && <Text sx={{ color: 'muted', fontSize: 1 }}>
|
||||
{ description }
|
||||
</Text> }
|
||||
</FlexCol>
|
||||
)
|
||||
}
|
||||
25
components/bank/apply/form-container.js
Normal file
25
components/bank/apply/form-container.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Box } from 'theme-ui'
|
||||
|
||||
export default forwardRef(({ children }, 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,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
158
components/bank/apply/nav-button.js
Normal file
158
components/bank/apply/nav-button.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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/bank/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('/bank')
|
||||
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;
|
||||
|
||||
// Save form data
|
||||
new FormData(form.current).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)
|
||||
) {
|
||||
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('/bank/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>
|
||||
)
|
||||
}
|
||||
75
components/bank/apply/org-form.js
Normal file
75
components/bank/apply/org-form.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Input, Textarea } from 'theme-ui'
|
||||
import Checkbox from './checkbox'
|
||||
import AddressInput from './address-input'
|
||||
import Field from './field'
|
||||
import AutofillColourFix from './autofill-colour-fix'
|
||||
|
||||
export default function OrganizationInfoForm({ requiredFields }) {
|
||||
const [org, setOrg] = useState('organization')
|
||||
|
||||
useEffect(() => {
|
||||
if (navigator.language === 'en-GB') setOrg('organisation')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field name='eventName' label={`${org} name`} requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='eventName'
|
||||
id='eventName'
|
||||
placeholder='Shelburne School Hackathon'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name='eventWebsite'
|
||||
label={`${org} website`}
|
||||
description='If you don’t have one yet, you can leave this blank.'
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name='eventWebsite'
|
||||
id='eventWebsite'
|
||||
type='url'
|
||||
placeholder='hackclub.com'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field name='eventLocation' label={`${org} location`} requiredFields={requiredFields}>
|
||||
<AddressInput isPersonalAddressInput={false} name='eventLocation' />
|
||||
</Field>
|
||||
<Field
|
||||
name='transparent'
|
||||
label='Transparency mode'
|
||||
col={false}
|
||||
description={`
|
||||
Transparent accounts’ balances and donations are public.
|
||||
You choose who has access to personal details.
|
||||
This can be changed later.
|
||||
As an example, Hack Club's finances are transparent!
|
||||
`}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Checkbox defaultChecked={true} name='transparent' />
|
||||
</Field>
|
||||
<Field
|
||||
name='eventDescription'
|
||||
label={`Tell us about your ${org}!`}
|
||||
description='1 or 2 sentences will suffice'
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Textarea
|
||||
name='eventDescription'
|
||||
id='eventDescription'
|
||||
rows={3}
|
||||
sx={{
|
||||
resize: 'vertical',
|
||||
width: '100%',
|
||||
...AutofillColourFix
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
90
components/bank/apply/personal-form.js
Normal file
90
components/bank/apply/personal-form.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Input, Flex } from 'theme-ui'
|
||||
import Checkbox from './checkbox'
|
||||
import AddressInput from './address-input'
|
||||
import Field from './field'
|
||||
import AutofillColourFix from './autofill-colour-fix'
|
||||
|
||||
export default function PersonalInfoForm({ setValidationResult, requiredFields }) {
|
||||
return (
|
||||
<>
|
||||
<Flex sx={{ justifyContent: 'space-between', gap: 4 }}>
|
||||
<Field name='firstName' label='First name' requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='firstName'
|
||||
id='firstName'
|
||||
placeholder='Fiona'
|
||||
sx={{...AutofillColourFix}}
|
||||
|
||||
/>
|
||||
</Field>
|
||||
<Field name='lastName' label='Last name' requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='lastName'
|
||||
id='lastName'
|
||||
placeholder='Hacksworth'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
</Flex>
|
||||
<Field name='userEmail' label='Email' requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='userEmail'
|
||||
id='userEmail'
|
||||
type='email'
|
||||
placeholder='fiona@hackclub.com'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name='userPhone'
|
||||
label='Phone'
|
||||
description='We’ll only use this if we need to get in touch with you urgently.'
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name='userPhone'
|
||||
id='userPhone'
|
||||
type='tel'
|
||||
placeholder='(123) 456-7890'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field name='userBirthday' label='Birthday' requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='userBirthday'
|
||||
id='userBirthday'
|
||||
type='date'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field name='referredBy' label='Who were you referred by?' requiredFields={requiredFields}>
|
||||
<Input
|
||||
name='referredBy'
|
||||
id='referredBy'
|
||||
placeholder='Max'
|
||||
sx={{...AutofillColourFix}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name='returningUser'
|
||||
label='Have you used Bank before?'
|
||||
col={false}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Checkbox name='returningUser' />
|
||||
</Field>
|
||||
<Field
|
||||
name='userAddress'
|
||||
label='Address'
|
||||
description='This is so we can send you some swag and goodies if you ever request them!'
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<AddressInput
|
||||
name='userAddress'
|
||||
isPersonalAddressInput={true}
|
||||
setValidationResult={setValidationResult}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)
|
||||
}
|
||||
85
components/bank/apply/progress.js
Normal file
85
components/bank/apply/progress.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
82
components/bank/apply/watermark.js
Normal file
82
components/bank/apply/watermark.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useRef, useEffect } from 'react'
|
||||
import { Box } from 'theme-ui'
|
||||
|
||||
export default function Watermark() {
|
||||
/* This is going to come back to haunt me one day.
|
||||
It's an abomination but it works ... for now */
|
||||
|
||||
const shineRef = useRef()
|
||||
const svgRef = useRef()
|
||||
|
||||
// Mouse move event
|
||||
const handleMouseMove = ({ clientX, clientY }) => {
|
||||
if (!shineRef.current || !svgRef.current) return
|
||||
|
||||
const svgWidth = svgRef.current.clientWidth / 100
|
||||
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`
|
||||
}
|
||||
|
||||
// Bind event
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
top: -330,
|
||||
left: -480,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
width: '4600px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="my-clip-path">
|
||||
<path d="M16.194 8.096A2.397 2.397 0 0 0 16 8.018V6c.358 0 .735.149.997.264.297.13.676.326 1.077.555a37.817 37.817 0 0 1 2.878 1.864c2.15 1.518 2.548 1.817 4.755 3.61a.999.999 0 1 1-1.414 1.414C22 12 21.9 11.799 19.798 10.317a35.088 35.088 0 0 0-2.716-1.761 9.091 9.091 0 0 0-.888-.46zM15.806 8.096c.09-.04.153-.064.194-.078V6c-.358 0-.735.149-.997.264-.297.13-.676.326-1.077.555a37.817 37.817 0 0 0-2.878 1.864C8.9 10.201 8.5 10.5 6.293 12.293a.999.999 0 1 0 1.414 1.414c2.294-1.707 2.394-1.908 4.495-3.39a35.088 35.088 0 0 1 2.716-1.761c.365-.209.65-.357.888-.46zM7 24a1 1 0 0 1 1-1h16a1 1 0 0 1 0 2H8a1 1 0 0 1-1-1z"></path>
|
||||
<path d="M16 22a1 1 0 0 1-1-1v-7a1 1 0 0 1 2 0v7a1 1 0 0 1-1 1zM21 22a1 1 0 0 1-1-1v-7a1 1 0 0 1 2 0v7a1 1 0 0 1-1 1zM11 22a1 1 0 0 1-1-1v-7a1 1 0 0 1 2 0v7a1 1 0 0 1-1 1z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="clip-container">
|
||||
<foreignObject id="clip-content" width="100%" height="100%">
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#1d181f',
|
||||
clipPath: 'url(#my-clip-path)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={shineRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: '2px',
|
||||
height: '2px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'red',
|
||||
filter: 'blur(2px)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</foreignObject>
|
||||
</g>
|
||||
</svg>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
Label,
|
||||
Input,
|
||||
Button,
|
||||
Select,
|
||||
Text,
|
||||
Container,
|
||||
Textarea,
|
||||
Divider,
|
||||
Link,
|
||||
Flex
|
||||
} from 'theme-ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import Icon from '../icon'
|
||||
import countries from '../../lib/countries'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function BankApplyForm() {
|
||||
const { query } = useRouter()
|
||||
|
||||
const [values, setValues] = useState({
|
||||
eventName: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
userEmail: '',
|
||||
eventWebsite: '',
|
||||
eventLocation: '',
|
||||
userPhone: '',
|
||||
returningUser: '',
|
||||
transparent: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setValues({
|
||||
eventName: query.eventName || '',
|
||||
firstName: query.firstName || '',
|
||||
lastName: query.lastName || '',
|
||||
userEmail: query.userEmail || '',
|
||||
eventWebsite: query.eventWebsite || '',
|
||||
eventLocation: query.eventLocation || '',
|
||||
userPhone: query.userPhone || '',
|
||||
returningUser: query.returningUser || '',
|
||||
transparent: query.transparent || true
|
||||
})
|
||||
}, [query])
|
||||
|
||||
const handleChange = e => {
|
||||
let isRadio = e.target.type === 'radio'
|
||||
let newValue = e.target.value
|
||||
if (isRadio) {
|
||||
newValue = e.target.value.toString() === 'true'
|
||||
}
|
||||
setValues({ ...values, [e.target.name]: newValue })
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex sx={{ flexDirection: 'column', pl: 3 }}>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: [36, null, 48],
|
||||
fontWeight: 'bold',
|
||||
color: 'primary'
|
||||
}}
|
||||
>
|
||||
Apply for Hack Club Bank
|
||||
</Text>
|
||||
<Text sx={{ fontSize: 18, mb: 2 }}>
|
||||
Hack Club Bank is open to all Hack Clubs, hackathons, and charitable
|
||||
organizations in the US and Canada. There are three steps to getting
|
||||
on Hack Club Bank:
|
||||
<ol>
|
||||
<li>Fill out this form</li>
|
||||
<li>
|
||||
Have a 30 minute introductory call with a member of the Hack Club
|
||||
Bank team
|
||||
</li>
|
||||
<li>
|
||||
Sign the Hack Club Bank fiscal sponsorship agreement (sent right
|
||||
after the call)
|
||||
</li>
|
||||
</ol>
|
||||
If you have any questions, give us a shout at{' '}
|
||||
<Link as="a" href="mailto:bank@hackclub.com">
|
||||
bank@hackclub.com
|
||||
</Link>{' '}
|
||||
or in the <strong>#bank</strong> channel on the{' '}
|
||||
<Link href="/slack">Hack Club Slack</Link>!
|
||||
</Text>
|
||||
</Flex>
|
||||
<Base method="POST" action="/api/bank/apply">
|
||||
<Text variant="headline" sx={{ color: 'primary' }}>
|
||||
Your Organization
|
||||
</Text>
|
||||
<Divider sx={{ borderColor: 'slate', mt: -2 }} />
|
||||
<Field
|
||||
label="Organization name"
|
||||
name="eventName"
|
||||
placeholder="Windy City Hacks"
|
||||
helperText="What's the name of your organization?"
|
||||
value={values.eventName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Label
|
||||
htmlFor="transparent"
|
||||
sx={{ color: 'smoke', fontSize: 18, my: 2 }}
|
||||
>
|
||||
Would you like to make your account transparent?
|
||||
<Select
|
||||
name="transparent"
|
||||
sx={{ bg: 'dark', mt: 1 }}
|
||||
value={values.transparent}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="Yes, please!">Yes, please!</option>
|
||||
<option value="No, thanks.">No, thanks.</option>
|
||||
</Select>
|
||||
</Label>
|
||||
<HelperText>
|
||||
This can be changed at anytime. For transparent accounts, anyone can
|
||||
see your balance and donations. You choose who has access to personal
|
||||
details.{' '}
|
||||
<Link as="a" href="https://bank.hackclub.com/hq" target="_blank">
|
||||
Hack Club's finances
|
||||
</Link>{' '}
|
||||
are transparent, for example.
|
||||
</HelperText>
|
||||
<Field
|
||||
label="Organization website"
|
||||
name="eventWebsite"
|
||||
placeholder="https://hackclub.com"
|
||||
type="url"
|
||||
helperText="If you don't have one yet, you can leave this blank."
|
||||
value={values.eventWebsite}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
label="Organization location"
|
||||
name="eventLocation"
|
||||
placeholder="San Francisco, CA"
|
||||
type="text"
|
||||
helperText="If applicable, please format as: City, State."
|
||||
value={values.eventLocation}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<Label
|
||||
htmlFor="eventCountry"
|
||||
sx={{ color: 'smoke', fontSize: 18, pb: 2, my: 2 }}
|
||||
>
|
||||
Country
|
||||
<Select
|
||||
name="eventCountry"
|
||||
defaultValue="Choose a country"
|
||||
sx={{ bg: 'dark', my: 1 }}
|
||||
>
|
||||
<option value="" selected disabled>
|
||||
Choose a country
|
||||
</option>
|
||||
<option value="United States">United States (US)</option>
|
||||
<option value="Canada">Canada (CA)</option>
|
||||
</Select>
|
||||
<HelperText>
|
||||
We're currently only able to support organizations operating out of
|
||||
the United States or Canada. If you're outside of those countries,
|
||||
and might be eligible to run on Bank, please shoot us an email on{' '}
|
||||
<Link as="a" href="mailto:bank@hackclub.com">
|
||||
bank@hackclub.com
|
||||
</Link>
|
||||
!{' '}
|
||||
</HelperText>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
htmlFor="eventDescription"
|
||||
sx={{ color: 'smoke', fontSize: 18, my: 2 }}
|
||||
>
|
||||
Tell us about your organization!
|
||||
<Textarea
|
||||
name="eventDescription"
|
||||
sx={{ bg: 'dark', my: 1 }}
|
||||
required
|
||||
/>
|
||||
<HelperText>
|
||||
1-2 sentences summarizing what you'd like to use Hack Club Bank for.
|
||||
This is just to help us know what to expect during the call!
|
||||
</HelperText>
|
||||
</Label>
|
||||
|
||||
<Text variant="headline" sx={{ color: 'primary' }}>
|
||||
Your Profile
|
||||
</Text>
|
||||
<Divider sx={{ borderColor: 'slate', mt: -2 }} />
|
||||
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
||||
<Field
|
||||
label="First name"
|
||||
name="firstName"
|
||||
placeholder="Fiona"
|
||||
value={values.firstName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Last name"
|
||||
name="lastName"
|
||||
placeholder="Hackworth"
|
||||
value={values.lastName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
<Field
|
||||
label="Email address"
|
||||
name="userEmail"
|
||||
placeholder="fiona@hackclub.com"
|
||||
type="email"
|
||||
value={values.userEmail}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Phone"
|
||||
name="userPhone"
|
||||
placeholder="1-855-625-HACK"
|
||||
type="tel"
|
||||
helperText="We'll only use this if we need to get in touch with you urgently."
|
||||
value={values.userPhone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Birthday"
|
||||
name="userBirthday"
|
||||
type="date"
|
||||
width="fit-content"
|
||||
sx={{ height: '44px' }}
|
||||
required
|
||||
/>
|
||||
|
||||
<Field
|
||||
label="How did you hear about Hack Club Bank?"
|
||||
name="referredBy"
|
||||
placeholder="Word of mouth, hackathon, etc."
|
||||
value={values.referredBy}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
|
||||
<Label
|
||||
htmlFor="returningUser"
|
||||
sx={{ color: 'smoke', fontSize: 18, my: 2 }}
|
||||
>
|
||||
Have you used Hack Club Bank before?
|
||||
<Select
|
||||
name="returningUser"
|
||||
sx={{ bg: 'dark', mt: 1 }}
|
||||
value={values.returningUser}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="No, first time!">No, first time!</option>
|
||||
<option value="Yes, I have used Hack Club Bank before">
|
||||
Yes, I have used Hack Club Bank before
|
||||
</option>
|
||||
</Select>
|
||||
</Label>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Text
|
||||
variant="subheadline"
|
||||
sx={{ mt: 3, mb: 1, display: 'block', fontSize: 3 }}
|
||||
>
|
||||
Mailing Address
|
||||
</Text>
|
||||
<HelperText>
|
||||
This is so we can send you some swag and goodies if you ever request
|
||||
them!
|
||||
</HelperText>
|
||||
</Box>
|
||||
<Field
|
||||
label="Address (line 1)"
|
||||
name="addressLine1"
|
||||
placeholder="8605 Santa Monica Blvd"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Address (line 2)"
|
||||
name="addressLine2"
|
||||
placeholder="Suite 86294"
|
||||
type="text"
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
columnGap: 2
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
label="City"
|
||||
name="addressCity"
|
||||
placeholder="West Hollywood"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="State"
|
||||
name="addressState"
|
||||
placeholder="CA"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Zip Code"
|
||||
name="addressZip"
|
||||
placeholder="90069"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
label="Country"
|
||||
name="addressCountry"
|
||||
placeholder="USA"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
sx={{ bg: 'red', mt: [2, 3], py: 2, fontSize: 24 }}
|
||||
type="submit"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</Base>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({
|
||||
placeholder,
|
||||
label,
|
||||
name,
|
||||
type,
|
||||
helperText,
|
||||
width,
|
||||
value,
|
||||
onChange,
|
||||
required,
|
||||
sx
|
||||
}) {
|
||||
let isRadio = type === 'radio'
|
||||
let isCheckbox = type === 'checkbox'
|
||||
if (isCheckbox) {
|
||||
return (
|
||||
<>
|
||||
<Label
|
||||
htmlFor={name}
|
||||
sx={{
|
||||
color: 'smoke',
|
||||
fontSize: 18,
|
||||
my: 2,
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
required={required}
|
||||
sx={{ mr: 2 }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Box sx={{ my: 2 }}>
|
||||
<Label htmlFor={name} sx={{ color: 'smoke', fontSize: 18 }}>
|
||||
{label}
|
||||
<Input
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
type={type}
|
||||
sx={{
|
||||
bg: 'dark',
|
||||
width: `${width ? width : '100%'}`,
|
||||
my: helperText ? 1 : 0,
|
||||
mt: 1,
|
||||
...sx
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
required={required}
|
||||
/>
|
||||
{helperText && <HelperText>{helperText}</HelperText>}
|
||||
</Label>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function HelperText({ children }) {
|
||||
return (
|
||||
<Text variant="caption" sx={{ color: 'muted', fontSize: 16 }}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function Base({ children, action, method }) {
|
||||
return (
|
||||
<Container
|
||||
as="form"
|
||||
sx={{ display: 'grid', gridTemplateColumns: '1fr' }}
|
||||
action={action}
|
||||
method={method}
|
||||
variant="copy"
|
||||
>
|
||||
{children}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { Box, Input, Label, Button } from 'theme-ui'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
function Base({ children, action, target, method, onSubmit }) {
|
||||
return (
|
||||
<Box
|
||||
as="form"
|
||||
sx={{ display: 'grid', gridTemplateColumns: '1fr' }}
|
||||
action={action}
|
||||
target={target}
|
||||
method={method}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ placeholder, label, name, type, value, onChange }) {
|
||||
return (
|
||||
<Box sx={{ my: 2 }}>
|
||||
<Label htmlFor={name} sx={{ color: 'muted', fontSize: 18 }}>
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
id={name}
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
type={type}
|
||||
sx={{
|
||||
bg: 'dark'
|
||||
}}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Signup() {
|
||||
const router = useRouter()
|
||||
|
||||
const [values, setValues] = useState({
|
||||
eventName: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
userEmail: ''
|
||||
})
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
|
||||
const params = new URLSearchParams({
|
||||
eventName: e.target.eventName.value,
|
||||
firstName: e.target.firstName.value,
|
||||
lastName: e.target.lastName.value,
|
||||
userEmail: e.target.userEmail.value
|
||||
})
|
||||
|
||||
router.push(`/bank/apply/?${params}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Base method="get" action="/bank/apply" onSubmit={handleSubmit}>
|
||||
<Field
|
||||
label="Organization name"
|
||||
name="eventName"
|
||||
placeholder="Windy City Hacks"
|
||||
value={values.eventName}
|
||||
onChange={e => setValues({ ...values, eventName: e.target.value })}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<Field
|
||||
label="First name"
|
||||
name="firstName"
|
||||
placeholder="Fiona"
|
||||
value={values.firstName}
|
||||
onChange={e => setValues({ ...values, firstName: e.target.value })}
|
||||
/>
|
||||
<Field
|
||||
label="Last name"
|
||||
name="lastName"
|
||||
placeholder="Hackworth"
|
||||
value={values.lastName}
|
||||
onChange={e => setValues({ ...values, lastName: e.target.value })}
|
||||
/>
|
||||
</Box>
|
||||
<Field
|
||||
label="Email address"
|
||||
name="userEmail"
|
||||
placeholder="fiona@hackclub.com"
|
||||
type="email"
|
||||
value={values.userEmail}
|
||||
onChange={e => setValues({ ...values, userEmail: e.target.value })}
|
||||
/>
|
||||
<Button sx={{ bg: 'blue', mt: [2, 3], py: 3 }} type="submit">{`Finish ${
|
||||
11 - Object.values(values).filter(n => n !== '').length
|
||||
} fields to apply`}</Button>
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,41 +12,42 @@ import {
|
|||
import { Fade } from 'react-reveal'
|
||||
import Timeline from './timeline'
|
||||
import Stats from './stats'
|
||||
import Signup from './signup'
|
||||
import ApplyButton from './apply-button'
|
||||
|
||||
export default function Start() {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
as="section"
|
||||
id="apply"
|
||||
sx={{
|
||||
pt: 6,
|
||||
zIndex: -999
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
px={3}
|
||||
mb={[4, 5]}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Heading variant="ultratitle" color="white" mb={2}>
|
||||
Sign up for Hack Club Bank.
|
||||
</Heading>
|
||||
<Container variant="narrow" sx={{ color: 'muted' }}>
|
||||
<Text variant="lead">
|
||||
Open to all registered Hack Clubs, hackathons, and charitable
|
||||
organizations in the US and Canada.
|
||||
<Box as="section" id="apply" py={6}>
|
||||
<Flex sx={{ flexDirection: 'column', alignItems: 'center', gap: 5, mx: 4 }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
textAlign: 'center',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Heading variant="ultratitle" color="white">
|
||||
Sign up for Hack Club Bank.
|
||||
</Heading>
|
||||
<Text color='muted' variant='lead' m='0 !important'>
|
||||
Open to Hack Clubs, hackathons, and charitable organizations in the US and Canada.
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
<Timeline />
|
||||
<Grid mt={[4, 5]} mb={[3, 4]} px={3} columns={[1, 1, '1fr 1fr']}>
|
||||
</Flex>
|
||||
<Stats />
|
||||
<Timeline />
|
||||
<Flex sx={{ flexDirection: 'column', textAlign: 'center', gap: 4, mx: 3 }}>
|
||||
<ApplyButton />
|
||||
<Text color='muted' sx={{ fontSize: 18 }}>We run Hack Club HQ on Bank!{' '}
|
||||
<Link
|
||||
href='https://bank.hackclub.com/hq'
|
||||
color='primary'
|
||||
>
|
||||
See our finances.
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* <Grid mt={[4, 5]} mb={[3, 4]} px={3} columns={[1, 1, '1fr 1fr']}>
|
||||
<Fade bottom>
|
||||
<Card
|
||||
variant="primary"
|
||||
|
|
@ -64,14 +65,7 @@ export default function Start() {
|
|||
</Card>
|
||||
</Fade>
|
||||
<Container variant="narrow" sx={{ pr: [null, null, 2, 6], m: 0 }}>
|
||||
<Stats
|
||||
color="smoke"
|
||||
fontSize={[7, 8]}
|
||||
my={[3, 4]}
|
||||
px={0}
|
||||
width="auto"
|
||||
align="left"
|
||||
/>
|
||||
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 18,
|
||||
|
|
@ -103,7 +97,7 @@ export default function Start() {
|
|||
Hack Club does not directly provide banking services. Banking services
|
||||
provided by FDIC-certified financial institutions.
|
||||
</Text>
|
||||
</Container>
|
||||
</Container> */}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,80 +1,96 @@
|
|||
import { Text, Box } from 'theme-ui'
|
||||
import { keyframes } from '@emotion/react'
|
||||
import { timeSince } from '../../lib/helpers'
|
||||
import useSWR from 'swr'
|
||||
import Stat from '../stat'
|
||||
import fetcher from '../../lib/fetcher'
|
||||
import { Text, Box, Flex } from 'theme-ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const renderMoney = amount =>
|
||||
Math.floor(amount / 100)
|
||||
.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
})
|
||||
.replace('.00', '')
|
||||
const easeInOutExpo = (x) =>
|
||||
x === 0
|
||||
? 0
|
||||
: x === 1
|
||||
? 1
|
||||
: x < 0.5
|
||||
? Math.pow(2, 20 * x - 10) / 2
|
||||
: (2 - Math.pow(2, -20 * x + 10)) / 2;
|
||||
|
||||
function startMoneyAnimation(setBalance, amount, duration = 2_000, moneyFormatter) {
|
||||
const startTime = performance.now();
|
||||
|
||||
const flashing = keyframes({
|
||||
from: { opacity: 0 },
|
||||
'50%': { opacity: 1 },
|
||||
to: { opacity: 0 }
|
||||
})
|
||||
function animate() {
|
||||
const time = performance.now() - startTime;
|
||||
const progress = time / duration;
|
||||
const easedProgress = easeInOutExpo(progress);
|
||||
|
||||
function Dot() {
|
||||
return (
|
||||
<Text
|
||||
sx={{
|
||||
bg: 'green',
|
||||
color: 'white',
|
||||
borderRadius: 'circle',
|
||||
display: 'inline-block',
|
||||
lineHeight: 0,
|
||||
width: '.4em',
|
||||
height: '.4em',
|
||||
marginRight: '.4em',
|
||||
marginBottom: '.12em',
|
||||
animationName: `${flashing}`,
|
||||
animationDuration: '3s',
|
||||
animationTimingFunction: 'ease-in-out',
|
||||
animationIterationCount: 'infinite'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
setBalance(moneyFormatter(amount * easedProgress))
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
setBalance(moneyFormatter(amount))
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function formatMoney(amount) {
|
||||
const normalisedAmount = amount / 100
|
||||
return normalisedAmount
|
||||
.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
|
||||
.split('.')
|
||||
}
|
||||
|
||||
const Stats = props => {
|
||||
const { data } = useSWR('https://bank.hackclub.com/stats', fetcher, {
|
||||
fallbackData: {
|
||||
transactions_volume: 500 * 1000 * 1000,
|
||||
raised: 200 * 1000 * 500,
|
||||
last_transaction_date: Date.now()
|
||||
const Stats = () => {
|
||||
const [transactedRaw, setTransactedRaw] = useState() // The raw amount transacted (in cents).
|
||||
const [balance, setBalance] = useState(0) // A formatted balance string, split by decimal
|
||||
|
||||
useEffect(() => {
|
||||
if (!transactedRaw) {
|
||||
fetch('https://bank.hackclub.com/stats')
|
||||
.then(res => res.json())
|
||||
.then(data => setTransactedRaw(data.transactions_volume))
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
setTransactedRaw(830796389)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let observer = new IntersectionObserver(
|
||||
(e) => {
|
||||
if (e[0].isIntersecting) {
|
||||
console.info('intersecting')
|
||||
startMoneyAnimation(setBalance, transactedRaw, 2_500, formatMoney)
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 }
|
||||
);
|
||||
observer.observe(document.querySelector("#parent"));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [transactedRaw])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text
|
||||
variant="lead"
|
||||
fontSize={[2, 3]}
|
||||
color="muted"
|
||||
mt={[2, 4]}
|
||||
mb={[2, 3]}
|
||||
>
|
||||
<Dot />
|
||||
As of {timeSince(data?.last_transaction_date * 1000, false, true)}...
|
||||
</Text>
|
||||
<Box>
|
||||
<Stat
|
||||
{...props}
|
||||
value={renderMoney(data?.raised)}
|
||||
label="raised on Hack Club Bank"
|
||||
/>
|
||||
<Stat
|
||||
{...props}
|
||||
fontSize={[3, 4, 5]}
|
||||
value={renderMoney(data?.transactions_volume)}
|
||||
label="total amount transacted"
|
||||
/>
|
||||
</Box>
|
||||
<Box id='parent'>
|
||||
<Flex sx={{ flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Text sx={{ fontSize: [3, 4] }}>
|
||||
So far we have enabled
|
||||
</Text>
|
||||
{ transactedRaw ?
|
||||
<>
|
||||
<Text variant='title' color='green' sx={{
|
||||
color: 'green',
|
||||
fontSize: [5, 6]
|
||||
}}
|
||||
>
|
||||
{ balance[0] }
|
||||
<Text sx={{ fontSize: [3, 4] }}>.{ balance[1] }</Text>
|
||||
</Text>
|
||||
</>
|
||||
:
|
||||
<Text variant='title' color='green' sx={{
|
||||
color: 'green',
|
||||
fontSize: [5, 6]
|
||||
}}>...</Text>
|
||||
}
|
||||
<Text sx={{ fontSize: [3, 4] }}>in transactions</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,147 +1,55 @@
|
|||
import { Box, Flex, Container, Text, Badge, Link } from 'theme-ui'
|
||||
import { Slide } from 'react-reveal'
|
||||
import Icon from '../icon'
|
||||
import { Flex, Text, Image, Box, Container } from 'theme-ui'
|
||||
import Slide from 'react-reveal'
|
||||
|
||||
function Timeline({ children }) {
|
||||
function Step({ stepIndex, label }) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{ flexDirection: ['column', null, 'row'], justifyContent: 'center' }}
|
||||
>
|
||||
{children}
|
||||
<Flex sx={{
|
||||
flexDirection: ['row', null, 'column'],
|
||||
flex: '1 0 0',
|
||||
alignItems: 'center',
|
||||
maxWidth: ['24rem', null, '12rem'],
|
||||
gap: 3
|
||||
}}>
|
||||
<Image src={`/bank/timeline-steps/step${stepIndex}.svg`} sx={{ flexShrink: 0 }} />
|
||||
<Text variant='lead' sx={{
|
||||
textAlign: ['left', null, 'center'],
|
||||
margin: '0px !important'
|
||||
}}>{label}</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineStep({ children }) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
marginX: [4, null, null],
|
||||
paddingX: [null, null, 3, 4],
|
||||
paddingY: [4, null, 0],
|
||||
flexDirection: ['row', null, 'column'],
|
||||
alignItems: 'center',
|
||||
'&:before': {
|
||||
content: '""',
|
||||
background: '#3c4858',
|
||||
height: ['420px', null, '4px'],
|
||||
width: ['4px', null, '48%'],
|
||||
marginLeft: [26, null, 0],
|
||||
marginTop: [null, null, '34px'],
|
||||
position: 'absolute',
|
||||
zIndex: -1
|
||||
},
|
||||
'&:first-of-type:before': {
|
||||
top: [0, null, 'auto'],
|
||||
width: [0, null, 0],
|
||||
left: [0, null, 0]
|
||||
},
|
||||
'&:last-of-type:before': {
|
||||
bottom: [0, null, 'auto'],
|
||||
left: [null, null, 0],
|
||||
width: [null, null, 0]
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default function Timeline() {
|
||||
const labels = [
|
||||
'Register your organization for Bank',
|
||||
'Explore the interface in Playground mode',
|
||||
'Hop on an intro call with the Bank team',
|
||||
'Start fundraising!'
|
||||
]
|
||||
const stepSideLength = 64;
|
||||
|
||||
function Circle({ children }) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
padding: 12,
|
||||
background: 'red',
|
||||
color: 'white',
|
||||
backgroundImage:
|
||||
'radial-gradient(ellipse farthest-corner at top left, #ff8c37, #ec3750)',
|
||||
borderRadius: '100%',
|
||||
display: 'inline-block',
|
||||
lineHeight: 0,
|
||||
position: 'relative',
|
||||
zIndex: 999
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Step({ icon, name, duration, href }) {
|
||||
return (
|
||||
<TimelineStep pb={4}>
|
||||
<Slide left>
|
||||
<Circle>
|
||||
{href ? (
|
||||
<Link href={href} sx={{ cursor: 'pointer' }}>
|
||||
<Icon glyph={icon} size={48} color="white" />
|
||||
</Link>
|
||||
) : (
|
||||
<Icon glyph={icon} size={48} />
|
||||
)}
|
||||
</Circle>
|
||||
<Container
|
||||
sx={{
|
||||
marginTop: 3,
|
||||
display: 'flex',
|
||||
justifyContent: ['left', null, 'center'],
|
||||
flexDirection: 'column',
|
||||
textAlign: ['left', null, 'center']
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
variant="pill"
|
||||
sx={{
|
||||
bg: 'muted',
|
||||
color: 'darker',
|
||||
fontWeight: 'normal',
|
||||
textTransform: 'uppercase',
|
||||
width: 64,
|
||||
fontSize: 18,
|
||||
px: 2,
|
||||
mx: [null, null, 'auto']
|
||||
}}
|
||||
>
|
||||
{duration}
|
||||
</Badge>
|
||||
<Text
|
||||
sx={{ color: 'white', fontSize: 24, maxWidth: [300, null, 550] }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</Container>
|
||||
</Slide>
|
||||
</TimelineStep>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RealTimeline() {
|
||||
return (
|
||||
<Timeline px={3}>
|
||||
<Step
|
||||
icon="send"
|
||||
name="Submit an application for your organization"
|
||||
duration="Step 1"
|
||||
href="/bank/apply"
|
||||
/>
|
||||
<Step
|
||||
icon="welcome"
|
||||
name="Hop on an intro call with the Bank team"
|
||||
duration="Step 2"
|
||||
/>
|
||||
<Step
|
||||
icon="post"
|
||||
name="Sign the contract & unlock full access"
|
||||
duration="Step 3"
|
||||
/>
|
||||
<Step
|
||||
icon="friend"
|
||||
name="Invite your team & start fundraising"
|
||||
duration="Step 4"
|
||||
mb={0}
|
||||
/>
|
||||
</Timeline>
|
||||
<Slide>
|
||||
<Flex sx={{
|
||||
flexDirection: ['column', null, 'row'],
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
maxWidth: ['300px', null, '1200px'],
|
||||
mx: 'auto',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{labels.map((label, idx) => <Step stepIndex={idx + 1} label={label} key={idx} />)}
|
||||
<Box sx={{
|
||||
border: 'solid #8492a6',
|
||||
borderWidth: '3px 3px 0 0',
|
||||
position: 'absolute',
|
||||
top: stepSideLength / 2,
|
||||
left: '10%', // TODO: make this dynamic
|
||||
right: ['auto', null, '10%'],
|
||||
bottom: [stepSideLength / 2, null, 'auto'],
|
||||
zIndex: -1,
|
||||
}} />
|
||||
</Flex>
|
||||
</Slide>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
9
components/flex-col.js
Normal file
9
components/flex-col.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Flex } from 'theme-ui'
|
||||
|
||||
export default function FlexCol({ children, ...props }) {
|
||||
return (
|
||||
<Flex sx={{ flexDirection: 'column', ...props }}>
|
||||
{ children }
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ const Footer = ({ dark = false, children, ...props }) => (
|
|||
/>
|
||||
<Service
|
||||
href="https://figma.com/@hackclub"
|
||||
icon="figma"
|
||||
icon="figma-fill"
|
||||
name="Figma"
|
||||
/>
|
||||
<Service
|
||||
|
|
@ -169,7 +169,7 @@ const Footer = ({ dark = false, children, ...props }) => (
|
|||
icon="instagram"
|
||||
name="Instagram"
|
||||
/>
|
||||
<Service href="mailto:team@hackclub.com" icon="email" />
|
||||
<Service href="mailto:team@hackclub.com" icon="email-fill" />
|
||||
</Grid>
|
||||
<Text my={2}>
|
||||
<Link href="tel:1-855-625-HACK">1-855-625-HACK</Link>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/** @jsxRuntime classic */
|
||||
/** @jsx jsx */
|
||||
import React from 'react'
|
||||
import { jsx } from 'theme-ui'
|
||||
import Icon from '@hackclub/icons'
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function Game({ game, gameImage, gameImage1, ...props }) {
|
|||
>
|
||||
<Box
|
||||
as="a"
|
||||
href={`https://editor.sprig.hackclub.com/?file=https://raw.githubusercontent.com/hackclub/sprig/main/games/${game.filename}.js`}
|
||||
href={`https://editor.sprig.hackclub.com/?file=https://raw.githubusercontent.com/hackclub/sprig/main/games/${game?.filename}.js`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
|
|
@ -129,7 +129,7 @@ function Game({ game, gameImage, gameImage1, ...props }) {
|
|||
lineHeight: '1.4rem'
|
||||
}}
|
||||
>
|
||||
{game.title}
|
||||
{game?.title}
|
||||
</Text>
|
||||
<Text
|
||||
as="h4"
|
||||
|
|
@ -146,7 +146,7 @@ function Game({ game, gameImage, gameImage1, ...props }) {
|
|||
lineHeight: '1rem'
|
||||
}}
|
||||
>
|
||||
by {game.author}
|
||||
by {game?.author}
|
||||
</Text>
|
||||
<Text
|
||||
as="span"
|
||||
|
|
@ -159,7 +159,7 @@ function Game({ game, gameImage, gameImage1, ...props }) {
|
|||
mb: 1
|
||||
}}
|
||||
>
|
||||
<RelativeTime value={game.addedOn} titleFormat="YYYY-MM-DD" />
|
||||
<RelativeTime value={game?.addedOn} titleFormat="YYYY-MM-DD" />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -210,6 +210,7 @@ export default function Sprig({ stars, game, gameImage, gameImage1 }) {
|
|||
Build a Sprig game
|
||||
</Buttons>
|
||||
<Buttons
|
||||
|
||||
content="learn more on our github"
|
||||
id="8"
|
||||
link="https://github.com/hackclub/sprig"
|
||||
|
|
|
|||
81
lib/bank/apply/address-validation.js
Normal file
81
lib/bank/apply/address-validation.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
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.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.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
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
"geopattern": "^1.2.3",
|
||||
"globby": "^11.0.4",
|
||||
"graphql": "^16.6.0",
|
||||
"js-confetti": "^0.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^12.3.1",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
|
|
|
|||
|
|
@ -18,14 +18,12 @@ export default async function handler(req, res) {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.HCB_API_TOKEN}`
|
||||
Authorization: `Bearer ${process.env.HCB_API_TOKEN || ""}`
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(async r => {
|
||||
console.log(data)
|
||||
|
||||
const application = await applicationsTable.create({
|
||||
await applicationsTable.create({
|
||||
'First Name': data.firstName,
|
||||
'Last Name': data.lastName,
|
||||
'Email Address': data.userEmail,
|
||||
|
|
@ -37,20 +35,19 @@ export default async function handler(req, res) {
|
|||
'Mailing Address': data.mailingAddress,
|
||||
'Address Line 1': data.addressLine1,
|
||||
'Address Line 2': data.addressLine2,
|
||||
City: data.addressCity,
|
||||
State: data.addressState,
|
||||
'City': data.addressCity,
|
||||
'State': data.addressState,
|
||||
'Zip Code': data.addressZip,
|
||||
'Address Country': data.addressCountry,
|
||||
Country: data.eventCountry,
|
||||
'Country': data.eventCountry,
|
||||
'Event Location': data.eventLocation,
|
||||
'Have you used Hack Club Bank for any previous events?':
|
||||
data.returningUser,
|
||||
'Have you used Hack Club Bank for any previous events?': data.returningUser,
|
||||
'How did you hear about HCB?': data.referredBy,
|
||||
Transparent: data.transparent,
|
||||
'Transparent': data.transparent,
|
||||
'HCB account URL': `https://bank.hackclub.com/${r.slug}`
|
||||
})
|
||||
res.writeHead(302, { Location: '/bank/apply/success' }).end()
|
||||
console.log(r.statusText)
|
||||
console.log(r)
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export async function getGames() {
|
|||
.sort((a, b) => new Date(b.addedOn) - new Date(a.addedOn))
|
||||
.slice(0, 4)
|
||||
|
||||
return games
|
||||
return []
|
||||
}
|
||||
|
||||
export default async function Games(req, res) {
|
||||
|
|
|
|||
|
|
@ -1,31 +1,133 @@
|
|||
import BankApplyForm from '../../components/bank/form'
|
||||
import { Box, Container, Card } from 'theme-ui'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Box, Flex, Text } from 'theme-ui'
|
||||
import ForceTheme from '../../components/force-theme'
|
||||
import Head from 'next/head'
|
||||
import Meta from '@hackclub/meta'
|
||||
import GeoPattern from 'geopattern'
|
||||
import { useEffect } from 'react'
|
||||
import FlexCol from '../../components/flex-col'
|
||||
import Progress from '../../components/bank/apply/progress'
|
||||
import NavButton from '../../components/bank/apply/nav-button'
|
||||
import Watermark from '../../components/bank/apply/watermark'
|
||||
import FormContainer from '../../components/bank/apply/form-container'
|
||||
import BankInfo from '../../components/bank/apply/bank-info'
|
||||
import OrganizationInfoForm from '../../components/bank/apply/org-form'
|
||||
import PersonalInfoForm from '../../components/bank/apply/personal-form'
|
||||
import AlertModal from '../../components/bank/apply/alert-modal'
|
||||
import { search, geocode } from '../../lib/bank/apply/address-validation'
|
||||
|
||||
export default function Apply() {
|
||||
const bg = GeoPattern.generate(new Date()).toDataUrl()
|
||||
const router = useRouter()
|
||||
const [step, setStep] = useState(1)
|
||||
const formContainer = useRef()
|
||||
const [formError, setFormError] = useState(null)
|
||||
|
||||
const requiredFields = [
|
||||
[],
|
||||
['eventName', 'eventLocation'],
|
||||
['firstName', 'lastName', 'userEmail']
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`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.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, step: 1 } },
|
||||
undefined,
|
||||
{}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
async
|
||||
src='https://maps.googleapis.com/maps/api/js?key=AIzaSyApxZZ8-Eh_6RgHUu8-BAOpx3xhfF2yK9U&libraries=places&mapInit=foo'
|
||||
></script>
|
||||
|
||||
<Meta as={Head} title="Apply for Hack Club Bank" />
|
||||
<ForceTheme theme="dark" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
backgroundImage: bg,
|
||||
py: 4,
|
||||
backgroundAttachment: 'fixed'
|
||||
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]
|
||||
}}
|
||||
>
|
||||
<ForceTheme theme="dark" />
|
||||
<Container variant="copy">
|
||||
<Card variant="primary">
|
||||
<BankApplyForm />
|
||||
</Card>
|
||||
</Container>
|
||||
<Box sx={{ gridArea: 'title' }}>
|
||||
<FlexCol gap={[4, null, null, '20vh']}>
|
||||
<Text variant='title'>Let's get you<br />set up on bank.</Text>
|
||||
<Progress />
|
||||
</FlexCol>
|
||||
</Box>
|
||||
<Box sx={{ gridArea: 'form', overflowY: 'auto' }}>
|
||||
<FormContainer ref={formContainer}>
|
||||
{ step === 1 && <BankInfo /> }
|
||||
{ step === 2 && <OrganizationInfoForm requiredFields={requiredFields} /> }
|
||||
{ step === 3 && <PersonalInfoForm requiredFields={requiredFields} /> }
|
||||
</FormContainer>
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{
|
||||
gridArea: 'nav',
|
||||
alignSelf: 'end',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<NavButton isBack={true} form={formContainer} />
|
||||
<NavButton
|
||||
isBack={false}
|
||||
form={formContainer}
|
||||
setFormError={setFormError}
|
||||
requiredFields={requiredFields}
|
||||
clickHandler={async () => {
|
||||
//TODO: Put this somewhere else
|
||||
|
||||
// Validate the address
|
||||
if (step === 3) {
|
||||
// Get the raw personal address input
|
||||
const userAddress = sessionStorage.getItem('bank-signup-userAddressRaw')
|
||||
if (!userAddress) return
|
||||
|
||||
const result = await geocode(userAddress)
|
||||
|
||||
const addrComp = (type) =>
|
||||
result.results[0].structuredAddress[type]
|
||||
|
||||
const thoroughfare = addrComp('fullThoroughfare')
|
||||
const city = addrComp('locality')
|
||||
const state = addrComp('administrativeArea')
|
||||
const postalCode = addrComp('postal_code')
|
||||
const country = result.results[0].country
|
||||
const countryCode = result.results[0].countryCode
|
||||
|
||||
sessionStorage.setItem('bank-signup-addressLine1', thoroughfare)
|
||||
sessionStorage.setItem('bank-signup-addressCity', city ?? '')
|
||||
sessionStorage.setItem('bank-signup-addressState', state ?? '')
|
||||
sessionStorage.setItem('bank-signup-addressZip', postalCode ?? '')
|
||||
sessionStorage.setItem('bank-signup-addressCountry', country ?? '')
|
||||
sessionStorage.setItem('bank-signup-addressCountryCode', countryCode ?? '')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
<AlertModal formError={formError} setFormError={setFormError} />
|
||||
<Watermark />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
477
pages/bank/fiscal-sponsorship.js
Normal file
477
pages/bank/fiscal-sponsorship.js
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
import { Box, Card, Container, Flex, Link, Text } from 'theme-ui'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { keyframes } from '@emotion/react'
|
||||
import FlexCol from '../../components/flex-col'
|
||||
import Meta from '@hackclub/meta'
|
||||
import Head from 'next/head'
|
||||
import ForceTheme from '../../components/force-theme'
|
||||
import Nav from '../../components/nav'
|
||||
import Footer from '../../components/footer'
|
||||
import Icon from '../../components/icon'
|
||||
import Tilt from '../../components/tilt'
|
||||
|
||||
function Bullet({ glow=true, icon, href, children }) {
|
||||
let effectColours = [
|
||||
'#ec3750',
|
||||
'#ff8c37',
|
||||
'#f1c40f',
|
||||
'#33d6a6',
|
||||
'#5bc0de',
|
||||
'#338eda',
|
||||
'#a633d6',
|
||||
]
|
||||
|
||||
// Raw doggen this trig, no AI this is natty
|
||||
function keyframeGenerator(spread, blur, colours, opacity = 0.5) {
|
||||
let hexOpacity =
|
||||
Math.max(Math.min(Math.round(opacity * 255), 255), 0).toString(16).padStart(2, '0');
|
||||
|
||||
let final = {}
|
||||
for (let i = 0; i <= 100; i++) {
|
||||
let baseX = Math.sin(i * Math.PI / 50) // 50 keyframes for each pi radians
|
||||
let baseY = -Math.cos(i * Math.PI / 50)
|
||||
|
||||
// Ensure no scientific notation
|
||||
const roundFactor = 1_000_000
|
||||
baseX = Math.round(baseX * roundFactor) / roundFactor
|
||||
baseY = Math.round(baseY * roundFactor) / roundFactor
|
||||
|
||||
let boxShadow = ''
|
||||
for (let c = 0; c < colours.length; c++) {
|
||||
// Rotate by 2pi / colours.length * c radians
|
||||
let x = baseX * Math.cos(2 * Math.PI * c / colours.length) - baseY * Math.sin(2 * Math.PI * c / colours.length)
|
||||
let y = baseX * Math.sin(2 * Math.PI * c / colours.length) + baseY * Math.cos(2 * Math.PI * c / colours.length)
|
||||
|
||||
boxShadow += `${x * spread}px ${y * spread}px ${blur}px ${colours[c]}${hexOpacity},`
|
||||
}
|
||||
|
||||
// Remove trailing comma
|
||||
boxShadow = boxShadow.slice(0, -1)
|
||||
|
||||
final[i + '%'] = { boxShadow }
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
const shadowSpread = glow ? 5 : 0
|
||||
const shadowBlur = glow ? 10 : 0
|
||||
const animatedBoxShadow = keyframes(keyframeGenerator(shadowSpread, shadowBlur, effectColours))
|
||||
|
||||
const borderWidth = '2px'
|
||||
|
||||
return (
|
||||
<Tilt>
|
||||
<Flex
|
||||
as='a'
|
||||
{...href && { href }}
|
||||
target='_blank'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
|
||||
width: '20rem',
|
||||
|
||||
borderWidth,
|
||||
borderRadius: '8px !important',
|
||||
p: '8px !important',
|
||||
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
|
||||
backgroundColor: 'var(--theme-ui-colors-darkless)',
|
||||
|
||||
'&:hover::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
boxShadow: `linear-gradient(60deg, ${effectColours[0]}, ${effectColours[1]}, ${effectColours[2]}, ${effectColours[3]}, ${effectColours[4]})`,
|
||||
borderRadius: 'inherit',
|
||||
zIndex: -1,
|
||||
animation: `${animatedBoxShadow} 5s ease infinite`,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon glyph={icon} size={42} sx={{ color: 'red', flexShrink: 0 }} />
|
||||
<Box sx={{ textAlign: 'left' }}>{children}</Box>
|
||||
{ href &&
|
||||
<Icon glyph='external-fill' size={32} sx={{ position: 'absolute', top: 0, right: 0, translate: '50% -50%', color: 'muted' }} />
|
||||
}
|
||||
</Flex>
|
||||
</Tilt>
|
||||
)
|
||||
}
|
||||
|
||||
function BulletBox({ padding = '2rem', children }) {
|
||||
return (
|
||||
<Box
|
||||
as='ul'
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
gridGap: '2rem',
|
||||
padding,
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ id, children }) {
|
||||
return (
|
||||
<Flex as='section' id={id} sx={{ flexDirection: 'column', pt: 5 }}>
|
||||
{ children }
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FiscalSponsorship() {
|
||||
const gridRef = useRef()
|
||||
const glowRef = useRef()
|
||||
|
||||
const scrollPos = 0
|
||||
const mousePos = [0, 0]
|
||||
|
||||
const setGlowMaskPosition = () => {
|
||||
const finalPos = [-mousePos[0], -mousePos[1] + scrollPos]
|
||||
glowRef.current.style.maskPosition = `${finalPos[0]}px ${finalPos[1]}px`;
|
||||
glowRef.current.style.WebkitMaskPosition = `${finalPos[0]}px ${finalPos[1]}px`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = (e) => {
|
||||
const s = -window.scrollY / 10
|
||||
|
||||
gridRef.current.style.transform = `translateY(${s}px)`
|
||||
|
||||
scrollPos = s
|
||||
setGlowMaskPosition()
|
||||
}
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
const x = e.clientX
|
||||
const y = e.clientY
|
||||
glowRef.current.style.left = x + 'px'
|
||||
glowRef.current.style.top = y + 'px'
|
||||
|
||||
mousePos = [x, y]
|
||||
setGlowMaskPosition()
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box as="main" key="main" sx={{ position: 'relative' }}>
|
||||
<style>
|
||||
{`*{
|
||||
scroll-behavior: smooth;
|
||||
}`}
|
||||
</style>
|
||||
<Box
|
||||
ref={gridRef}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
height: '1000%',
|
||||
zIndex: -2,
|
||||
backgroundSize: '20px 20px',
|
||||
backgroundImage: `linear-gradient(to right, #23262D 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #23262D 1px, transparent 1px) `,
|
||||
backgroundPosition: 'top left',
|
||||
maskImage: `linear-gradient(180deg, transparent 0%, white 3%)`,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
ref={glowRef}
|
||||
sx={{
|
||||
pointerEvents: 'none',
|
||||
zIndex: -2,
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: '20rem',
|
||||
height: '20rem',
|
||||
background: 'red',
|
||||
opacity: 0.2,
|
||||
borderRadius: '50%',
|
||||
filter: 'blur(2rem)',
|
||||
translate: '-50% -50%',
|
||||
// Mask it to the grid background
|
||||
maskImage: `linear-gradient(to right, #23262D 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #23262D 1px, transparent 1px) `,
|
||||
maskSize: '20px 20px, 20px 20px, cover',
|
||||
maskPosition: '0px 0px',
|
||||
}}
|
||||
/>
|
||||
<Nav dark />
|
||||
<ForceTheme theme="dark" />
|
||||
<Meta
|
||||
as={Head}
|
||||
title="Fiscal Sponsorship"
|
||||
description="What is fiscal sponsorship?"
|
||||
image="/bank/og-image.png"
|
||||
>
|
||||
<title>Fiscal Sponsorship — Hack Club Bank</title>
|
||||
</Meta>
|
||||
|
||||
<Container sx={{ textAlign: 'center', mt: 6 }}>
|
||||
<FlexCol gap={4} alignItems='center'>
|
||||
<FlexCol gap={4} alignItems='center'>
|
||||
<Text variant='ultratitle'>Fiscal sponsorship</Text>
|
||||
<Text variant='title'>
|
||||
The fast track to <Text sx={{ color: 'red', textShadow: '0 0 50px var(--theme-ui-colors-red)' }}>501(c)(3) nonprofit </Text>
|
||||
status for your startup
|
||||
</Text>
|
||||
</FlexCol>
|
||||
<Text variant='headline'>
|
||||
Building a project, event, or organization on a
|
||||
mission to serve the public good or your community?
|
||||
Obtaining 501(c)(3) public charity status in the U.S.
|
||||
just got easier through fiscal sponsorship.
|
||||
</Text>
|
||||
|
||||
<FlexCol gap={1} alignItems='center'>
|
||||
<Text variant='headline'>Jump to:</Text>
|
||||
<BulletBox padding='0'>
|
||||
<Link sx={{fontSize: 2 }} href='#what-is'>What’s Fiscal Sponsorship?</Link>
|
||||
<Link sx={{fontSize: 2 }} href='#requirements'>Requirements for Fiscal Sponsorship</Link>
|
||||
<Link sx={{fontSize: 2 }} href='#partner'>Partner with Hack Club Bank</Link>
|
||||
</BulletBox>
|
||||
</FlexCol>
|
||||
<>
|
||||
<Text variant='lead'>
|
||||
Every year, thousands of organizations from around
|
||||
the world apply through the IRS to become a U.S. 501(c)(3)
|
||||
for charitable recognition and tax exemptions.
|
||||
The process for obtaining legal status can take anywhere
|
||||
from 2-12 months, and as a nonprofit organizer,
|
||||
it’s important to know that this can mean:
|
||||
</Text>
|
||||
<BulletBox>
|
||||
<Bullet glow={false} icon='sad'>
|
||||
$3,000 in up-front costs, from filing different
|
||||
applications and forms to support from legal counsel.
|
||||
</Bullet>
|
||||
<Bullet glow={false} icon='sad'>
|
||||
The potential for the IRS to reject an application
|
||||
(and not return your money).
|
||||
</Bullet>
|
||||
<Bullet glow={false} icon='sad'>
|
||||
An annual filing fee of $2,500.
|
||||
</Bullet>
|
||||
<Bullet glow={false} icon='sad'>
|
||||
Hiring bookkeepers and accountants to prepare
|
||||
taxes and provide upkeep to stay in good standing.
|
||||
</Bullet>
|
||||
<Bullet glow={false} icon='sad'>
|
||||
Up to $5,000 to close down shop if you lose or terminate status.
|
||||
</Bullet>
|
||||
</BulletBox>
|
||||
</>
|
||||
<Text variant='lead'>
|
||||
Unfortunately, between the price and time needed to
|
||||
organize a nonprofit, these barriers prevent many
|
||||
charitable initiatives from exiting an idea phase
|
||||
to go on to making a valuable impact throughout the world.
|
||||
</Text>
|
||||
<Text variant='lead'>
|
||||
So 501(c)(3) status is cool and all,
|
||||
but why go through the hassle of applying
|
||||
when it’s so expensive and time consuming?
|
||||
</Text>
|
||||
|
||||
<Text variant='headline'>
|
||||
As a legally recognized 501(c)(3) nonprofit
|
||||
in the U.S., there are loads of legal tax
|
||||
benefits that make the status worth it, like:
|
||||
</Text>
|
||||
<BulletBox>
|
||||
<Bullet icon='payment'>
|
||||
The ability to receive <b>tax deductible
|
||||
donations</b> from sponsors.
|
||||
</Bullet>
|
||||
<Bullet icon='member-add'>
|
||||
Reduced taxable income for your U.S.
|
||||
supporters, which <b>incentivizes giving</b>.
|
||||
</Bullet>
|
||||
<Bullet icon='leader'>
|
||||
<b>Exemption</b> from U.S. federal
|
||||
income tax and unemployment tax.
|
||||
</Bullet>
|
||||
<Bullet icon='bolt'>
|
||||
Potential exemption from state
|
||||
income, sales, and employment taxes.
|
||||
</Bullet>
|
||||
<Bullet icon='email'>
|
||||
Potential for reduced rates on postage,
|
||||
marketing, advertising, legal counsel, and more.
|
||||
</Bullet>
|
||||
</BulletBox>
|
||||
<Text variant='lead'>
|
||||
As it turns out, there’s an alternative route
|
||||
for startups, student-led initiatives,
|
||||
or anyone looking to avoid a headache
|
||||
with the IRS to obtain all the benefits of
|
||||
501(c)(3) status. To avoid the traditional
|
||||
filing route, go for fiscal sponsorship.
|
||||
</Text>
|
||||
|
||||
<Section id='what-is'>
|
||||
<Text variant='title'>
|
||||
What’s Fiscal Sponsorship?
|
||||
</Text>
|
||||
<Text variant='lead'>
|
||||
By legally partnering with an existing
|
||||
nonprofit offering fiscal sponsorship,
|
||||
projects and events can claim all the
|
||||
legal benefits of individual 501(c)(3)
|
||||
status. Piggy-backing off this existing
|
||||
status, organizations also gain access
|
||||
to resources from their fiscal sponsor like:
|
||||
</Text>
|
||||
<BulletBox>
|
||||
<Bullet icon='docs'>
|
||||
Bookkeeping and administration ensuring
|
||||
that all paperwork and taxes are filed.
|
||||
</Bullet>
|
||||
<Bullet icon='bag'>
|
||||
Fully established HR and benefits,
|
||||
which can vary by the fiscal sponsor.
|
||||
</Bullet>
|
||||
<Bullet icon='admin'>
|
||||
A board of directors - you
|
||||
don’t have to organize your own!
|
||||
</Bullet>
|
||||
<Bullet icon='payment'>
|
||||
Fully transparent operational fees,
|
||||
typically ranging from 7-12% that
|
||||
prevent you from paying operating costs.
|
||||
</Bullet>
|
||||
<Bullet icon='door-leave'>
|
||||
The ability to terminate your fiscal
|
||||
sponsorship agreement and file for
|
||||
separate tax-exempt status at any point.
|
||||
</Bullet>
|
||||
</BulletBox>
|
||||
<Text variant='lead'>
|
||||
If you’re brand new to nonprofit organizing
|
||||
or unsure where your project will take you,
|
||||
fiscal sponsorship is a great tool to help
|
||||
manage your finances and gauge whether becoming
|
||||
an independent nonprofit down the line is
|
||||
practical or financially feasible.
|
||||
</Text>
|
||||
<Text variant='lead'>
|
||||
Fiscal sponsorship makes it so that hacks
|
||||
aren’t just for folding that stubborn fitted
|
||||
sheet or sending an email to hundreds of
|
||||
people in seconds; they’re also for obtaining
|
||||
nonprofit status.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section id='requirements'>
|
||||
<Text variant='title'>
|
||||
Requirements for Fiscal Sponsorship
|
||||
</Text>
|
||||
<Text variant='lead'>
|
||||
Depending on the fiscal sponsor you choose,
|
||||
requirements for working together can vary.
|
||||
Fiscal sponsors generally ask that your
|
||||
nonprofit’s goals be similar to theirs.
|
||||
They usually also ask that your organization
|
||||
or event commits to remaining charitable in
|
||||
nature and refrains from activites that may
|
||||
result in loss of 501(c)(3) status.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Section id='partner'>
|
||||
<Text variant='title'>
|
||||
Partner with Hack Club Bank
|
||||
</Text>
|
||||
<Text variant='lead'>
|
||||
While many fiscal sponsors require that their
|
||||
partners relate to their mission in similar ways,
|
||||
at Hack Club Bank, we’ve built our infrastructure
|
||||
to support hundreds of causes in all areas of
|
||||
charitability. Check out some of the resources
|
||||
we’ve built our fiscal sponsorship foundation on:
|
||||
</Text>
|
||||
<BulletBox>
|
||||
<Bullet icon='bank-account'>
|
||||
A web interface that looks and operates
|
||||
just like a bank account
|
||||
</Bullet>
|
||||
<Bullet icon='card'>
|
||||
Fee-free invoicing, ACH or check
|
||||
transfers, and reimbursements
|
||||
</Bullet>
|
||||
<Bullet icon='link'>
|
||||
A customizable and embeddable
|
||||
donations URL
|
||||
</Bullet>
|
||||
<Bullet icon='google'>
|
||||
Perks like Google Workspace,
|
||||
PVSA certification, and 1Password credits
|
||||
</Bullet>
|
||||
<Bullet icon='purse' href='https://bank.hackclub.com/hq'>
|
||||
Optional transparency mode to show
|
||||
off finances to donors and the public
|
||||
</Bullet>
|
||||
<Bullet icon='friend'>
|
||||
24 hour weekday turnaround time from a
|
||||
full-time support team for all queries
|
||||
</Bullet>
|
||||
<Bullet icon='everything' href='http://hackclub.com/bank'>
|
||||
...and more!
|
||||
</Bullet>
|
||||
</BulletBox>
|
||||
<Text variant='lead'>
|
||||
Looking for nonprofit status and not a religious or
|
||||
political organization? We’d love to meet you and
|
||||
chat about working together. Feel free to apply
|
||||
here or email our team if you have more questions
|
||||
about fiscal sponsorship!
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
<Text variant='lead'>
|
||||
At its core, Hack Club is a nonprofit encouraging
|
||||
students to learn how to code by building and making
|
||||
cool things. Hack Club Bank was built out by teenagers
|
||||
at Hack Club and continues to be a real-world space
|
||||
that teens can hack on every day.
|
||||
</Text>
|
||||
</FlexCol>
|
||||
</Container>
|
||||
<Box sx={{
|
||||
height: '100px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&::after': {
|
||||
content: '""',
|
||||
width: '500%',
|
||||
height: '100%',
|
||||
|
||||
position: 'absolute',
|
||||
translate: '-50% 100%',
|
||||
boxShadow: '0 -64px 64px #17171d',
|
||||
}
|
||||
}}/>
|
||||
<Footer dark />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ import Start from '../../components/bank/start'
|
|||
import Nonprofits from '../../components/bank/nonprofits'
|
||||
|
||||
const styles = `
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
::selection {
|
||||
background-color: #e42d42;
|
||||
color: #ffffff;
|
||||
|
|
|
|||
|
|
@ -1,77 +1,119 @@
|
|||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Container, Text, Flex, Link, Image } from 'theme-ui'
|
||||
import { useEffect } from 'react'
|
||||
import { Box, Button, Card, Container, Divider, Text, Link, Flex, Image } from 'theme-ui'
|
||||
import ForceTheme from '../../components/force-theme'
|
||||
import JSConfetti from 'js-confetti'
|
||||
import Icon from '../../components/icon'
|
||||
import FlexCol from '../../components/flex-col'
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ApplicationSuccess() {
|
||||
const router = useRouter()
|
||||
|
||||
const [counter, setCounter] = useState(15)
|
||||
|
||||
const interval = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
interval.current = setInterval(() => {
|
||||
setCounter(c => {
|
||||
if (c <= 1) {
|
||||
clearInterval(interval.current)
|
||||
router.push('/bank')
|
||||
return 0
|
||||
} else {
|
||||
return c - 1
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval.current)
|
||||
}
|
||||
const jsConfetti = new JSConfetti()
|
||||
jsConfetti.addConfetti({
|
||||
confettiColors: [ // Hack Club colours!
|
||||
'#ec3750',
|
||||
'#ff8c37',
|
||||
'#f1c40f',
|
||||
'#33d6a6',
|
||||
'#5bc0de',
|
||||
'#338eda',
|
||||
'#a633d6',
|
||||
],
|
||||
})
|
||||
}, [router])
|
||||
|
||||
const cancelRedirect = () => {
|
||||
clearInterval(interval.current)
|
||||
setCounter(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container variant="copy">
|
||||
<ForceTheme theme="dark" />
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
pt: 6
|
||||
}}
|
||||
<FlexCol
|
||||
height="100vh"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
py={5}
|
||||
gap={4}
|
||||
>
|
||||
<Image
|
||||
src="https://cloud-5mgrt4f4s-hack-club-bot.vercel.app/0frame.svg"
|
||||
alt="Dinosaur partying"
|
||||
sx={{ width: '40%', mx: 'auto', mb: 4 }}
|
||||
/>
|
||||
<Text variant="title" sx={{ textAlign: 'center' }}>
|
||||
Thanks for applying!
|
||||
</Text>
|
||||
<Text variant="lead">
|
||||
We’ll reach out to schedule your introductory call within 24 hours on
|
||||
weekdays. If you have any questions about your application, please
|
||||
don’t hesitate to reach out at{' '}
|
||||
<Link as="a" href="mailto:bank@hackclub.com">
|
||||
bank@hackclub.com
|
||||
</Link>{' '}
|
||||
or on the <Link href="/slack">Hack Club Slack</Link> in the{' '}
|
||||
<strong>#bank</strong> channel.
|
||||
</Text>
|
||||
{counter !== null && (
|
||||
<Flex sx={{ justifyContent: 'center' }}>
|
||||
<Text sx={{ mr: 3 }}>Redirecting in {counter} seconds.</Text>
|
||||
<Link sx={{ cursor: 'pointer' }} onClick={() => cancelRedirect()}>
|
||||
Cancel
|
||||
</Link>
|
||||
<FlexCol gap={4} alignItems="center">
|
||||
<Image
|
||||
src="/bank/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 Bank and explore the dashboard
|
||||
</Text>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
|
||||
<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:bank@hackclub.com">
|
||||
bank@hackclub.com
|
||||
</Option>
|
||||
<Option
|
||||
icon="slack"
|
||||
label="Slack"
|
||||
link="https://hackclub.slack.com/channels/bank"
|
||||
>
|
||||
#bank
|
||||
</Option>
|
||||
<Option
|
||||
icon="help"
|
||||
label="FAQ"
|
||||
link="https://bank.hackclub.com/faq"
|
||||
>
|
||||
FAQ
|
||||
</Option>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</FlexCol>
|
||||
|
||||
<Button as="a" href="https://bank.hackclub.com">
|
||||
<Flex sx={{ alignItems: "center", px: [2, null, 3], py: 2 }}>
|
||||
<Icon glyph="bank-account" size={36} />
|
||||
<Text sx={{ fontSize: 3 }}>Head to Bank!</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
</FlexCol>
|
||||
</Container>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
3
public/bank/apply/Squircle.svg
Normal file
3
public/bank/apply/Squircle.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg 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="white" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
31
public/bank/apply/party-orpheus.svg
Normal file
31
public/bank/apply/party-orpheus.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 35 KiB |
4
public/bank/timeline-steps/step1.svg
Normal file
4
public/bank/timeline-steps/step1.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M62.5 32C62.5 39.3388 62.331 44.8938 61.6443 49.1353C60.9591 53.3673 59.7873 56.0914 57.9393 57.9393C56.0914 59.7873 53.3673 60.9591 49.1353 61.6443C44.8938 62.331 39.3388 62.5 32 62.5C24.6612 62.5 19.1062 62.331 14.8647 61.6443C10.6327 60.9591 7.90865 59.7873 6.06066 57.9393C4.21267 56.0914 3.0409 53.3673 2.35572 49.1353C1.66901 44.8938 1.5 39.3388 1.5 32C1.5 24.6612 1.66901 19.1062 2.35572 14.8647C3.0409 10.6327 4.21267 7.90865 6.06066 6.06066C7.90865 4.21267 10.6327 3.0409 14.8647 2.35572C19.1062 1.66901 24.6612 1.5 32 1.5C39.3388 1.5 44.8938 1.66901 49.1353 2.35572C53.3673 3.0409 56.0914 4.21267 57.9393 6.06066C59.7873 7.90865 60.9591 10.6327 61.6443 14.8647C62.331 19.1062 62.5 24.6612 62.5 32Z" fill="#17171D" stroke="#8492A6" stroke-width="3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0683 19.9519V27.7557L30.8901 29.9623C31.8535 30.1587 32.372 30.9439 32.4073 31.6804C32.4073 31.6937 32.4088 31.7129 32.4088 31.7306C32.4088 31.7365 32.4088 31.7424 32.4088 31.7483C32.4088 31.7557 32.4088 31.7616 32.4088 31.7675C32.4088 31.7852 32.4073 31.8044 32.4073 31.8177C32.372 32.5528 31.8535 33.3395 30.8901 33.5358L20.0683 35.7425V44.0481C20.099 44.0496 20.1434 44.0451 20.2063 44.023C26.9351 41.7735 43.8474 32.4982 43.8474 31.7601C43.8474 31.0221 26.8568 22.2058 20.2063 19.9755C20.1434 19.9549 20.099 19.9504 20.0683 19.9519ZM47 31.7483V31.7321V31.7277C46.9831 30.6974 46.4017 29.8251 45.5809 29.2406C40.0105 25.273 27.92 19.4368 21.2158 17.1888C20.2585 16.867 19.2214 16.9556 18.3884 17.4619C17.5385 17.977 17 18.8759 17 19.9224V28.3535C17 29.3985 17.7609 30.3018 18.8241 30.5188L24.8548 31.7483L18.8241 32.9793C17.7609 33.1948 17 34.0982 17 35.1447V44.0776C17 45.1241 17.5385 46.0215 18.3884 46.5381C19.2214 47.0444 20.2585 47.133 21.2158 46.8112C27.9215 44.5573 40.0703 38.1764 45.5794 34.2576C46.4017 33.6716 46.9831 32.8007 46.9985 31.7705V31.766L47 31.7483Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
8
public/bank/timeline-steps/step2.svg
Normal file
8
public/bank/timeline-steps/step2.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M62.8333 32C62.8333 39.3388 62.6642 44.8938 61.9775 49.1353C61.2924 53.3673 60.1206 56.0914 58.2726 57.9393C56.4246 59.7873 53.7005 60.9591 49.4685 61.6443C45.2271 62.331 39.672 62.5 32.3333 62.5C24.9945 62.5 19.4394 62.331 15.198 61.6443C10.966 60.9591 8.2419 59.7873 6.39391 57.9393C4.54593 56.0914 3.37415 53.3673 2.68897 49.1353C2.00226 44.8938 1.83325 39.3388 1.83325 32C1.83325 24.6612 2.00226 19.1062 2.68897 14.8647C3.37415 10.6327 4.54593 7.90865 6.39391 6.06066C8.2419 4.21267 10.966 3.0409 15.198 2.35572C19.4394 1.66901 24.9945 1.5 32.3333 1.5C39.672 1.5 45.2271 1.66901 49.4685 2.35572C53.7005 3.0409 56.4246 4.21267 58.2726 6.06066C60.1206 7.90865 61.2924 10.6327 61.9775 14.8647C62.6642 19.1062 62.8333 24.6612 62.8333 32Z" fill="#17171D" stroke="#8492A6" stroke-width="3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.954 32.0669C43.2604 33.0422 42.1883 33.6596 40.89 32.849L36.6569 35.2875C33.726 36.9761 32.0907 37.5273 30.8506 37.072L28.3597 38.5081C23.6704 41.2099 21.4516 38.8944 20.3767 36.868C19.3005 34.8417 18.2554 31.8494 22.9448 29.1475L25.4356 27.7128C25.6604 26.4132 26.9573 25.2757 29.8881 23.5871L34.2919 21.05C34.3189 19.4749 35.4195 18.8399 36.8315 18.0267C39.3955 16.5501 40.9347 15.6626 44.9959 22.6833C49.0572 29.7027 48.0311 30.2931 44.954 32.0669ZM41.4708 29.6744C40.8332 28.8746 40.1157 27.7588 39.1342 26.0606C37.2227 22.7549 36.9939 21.4837 36.9967 21.1257C37.1808 20.942 37.4502 20.788 37.9186 20.5205L37.9348 20.511C38.012 20.4664 38.0959 20.4192 38.1852 20.3665C38.2814 20.3124 38.3694 20.2611 38.4519 20.2125C38.9176 19.9423 39.1857 19.7869 39.4375 19.7194C39.7502 19.8964 40.7398 20.7285 42.6513 24.0342C43.6341 25.731 44.2446 26.909 44.6196 27.8601C44.8132 28.3288 44.8795 28.7139 44.9107 28.926C44.6941 29.0989 44.38 29.2799 43.8913 29.5595L43.8899 29.5609C43.7992 29.6122 43.7031 29.6676 43.6002 29.727C43.4974 29.7851 43.4012 29.8419 43.3119 29.8932C42.8232 30.1769 42.5091 30.3579 42.2505 30.4579C42.0827 30.3269 41.7808 30.0756 41.4708 29.6744ZM38.8202 30.9213L38.9596 30.8402C38.3355 29.9837 37.6275 28.8584 36.7895 27.4116C35.9516 25.9634 35.3518 24.7759 34.9511 23.7911L34.7589 23.9005L31.2419 25.9269C29.8529 26.7253 29.0393 27.263 28.4775 27.7871C28.2636 27.9668 28.1729 28.0951 28.135 28.1654V28.1667C28.1404 28.2397 28.1553 28.4437 28.3191 28.8084C28.5912 29.4515 29.0339 30.2174 29.6593 31.2982L29.8502 31.6292C30.477 32.7112 30.9183 33.4759 31.3407 34.0325C31.5762 34.3567 31.7455 34.4715 31.8064 34.5134H31.8077C31.8876 34.5161 32.0433 34.5012 32.3073 34.4067C33.0437 34.1838 33.9169 33.7474 35.3031 32.9477L38.8202 30.9213ZM26.099 30.4498L26.057 30.4741L24.2985 31.4887C22.406 32.5775 22.2151 33.3867 22.1785 33.6407C22.1176 34.077 22.2746 34.6714 22.7688 35.6036C23.183 36.3844 23.6081 36.7924 23.9926 36.9424C24.2579 37.045 25.1067 37.2612 27.006 36.1683L28.7645 35.1551L28.8065 35.1294C28.3828 34.4958 27.936 33.7217 27.4121 32.818L27.4108 32.8153C26.8869 31.9088 26.4388 31.1347 26.099 30.4498Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3844 40.5588C18.7373 40.9316 17.9102 40.7101 17.5352 40.0643L16.1814 37.7245C15.8078 37.0774 16.0298 36.252 16.6769 35.8778C17.3253 35.505 18.1525 35.7265 18.5275 36.3736L19.8812 38.7134C20.2549 39.3592 20.0328 40.1846 19.3844 40.5588Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.2968 37.892C35.5739 38.0852 35.1461 38.8269 35.3397 39.5469L37.0711 45.9976C37.2295 46.5893 37.7656 47 38.3788 47C39.2696 47 39.9167 46.157 39.6866 45.2992L37.9538 38.8485C37.7615 38.1271 37.0183 37.6989 36.2968 37.892Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.2816 45.6491C32.2816 46.3948 31.6751 47 30.9278 47C30.1792 47 29.5741 46.3948 29.5741 45.6491V41.5963C29.5741 40.8492 30.1792 40.2454 30.9278 40.2454C31.6751 40.2454 32.2816 40.8492 32.2816 41.5963V45.6491Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8756 45.9976C25.7172 46.5893 25.1798 47 24.5679 47C23.6771 47 23.03 46.157 23.2602 45.2992L23.9032 42.9013C24.0968 42.1799 24.84 41.7516 25.5615 41.9448C26.2844 42.138 26.7122 42.8797 26.5186 43.5997L25.8756 45.9976Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
4
public/bank/timeline-steps/step3.svg
Normal file
4
public/bank/timeline-steps/step3.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.1665 32C63.1665 39.3388 62.9975 44.8938 62.3108 49.1353C61.6256 53.3673 60.4538 56.0914 58.6058 57.9393C56.7579 59.7873 54.0338 60.9591 49.8018 61.6443C45.5604 62.331 40.0053 62.5 32.6665 62.5C25.3278 62.5 19.7727 62.331 15.5312 61.6443C11.2992 60.9591 8.57515 59.7873 6.72716 57.9393C4.87918 56.0914 3.70741 53.3673 3.02222 49.1353C2.33552 44.8938 2.1665 39.3388 2.1665 32C2.1665 24.6612 2.33552 19.1062 3.02222 14.8647C3.70741 10.6327 4.87918 7.90865 6.72716 6.06066C8.57515 4.21267 11.2992 3.0409 15.5312 2.35572C19.7727 1.66901 25.3278 1.5 32.6665 1.5C40.0053 1.5 45.5604 1.66901 49.8018 2.35572C54.0338 3.0409 56.7579 4.21267 58.6058 6.06066C60.4538 7.90865 61.6256 10.6327 62.3108 14.8647C62.9975 19.1062 63.1665 24.6612 63.1665 32Z" fill="#17171D" stroke="#8492A6" stroke-width="3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.1196 22.6893C43.4497 23.2656 43.1882 23.9851 42.5847 24.262L42.4628 24.3176C41.9457 24.5554 41.3351 24.333 41.0499 23.8395C39.6311 21.3806 37.5851 19.5097 35.2315 18.3098C34.6481 18.0128 34.3547 17.3229 34.6055 16.7171C34.854 16.1148 35.5451 15.8237 36.1296 16.1124C38.9519 17.5075 41.4073 19.7251 43.0994 22.6562L43.1196 22.6893ZM40.1151 24.3791C40.3695 24.8181 40.1695 25.3696 39.7092 25.5814L39.0371 25.889C38.6809 26.0523 38.262 25.9021 38.0656 25.5624C37.042 23.7886 35.5877 22.4195 33.9109 21.5095C33.3346 21.196 33.04 20.5084 33.2908 19.9026C33.5393 19.3003 34.2304 19.008 34.809 19.3062C36.9544 20.4161 38.8182 22.132 40.1151 24.3791ZM43.9313 30.8448C43.7775 31.1856 43.6189 31.5382 43.5337 31.898C43.2024 33.2931 43.5337 35.0125 43.5337 35.0125C43.5337 35.0125 44.1408 38.3672 43.5337 40.0878C42.522 42.955 40.6097 45.1205 37.4834 45.1205C35.9663 45.1205 34.2115 43.6922 32.8021 42.5444L32.7998 42.5432C32.3406 42.1693 31.9194 41.8261 31.5537 41.5717C30.5988 40.9055 28.5883 39.747 27.1624 38.9246C26.3696 38.4678 25.7578 38.1152 25.6099 38.0099C24.8632 37.4797 24.7448 36.4644 25.1602 35.8006C25.5755 35.1367 26.6334 34.7238 27.4428 35.3628C28.2522 36.0018 30.9289 37.6596 30.9289 37.6596C30.9289 37.6596 26.6216 33.3073 25.8607 32.6648C25.0987 32.021 25.0004 30.8898 25.6099 30.2816C26.2181 29.6733 27.1766 29.6449 27.7387 30.1029C28.3007 30.5608 32.9027 35.0125 32.9027 35.0125C32.9027 35.0125 28.1848 29.0604 27.7387 28.5101C27.2937 27.9599 27.1671 26.9943 27.7387 26.4227C28.3114 25.85 29.3066 25.8476 29.8994 26.4227C30.4911 26.9978 35.08 32.5453 35.08 32.5453C35.08 32.5453 31.9217 27.2463 31.5537 26.4227C31.1869 25.5991 31.5182 24.5329 32.2838 24.2454C33.0506 23.959 33.9216 24.1815 34.4162 24.8726C34.5523 25.0607 34.9286 25.753 35.422 26.6606C36.7379 29.0793 38.8868 33.0292 39.508 33.0292C40.0725 33.0292 40.1127 32.7074 40.1908 32.0767C40.2311 31.7524 40.2819 31.3454 40.4192 30.8602C40.7825 29.5751 42.8722 27.4061 44.0532 28.7835C44.581 29.4 44.2674 30.0958 43.9313 30.8448ZM44.8804 45.7488C42.2534 48.6149 40.8902 48.1687 39.9944 47.5096C38.0774 46.1003 38.3306 46.0068 39.3944 45.6104C40.1612 45.3264 41.3469 44.8862 42.4462 43.6863C43.4438 42.5976 43.8982 41.5906 44.2047 40.9102C44.7064 39.7991 44.8153 39.5588 46.2566 41.2569C46.9772 42.1042 47.5085 42.8828 44.8804 45.7488ZM20.1914 35.4196C19.6317 35.5154 19.2554 36.0526 19.4021 36.6017C20.1441 39.3707 21.6516 41.7291 23.6361 43.5112C24.1201 43.9466 24.8644 43.8437 25.2608 43.3254C25.6584 42.8059 25.5531 42.0639 25.0726 41.6178C23.4905 40.1481 22.2894 38.23 21.6883 35.9899C21.5712 35.5521 21.1487 35.2574 20.7014 35.3332L20.1914 35.4196ZM16.9858 35.9639C17.6082 35.8586 18.1963 36.2716 18.3596 36.881C19.1655 39.8854 20.8079 42.4414 22.971 44.3643C23.4609 44.7998 23.5639 45.5417 23.1663 46.0612C22.7699 46.5795 22.0267 46.6825 21.5345 46.2541C18.9702 44.0212 17.0201 41.0262 16.0734 37.4939C16.0604 37.4454 16.0486 37.3969 16.0356 37.3472C15.8687 36.7058 16.3077 36.0799 16.9621 35.9686L16.9858 35.9639Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
4
public/bank/timeline-steps/step4.svg
Normal file
4
public/bank/timeline-steps/step4.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M62.5 32C62.5 39.3388 62.331 44.8938 61.6443 49.1353C60.9591 53.3673 59.7873 56.0914 57.9393 57.9393C56.0914 59.7873 53.3673 60.9591 49.1353 61.6443C44.8938 62.331 39.3388 62.5 32 62.5C24.6612 62.5 19.1062 62.331 14.8647 61.6443C10.6327 60.9591 7.90865 59.7873 6.06066 57.9393C4.21267 56.0914 3.0409 53.3673 2.35572 49.1353C1.66901 44.8938 1.5 39.3388 1.5 32C1.5 24.6612 1.66901 19.1062 2.35572 14.8647C3.0409 10.6327 4.21267 7.90865 6.06066 6.06066C7.90865 4.21267 10.6327 3.0409 14.8647 2.35572C19.1062 1.66901 24.6612 1.5 32 1.5C39.3388 1.5 44.8938 1.66901 49.1353 2.35572C53.3673 3.0409 56.0914 4.21267 57.9393 6.06066C59.7873 7.90865 60.9591 10.6327 61.6443 14.8647C62.331 19.1062 62.5 24.6612 62.5 32Z" fill="#17171D" stroke="#8492A6" stroke-width="3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.9875 32.3894C38.4527 31.0109 38.9301 28.782 38.9301 25.5714C38.9301 20.8376 35.1989 17 30.5964 17C25.9938 17 22.2627 20.8376 22.2627 25.5714C22.2627 28.782 22.74 31.0109 24.2052 32.3894C21.4967 33.9137 19.3588 36.3767 18.2054 39.3531C17.9343 40.0527 17.8489 40.9616 18.4735 41.3549C19.7935 42.1863 21.1943 40.5711 21.9822 39.2007C23.7228 36.1733 26.929 34.1429 30.5964 34.1429C30.7711 34.1429 30.9447 34.1475 31.1172 34.1566C33.003 34.256 35.0718 34.0619 36.5172 32.8123C36.6787 32.6728 36.8365 32.5316 36.9875 32.3894ZM35.5966 25.5714C35.5966 28.4559 35.1444 29.4133 34.8139 29.7816C34.5427 30.0836 33.6811 30.7143 30.5964 30.7143C27.5117 30.7143 26.6499 30.0836 26.3789 29.7816C26.0484 29.4133 25.5962 28.4559 25.5962 25.5714C25.5962 22.7311 27.8348 20.4286 30.5964 20.4286C33.3579 20.4286 35.5966 22.7311 35.5966 25.5714ZM38.9301 37.3657C41.0943 35.1714 43.2586 35.72 44.3406 36.8171C48.6691 41.2057 40.2575 47 38.9301 47C37.6025 47 29.1909 41.2057 33.5194 36.8171C34.6016 35.72 36.7658 35.1714 38.9301 37.3657Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Loading…
Add table
Reference in a new issue