mirror of
https://github.com/System-End/site.git
synced 2026-04-19 19:45:07 +00:00
Merge branch 'main' into hw
This commit is contained in:
commit
a53220c3f9
195 changed files with 8185 additions and 5879 deletions
32
.github/workflows/caniuse-update.yml
vendored
Normal file
32
.github/workflows/caniuse-update.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Update Browserslist database
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 1,15 * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-browserslist-database:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
- name: Update Browserslist database and create PR if applies
|
||||
uses: c2corg/browserslist-update-action@v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: browserslist-update
|
||||
base_branch: main
|
||||
commit_message: 'build: update Browserslist db'
|
||||
title: 'build: update Browserslist db'
|
||||
body: Auto-generated by [browserslist-update-action](https://github.com/c2corg/browserslist-update-action/). Caniuse database has been updated. Review changes, merge this PR, and be merry.
|
||||
7
.prettierrc
Executable file
7
.prettierrc
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80,
|
||||
"semi": false
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Card, Text } from 'theme-ui'
|
||||
import { Card, Text, Box } from 'theme-ui'
|
||||
import { keyframes } from '@emotion/react'
|
||||
import Icon from './icon'
|
||||
import Image from 'next/image'
|
||||
|
||||
const unfold = keyframes({
|
||||
from: { transform: 'scaleY(0)' },
|
||||
|
|
@ -12,6 +13,8 @@ const Announcement = ({
|
|||
copy,
|
||||
iconLeft,
|
||||
iconRight,
|
||||
imgSrc,
|
||||
imgAlt,
|
||||
color = 'accent',
|
||||
sx = {},
|
||||
...props
|
||||
|
|
@ -48,6 +51,16 @@ const Announcement = ({
|
|||
sx={{ mr: [2, 3], ml: 2, color, display: ['none', 'block'] }}
|
||||
/>
|
||||
)}
|
||||
{imgSrc && (
|
||||
<Box sx={{ mr: [2,3], width: 32, flexShrink: 0 }}>
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt={imgAlt}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
as="p"
|
||||
sx={{ flex: '1 1 auto', strong: { display: ['inline', 'block'] } }}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ It was a huge honor last month to have Elon [spend an hour in an ask-me-anything
|
|||
|
||||
When hackers see problems in the world, we don’t blame someone else: we try to take them on to solve. Elon is very selective about the nonprofits he supports and I’m proud Hack Club is one of them.
|
||||
|
||||
So…how will Hack Club invest $500,000? We want to use this to help 1,000 more students start and join Hack Clubs in their towns ([see the worldwide map](https://hackclub.com/map/)). For those already in Hack Club, we look to you to help us make a higher-quality experience. We plan to continue much of what we’re already doing (and [what I wrote about in January](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)): spending as little money as possible at all times, growing slowly, adding diverse staff to make Hack Club better (video game designers, software engineers, media producers, and more). We are pushing hard to try and make the [Hack Club Slack](https://hackclub.com/) the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/hcb/).
|
||||
So…how will Hack Club invest $500,000? We want to use this to help 1,000 more students start and join Hack Clubs in their towns ([see the worldwide map](https://hackclub.com/map/)). For those already in Hack Club, we look to you to help us make a higher-quality experience. We plan to continue much of what we’re already doing (and [what I wrote about in January](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)): spending as little money as possible at all times, growing slowly, adding diverse staff to make Hack Club better (video game designers, software engineers, media producers, and more). We are pushing hard to try and make the [Hack Club Slack](https://hackclub.com/) the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/fiscal-sponsorship/).
|
||||
|
||||
We’ll be fully transparent in how we spend this money. One thing we’ve been working toward after winning the [Frank Grant](https://grant.frank.ly/) is open sourcing our finances. Hack Club HQ has been running on HCB since February, and starting today, [**you can see our finances publicly**](https://hcb.hackclub.com/hq). Through HCB, you can track how we spend every dollar of Elon’s gift. Soon, we’ll also launch [Frank’s](https://frank.ly/) transparency tools on [hackclub.com](https://hackclub.com/).
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ In 2014, Hack Club was founded, and Tom joined as Hack Club’s first board memb
|
|||
|
||||
Tom and Theresa also helped fund [The Hacker Zephyr](https://hack.af/zephyrdoc), an epic, cross-country train hackathon taken by 42 teen hackers in the summer of 2021. Tom even hacked alongside Hack Clubbers onboard.
|
||||
|
||||
With this gift, we will continue to build the engineering team at Hack Club, including a Tech Lead for [HCB](https://hackclub.com/hcb), and new engineers to support clubs, the Hack Club online community, and events.
|
||||
With this gift, we will continue to build the engineering team at Hack Club, including a Tech Lead for [HCB](https://hackclub.com/fiscal-sponsorship), and new engineers to support clubs, the Hack Club online community, and events.
|
||||
|
||||
One of our goals in 2022 is to improve Hack Club and to support more teenagers in joining the community. Thank you Tom and Theresa for helping make this possible.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Today, we're excited to announce Elon is donating $1 million to Hack Club.
|
|||
|
||||
This gift will help launch a number of ideas we've been discussing, including helping more in-person hackathons get off the ground, providing more direct 1:1 technical support on the [Hack Club Slack](https://hackclub.com/slack/), and starting up cool new projects like [The Hacker Zephyr](https://github.com/hackclub/the-hacker-zephyr). We also want to use his gift to help 1,000 more teenagers start and join Hack Clubs in their towns.
|
||||
|
||||
We will be spending every dollar as wisely as possible, growing thoughtfully, and adding diverse staff to make Hack Club better. We are pushing hard to try and make the Hack Club Slack the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/hcb/).
|
||||
We will be spending every dollar as wisely as possible, growing thoughtfully, and adding diverse staff to make Hack Club better. We are pushing hard to try and make the Hack Club Slack the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/fiscal-sponsorship/).
|
||||
|
||||
Elon is very selective about the nonprofits he supports and we're proud Hack Club is one of them.
|
||||
|
||||
|
|
|
|||
67
components/bin/rsvp-form.js
Normal file
67
components/bin/rsvp-form.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Checkbox, Input, Label, Text, Box } from 'theme-ui'
|
||||
import useForm from '../../lib/use-form'
|
||||
import Submit from '../submit'
|
||||
import { Slide } from 'react-reveal'
|
||||
|
||||
export default function RsvpForm() {
|
||||
const { status, formProps, useField } = useForm('/api/bin/rsvp', null, {
|
||||
clearOnSubmit: 60000,
|
||||
method: 'POST',
|
||||
initData: {}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<form {...formProps}>
|
||||
<Label>
|
||||
<Text>Email</Text>
|
||||
<Input
|
||||
{...useField('email')}
|
||||
placeholder="fiona@hackclub.com"
|
||||
required
|
||||
sx={{ border: '1px solid', borderColor: 'muted', mb: 2 }}
|
||||
/>
|
||||
</Label>
|
||||
<Label variant="labelHoriz" sx={{ fontSize: 1, pt: 2 }}>
|
||||
<Checkbox
|
||||
{...useField('high_schooler', 'checkbox')}
|
||||
defaultChecked={false}
|
||||
/>
|
||||
I am a current high school student or younger.
|
||||
</Label>
|
||||
<Label variant="labelHoriz" sx={{ fontSize: 1, pt: 2 }}>
|
||||
<Checkbox {...useField('stickers', 'checkbox')} />I want a sticker
|
||||
sheet.
|
||||
</Label>
|
||||
<Box sx={{ display: useField('stickers', 'checkbox').checked ? 'block' : 'none' }}>
|
||||
<Slide left delay={20}>
|
||||
<Label mt={2}>
|
||||
Address (line 1)
|
||||
<Input
|
||||
{...useField('address_line_1')}
|
||||
placeholder="1 Hacker Way"
|
||||
sx={{ border: '1px solid', borderColor: 'muted' }}
|
||||
/>
|
||||
</Label>
|
||||
<Label mt={2}>
|
||||
Address (zip code)
|
||||
<Input
|
||||
{...useField('address_zip')}
|
||||
placeholder="01337"
|
||||
sx={{ border: '1px solid', borderColor: 'muted' }}
|
||||
/>
|
||||
</Label>
|
||||
</Slide>
|
||||
</Box>
|
||||
<Submit
|
||||
status={status}
|
||||
labels={{
|
||||
default: 'Submit',
|
||||
error: 'Something went wrong',
|
||||
success: 'Success!'
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ import Icon from '@hackclub/icons'
|
|||
import { useState } from 'react'
|
||||
|
||||
export default function Bio({ popup = true, spanTwo = false, ...props }) {
|
||||
let { img, name, teamRole, pronouns, text, subrole, href, video } = props
|
||||
let { img, name, teamRole, pronouns, text, subrole, email, href, video } =
|
||||
props
|
||||
const [expand, setExpand] = useState(false)
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,7 +26,7 @@ export default function Bio({ popup = true, spanTwo = false, ...props }) {
|
|||
overflowY: 'hidden',
|
||||
overscrollBehavior: 'contain',
|
||||
gridColumn: !spanTwo ? null : [null, null, `1 / span 2`],
|
||||
position: 'relative',
|
||||
position: 'relative'
|
||||
}}
|
||||
as={href && !text ? 'a' : 'div'}
|
||||
href={href}
|
||||
|
|
@ -41,10 +42,7 @@ export default function Bio({ popup = true, spanTwo = false, ...props }) {
|
|||
width={64}
|
||||
height={64}
|
||||
mr={3}
|
||||
src={
|
||||
img ||
|
||||
require(`../public/team/${name.split(' ')[0].toLowerCase()}.jpg`)
|
||||
}
|
||||
src={img}
|
||||
alt={name}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
|
|
@ -97,6 +95,13 @@ export default function Bio({ popup = true, spanTwo = false, ...props }) {
|
|||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{!popup && email && (
|
||||
<Text color="muted" as={'a'} href={`mailto:${email}@hackclub.com`}>
|
||||
{email}@hackclub.com
|
||||
<br />
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!popup && (
|
||||
<>
|
||||
<Text mt={2} mb={0} color="black">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ a lot of what we’ve already been doing (and [what I wrote about at the beginni
|
|||
of the year](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)):
|
||||
we’ll spend as little money as possible at all times, and we’ll hire a small
|
||||
number of diverse staff from video game engineers to media producers to make
|
||||
Hack Club better. We are pushing hard now to expand users of [HCB](https://hackclub.com/hcb/),
|
||||
Hack Club better. We are pushing hard now to expand users of [HCB](https://hackclub.com/fiscal-sponsorship/),
|
||||
and continuing to try and make the Hack Club Slack the best place to be a teenager on the intenet.
|
||||
|
||||
We’ll have a proper announcement in a few weeks, but one thing we’re doing after
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import Icon from '../../icon'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export default function Checkbox({ name, defaultChecked = false, size = 38 }) {
|
||||
const [checked, setChecked] = useState(defaultChecked)
|
||||
const toggle = () => setChecked(!checked)
|
||||
const router = useRouter()
|
||||
|
||||
/* Fill in the field with the value from sessionStorage.
|
||||
For other input elements, the value is set in the Field component,
|
||||
but these checkboxes hold their state in useState rather than in the DOM. */
|
||||
useEffect(() => {
|
||||
const value = sessionStorage.getItem('bank-signup-' + name)
|
||||
const value = router.query[name] || sessionStorage.getItem('bank-signup-' + name)
|
||||
if (value) {
|
||||
const input = document.getElementById(name)
|
||||
input && setChecked(value === 'true')
|
||||
input && setChecked(!!value)
|
||||
}
|
||||
}, [name])
|
||||
}, [router.query, name])
|
||||
|
||||
return (
|
||||
<>
|
||||
80
components/fiscal-sponsorship/apply/field.js
Normal file
80
components/fiscal-sponsorship/apply/field.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { Flex, Label, Text } from 'theme-ui'
|
||||
|
||||
export default function Field({
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
col = true,
|
||||
requiredFields,
|
||||
children
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const isRequired = requiredFields.includes(name)
|
||||
|
||||
/* Fill in the field input element with the value from sessionStorage.
|
||||
Note: the custom checkbox component does this in its own useEffect hook. */
|
||||
useEffect(() => {
|
||||
const value =
|
||||
router.query[name] || sessionStorage.getItem('bank-signup-' + name)
|
||||
if (value) {
|
||||
const input = document.getElementById(name)
|
||||
if (input) input.value = value
|
||||
}
|
||||
}, [router.query, name])
|
||||
|
||||
return (
|
||||
<Flex
|
||||
aria-required={isRequired}
|
||||
sx={{
|
||||
flexDirection: col ? 'column' : 'row',
|
||||
alignItems: col ? 'flex-start' : 'center',
|
||||
gap: 1,
|
||||
width: '100%',
|
||||
// Wrapper around Select
|
||||
'> div': {
|
||||
width: '100%'
|
||||
},
|
||||
'input, select, textarea': {
|
||||
border: '1px solid',
|
||||
borderColor: 'smoke',
|
||||
outlineColor: 'blue',
|
||||
'&:-webkit-autofill': {
|
||||
boxShadow: '0 0 0 100px white inset !important',
|
||||
WebkitTextFillColor: 'black !important'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
htmlFor={name}
|
||||
sx={{
|
||||
fontSize: 2,
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{isRequired && (
|
||||
<Text
|
||||
as="span"
|
||||
sx={{
|
||||
color: 'red',
|
||||
fontWeight: 'bold',
|
||||
ml: 1
|
||||
}}
|
||||
title="Required"
|
||||
>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Label>
|
||||
{children}
|
||||
{description && (
|
||||
<Text as="p" variant="caption">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
39
components/fiscal-sponsorship/apply/form-container.js
Normal file
39
components/fiscal-sponsorship/apply/form-container.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Box, Container } from 'theme-ui'
|
||||
|
||||
const formContainer = forwardRef(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
as="form"
|
||||
sx={{
|
||||
bg: 'snow',
|
||||
px: [3, 5],
|
||||
py: 5,
|
||||
minHeight: '100dvb',
|
||||
'&.has-errors div[aria-required="true"] input:placeholder-shown': {
|
||||
borderColor: 'primary'
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Container
|
||||
variant="copy"
|
||||
sx={{
|
||||
ml: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
columnGap: 4,
|
||||
rowGap: 3,
|
||||
px: 0
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
// https://stackoverflow.com/a/67993106/10652680
|
||||
formContainer.displayName = 'formContainer'
|
||||
export default formContainer
|
||||
66
components/fiscal-sponsorship/apply/hcb-info.js
Normal file
66
components/fiscal-sponsorship/apply/hcb-info.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Box, Link, Heading } from 'theme-ui'
|
||||
import Icon from '../../icon'
|
||||
|
||||
export default function HCBInfo() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
gridArea: 'info',
|
||||
alignItems: 'start',
|
||||
mark: { color: '#ec555c', bg: 'inherit' },
|
||||
ul: { pl: [3, 0], color: 'muted', mb: 4 },
|
||||
p: { color: 'muted', mb: 0 }
|
||||
}}
|
||||
>
|
||||
<Heading variant="subheadline">
|
||||
HCB is a{' '}
|
||||
<Link
|
||||
href="https://en.wikipedia.org/wiki/Fiscal_sponsorship"
|
||||
target="_blank"
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'flex-end',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
fiscal sponsor
|
||||
<Icon glyph="external" size={24} aria-hidden />
|
||||
</Link>
|
||||
</Heading>
|
||||
<ul>
|
||||
<li>Nonprofit status.</li>
|
||||
<li>Tax-deductable donations.</li>
|
||||
</ul>
|
||||
<Heading variant="subheadline">
|
||||
HCB provides a financial platform.
|
||||
</Heading>
|
||||
<ul>
|
||||
<li>A donations page and invoicing system.</li>
|
||||
<li>Transfer money electronically.</li>
|
||||
<li>Order cards for you and your team to make purchases.</li>
|
||||
</ul>
|
||||
<Heading variant="subheadline">HCB is not a bank.</Heading>
|
||||
<ul>
|
||||
<li>
|
||||
We partner with{' '}
|
||||
<Link href="https://column.com" target="_blank">
|
||||
Column Bank
|
||||
</Link>{' '}
|
||||
to offer a bank account to fiscally-sponsored projects.
|
||||
</li>
|
||||
<li>
|
||||
You can't deposit or withdraw cash. But you can receive any kind of
|
||||
electronic payment!
|
||||
</li>
|
||||
</ul>
|
||||
<Heading variant="subheadline">HCB is not for for-profits.</Heading>
|
||||
<p>
|
||||
If you’re looking to set up a for-profit entity, consider{' '}
|
||||
<Link href="https://stripe.com/atlas" target="_blank">
|
||||
Stripe Atlas
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Input, Textarea } from 'theme-ui'
|
||||
import { Input, Select, Textarea } from 'theme-ui'
|
||||
import Checkbox from './checkbox'
|
||||
import AddressInput from './address-input'
|
||||
import Field from './field'
|
||||
import AutofillColourFix from './autofill-colour-fix'
|
||||
// This is using country-list instead of country-list-js as it has a smaller bundle size
|
||||
import { getNames } from 'country-list'
|
||||
|
||||
export default function OrganizationInfoForm({ requiredFields }) {
|
||||
const [org, setOrg] = useState('organization')
|
||||
const [org, setOrg] = useState('Organization')
|
||||
|
||||
useEffect(() => {
|
||||
if (navigator.language === 'en-GB') setOrg('organisation')
|
||||
if (navigator.language === 'en-GB') setOrg('Organisation')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
@ -23,7 +23,6 @@ export default function OrganizationInfoForm({ requiredFields }) {
|
|||
name="eventName"
|
||||
id="eventName"
|
||||
placeholder="Shelburne School Hackathon"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
|
|
@ -35,23 +34,30 @@ export default function OrganizationInfoForm({ requiredFields }) {
|
|||
<Input
|
||||
name="eventWebsite"
|
||||
id="eventWebsite"
|
||||
type="url"
|
||||
inputMode="url"
|
||||
placeholder="hackclub.com"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name="eventLocation"
|
||||
label={`${org} location`}
|
||||
description="If your organization runs online, put your own address."
|
||||
label={`Primary country of operations`}
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<AddressInput isPersonalAddressInput={false} name="eventLocation" />
|
||||
<Select name="eventLocation" id="eventLocation">
|
||||
{getNames()
|
||||
.sort()
|
||||
.sort(item => (item === 'United States of America' ? -1 : 1))
|
||||
.map(country => (
|
||||
<option key={country} value={country}>
|
||||
{country}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field
|
||||
name="transparent"
|
||||
label="Transparency mode"
|
||||
col={false}
|
||||
col={true}
|
||||
description={`
|
||||
Transparent accounts’ balances and donations are public.
|
||||
You choose who has access to personal details.
|
||||
|
|
@ -64,8 +70,8 @@ export default function OrganizationInfoForm({ requiredFields }) {
|
|||
</Field>
|
||||
<Field
|
||||
name="eventDescription"
|
||||
label={`Tell us about your ${org}!`}
|
||||
description="1 or 2 sentences will suffice"
|
||||
label={`Tell us about your ${org.toLowerCase()}`}
|
||||
description="2–4 sentences will suffice."
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Textarea
|
||||
|
|
@ -73,9 +79,7 @@ export default function OrganizationInfoForm({ requiredFields }) {
|
|||
id="eventDescription"
|
||||
rows={3}
|
||||
sx={{
|
||||
resize: 'vertical',
|
||||
width: '100%',
|
||||
...AutofillColourFix
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
|
@ -1,18 +1,9 @@
|
|||
import { Input, Flex, Label, Radio } from 'theme-ui'
|
||||
import Checkbox from './checkbox'
|
||||
import AddressInput from './address-input'
|
||||
import { Input, Flex, Label, Radio, Grid, Select } from 'theme-ui'
|
||||
import Field from './field'
|
||||
import AutofillColourFix from './autofill-colour-fix'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function PersonalInfoForm({
|
||||
setValidationResult,
|
||||
requiredFields
|
||||
}) {
|
||||
export default function PersonalInfoForm({ requiredFields }) {
|
||||
const [selectedContactOption, setSelectedContactOption] = useState('Email')
|
||||
const [email, setEmail] = useState(
|
||||
window.sessionStorage.getItem('bank-signup-userEmail')
|
||||
) // For display only, is not used for data submission.
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -22,24 +13,14 @@ export default function PersonalInfoForm({
|
|||
label="First name"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="firstName"
|
||||
id="firstName"
|
||||
placeholder="Fiona"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
<Input name="firstName" id="firstName" placeholder="Fiona" />
|
||||
</Field>
|
||||
<Field
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="lastName"
|
||||
id="lastName"
|
||||
placeholder="Hacksworth"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
<Input name="lastName" id="lastName" placeholder="Hacksworth" />
|
||||
</Field>
|
||||
</Flex>
|
||||
<Field name="userEmail" label="Email" requiredFields={requiredFields}>
|
||||
|
|
@ -48,10 +29,79 @@ export default function PersonalInfoForm({
|
|||
id="userEmail"
|
||||
type="email"
|
||||
placeholder="fiona@hackclub.com"
|
||||
onInput={e => setEmail(e.target.value)}
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="contactOption"
|
||||
label="Preferred contact channel"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Grid
|
||||
columns={[null, 2]}
|
||||
sx={{
|
||||
rowGap: 2,
|
||||
columnGap: 4,
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
name="contactOption"
|
||||
value="Email"
|
||||
defaultChecked={true}
|
||||
onInput={() => setSelectedContactOption('Email')}
|
||||
/>
|
||||
Email
|
||||
</Label>
|
||||
<Grid
|
||||
sx={{
|
||||
columnGap: 0,
|
||||
rowGap: 2,
|
||||
gridTemplateColumns: 'auto 1fr'
|
||||
}}
|
||||
>
|
||||
<Label
|
||||
sx={{
|
||||
display: 'contents',
|
||||
'~ div > label': { fontSize: 1 }
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
name="contactOption"
|
||||
value="Slack"
|
||||
onInput={() => setSelectedContactOption('Slack')}
|
||||
/>
|
||||
Hack Club Slack
|
||||
</Label>
|
||||
{selectedContactOption === 'Slack' ? (
|
||||
<>
|
||||
<div />
|
||||
<Field
|
||||
label="Your Hack Club Slack username"
|
||||
description="For teenagers only!"
|
||||
name="slackUsername"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="slackUsername"
|
||||
id="slackUsername"
|
||||
placeholder="FionaH"
|
||||
autocomplete="off"
|
||||
data-1p-ignore
|
||||
autoFocus
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Field>
|
||||
<Field
|
||||
name="userPhone"
|
||||
label="Phone"
|
||||
|
|
@ -62,23 +112,31 @@ export default function PersonalInfoForm({
|
|||
name="userPhone"
|
||||
id="userPhone"
|
||||
type="tel"
|
||||
placeholder="(123) 456-7890"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
placeholder="+1 (844) 237 2290"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
name="userBirthday"
|
||||
label="Birthday"
|
||||
label="Birth year"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="userBirthday"
|
||||
id="userBirthday"
|
||||
type="date"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
<Select name="userBirthday" id="userBirthday" defaultValue="">
|
||||
<option value="" disabled>
|
||||
Select a year
|
||||
</option>
|
||||
{/* show a century of years starting from 13 years ago */}
|
||||
{Array.from({ length: 98 }, (_, i) => {
|
||||
const year = new Date().getFullYear() - 13 - i
|
||||
return (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</Field>
|
||||
<Field
|
||||
|
||||
{/* <Field
|
||||
name="referredBy"
|
||||
label="Who were you referred by?"
|
||||
requiredFields={requiredFields}
|
||||
|
|
@ -87,7 +145,6 @@ export default function PersonalInfoForm({
|
|||
name="referredBy"
|
||||
id="referredBy"
|
||||
placeholder="Max"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
|
|
@ -111,67 +168,17 @@ export default function PersonalInfoForm({
|
|||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="contactOption"
|
||||
label="Preferred contact channel"
|
||||
description="So we know where to message you about your application!"
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Flex sx={{ gap: 4 }}>
|
||||
<Label
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
name="contactOption"
|
||||
value="Email"
|
||||
defaultChecked={true}
|
||||
onInput={() => setSelectedContactOption('Email')}
|
||||
/>
|
||||
Email
|
||||
</Label>
|
||||
<Label
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
name="contactOption"
|
||||
value="Slack"
|
||||
onInput={() => setSelectedContactOption('Slack')}
|
||||
/>
|
||||
Slack
|
||||
</Label>
|
||||
</Flex>
|
||||
{selectedContactOption === 'Slack' ? (
|
||||
<Field name="slackUsername" requiredFields={requiredFields}>
|
||||
<Input
|
||||
name="slackUsername"
|
||||
id="slackUsername"
|
||||
placeholder="Your name in the Hack Club Slack"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
</Field>
|
||||
) : selectedContactOption === 'Email' ? (
|
||||
<div>
|
||||
We'll use {email ?? 'whatever you put for your email above!'}
|
||||
</div>
|
||||
) : null}
|
||||
</Field>
|
||||
*/}
|
||||
<Field
|
||||
name="accommodations"
|
||||
label="Accessibility needs"
|
||||
description="Please specify any accommodations or accessibility needs you have so we can support you during onboarding and while using HCB"
|
||||
description="Please specify any accommodations, accessibility needs, or other important information so we can support you during onboarding and while using HCB."
|
||||
requiredFields={requiredFields}
|
||||
>
|
||||
<Input
|
||||
name="accommodations"
|
||||
id="accommodations"
|
||||
placeholder="I need a screen reader"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
placeholder="I use a screen reader/I need increased text size during onboarding"
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
65
components/fiscal-sponsorship/apply/submit.js
Normal file
65
components/fiscal-sponsorship/apply/submit.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
async function sendApplication() {
|
||||
// Get the form data from sessionStorage
|
||||
const data = {}
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i)
|
||||
if (key.startsWith('bank-signup-')) {
|
||||
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
|
||||
}
|
||||
}
|
||||
console.dir('Sending data:', data)
|
||||
|
||||
// Send the data
|
||||
try {
|
||||
return fetch('/api/fiscal-sponsorship/apply', {
|
||||
method: 'POST',
|
||||
cors: 'no-cors',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export function onSubmit({
|
||||
event,
|
||||
router,
|
||||
form,
|
||||
requiredFields,
|
||||
formError,
|
||||
setFormError,
|
||||
setIsSubmitting
|
||||
}) {
|
||||
event.preventDefault()
|
||||
/* Don't return from inside the loop since
|
||||
we want all input values to be saved every time */
|
||||
let wasError = false
|
||||
|
||||
const formData = new FormData(form.current)
|
||||
|
||||
// Save form data
|
||||
formData.forEach((value, key) => {
|
||||
sessionStorage.setItem('bank-signup-' + key, value)
|
||||
|
||||
// Check if there are empty required fields.
|
||||
if (
|
||||
((!value || value.trim() === '') && requiredFields.includes(key)) ||
|
||||
(formData.get('contactOption') === 'slack' &&
|
||||
(!formData.get('slackUsername') != null ||
|
||||
formData.get('slackUsername') === '')) // I'm so sorry for this
|
||||
) {
|
||||
setFormError('Please fill out all required fields.')
|
||||
wasError = true
|
||||
}
|
||||
})
|
||||
if (wasError) return
|
||||
|
||||
if (!formError) {
|
||||
setIsSubmitting(true)
|
||||
sendApplication().then(() => {
|
||||
router.push('/fiscal-sponsorship/apply/success')
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ export default function Watermark() {
|
|||
if (!shineRef.current || !svgRef.current) return
|
||||
|
||||
const svgWidth = svgRef.current.clientWidth / 100
|
||||
const svgFromTop = svgRef.current.getBoundingClientRect().top
|
||||
const svgFromLeft = svgRef.current.getBoundingClientRect().left
|
||||
// const svgFromTop = svgRef.current.getBoundingClientRect().top
|
||||
// const svgFromLeft = svgRef.current.getBoundingClientRect().left
|
||||
|
||||
shineRef.current.style.top = `${clientY / svgWidth + 6.2}px`
|
||||
shineRef.current.style.left = `${clientX / svgWidth + 9.2}px`
|
||||
|
|
@ -58,7 +58,7 @@ export default function Watermark() {
|
|||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#1d181f',
|
||||
backgroundColor: 'snow',
|
||||
clipPath: 'url(#my-clip-path)'
|
||||
}}
|
||||
>
|
||||
|
|
@ -69,7 +69,7 @@ export default function Watermark() {
|
|||
width: '2px',
|
||||
height: '2px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'red',
|
||||
backgroundColor: 'primary',
|
||||
filter: 'blur(2px)'
|
||||
}}
|
||||
/>
|
||||
36
components/fiscal-sponsorship/contact.js
Normal file
36
components/fiscal-sponsorship/contact.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Icon from '../icon'
|
||||
import { Flex, Link, Text } from 'theme-ui'
|
||||
|
||||
const phoneNumber = '+1 (844) 237-2290'
|
||||
const phoneNumberUri = '+1-844-237-2290'
|
||||
const email = 'hcb@hackclub.com'
|
||||
|
||||
export default function ContactBanner({ sx }) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
bg: 'sunken',
|
||||
color: 'slate',
|
||||
alignItems: 'center',
|
||||
p: 3,
|
||||
gap: [3, 2],
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
glyph="message"
|
||||
sx={{ color: 'inherit', flexShrink: 0, my: -1 }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Text
|
||||
sx={{
|
||||
textWrap: 'balance',
|
||||
a: { color: 'inherit', mx: '0.125em', whiteSpace: 'nowrap' }
|
||||
}}
|
||||
>
|
||||
Questions? Email <Link href={`mailto:${email}`}>{email}</Link>{' '}
|
||||
or call <Link href={`tel:${phoneNumberUri}`}>{phoneNumber}</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Card, Badge as ThemeBadge, Box, Heading, Text, Image } from 'theme-ui'
|
||||
import { Organization } from '../../../pages/hcb/climate'
|
||||
import Tilt from '../../../components/tilt'
|
||||
import { Organization } from '../../../pages/fiscal-sponsorship/climate'
|
||||
import Tilt from '../../tilt'
|
||||
import Icon from '@hackclub/icons'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
129
components/fiscal-sponsorship/features.js
Normal file
129
components/fiscal-sponsorship/features.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { Box, Heading, Link, Text, Container, Grid } from 'theme-ui'
|
||||
import Icon from '../icon'
|
||||
import { Balancer } from 'react-wrap-balancer'
|
||||
import Image from 'next/image'
|
||||
import imgLaptop from '../../public/fiscal-sponsorship/laptop.png'
|
||||
|
||||
export default function Features() {
|
||||
return (
|
||||
<Box sx={{ pt: 5, pb: [5, 6], bg: 'snow' }}>
|
||||
<Container>
|
||||
<Heading as="h2" variant="title" sx={{ mb: 3, maxWidth: 'copyUltra' }}>
|
||||
<Balancer>
|
||||
Powerful financial tools built by our nonprofit, for yours.
|
||||
</Balancer>
|
||||
</Heading>
|
||||
<Text as="p" variant="lead" sx={{ color: 'slate', maxWidth: '52ch' }}>
|
||||
Unlike other fiscal sponsors, we don’t license software from
|
||||
for-profit entities. Since day one, we’ve built beautiful, self-serve
|
||||
software to empower you to raise and spend money without
|
||||
administrative hassle.
|
||||
</Text>
|
||||
<Grid columns={[null, 2, 3]} sx={{ mt: 4, rowGap: 3, columnGap: 4 }}>
|
||||
<Module
|
||||
icon="bank-account"
|
||||
name="Receive foundation grants"
|
||||
body="with tax-deductible 501(c)(3) status."
|
||||
/>
|
||||
{/* Send money & reimburse via check, ACH, bank wire, PayPal, & more.
|
||||
Operate globally with a US Entity.
|
||||
Issue physical & virtual debit cards to your team.
|
||||
Get 24 hour support on weekdays.
|
||||
Pay team members with built-in payroll.
|
||||
Embed a custom donation form on your website.
|
||||
We file all your taxes automatically, including form 990. " */}
|
||||
<Module
|
||||
icon="card"
|
||||
name="Issue physical & virtual debit cards"
|
||||
body="with receipt tracking & Apple Pay."
|
||||
/>
|
||||
<Module
|
||||
icon="web"
|
||||
name="Operate globally"
|
||||
body="with a U.S. legal entity."
|
||||
/>
|
||||
<Module
|
||||
icon="payment-transfer"
|
||||
name="Send money & reimburse"
|
||||
body="via check, ACH, bank wire, PayPal, & more."
|
||||
/>
|
||||
<Module
|
||||
icon="explore"
|
||||
name="Make your finances transparent"
|
||||
body="to your team and optionally, public."
|
||||
/>
|
||||
<Module
|
||||
icon="docs"
|
||||
name="We file all your taxes"
|
||||
body="automatically, including form 990."
|
||||
/>
|
||||
<Module
|
||||
icon="admin"
|
||||
name="Pay team members"
|
||||
body="with built-in payroll."
|
||||
/>
|
||||
<Module
|
||||
icon="support"
|
||||
name="Accept donations of any size"
|
||||
body="with a custom, embeddable online form."
|
||||
/>
|
||||
<Module
|
||||
icon="leader"
|
||||
name="Get 24-hour support"
|
||||
body="on weekdays with a dedicated point of contact."
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
<Container variant="copy" sx={{ mt: [4, 5] }}>
|
||||
<Laptop
|
||||
href="https://hcb.hackclub.com/reboot"
|
||||
title="See Reboot’s finances in public"
|
||||
/>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Module({ icon, name, body }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'start' }}>
|
||||
<Icon
|
||||
size={48}
|
||||
glyph={icon}
|
||||
sx={{ flexShrink: 0, marginRight: 3, color: 'primary' }}
|
||||
/>
|
||||
<Text
|
||||
as="p"
|
||||
sx={{
|
||||
color: 'slate',
|
||||
lineHeight: '1.375',
|
||||
fontSize: 20,
|
||||
m: 0
|
||||
}}
|
||||
>
|
||||
<Balancer>
|
||||
<Text as="strong" color="slate">
|
||||
{name}
|
||||
</Text>{' '}
|
||||
{body}
|
||||
</Balancer>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Laptop({ href, title, sx }) {
|
||||
return (
|
||||
<Link href={href} title={title} sx={{ textAlign: 'center' }}>
|
||||
<Image
|
||||
src={imgLaptop}
|
||||
alt="Laptop"
|
||||
style={{ width: '100%', height: 'auto' }}
|
||||
unoptimized
|
||||
/>
|
||||
<Text variant="caption" as="p" sx={{ color: 'primary', mt: 2 }}>
|
||||
See <i>Reboot</i>’s finances in Transparency Mode
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
32
components/fiscal-sponsorship/first/apply-button.js
Normal file
32
components/fiscal-sponsorship/first/apply-button.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Button, Text, Image, Flex } from 'theme-ui'
|
||||
import Icon from '../../icon'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ApplyButton() {
|
||||
return (
|
||||
<Link href="/fiscal-sponsorship/apply" passHref legacyBehavior>
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
as="a"
|
||||
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>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ export default function Features() {
|
|||
<Box sx={{ py: 5 }}>
|
||||
<Box as="a" href="#testimonials">
|
||||
<Image
|
||||
src="/hcb/meet-teams-using-hcb.svg"
|
||||
src="/fiscal-sponsorship/meet-teams-using-hcb.svg"
|
||||
alt="yeah"
|
||||
width={200}
|
||||
height={100}
|
||||
|
|
@ -65,7 +65,7 @@ export default function Features() {
|
|||
target="_blank"
|
||||
>
|
||||
<NextImage
|
||||
src="/hcb/poseidon-dashboard.png"
|
||||
src="/fiscal-sponsorship/poseidon-dashboard.png"
|
||||
alt="iPad"
|
||||
width={500}
|
||||
height={300}
|
||||
|
|
@ -245,11 +245,11 @@ export default function Features() {
|
|||
</Flex>
|
||||
</Card>
|
||||
</Tilt> */}
|
||||
<Module
|
||||
{/* <Module
|
||||
icon="rep"
|
||||
name="No start-up costs"
|
||||
body="All fees waived on your first $25k until September 1st, 2023. Then, just 7% of revenue (as compared to 10-14% charged by other fiscal sponsors). "
|
||||
/>
|
||||
/> */}
|
||||
</Masonry>
|
||||
</Container>
|
||||
<Container
|
||||
|
|
@ -103,7 +103,7 @@ export default function Signup() {
|
|||
} else {
|
||||
setEventName('')
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
}, 200)
|
||||
)
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ export default function Signup() {
|
|||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
await fetch('/api/hcb/demo', {
|
||||
await fetch('/api/fiscal-sponsorship/demo', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
eventName,
|
||||
|
|
@ -144,7 +144,7 @@ export default function Signup() {
|
|||
<Base
|
||||
id="form"
|
||||
method="POST"
|
||||
action="/api/hcb/demo"
|
||||
action="/api/fiscal-sponsorship/demo"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Grid sx={{ gridTemplateColumns: '1fr 2fr', alignItems: 'center' }}>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Box, Link, Text, Heading, Flex } from 'theme-ui'
|
||||
import Timeline from './timeline'
|
||||
import Stats from './stats'
|
||||
import ApplyButton from './apply-button'
|
||||
|
||||
|
|
@ -26,7 +25,6 @@ export default function Start({ stats }) {
|
|||
</Text>
|
||||
</Flex>
|
||||
<Stats stats={stats} />
|
||||
<Timeline />
|
||||
<Flex
|
||||
sx={{ flexDirection: 'column', textAlign: 'center', gap: 4, mx: 3 }}
|
||||
>
|
||||
|
|
@ -5,10 +5,10 @@ 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
|
||||
? 1
|
||||
: x < 0.5
|
||||
? Math.pow(2, 20 * x - 10) / 2
|
||||
: (2 - Math.pow(2, -20 * x + 10)) / 2
|
||||
|
||||
function startMoneyAnimation(
|
||||
setBalance,
|
||||
|
|
@ -19,8 +19,6 @@ const Base = styled(Box)`
|
|||
}
|
||||
`
|
||||
|
||||
|
||||
|
||||
const Logo = props => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -51,7 +49,12 @@ const Service = ({ href, icon, name = '', ...props }) => (
|
|||
</Link>
|
||||
)
|
||||
|
||||
const Footer = ({ dark = false, children, ...props }) => (
|
||||
const Footer = ({
|
||||
dark = false,
|
||||
email = 'team@hackclub.com',
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<Base
|
||||
color={dark ? 'muted' : 'slate'}
|
||||
py={[4, 5]}
|
||||
|
|
@ -173,7 +176,7 @@ const Footer = ({ dark = false, children, ...props }) => (
|
|||
icon="instagram"
|
||||
name="Instagram"
|
||||
/>
|
||||
<Service href="mailto:team@hackclub.com" icon="email-fill" />
|
||||
<Service href={`mailto:${email}`} icon="email-fill" />
|
||||
</Grid>
|
||||
<Text my={2}>
|
||||
<Link href="tel:1-855-625-HACK">1-855-625-HACK</Link>
|
||||
|
|
|
|||
|
|
@ -29,11 +29,7 @@ const Content = () => (
|
|||
<ListItem
|
||||
knew
|
||||
icon="payment"
|
||||
leadText={
|
||||
<>
|
||||
$500 grants.
|
||||
</>
|
||||
}
|
||||
leadText={<>$500 grants.</>}
|
||||
body={
|
||||
<>
|
||||
Running on HCB? Get a $500 grant once you have a venue, provided
|
||||
|
|
@ -60,7 +56,7 @@ const Content = () => (
|
|||
debit cards, a domain name, stickers, and more.`}
|
||||
/>
|
||||
</List>
|
||||
<NextLink href="/hcb" passHref>
|
||||
<NextLink href="/fiscal-sponsorship" passHref>
|
||||
<Button as="a" variant="outlineLg" sx={{ width: [null, null, 500] }}>
|
||||
Apply
|
||||
<Box as="span" sx={{ display: ['none', 'inline', ''] }}>
|
||||
|
|
@ -157,7 +153,7 @@ const Static = () => (
|
|||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
backgroundImage: `url('/hcb/bg.webp')`,
|
||||
backgroundImage: `url('/fiscal-sponsorship/bg.webp')`,
|
||||
backgroundSize: 'cover'
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ export default function Recap() {
|
|||
name="$500 grants"
|
||||
desc={
|
||||
<>
|
||||
Join HCB to receive a $500 grant for your hackathon and a suite of financial tools.
|
||||
Join HCB to receive a $500 grant for your hackathon and a suite
|
||||
of financial tools.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import { Button, Text, Image, Flex } from 'theme-ui'
|
||||
import Icon from '../icon'
|
||||
|
||||
export default function ApplyButton() {
|
||||
return (
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
as="a"
|
||||
href="/hcb/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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
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, search } from '../../../lib/hcb/apply/address-validation'
|
||||
import Icon from '../../icon'
|
||||
|
||||
const approvedCountries = [
|
||||
'AT',
|
||||
'FI',
|
||||
'FR',
|
||||
'DE',
|
||||
'GR',
|
||||
'ES',
|
||||
'IT',
|
||||
'SE',
|
||||
'TR',
|
||||
'GB',
|
||||
'NO',
|
||||
'UA',
|
||||
'BR',
|
||||
'CO',
|
||||
'US',
|
||||
'CA',
|
||||
'MX',
|
||||
'JP',
|
||||
'PH',
|
||||
'MY',
|
||||
'SG'
|
||||
]
|
||||
|
||||
export default function AutoComplete({ name, isPersonalAddressInput }) {
|
||||
const input = useRef()
|
||||
const base = useRef()
|
||||
const [predictions, setPredictions] = useState(null)
|
||||
const [countryCode, setCountryCode] = useState(null)
|
||||
|
||||
const optionClicked = async prediction => {
|
||||
input.current.value = prediction.name
|
||||
await onInput(prediction.name)
|
||||
setPredictions(null)
|
||||
}
|
||||
const clickOutside = e => {
|
||||
if (input.current && !input.current.contains(e.target)) {
|
||||
setPredictions(null)
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = async value => {
|
||||
setPredictions(value ? (await search(value)).results : null)
|
||||
|
||||
if (isPersonalAddressInput) return
|
||||
geocode(value)
|
||||
.then(res => {
|
||||
const country = res?.results[0]?.country
|
||||
const countryCode = res?.results[0]?.countryCode
|
||||
|
||||
setCountryCode(countryCode)
|
||||
|
||||
sessionStorage.setItem('bank-signup-eventCountry', country)
|
||||
sessionStorage.setItem('bank-signup-eventCountryCode', countryCode)
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
const onInputWrapper = async e => {
|
||||
if (e.target.value) await onInput(e.target.value)
|
||||
}
|
||||
|
||||
//TODO: Close suggestions view when focus is lost via tabbing.
|
||||
//TODO: Navigate suggestions with arrow keys.
|
||||
|
||||
useEffect(() => {
|
||||
const inputEl = input.current
|
||||
if (!inputEl) return
|
||||
|
||||
document.addEventListener('click', clickOutside)
|
||||
inputEl.addEventListener('input', onInputWrapper)
|
||||
inputEl.addEventListener('focus', onInputWrapper)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', clickOutside)
|
||||
inputEl.removeEventListener('input', onInputWrapper)
|
||||
inputEl.removeEventListener('focus', onInputWrapper)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative', width: '100%' }}>
|
||||
<FlexCol flexDirection="column" position="relative" width="100%" gap="2">
|
||||
<Input
|
||||
ref={input}
|
||||
name={name}
|
||||
id={name}
|
||||
placeholder="Shelburne, VT"
|
||||
autoComplete="off"
|
||||
sx={{ ...AutofillColourFix }}
|
||||
/>
|
||||
<Box>
|
||||
{/* {String(countryCode)} */}
|
||||
{countryCode && !approvedCountries.includes(countryCode) && (
|
||||
<Flex sx={{ alignItems: 'center' }}>
|
||||
<Icon
|
||||
glyph="sad"
|
||||
size="2.5rem"
|
||||
sx={{ color: 'red', mr: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Text
|
||||
as="label"
|
||||
htmlFor={name}
|
||||
sx={{
|
||||
color: 'red'
|
||||
// fontWeight: 'medium',
|
||||
}}
|
||||
>
|
||||
Currently, we only have first-class support for organizations in
|
||||
select countries.
|
||||
<br />
|
||||
If you're somewhere else, you can still use bank!
|
||||
<br />
|
||||
Please contact us at hcb@hackclub.com
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</FlexCol>
|
||||
{predictions && predictions.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
background: '#47454f',
|
||||
border: '1px solid #696675',
|
||||
width: '100%',
|
||||
p: 3,
|
||||
borderRadius: '4px',
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 0.5em)'
|
||||
}}
|
||||
>
|
||||
<FlexCol gap={1}>
|
||||
{predictions.map((prediction, idx) => (
|
||||
<>
|
||||
<Text
|
||||
as="button"
|
||||
onClick={() => optionClicked(prediction)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: '#d1cbe7',
|
||||
'&:hover': {
|
||||
color: 'white'
|
||||
},
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
textAlign: 'inherit'
|
||||
}}
|
||||
key={idx}
|
||||
>
|
||||
{prediction.name}
|
||||
</Text>
|
||||
|
||||
{idx < predictions.length - 1 && (
|
||||
<hr
|
||||
style={{
|
||||
width: '100%',
|
||||
color: '#8492a6'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</FlexCol>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { Box, Button, Flex, Text } from 'theme-ui'
|
||||
import Icon from '../../icon'
|
||||
|
||||
export default function AlertModal({ formError, setFormError }) {
|
||||
if (!formError) return null
|
||||
|
||||
const close = () => setFormError(null)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
onClick={close}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: '#000000',
|
||||
opacity: 0.5,
|
||||
zIndex: 1000
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
background: '#252429',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 1001,
|
||||
padding: 4,
|
||||
borderRadius: 'default'
|
||||
}}
|
||||
>
|
||||
<Text variant="title">Oops!</Text>
|
||||
<Text variant="lead">{formError}</Text>
|
||||
<Button onClick={close}>Dismiss</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
//TODO: Move to main theme
|
||||
|
||||
const autofillColourFix = {
|
||||
'&:-webkit-autofill': {
|
||||
boxShadow: '0 0 0 100px #252429 inset !important',
|
||||
WebkitTextFillColor: 'white'
|
||||
}
|
||||
}
|
||||
|
||||
export default autofillColourFix
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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
|
||||
}
|
||||
}, [name])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { forwardRef } from 'react'
|
||||
import { Box } from 'theme-ui'
|
||||
|
||||
const formContainer = 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>
|
||||
)
|
||||
})
|
||||
|
||||
// https://stackoverflow.com/a/67993106/10652680
|
||||
formContainer.displayName = 'formContainer'
|
||||
export default formContainer
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { Box, Flex, Link, Text } from 'theme-ui'
|
||||
import Icon from '../../icon'
|
||||
import FlexCol from '../../flex-col'
|
||||
|
||||
export default function HCBInfo() {
|
||||
return (
|
||||
<Box>
|
||||
<FlexCol gap={4}>
|
||||
<FlexCol gap={4}>
|
||||
<Text sx={{ fontSize: 36 }}>
|
||||
What HCB <i>is</i>
|
||||
</Text>
|
||||
<FlexCol gap={3} ml={3}>
|
||||
<FlexCol gap={2}>
|
||||
<Flex sx={{ alignItems: 'center', gap: 2 }}>
|
||||
<Link
|
||||
color="white"
|
||||
href="/hcb/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 HCB <i>is not</i>
|
||||
</Text>
|
||||
<FlexCol gap={3} ml={3}>
|
||||
<FlexCol gap={2}>
|
||||
<Text sx={{ fontSize: 3 }}>
|
||||
A bank!{' '}
|
||||
<Text sx={{ color: 'muted', fontSize: 2 }}>(we're better)</Text>
|
||||
</Text>
|
||||
<Text sx={{ color: 'muted' }}>
|
||||
<ul>
|
||||
<li>
|
||||
Rather than setting up a standard bank account, you'll get a
|
||||
restricted fund within Hack Club accounts.
|
||||
</li>
|
||||
<li>You can't deposit or withdraw cash. But you can receive any kind of electronic payment!</li>
|
||||
</ul>
|
||||
</Text>
|
||||
</FlexCol>
|
||||
<FlexCol gap={2}>
|
||||
<Text sx={{ fontSize: 3 }}>For-profit</Text>
|
||||
<Text sx={{ color: 'muted' }}>
|
||||
<ul>
|
||||
<li>
|
||||
If you’re a for-profit entity, then HCB is not for you.
|
||||
Consider setting up a business.
|
||||
</li>
|
||||
</ul>
|
||||
</Text>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</FlexCol>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Flex, Text, Spinner } from 'theme-ui'
|
||||
|
||||
async function sendApplication() {
|
||||
// Get the form data from sessionStorage
|
||||
const data = {}
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i)
|
||||
if (key.startsWith('bank-signup-')) {
|
||||
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
|
||||
}
|
||||
}
|
||||
console.dir('Sending data:', data)
|
||||
|
||||
// Send the data
|
||||
try {
|
||||
const res = await fetch('/api/hcb/apply', {
|
||||
method: 'POST',
|
||||
cors: 'no-cors',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function NavIcon({ isBack }) {
|
||||
const style = {
|
||||
height: '1em',
|
||||
fill: 'white',
|
||||
margin: 0,
|
||||
flexShrink: 0
|
||||
}
|
||||
|
||||
return isBack ? (
|
||||
<svg style={style} viewBox="10.73 7.72 9.27 16.53">
|
||||
<g>
|
||||
<path d="M19.768,23.89c0.354,-0.424 0.296,-1.055 -0.128,-1.408c-1.645,-1.377 -5.465,-4.762 -6.774,-6.482c1.331,-1.749 5.1,-5.085 6.774,-6.482c0.424,-0.353 0.482,-0.984 0.128,-1.408c-0.353,-0.425 -0.984,-0.482 -1.409,-0.128c-1.839,1.532 -5.799,4.993 -7.2,6.964c-0.219,0.312 -0.409,0.664 -0.409,1.054c0,0.39 0.19,0.742 0.409,1.053c1.373,1.932 5.399,5.462 7.2,6.964l0.001,0.001c0.424,0.354 1.055,0.296 1.408,-0.128Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
) : (
|
||||
<svg style={style} viewBox="12.75 7.72 9.25 16.53">
|
||||
<g>
|
||||
<path d="M12.982,23.89c-0.354,-0.424 -0.296,-1.055 0.128,-1.408c1.645,-1.377 5.465,-4.762 6.774,-6.482c-1.331,-1.749 -5.1,-5.085 -6.774,-6.482c-0.424,-0.353 -0.482,-0.984 -0.128,-1.408c0.353,-0.425 0.984,-0.482 1.409,-0.128c1.839,1.532 5.799,4.993 7.2,6.964c0.219,0.312 0.409,0.664 0.409,1.054c0,0.39 -0.19,0.742 -0.409,1.053c-1.373,1.932 -5.399,5.462 -7.2,6.964l-0.001,0.001c-0.424,0.354 -1.055,0.296 -1.408,-0.128Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default function NavButton({
|
||||
isBack,
|
||||
form,
|
||||
clickHandler,
|
||||
requiredFields,
|
||||
setFormError
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [spinner, setSpinner] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setSpinner(false)
|
||||
}, [router.query.step])
|
||||
|
||||
const minStep = 1
|
||||
const maxStep = 3
|
||||
|
||||
const click = async () => {
|
||||
setSpinner(true)
|
||||
|
||||
let step = parseInt(router.query.step)
|
||||
|
||||
async function setStep(s) {
|
||||
await router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, step: s }
|
||||
},
|
||||
undefined,
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
if (!step) {
|
||||
// Set the step query param to minStep if it's not there.
|
||||
await setStep(minStep)
|
||||
} else if (step === minStep && isBack) {
|
||||
await router.push('/hcb')
|
||||
return
|
||||
} else if (step < minStep) {
|
||||
// Set the step query param to minStep if it's lower than that.
|
||||
await setStep(minStep)
|
||||
}
|
||||
|
||||
/* Don't return from inside the loop since
|
||||
we want all input values to be saved every time */
|
||||
let wasError = false
|
||||
|
||||
const formData = new FormData(form.current)
|
||||
|
||||
// Save form data
|
||||
formData.forEach((value, key) => {
|
||||
sessionStorage.setItem('bank-signup-' + key, value)
|
||||
|
||||
// Check if there are empty required fields.
|
||||
if (
|
||||
(!isBack &&
|
||||
(!value || value.trim() === '') &&
|
||||
requiredFields[step - 1].includes(key)) ||
|
||||
(!isBack &&
|
||||
formData.get('contactOption') === 'slack' &&
|
||||
!formData.get('slackUsername')) // I'm so sorry for this
|
||||
) {
|
||||
setFormError('Please fill all required fields')
|
||||
wasError = true
|
||||
setSpinner(false)
|
||||
}
|
||||
})
|
||||
if (wasError) return
|
||||
|
||||
// Run the parent's click handler for this button.
|
||||
if (clickHandler) await clickHandler()
|
||||
|
||||
if (step >= maxStep && !isBack) {
|
||||
await sendApplication()
|
||||
await router.push('/hcb/apply/success')
|
||||
return
|
||||
} else {
|
||||
step += isBack ? -1 : 1
|
||||
}
|
||||
await setStep(step)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={isBack ? 'outline' : 'ctaLg'}
|
||||
sx={{
|
||||
color: 'white',
|
||||
width: '100%',
|
||||
maxWidth: isBack ? '8rem' : '10rem',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={click}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: isBack ? 'row' : 'row-reverse',
|
||||
justifyContent: 'center',
|
||||
placeItems: 'center',
|
||||
fontSize: isBack ? 2 : 4,
|
||||
gap: [2, null, null, 3]
|
||||
}}
|
||||
>
|
||||
<NavIcon isBack={isBack} />
|
||||
<Text
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{isBack ? 'Back' : 'Next'}
|
||||
</Text>
|
||||
</Flex>
|
||||
{!isBack && spinner && (
|
||||
<Spinner
|
||||
sx={{
|
||||
height: '32px',
|
||||
color: 'white',
|
||||
position: 'absolute',
|
||||
right: '-0.3rem',
|
||||
margin: '0 !important'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { Box, Flex, Text } from 'theme-ui'
|
||||
import FlexCol from '../../flex-col'
|
||||
|
||||
function StepIcon({ completed, number }) {
|
||||
let strokeColour = completed ? '#33d6a6' : '#8492a6'
|
||||
let fillColour = completed ? '#33d6a6' : 'none'
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<svg
|
||||
style={{ translate: '0 1px' }}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20 38C36.5 38 38 36.5 38 20C38 3.5 36.5 2 20 2C3.5 2 2 3.5 2 20C2 36.5 3.5 38 20 38Z"
|
||||
stroke={strokeColour}
|
||||
fill={fillColour}
|
||||
stroke-width="3"
|
||||
/>
|
||||
</svg>
|
||||
<Flex
|
||||
sx={{
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
inset: '0'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
sx={{
|
||||
color: 'white',
|
||||
fontSize: 2
|
||||
}}
|
||||
>
|
||||
{number}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Step({ number, label, completed }) {
|
||||
return (
|
||||
<Flex sx={{ lineHeight: '1', alignItems: 'center', gap: '4' }}>
|
||||
<StepIcon completed={completed} number={number + 1} />
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: '3',
|
||||
display: ['none', null, null, 'block']
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Progress() {
|
||||
const router = useRouter()
|
||||
const step = parseInt(router.query.step)
|
||||
|
||||
const labels = ['Intro', 'Organization info', 'Personal info']
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
gap: 3,
|
||||
translate: [0, null, null, '-1rem 0'],
|
||||
flexDirection: ['row', null, null, 'column']
|
||||
}}
|
||||
>
|
||||
{labels.map((label, i) => (
|
||||
<Step number={i} label={label} completed={step > i} key={i} />
|
||||
))}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
import { Box, Container, Heading, Text, Link, Grid } from 'theme-ui'
|
||||
import Run from './run'
|
||||
import { Fade } from 'react-reveal'
|
||||
import Icon from '../icon'
|
||||
|
||||
export default function Everything({ fee, partner = false }) {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 6,
|
||||
pb: [3, 6],
|
||||
marginTop: 6,
|
||||
bg: 'darker'
|
||||
}}
|
||||
>
|
||||
<Container mb={[4, 5]} px={3} sx={{ textAlign: 'center' }}>
|
||||
<Heading variant="ultratitle" sx={{ color: 'smoke' }}>
|
||||
Everything you’ll need.
|
||||
</Heading>
|
||||
</Container>
|
||||
<Container px={[3, null, 5]}>
|
||||
<List>
|
||||
{Object.entries({
|
||||
'Legal entity with 501(c)(3) status': 'briefcase',
|
||||
'We do your taxes': 'checkmark',
|
||||
'Share access with your whole team': 'member-add',
|
||||
'Backed by a bank account under the hood': 'bank-account',
|
||||
'Instant invoice sending': 'transactions',
|
||||
'Real-time dashboard of finances': 'analytics',
|
||||
'Transaction data export': 'download',
|
||||
'Record shared notes on transactions': 'docs',
|
||||
'24-hour response support': 'clock',
|
||||
'Reimbursement process': 'enter'
|
||||
// 'Instant deposits': 'bolt'
|
||||
}).map(([item, icon = 'enter']) => (
|
||||
<ListItem key={item} icon={icon}>
|
||||
{item}
|
||||
</ListItem>
|
||||
))}
|
||||
{Object.entries({
|
||||
'Physical check sending & voiding': '',
|
||||
'Online direct deposit & ACH transfers': '',
|
||||
'Generate attendee legal waivers': '',
|
||||
'Virtual debit cards (with Apple & Google Pay)': '',
|
||||
'Debit card transaction paper trail': '',
|
||||
'Transparency Mode': ''
|
||||
}).map(([item, date]) => (
|
||||
<ListItem
|
||||
key={item}
|
||||
icon={
|
||||
item.includes('signup')
|
||||
? 'bolt'
|
||||
: item.includes('card')
|
||||
? 'card'
|
||||
: item.includes('Transparency')
|
||||
? 'explore'
|
||||
: item.includes('Physical')
|
||||
? 'email'
|
||||
: 'enter'
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</ListItem>
|
||||
))}
|
||||
{!partner
|
||||
? Object.entries({
|
||||
'Online, embeddable donation form': ''
|
||||
}).map(([item, date]) => (
|
||||
<ListItem
|
||||
key={item}
|
||||
icon={
|
||||
item.startsWith('Instant')
|
||||
? 'bolt'
|
||||
: item.includes('form')
|
||||
? 'link'
|
||||
: 'enter'
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</ListItem>
|
||||
))
|
||||
: ''}
|
||||
</List>
|
||||
</Container>
|
||||
<Run />
|
||||
<Container px={3} mt={4}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontSize: 32, mr: 2 }}>You pay just</Text>
|
||||
<Percentage fee={fee} />
|
||||
<Text sx={{ fontSize: 32, ml: 2 }}>
|
||||
of revenue. No upfront costs.
|
||||
</Text>
|
||||
</Box>
|
||||
<Container
|
||||
variant="copy"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
lineHeight: '1.25',
|
||||
letterSpacing: '-.03ch',
|
||||
marginTop: 4,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<Container variant="narrow">
|
||||
<Text sx={{ color: 'muted', lineHeight: 1.375 }}>
|
||||
HCB is a{' '}
|
||||
<Link
|
||||
color="primary"
|
||||
href="https://en.wikipedia.org/wiki/Fiscal_sponsorship"
|
||||
hoverline
|
||||
>
|
||||
fiscal sponsor
|
||||
</Link>
|
||||
. Other fiscal sponsors' fees typically vary between 7-14%
|
||||
of revenue. Hack Club is a 501(c)(3) nonprofit.
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function List({ children }) {
|
||||
return (
|
||||
<Container>
|
||||
<ol
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
listStyle: 'none'
|
||||
}}
|
||||
>
|
||||
<Grid gap={2} columns={[1, 1, '1fr 1fr']}>
|
||||
{children}
|
||||
</Grid>
|
||||
</ol>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function ListItem({ icon = 'enter', start, ...props }) {
|
||||
return (
|
||||
<Fade left>
|
||||
<li
|
||||
style={{
|
||||
lineHeight: 1.25,
|
||||
breakInside: 'avoid',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 4,
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
{start || (
|
||||
<Icon
|
||||
glyph={icon}
|
||||
sx={{ color: 'muted', marginRight: 2 }}
|
||||
size={32}
|
||||
mr={2}
|
||||
/>
|
||||
)}
|
||||
<Text sx={{ fontSize: 24, color: 'smoke' }} {...props} />
|
||||
</li>
|
||||
</Fade>
|
||||
)
|
||||
}
|
||||
|
||||
function Percentage({ fee }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
bg: 'slate',
|
||||
color: 'green',
|
||||
width: [fee.length === 1 ? 70 : 80, fee.length === 1 ? 128 : 138],
|
||||
height: [fee.length === 1 ? 70 : 80, fee.length === 1 ? 128 : 138],
|
||||
borderRadius: 'circle',
|
||||
fontWeight: 'bold',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(255, 255, 255, 0.125)',
|
||||
fontSize: [48, 84],
|
||||
'&:after': {
|
||||
content: '"%"',
|
||||
fontSize: [24, 40],
|
||||
fontWeight: 'normal',
|
||||
marginRight: fee.length === 1 ? -2 : 0,
|
||||
marginLeft: [null, fee.length === 1 ? 2 : 0],
|
||||
color: 'muted'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{fee}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const recent = dt => {
|
||||
const past = new Date()
|
||||
past.setMonth(past.getMonth() - 2)
|
||||
return new Date(dt) > past
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import { Box, Heading, Link, Text, Container, Grid } from 'theme-ui'
|
||||
import Icon from '../icon'
|
||||
|
||||
export default function Features({ partner = false }) {
|
||||
return (
|
||||
<Box sx={{ py: 6 }}>
|
||||
<Container>
|
||||
<Text variant="heading" sx={{ fontSize: 50 }}>
|
||||
A full-stack toolkit for organizing anything.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Text sx={{ color: 'muted', maxWidth: '48', fontSize: 28 }}>
|
||||
Invoice sponsors, issue debit cards to your team, and view transaction
|
||||
history.
|
||||
<br />
|
||||
Ongoing support so you can focus on organizing, not the paperwork.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
</Container>
|
||||
<Container>
|
||||
<Grid gap={4} columns={[1, null, 3]}>
|
||||
<Box>
|
||||
<Module
|
||||
icon="bank-account"
|
||||
name="Fund"
|
||||
body={
|
||||
<>A fund under the hood with a custom, beautiful dashboard.</>
|
||||
}
|
||||
/>
|
||||
<ModuleDetails>
|
||||
<Document
|
||||
name="501(c)(3) nonprofit status"
|
||||
cost="Become part of Hack Club's legal entity, getting the benefits of our tax status."
|
||||
/>
|
||||
<Document
|
||||
name="Tax filings (990, end-of-year)"
|
||||
cost="We handle all filings with the IRS, so you can focus on your event, not hiring CPAs."
|
||||
/>
|
||||
</ModuleDetails>
|
||||
</Box>
|
||||
<Laptop
|
||||
href="https://hcb.hackclub.com/the-innovation-circuit"
|
||||
title="See The Innovation Circuit’s finances in public"
|
||||
sx={{
|
||||
gridColumn: [null, null, 'span 2'],
|
||||
gridRow: [null, null, 'span 2']
|
||||
}}
|
||||
/>
|
||||
<Module
|
||||
icon="card"
|
||||
name="Debit cards"
|
||||
body={
|
||||
<>
|
||||
Issue physical debit cards to all your teammates, backed by{' '}
|
||||
<Link
|
||||
href="https://stripe.com/issuing"
|
||||
color="smoke"
|
||||
hoverline
|
||||
target="_blank"
|
||||
>
|
||||
Stripe
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Module
|
||||
icon="analytics"
|
||||
name="Balance & history"
|
||||
body="Check real-time account balance + transaction history online anytime."
|
||||
/>
|
||||
{/* <Module
|
||||
icon="bolt"
|
||||
name="Instant deposits"
|
||||
body="Receive donations and invoice payments instantly once they're paid."
|
||||
/> */}
|
||||
{/* <Module
|
||||
icon="payment"
|
||||
name="Built-in invoicing"
|
||||
body={
|
||||
<>
|
||||
Accept sponsor payments with instant invoicing, powered by{' '}
|
||||
<Link
|
||||
href="https://stripe.com/invoicing"
|
||||
color="smoke"
|
||||
hoverline
|
||||
target="_blank"
|
||||
>
|
||||
Stripe
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/> */}
|
||||
<Module
|
||||
icon="docs"
|
||||
name="Pre-written forms"
|
||||
body="Download liability + photo forms custom written by expert lawyers."
|
||||
/>
|
||||
<Module
|
||||
icon="payment-transfer"
|
||||
name="Transfer money"
|
||||
body="Flexible money transfer options including ACH, check, and PayPal."
|
||||
/>
|
||||
<Module
|
||||
icon="explore"
|
||||
name="Transparency Mode"
|
||||
body="If you’d like, show your finances on public pages for full transparency."
|
||||
/>
|
||||
<Module
|
||||
icon="email"
|
||||
name="Postal"
|
||||
body={
|
||||
<>
|
||||
Send email newsletters for free using our hosted instance of{' '}
|
||||
<Link
|
||||
href="https://sendy.co/"
|
||||
color="smoke"
|
||||
hoverline
|
||||
target="_blank"
|
||||
>
|
||||
Sendy
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!partner && (
|
||||
<Module
|
||||
icon="friend"
|
||||
name="Donation Page"
|
||||
body="Receive donations through a custom, online embeddable form."
|
||||
/>
|
||||
)}
|
||||
<Module
|
||||
icon="flag"
|
||||
name="PVSA Awards"
|
||||
body={
|
||||
<>
|
||||
Issue the{' '}
|
||||
<Link
|
||||
href="https://presidentialserviceawards.gov"
|
||||
color="smoke"
|
||||
hoverline
|
||||
target="_blank"
|
||||
>
|
||||
President's Volunteer Service Award
|
||||
</Link>{' '}
|
||||
to your volunteers.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!partner && (
|
||||
<Module
|
||||
icon="web"
|
||||
name="Free Domains"
|
||||
body="We'll pay up to $20 for your organization's domain name for a year."
|
||||
/>
|
||||
)}
|
||||
|
||||
<Module
|
||||
icon="support"
|
||||
name="Support anytime"
|
||||
body="We’ll never leave you hanging with 24hr response time on weekdays."
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
<Container
|
||||
variant="copy"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="lead"
|
||||
sx={{
|
||||
color: 'muted',
|
||||
fontSize: 3,
|
||||
marginTop: [4, 5]
|
||||
}}
|
||||
>
|
||||
Have more questions? <br /> Check out the{' '}
|
||||
<Link
|
||||
href="https://hcb.hackclub.com/faq"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
hoverline
|
||||
>
|
||||
HCB FAQ
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Module({ icon, name, body }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'start' }}>
|
||||
<Icon
|
||||
size={48}
|
||||
glyph={icon}
|
||||
sx={{ flexShrink: 0, marginRight: 3, color: 'primary' }}
|
||||
/>
|
||||
<Box>
|
||||
<Heading sx={{ color: 'snow', lineHeight: '1.5' }}>{name}</Heading>
|
||||
<Text sx={{ color: 'muted', lineHeight: '1.375', fontSize: 17 }}>
|
||||
{body}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function ModuleDetails({ children }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bg: '#252429',
|
||||
color: 'smoke',
|
||||
mt: 4,
|
||||
ml: 0,
|
||||
py: 3,
|
||||
px: 2,
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.0625)',
|
||||
borderRadius: 'default'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Document({ name, cost }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Icon
|
||||
size={28}
|
||||
mr={1}
|
||||
glyph="payment"
|
||||
sx={{ flexShrink: 0, color: 'green' }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Text fontSize={2}>{name}</Text>
|
||||
|
||||
{cost && (
|
||||
<Text fontSize={1} color="muted" style={{ lineHeight: '1.375' }}>
|
||||
{cost}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Laptop({ href, title, sx }) {
|
||||
return (
|
||||
<Link href={href} title={title} sx={sx}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '16rem',
|
||||
backgroundSize: 'auto 115%',
|
||||
backgroundImage: "url('/hcb/laptop-dark.png')",
|
||||
backgroundPosition: 'center top',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
></Box>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Heading,
|
||||
Link,
|
||||
Flex,
|
||||
Text,
|
||||
Container,
|
||||
Badge
|
||||
} from 'theme-ui'
|
||||
import Fade from 'react-reveal/Fade'
|
||||
import ScrollHint from '../scroll-hint'
|
||||
import Image from 'next/image'
|
||||
import hero from '../../public/hcb/bg.webp'
|
||||
|
||||
export default function Landing({ showButton = true, eventsCount }) {
|
||||
return (
|
||||
<>
|
||||
<Slide>
|
||||
<Vignette />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
bottom: 5,
|
||||
mx: 'auto',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
zIndex: '100',
|
||||
paddingTop: '96px'
|
||||
}}
|
||||
>
|
||||
<Fade duration={625} bottom>
|
||||
<Container
|
||||
variant="container"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
variant="ultratitle"
|
||||
sx={{
|
||||
marginBottom: 4,
|
||||
textShadow: '0 0 16px rgba(0, 0, 0, 1)',
|
||||
letterSpacing: '-0.02em',
|
||||
'@media screen and (max-height: 600px)': {
|
||||
lineHeight: 0.875
|
||||
},
|
||||
'@media screen and (min-height: 610px)': {
|
||||
lineHeight: 1.125
|
||||
}
|
||||
}}
|
||||
as="h1"
|
||||
>
|
||||
<Underline>Become a nonprofit</Underline> with HCB
|
||||
</Heading>
|
||||
<Flex
|
||||
sx={{
|
||||
gap: 3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/hcb/hcb-icon-icon-dark.png"
|
||||
alt="HCB Icon"
|
||||
height={64}
|
||||
sx={{
|
||||
margin: 'auto'
|
||||
}}
|
||||
/>
|
||||
<Text as="h2" sx={{ fontSize: 4 }}>
|
||||
HCB by
|
||||
</Text>
|
||||
<img
|
||||
src="https://assets.hackclub.com/flag-standalone.svg"
|
||||
alt="hack club flag"
|
||||
height={48}
|
||||
/>
|
||||
</Flex>
|
||||
<Container variant="copy">
|
||||
<Text
|
||||
variant="lead"
|
||||
sx={{
|
||||
textShadow: '0 3px 6px rgba(0, 0, 0, 0.5)',
|
||||
'@media screen and (max-height: 600px)': {
|
||||
lineHeight: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
The team behind the{' '}
|
||||
<Link
|
||||
href="https://innovationcircuit.com"
|
||||
target="_blank"
|
||||
color="inherit"
|
||||
bold
|
||||
hoverline
|
||||
>
|
||||
Innovation Circuit
|
||||
</Link>{' '}
|
||||
is one of {Math.round((eventsCount - 50) / 100) * 100}+
|
||||
teams who use <strong>HCB</strong> to run world-class
|
||||
organizations, hackathons, and clubs.
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
</Fade>
|
||||
</Box>
|
||||
<br />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 3
|
||||
}}
|
||||
>
|
||||
{showButton && (
|
||||
<>
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
as="a"
|
||||
href="#apply"
|
||||
style={{ zIndex: '100' }}
|
||||
>
|
||||
Apply Now
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlineLg"
|
||||
as="a"
|
||||
href="https://hcb.hackclub.com"
|
||||
target="_blank"
|
||||
style={{ zIndex: '100' }}
|
||||
ml={3}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<ScrollHint />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 3,
|
||||
right: 2,
|
||||
display: ['none', 'block']
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
variant="pill"
|
||||
sx={{
|
||||
zIndex: '1',
|
||||
bg: 'muted',
|
||||
color: 'steel',
|
||||
fontWeight: 'normal'
|
||||
}}
|
||||
>
|
||||
Singapore
|
||||
</Badge>
|
||||
</Box>
|
||||
</Slide>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Underline({ children }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
backgroundImage: 'url(/underline-red.svg)',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 1rem',
|
||||
backgroundPosition: 'bottom center'
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Slide({ children }) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'end',
|
||||
width: '100vw',
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#000000',
|
||||
boxShadow: 'inset 0 0 4rem 1rem rgba(0, 0, 0, 0.5)',
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
width: '100%',
|
||||
minHeight: '100vh',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={hero}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
alt="Dark room with a stage and students sitting below"
|
||||
placeholder="blur"
|
||||
priority
|
||||
/>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Vignette() {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(to bottom,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.25) 25%,rgba(0, 0, 0, 0.6) 50%, rgba(0, 0, 0, 0.7) 100%)',
|
||||
height: '100vh',
|
||||
left: '0',
|
||||
right: '0',
|
||||
position: 'absolute',
|
||||
zIndex: '0'
|
||||
}}
|
||||
></Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
import { Box, Image, Text, Heading, Container, Grid, Link } from 'theme-ui'
|
||||
import { Slide } from 'react-reveal'
|
||||
import Stat from '../stat'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
|
||||
const orgs = [
|
||||
{
|
||||
logo: '/hcb/nonprofits/girlgenius.png',
|
||||
name: 'Girl Genius',
|
||||
director: 'Chloe Yan',
|
||||
role: 'Executive Director',
|
||||
budget: 5,
|
||||
website: 'girlgeniusmag.tech',
|
||||
description:
|
||||
'Girl Genius Magazine is a fully student-run publication inspiring the next generation of female and non-binary leaders in STEAM. Their journalism and inclusive online community are dedicated to breaking down tech’s lingering gender barriers. Becoming fiscally sponsored allowed them to publish more issues, host over 40 workshops, organize a conference, and reach a global audience of 11k readers (and counting).'
|
||||
},
|
||||
{
|
||||
logo: '/hcb/nonprofits/techshift.png',
|
||||
transparency: 'techshift',
|
||||
name: 'TechShift',
|
||||
director: 'Daniel Jin',
|
||||
role: 'Co-Executive Director',
|
||||
budget: 100,
|
||||
website: 'techshift.org',
|
||||
description:
|
||||
'Founded in 2017, TechShift supports a network of 30+ student-run chapters across 3 continents leading initiatives at the intersection of technology and social impact. With the help of HCB, they are bringing about a more equitable technological future through their mentorship programs, community partnerships, microgrants, and the STEM For Social Good Toolkit.'
|
||||
},
|
||||
{
|
||||
logo: '/hcb/nonprofits/projectboom.jpg',
|
||||
transparency: 'projectboom',
|
||||
name: 'Project Boom',
|
||||
director: 'Kunal Botla',
|
||||
role: 'Founder',
|
||||
budget: '5.6',
|
||||
budgetLabel: 'raised',
|
||||
website: 'projectboom.org',
|
||||
url: 'https://projectboom.org/',
|
||||
description:
|
||||
'Project Boom is a student-led organization with a simple mission: getting computers to those who need them. Instead of becoming e-waste, old machines are given new life to deserving students worldwide. Joining HCB provided Project Boom with a platform to easily accept and manage donations, helping them to repair and ship more computers than ever before.'
|
||||
},
|
||||
{
|
||||
logo: '/hcb/nonprofits/executebig.png',
|
||||
name: 'Execute Big',
|
||||
director: 'Mingjie Jiang',
|
||||
role: 'Co-Executive Director',
|
||||
budget: '4',
|
||||
budgetLabel: 'funded',
|
||||
website: 'executebig.org',
|
||||
description:
|
||||
'Execute Big began by using leftover hackathon funds to provide travel grants for students. HCB helped make possible their array of grants, fellowships, and innovative programs to share computer science with students nationally. Now their own 501(c)(3) nonprofit, they leverage existing resources to make STEM activities accessible to everyone.'
|
||||
}
|
||||
]
|
||||
|
||||
export default function Nonprofits() {
|
||||
return (
|
||||
<Container
|
||||
sx={{ pt: 6, pb: [2, null, 5], mx: 'auto', px: [null, null, 4] }}
|
||||
>
|
||||
<Container
|
||||
variant="copy"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
pb: 3
|
||||
}}
|
||||
>
|
||||
<Heading variant="title">Nonprofit? No problem.</Heading>
|
||||
<Text variant="lead" color="muted">
|
||||
HCB is a powerful, safe, and easy-to-use money thing, whether you're
|
||||
receiving your first donation or spending $100,000 a year.
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
<Grid
|
||||
gap={4}
|
||||
sx={{
|
||||
gridTemplateColumns: ['100%', null, null, '1fr 1fr']
|
||||
}}
|
||||
>
|
||||
{orgs.map(org => {
|
||||
const id = kebabCase(org.name)
|
||||
return (
|
||||
<Organization
|
||||
logo={org.logo}
|
||||
name={org.name}
|
||||
budget={org.budget}
|
||||
budgetLabel={org.budgetLabel}
|
||||
website={org.website}
|
||||
description={org.description}
|
||||
url={org.url}
|
||||
key={id}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
function Organization({
|
||||
logo,
|
||||
name,
|
||||
budget,
|
||||
budgetLabel,
|
||||
website,
|
||||
url,
|
||||
description
|
||||
}) {
|
||||
return (
|
||||
<Slide bottom>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'darkless',
|
||||
color: 'smoke',
|
||||
borderRadius: 'extra',
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
<Container sx={{ padding: 0, margin: 0 }}>
|
||||
<Box p={[3, null, 4]}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Image
|
||||
src={logo}
|
||||
alt={`${name} logo`}
|
||||
sx={{
|
||||
height: '4rem',
|
||||
width: '4rem',
|
||||
objectFit: 'cover',
|
||||
borderRadius: 'default'
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ ml: 3 }}>
|
||||
<Text
|
||||
color="white"
|
||||
variant="headline"
|
||||
sx={{
|
||||
fontSize: [28, null, 38],
|
||||
lineHeight: 1,
|
||||
letterSpacing: -0.1
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
<br />
|
||||
<Link
|
||||
href={url || `https://${website}`}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
color: 'muted',
|
||||
'&:hover': { textDecoration: 'underline' }
|
||||
}}
|
||||
>
|
||||
{website}
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
<DetailStat
|
||||
value={`$${budget}k`}
|
||||
label={budgetLabel ?? 'budget'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<br />
|
||||
<About>{description}</About>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Slide>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailStat({ value, label }) {
|
||||
return (
|
||||
<Box sx={{ px: 0, mb: [3, 0], ml: [-1, 0], mx: 3 }}>
|
||||
<Stat value={value} label={label} sm />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function About({ children }) {
|
||||
return (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 2,
|
||||
color: 'snow',
|
||||
textIndent: '-.375em',
|
||||
lineHeight: 'caption',
|
||||
fontSize: 18
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import { Box, Text, Container } from 'theme-ui'
|
||||
import { Fade } from 'react-reveal'
|
||||
import Icon from '../icon'
|
||||
|
||||
export default function Run() {
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
variant="container"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: ['column', null, 'row'],
|
||||
alignItems: 'center',
|
||||
width: ['100%', '100%', '100%', '85%'],
|
||||
bg: '#252429',
|
||||
color: 'smoke',
|
||||
px: 4,
|
||||
|
||||
borderRadius: 'default',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Container maxWidth={28} sx={{ mx: 0, py: 4 }}>
|
||||
<Text variant="heading" sx={{ fontSize: 48 }}>
|
||||
HCB doesn’t stop at closing ceremony.
|
||||
</Text>
|
||||
<br />
|
||||
<Text variant="lead" sx={{ color: 'muted', fontSize: 28 }}>
|
||||
Receiving and managing money is just the start. HCB helps you handle
|
||||
ongoing obligations while you’re organizing.
|
||||
</Text>
|
||||
</Container>
|
||||
<List>
|
||||
<ListItem
|
||||
icon="docs"
|
||||
body="We handle ongoing tax filings including end-of-year taxes"
|
||||
/>
|
||||
<ListItem
|
||||
icon="payment-docs"
|
||||
body="Our accountants regularly reconcile your books"
|
||||
/>
|
||||
<ListItem
|
||||
icon="history"
|
||||
body="You always have access to historical financial data"
|
||||
/>
|
||||
</List>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function List({ children }) {
|
||||
return (
|
||||
<Box>
|
||||
<ol style={{ listStyle: 'none', paddingLeft: 0 }}>{children}</ol>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function ListItem({ icon, body }) {
|
||||
return (
|
||||
<Fade bottom>
|
||||
<li
|
||||
style={{
|
||||
lineHeight: 1.25,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pl: 0
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
glyph={icon}
|
||||
size={45}
|
||||
sx={{ color: 'primary', flexShrink: 'none', flexShrink: 0, mr: 2 }}
|
||||
/>
|
||||
<Text fontSize={[32, 48]} variant="lead">
|
||||
{body}
|
||||
</Text>
|
||||
</li>
|
||||
</Fade>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Button,
|
||||
Image,
|
||||
Text,
|
||||
Heading,
|
||||
Container,
|
||||
Grid,
|
||||
Link
|
||||
} from 'theme-ui'
|
||||
import { Slide } from 'react-reveal'
|
||||
import Stat from '../stat'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
|
||||
const events = [
|
||||
{
|
||||
transparency: 'hackpenn',
|
||||
name: 'Hack Pennsylvania',
|
||||
location: 'State College, PA',
|
||||
organizer: 'Joy Liu',
|
||||
budget: 15,
|
||||
attendees: 115,
|
||||
testimonial:
|
||||
'For me, HCB unlocked organizing hackathons. Even after as a club leader, raising money seemed insurmountable. HCB directly enabled organizing events in my community with event bank accounts [sic] & a supportive community. I couldn’t recommend it more highly.'
|
||||
},
|
||||
{
|
||||
name: 'Teenhacks LI',
|
||||
location: 'Long Island, NY',
|
||||
organizer: 'Wesley Pergament',
|
||||
budget: 35,
|
||||
attendees: 300,
|
||||
testimonial:
|
||||
'For our hackathon, HCB has given us the tools to make sure our organization is professional with sponsors. HCB and their team have created an easily manageable resource to make sure any event is run successfully. We would highly recommend any organization be a part of the Hack Club ecosystem.'
|
||||
},
|
||||
{
|
||||
transparency: 'mahacks',
|
||||
name: 'MAHacks',
|
||||
location: 'Boston, MA',
|
||||
organizer: 'Kat Huang',
|
||||
budget: 10,
|
||||
attendees: 70,
|
||||
testimonial:
|
||||
'HCB removed the barriers to starting fundraising for MAHacks. In mere days, vs months of nonprofit paperwork, HCB enabled my team to invoice sponsors professionally and manage our finances on a clear, up-to-date dashboard. I highly recommend using HCB & joining the Hack Club community.'
|
||||
},
|
||||
{
|
||||
transparency: 'dv-hacks',
|
||||
name: 'DV Hacks',
|
||||
location: 'Santa Clara, CA',
|
||||
organizer: 'Khushi Wadhwa',
|
||||
budget: 12,
|
||||
attendees: 150,
|
||||
testimonial:
|
||||
'HCB is an essential platform for any hackathon organizer! It made us look both professional and credible in front of our sponsors and it relieved us of legal/financial burdens. HCB was there for us every step of the way and for a first-year hackathon, that support was priceless.'
|
||||
}
|
||||
]
|
||||
|
||||
export default function Testimonials() {
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<Container
|
||||
variant="copy"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Heading variant="title">
|
||||
The best events across the country run on HCB.
|
||||
</Heading>
|
||||
<Text variant="lead" color="muted">
|
||||
Everywhere from Philadelphia to Phoenix to Portland, HCB powers
|
||||
events of all sizes.
|
||||
</Text>
|
||||
</Container>
|
||||
<Container
|
||||
sx={{
|
||||
color: 'smoke',
|
||||
px: [null, null, 4],
|
||||
mx: 'auto',
|
||||
mt: 2,
|
||||
borderRadius: 0,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
gap={4}
|
||||
sx={{
|
||||
gridTemplateColumns: ['100%', null, null, '1fr 1fr']
|
||||
}}
|
||||
>
|
||||
{events.map(event => {
|
||||
const id = kebabCase(event.name)
|
||||
return <Event {...event} img={`/hcb/events/${id}.jpg`} key={id} />
|
||||
})}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Event({
|
||||
img,
|
||||
name,
|
||||
location,
|
||||
budget,
|
||||
attendees,
|
||||
organizer,
|
||||
testimonial,
|
||||
transparency
|
||||
}) {
|
||||
return (
|
||||
<Slide bottom>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'darkless',
|
||||
color: 'smoke',
|
||||
borderRadius: 'extra',
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
<Container sx={{ padding: 0, margin: 0 }}>
|
||||
<Image
|
||||
src={img}
|
||||
alt={location}
|
||||
sx={{
|
||||
maxHeight: '20rem',
|
||||
objectFit: 'cover',
|
||||
width: '100%',
|
||||
borderRadius: 'default',
|
||||
mb: -3
|
||||
}}
|
||||
/>
|
||||
<Box p={[3, null, 4]}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: ['column', 'row', 'row'],
|
||||
alignItems: ['baseline', 'center'],
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: -3
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color="white"
|
||||
variant="headline"
|
||||
sx={{ fontSize: [48, null, 30], letterSpacing: -0.1 }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
my: 0,
|
||||
ml: -2,
|
||||
pl: 0
|
||||
}}
|
||||
>
|
||||
<DetailStat value={attendees} label="attendees" />
|
||||
<DetailStat value={`$${budget}k`} label="budget" />
|
||||
</Box>
|
||||
</Box>
|
||||
<br />
|
||||
<Quote>"{testimonial}"</Quote>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: ['0px', 3]
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mt: ['16px', '0px']
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={`/hackers/${organizer.split(' ')[0].toLowerCase()}.jpg`}
|
||||
size={48}
|
||||
mr={2}
|
||||
alt="Photo of ${organizer}"
|
||||
/>
|
||||
<Text
|
||||
color="white"
|
||||
sx={{
|
||||
fontSize: 19,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Text sx={{ fontWeight: 'bold', lineHeight: 1.125 }}>
|
||||
{organizer}
|
||||
</Text>
|
||||
<Text>Lead Organizer</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
{transparency && (
|
||||
<Link
|
||||
href={`https://hcb.hackclub.com/${transparency}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
sx={{ mt: ['16px', '0px'] }}
|
||||
>
|
||||
<Button
|
||||
mt={[null, null, 4, 0]}
|
||||
ml={[0, 'auto']}
|
||||
sx={{ textTransform: 'none' }}
|
||||
variant="primary"
|
||||
title="🎶 take a look, it's in our books 🎵"
|
||||
>
|
||||
See Finances
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Slide>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailStat({ value, label }) {
|
||||
return (
|
||||
<Box sx={{ px: 0, mb: [3, 0], ml: [-1, 0], mx: 3 }}>
|
||||
<Stat value={value} label={label} sm />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function Quote({ children }) {
|
||||
return (
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: 2,
|
||||
color: 'muted',
|
||||
textIndent: '-.375em',
|
||||
lineHeight: 'caption',
|
||||
fontSize: 18
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { Flex, Text, Image, Box, Container } from 'theme-ui'
|
||||
import Slide from 'react-reveal'
|
||||
|
||||
function Step({ stepIndex, label }) {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: ['row', null, 'column'],
|
||||
flex: '1 0 0',
|
||||
alignItems: 'center',
|
||||
maxWidth: ['24rem', null, '12rem'],
|
||||
gap: 3
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`/hcb/timeline-steps/step${stepIndex}.svg`}
|
||||
sx={{ flexShrink: 0 }}
|
||||
alt=""
|
||||
/>
|
||||
<Text
|
||||
variant="lead"
|
||||
sx={{
|
||||
textAlign: ['left', null, 'center'],
|
||||
margin: '0px !important'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Timeline() {
|
||||
const labels = [
|
||||
'Register your organization for HCB',
|
||||
'Explore the interface in Playground mode',
|
||||
'Hop on an intro call with our team',
|
||||
'Start fundraising!'
|
||||
]
|
||||
const stepSideLength = 64
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import CardModel from './card-model'
|
||||
import {Box, Flex, Grid, Image, Link, Text} from 'theme-ui'
|
||||
import { Box, Flex, Grid, Image, Link, Text } from 'theme-ui'
|
||||
import Buttons from './button'
|
||||
import Dot from '../../dot'
|
||||
import {formatDate} from '../../../lib/dates'
|
||||
import { formatDate } from '../../../lib/dates'
|
||||
|
||||
/** @jsxImportSource theme-ui */
|
||||
const Cover = () => (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import CardModel from "./card-model";
|
||||
import { Box, Flex, Grid, Image, Text, Link } from "theme-ui";
|
||||
import Buttons from "./button";
|
||||
import CardModel from './card-model'
|
||||
import { Box, Flex, Grid, Image, Text, Link } from 'theme-ui'
|
||||
import Buttons from './button'
|
||||
|
||||
/** @jsxImportSource theme-ui */
|
||||
|
||||
|
|
@ -10,47 +10,52 @@ export default function Haunted() {
|
|||
github_link="https://github.com/hackclub/www-hauntedhouse"
|
||||
color="white"
|
||||
sx={{
|
||||
backgroundSize: "cover",
|
||||
backgroundColor: "#95C9E5",
|
||||
border: "2px solid #EB6424",
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#95C9E5',
|
||||
border: '2px solid #EB6424'
|
||||
}}
|
||||
position={[null, "bottom", "bottom"]}
|
||||
position={[null, 'bottom', 'bottom']}
|
||||
highlight="#cc5600"
|
||||
image="/haunted/bg.webp"
|
||||
filter="brightness(0.7)"
|
||||
>
|
||||
<Grid columns={[1, 2]} sx={{ position: "relative", zIndex: 2 }}>
|
||||
<Grid columns={[1, 2]} sx={{ position: 'relative', zIndex: 2 }}>
|
||||
<Image
|
||||
src="/haunted/haunted-text.svg"
|
||||
sx={{
|
||||
width: ["200px", "250px", "300px"],
|
||||
mt: ["-10px", "-20px", "-20px"],
|
||||
position: "relative",
|
||||
width: ['200px', '250px', '300px'],
|
||||
mt: ['-10px', '-20px', '-20px'],
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
fontSize: ["36px", 4, 5],
|
||||
color: "white",
|
||||
fontSize: ['36px', 4, 5],
|
||||
color: 'white'
|
||||
}}
|
||||
alt="Haunted"
|
||||
/>
|
||||
<Box></Box>
|
||||
|
||||
|
||||
<Text as="p" variant="subtitle" sx={{ color: 'white' }}>
|
||||
Haunted House is a Chicago-based event full of sites and frights! Join us from October 28-29
|
||||
for a weekend of coding pushing the bounds of creativity, where fright meets byte!
|
||||
Haunted House is a Chicago-based event full of sites and frights! Join
|
||||
us from October 28-29 for a weekend of coding pushing the bounds of
|
||||
creativity, where fright meets byte!
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
flexDirection: 'column',
|
||||
mt: [3, 3, 4],
|
||||
alignItems: "end",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: 'end',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
></Flex>
|
||||
<Buttons id="14" link="https://haunted.hackclub.com" icon="welcome" primary="#EB6424">
|
||||
Sign Up
|
||||
</Buttons>
|
||||
<Buttons
|
||||
id="14"
|
||||
link="https://haunted.hackclub.com"
|
||||
icon="welcome"
|
||||
primary="#EB6424"
|
||||
>
|
||||
Sign Up
|
||||
</Buttons>
|
||||
</Grid>
|
||||
</CardModel>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export default function Haxidraw({ stars }) {
|
|||
variant="subtitle"
|
||||
sx={{ zIndex: 2, position: 'relative' }}
|
||||
>
|
||||
Blot is an open source drawing machine and online editor,
|
||||
designed to be a fun and beginner friendly introduction to digital
|
||||
Blot is an open source drawing machine and online editor, designed
|
||||
to be a fun and beginner friendly introduction to digital
|
||||
fabrication and generative art.
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ const Loading = () => (
|
|||
mr: '5px',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
'100%': { transform: 'rotate(360deg)' }
|
||||
}
|
||||
}}
|
||||
></Box>
|
||||
)
|
||||
|
|
@ -35,19 +35,19 @@ const MailingList = () => {
|
|||
const [data, setData] = useState({ finalHtml: [], names: [] })
|
||||
const formRef = useRef(null)
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
|
||||
let res = await fetch('/api/mailing-list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: e.target.name.value,
|
||||
email: e.target.email.value,
|
||||
}),
|
||||
email: e.target.email.value
|
||||
})
|
||||
})
|
||||
|
||||
formRef.current.reset()
|
||||
|
|
@ -66,22 +66,33 @@ const MailingList = () => {
|
|||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch('https://api.github.com/repos/hackclub/leaders-newsletter/contents/updates')
|
||||
fetch(
|
||||
'https://api.github.com/repos/hackclub/leaders-newsletter/contents/updates'
|
||||
)
|
||||
.then(response => response.json())
|
||||
.then(data => data.sort((a, b) => b.name.localeCompare(a.name))) // Makes sure we only get the latest two newsletters
|
||||
.then(data => data.slice(0, 2))
|
||||
.then(data => Promise.all(data.map(item => fetch(item.download_url)))) // Makes a separate fetch request for the content of each newsletter
|
||||
.then(responses => Promise.all(responses.map(response => response.text())))
|
||||
.then(markdown => Promise.all(markdown.map(markdown => markdownToHtml(markdown))))
|
||||
.then(html => html.map(html => html.replace(/<[^>]*>/g, '').replace(/The Hackening/g, ''))), // Chucks out all html tags + 'The Hackening'
|
||||
|
||||
fetch('https://api.github.com/repos/hackclub/leaders-newsletter/contents/updates')
|
||||
.then(responses =>
|
||||
Promise.all(responses.map(response => response.text()))
|
||||
)
|
||||
.then(markdown =>
|
||||
Promise.all(markdown.map(markdown => markdownToHtml(markdown)))
|
||||
)
|
||||
.then(html =>
|
||||
html.map(html =>
|
||||
html.replace(/<[^>]*>/g, '').replace(/The Hackening/g, '')
|
||||
)
|
||||
), // Chucks out all html tags + 'The Hackening'
|
||||
|
||||
fetch(
|
||||
'https://api.github.com/repos/hackclub/leaders-newsletter/contents/updates'
|
||||
)
|
||||
.then(response => response.json())
|
||||
.then(data => data.sort((a, b) => b.name.localeCompare(a.name)))
|
||||
.then(data => data.map(item => item.name.split('.')[0])) // Grabs the name and gets rid of the file extension
|
||||
])
|
||||
.then(([finalHtml, names]) => setData({ finalHtml, names }))
|
||||
}, [])
|
||||
]).then(([finalHtml, names]) => setData({ finalHtml, names }))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative', py: 6, background: 'darker' }}>
|
||||
|
|
@ -93,10 +104,12 @@ const MailingList = () => {
|
|||
background: 'rgb(255,255,255, 0.45)',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
backdropFilter: 'blur(8px)',
|
||||
backdropFilter: 'blur(8px)'
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: ['column', 'column', 'row'], gridGap: [0, 5] }}>
|
||||
<Flex
|
||||
sx={{ flexDirection: ['column', 'column', 'row'], gridGap: [0, 5] }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
placeItems: 'center',
|
||||
|
|
@ -104,16 +117,16 @@ const MailingList = () => {
|
|||
alignItems: ['left', 'left', 'center'],
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
width: ['100%', '100%', '75%'],
|
||||
width: ['100%', '100%', '75%']
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Text
|
||||
variant='title'
|
||||
variant="title"
|
||||
sx={{
|
||||
fontSize: [4, '36px', '42px', 6],
|
||||
zIndex: 2,
|
||||
textAlign: 'left',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
Join the newsletter
|
||||
|
|
@ -123,23 +136,24 @@ const MailingList = () => {
|
|||
color: 'darkless',
|
||||
mt: 2,
|
||||
fontSize: 3,
|
||||
textAlign: 'left',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
as='p'
|
||||
as="p"
|
||||
>
|
||||
We'll send you an email no more than once a month, when we work
|
||||
on something cool for you. Check out our{' '}
|
||||
We'll send you an email no more than once a month, when we
|
||||
work on something cool for you. Check out our{' '}
|
||||
<Link
|
||||
href='https://workshops.hackclub.com/leader-newsletters/'
|
||||
target='_blank'
|
||||
rel='noopener norefferer'
|
||||
href="https://workshops.hackclub.com/leader-newsletters/"
|
||||
target="_blank"
|
||||
rel="noopener norefferer"
|
||||
>
|
||||
previous issues
|
||||
</Link>.
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Box>
|
||||
<Grid
|
||||
as='form'
|
||||
as="form"
|
||||
ref={formRef}
|
||||
onSubmit={handleSubmit}
|
||||
gap={[2, 3]}
|
||||
|
|
@ -147,47 +161,47 @@ const MailingList = () => {
|
|||
textAlign: 'center',
|
||||
alignItems: 'end',
|
||||
input: { bg: 'sunken' },
|
||||
width: '100%',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Input
|
||||
autofillBackgroundColor='highlight'
|
||||
type='text'
|
||||
name='name'
|
||||
id='name'
|
||||
placeholder='Your Name'
|
||||
autofillBackgroundColor="highlight"
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Your Name"
|
||||
required
|
||||
sx={{
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
fontSize: 2,
|
||||
fontSize: 2
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<div>
|
||||
<Input
|
||||
autofillBackgroundColor='highlight'
|
||||
type='email'
|
||||
name='email'
|
||||
id='email'
|
||||
placeholder='Your Email'
|
||||
autofillBackgroundColor="highlight"
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Your Email"
|
||||
required
|
||||
sx={{
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
fontSize: 2,
|
||||
fontSize: 2
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button type='submit' sx={{ mt: [2, 0], fontSize: 2 }}>
|
||||
<Button type="submit" sx={{ mt: [2, 0], fontSize: 2 }}>
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loading /> Subscribe
|
||||
</>
|
||||
) : submitted ? (
|
||||
<>
|
||||
<Icon glyph='send' /> You're on the list!
|
||||
<Icon glyph="send" /> You're on the list!
|
||||
</>
|
||||
) : (
|
||||
'Subscribe'
|
||||
|
|
@ -200,28 +214,33 @@ const MailingList = () => {
|
|||
display: 'grid',
|
||||
gridGap: 4,
|
||||
mt: [4, 0],
|
||||
width: '100%',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{data.finalHtml.map((html, index) => (
|
||||
<MailCard
|
||||
issue={index + 1}
|
||||
body={html}
|
||||
date={format(parse('', '', new Date(data.names[index])), 'MMMM d, yyyy')}
|
||||
link={data.names[index]}
|
||||
key={index}
|
||||
/>
|
||||
)).reverse()}
|
||||
{data.finalHtml
|
||||
.map((html, index) => (
|
||||
<MailCard
|
||||
issue={index + 1}
|
||||
body={html}
|
||||
date={format(
|
||||
parse('', '', new Date(data.names[index])),
|
||||
'MMMM d, yyyy'
|
||||
)}
|
||||
link={data.names[index]}
|
||||
key={index}
|
||||
/>
|
||||
))
|
||||
.reverse()}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
<BGImg
|
||||
width={2544}
|
||||
height={2048}
|
||||
gradient='linear-gradient(rgba(0,0,0,0.125), rgba(0,0,0,0.25))'
|
||||
gradient="linear-gradient(rgba(0,0,0,0.125), rgba(0,0,0,0.25))"
|
||||
src={background}
|
||||
placeholder='blur'
|
||||
alt='Globe with hundreds of Hack Clubs'
|
||||
placeholder="blur"
|
||||
alt="Globe with hundreds of Hack Clubs"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {useEffect, useState} from 'react'
|
||||
import {Box, Flex, Grid, Text} from 'theme-ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, Flex, Grid, Text } from 'theme-ui'
|
||||
import CardModel from './card-model'
|
||||
import Buttons from './button'
|
||||
|
||||
|
|
@ -10,10 +10,10 @@ export default function Onboard({ stars }) {
|
|||
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
'https://api.github.com/search/issues?q=repo:hackclub/onboard+is:pr+is:merged+label:Submission',
|
||||
'https://api.github.com/search/issues?q=repo:hackclub/onboard+is:pr+is:merged+label:Submission'
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setProjects(data.total_count))
|
||||
.then(response => response.json())
|
||||
.then(data => setProjects(data.total_count))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
|
@ -23,23 +23,23 @@ export default function Onboard({ stars }) {
|
|||
backgroundImage: `linear-gradient(120deg, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.8) 20%, rgba(0, 0, 0, 0.4) 50%), url('https://cloud-fyrwj5rn5-hack-club-bot.vercel.app/0pcb.svg')`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
github_link='https://github.com/hackclub/onboard/'
|
||||
color='white'
|
||||
highlight='#87ffa1'
|
||||
github_link="https://github.com/hackclub/onboard/"
|
||||
color="white"
|
||||
highlight="#87ffa1"
|
||||
stars={stars}
|
||||
>
|
||||
<Text
|
||||
variant='title'
|
||||
as='h3'
|
||||
variant="title"
|
||||
as="h3"
|
||||
sx={{
|
||||
fontSize: ['36px', 4, 5],
|
||||
maxWidth: 'copyPlus',
|
||||
textShadow: '0 0 30px rgba(42, 252, 88, 0.6)',
|
||||
color: '#87ffa1',
|
||||
mt: ['38px', 0, 0],
|
||||
position: 'relative',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
OnBoard
|
||||
|
|
@ -47,8 +47,8 @@ export default function Onboard({ stars }) {
|
|||
<Grid columns={[1, 2]}>
|
||||
<Box>
|
||||
<Text
|
||||
as='p'
|
||||
variant='subheadline'
|
||||
as="p"
|
||||
variant="subheadline"
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
|
|
@ -63,32 +63,32 @@ export default function Onboard({ stars }) {
|
|||
>
|
||||
{projects} projects built
|
||||
</Text>
|
||||
<Text as='p' variant='subtitle'>
|
||||
<Text as="p" variant="subtitle">
|
||||
Circuit boards are magical. You design one, we'll print it.
|
||||
Completely for free! Get a $100 grant to fuel the creation of your dream
|
||||
project with OnBoard.
|
||||
Completely for free! Get a $100 grant to fuel the creation of your
|
||||
dream project with OnBoard.
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{ flexDirection: 'column', mt: [3, 3, 4], placeSelf: 'start' }}
|
||||
>
|
||||
<Buttons
|
||||
id='59'
|
||||
icon='emoji'
|
||||
link='https://github.com/hackclub/OnBoard/blob/main/README.md'
|
||||
primary='#87ffa1'
|
||||
color='black'
|
||||
id="59"
|
||||
icon="emoji"
|
||||
link="https://github.com/hackclub/OnBoard/blob/main/README.md"
|
||||
primary="#87ffa1"
|
||||
color="black"
|
||||
>
|
||||
Get a grant
|
||||
</Buttons>
|
||||
<Buttons icon='docs' link='https://jams.hackclub.com/tag/pcb' id='60'>
|
||||
<Buttons icon="docs" link="https://jams.hackclub.com/tag/pcb" id="60">
|
||||
Learn how to design a PCB
|
||||
</Buttons>
|
||||
<Buttons icon='friend' link='/slack?event=onboard' id='61'>
|
||||
<Buttons icon="friend" link="/slack?event=onboard" id="61">
|
||||
See what other hackers have built
|
||||
</Buttons>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</CardModel>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,60 +11,85 @@ export default function Pizza() {
|
|||
sx={{
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#95C9E5',
|
||||
border: "1px solid #EC3750" // Corrected the color value here
|
||||
border: '1px solid #EC3750' // Corrected the color value here
|
||||
}}
|
||||
position={[null, 'bottom', 'bottom']}
|
||||
highlight="#271932"
|
||||
image="https://cloud-4f5ohtb3u-hack-club-bot.vercel.app/0subtlegrain.png"
|
||||
>
|
||||
<Grid columns={[1, 2]} sx={{ position: 'relative', alignItems: "center", zIndex: 2 }}>
|
||||
<Box>
|
||||
<Text
|
||||
as="h3"
|
||||
variant="title"
|
||||
sx={{
|
||||
fontSize: ['36px', 4, 5],
|
||||
zIndex: 2,
|
||||
color: "#000",
|
||||
mb: "8px"
|
||||
}}
|
||||
>
|
||||
Start A Hack Club <br/> Get <Text
|
||||
sx={{
|
||||
|
||||
background: ["linear-gradient(180deg, #FF8C37 25%, #EC3750 100%)"],
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextStroke: 'currentColor',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
|
||||
|
||||
}}
|
||||
> $100 In Pizza</Text>
|
||||
</Text>
|
||||
|
||||
<Grid
|
||||
columns={[1, 2]}
|
||||
sx={{ position: 'relative', alignItems: 'center', zIndex: 2 }}
|
||||
>
|
||||
<Box>
|
||||
<Text
|
||||
as="h3"
|
||||
variant="title"
|
||||
sx={{
|
||||
fontSize: ['36px', 4, 5],
|
||||
zIndex: 2,
|
||||
color: '#000',
|
||||
mb: '8px'
|
||||
}}
|
||||
>
|
||||
Start A Hack Club <br /> Get{' '}
|
||||
<Text
|
||||
sx={{
|
||||
background: [
|
||||
'linear-gradient(180deg, #FF8C37 25%, #EC3750 100%)'
|
||||
],
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextStroke: 'currentColor',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
$100 In Pizza
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text as="p" variant="subtitle" sx={{ color: '#000', mb: 3 }}>
|
||||
GitHub is providing $100 pizza grants to every teen who starts a Hack Club at their school.
|
||||
</Text>
|
||||
|
||||
<Buttons id="14" link="/pizza" icon="welcome" primary="primary">
|
||||
<Text as="p" variant="subtitle" sx={{ color: '#000', mb: 3 }}>
|
||||
GitHub is providing $100 pizza grants to every teen who starts a
|
||||
Hack Club at their school.
|
||||
</Text>
|
||||
|
||||
<Buttons id="14" link="/pizza" icon="welcome" primary="primary">
|
||||
Get Your Pizza Grant
|
||||
</Buttons>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'end',
|
||||
justifyContent: 'flex-end',
|
||||
position: "relative"
|
||||
}}
|
||||
>
|
||||
<Image alt="Group of teenage hackers enjoying GitHub Hack Club Pizza Grant" sx={{borderRadius: "16px",
|
||||
border: "1px solid #EC3750",
|
||||
aspectRatio: "16/9", objectFit: "cover"}} src="https://cloud-8tc8qa1ew-hack-club-bot.vercel.app/0img_8975.jpg"/>
|
||||
<Text sx={{color: "#000", backgroundColor: "#fff", left: "16px", bottom: "16px", padding: "6px 8px", borderRadius: "16px", position: "absolute"}}>Newton South HS Hack Club in Boston</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'end',
|
||||
justifyContent: 'flex-end',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
alt="Group of teenage hackers enjoying GitHub Hack Club Pizza Grant"
|
||||
sx={{
|
||||
borderRadius: '16px',
|
||||
border: '1px solid #EC3750',
|
||||
aspectRatio: '16/9',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
src="https://cloud-8tc8qa1ew-hack-club-bot.vercel.app/0img_8975.jpg"
|
||||
/>
|
||||
<Text
|
||||
sx={{
|
||||
color: '#000',
|
||||
backgroundColor: '#fff',
|
||||
left: '16px',
|
||||
bottom: '16px',
|
||||
padding: '6px 8px',
|
||||
borderRadius: '16px',
|
||||
position: 'absolute'
|
||||
}}
|
||||
>
|
||||
Newton South HS Hack Club in Boston
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardModel>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Box, Grid, Image, Text} from 'theme-ui'
|
||||
import { Box, Grid, Image, Text } from 'theme-ui'
|
||||
import Buttons from './button'
|
||||
import CardModel from './card-model'
|
||||
import Tilt from '../../tilt'
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ function Game({ game, gameImage, gameImage1, ...props }) {
|
|||
mb: 1
|
||||
}}
|
||||
>
|
||||
<RelativeTime value={game['added on']} titleFormat="YYYY-MM-DD" />
|
||||
<RelativeTime value={game['addedOn']} titleFormat="YYYY-MM-DD" />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -257,7 +257,7 @@ export default function Sprig({ stars, game, gameImage, gameImage1 }) {
|
|||
>
|
||||
<Game
|
||||
game={game[0]}
|
||||
// gameImage={gameImage}
|
||||
// gameImage={gameImage}
|
||||
/>
|
||||
<Game
|
||||
game={game[1]}
|
||||
|
|
|
|||
109
components/index/cards/wonderland.js
Normal file
109
components/index/cards/wonderland.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import CardModel from './card-model'
|
||||
import { Box, Flex, Grid, Image, Text } from 'theme-ui'
|
||||
import Buttons from './button'
|
||||
|
||||
/** @jsxImportSource theme-ui */
|
||||
|
||||
export default function Wonderland() {
|
||||
return (
|
||||
<CardModel
|
||||
color="white"
|
||||
sx={{
|
||||
backgroundSize: 'cover',
|
||||
backgroundColor: '#95C9E5',
|
||||
border: '2px solid #fbbae4'
|
||||
}}
|
||||
position={[null, 'bottom', 'bottom']}
|
||||
image="/wonderland/banner.webp"
|
||||
highlight={'#fbbae4'}
|
||||
filter="brightness(0.6)"
|
||||
>
|
||||
<Grid columns={[1, 1, 2]} sx={{ position: 'relative', zIndex: 2 }}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Image
|
||||
src="/wonderland/wonderland.svg"
|
||||
sx={{
|
||||
width: ['300px', '350px', '400px'],
|
||||
mt: ['-10px', '-10px', '-10px'],
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
fontSize: ['36px', 4, 5],
|
||||
color: 'white',
|
||||
filter: 'drop-shadow(0 0 10px #fbbae4 )'
|
||||
}}
|
||||
alt="Wonderland"
|
||||
/>
|
||||
|
||||
<Text
|
||||
as="p"
|
||||
variant="subheadline"
|
||||
sx={{
|
||||
ml: '10px',
|
||||
mt: '-10px',
|
||||
mb: '10px',
|
||||
zIndex: 2,
|
||||
color: 'white',
|
||||
fontSize: ['24px !important'],
|
||||
|
||||
textShadow: '0 0 20px #fbbae4'
|
||||
}}
|
||||
// sx={{
|
||||
// fontSize: ['30px', '30px', '30px'],
|
||||
// fontWeight: 'bold'
|
||||
// }}
|
||||
>
|
||||
Boston, MA
|
||||
<br />
|
||||
February 23-25th
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Buttons
|
||||
icon="flag-fill"
|
||||
href="https://wonderland.hackclub.com/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
primary="#fbbae4"
|
||||
id="43"
|
||||
sx={{ color: '#000' }}
|
||||
>
|
||||
Join Us
|
||||
</Buttons>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
textShadow: '0 0 20px #fbbae4'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="p"
|
||||
variant="subtitle"
|
||||
sx={{
|
||||
fontSize: ['22px', '20px', '18px']
|
||||
}}
|
||||
>
|
||||
Only when we limit ourselves... do we become truly free.
|
||||
</Text>
|
||||
<Text
|
||||
as="p"
|
||||
variant="subtitle"
|
||||
sx={{
|
||||
fontSize: ['22px', '20px', '18px']
|
||||
}}
|
||||
>
|
||||
How would you and your friends use a 🥕 carrot or a 🧸 stuffed
|
||||
animal in a hackathon project? In Wonderland, you'll explore 🧰
|
||||
chests of random objects and software that you'll incorporate into
|
||||
hackathon projects.
|
||||
</Text>
|
||||
</Box>
|
||||
</Grid>
|
||||
</CardModel>
|
||||
)
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export default function GitHub({
|
|||
variant="pill"
|
||||
bg="snow"
|
||||
as="a"
|
||||
href={url || "https://github.com/hackclub"}
|
||||
href={url || 'https://github.com/hackclub'}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
sx={{
|
||||
|
|
@ -83,4 +83,4 @@ export default function GitHub({
|
|||
</Text>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Alongside the soccer players, theater kids, and band geeks, we aspire to create
|
|||
|
||||
**Job Description:**
|
||||
|
||||
[HCB](https://hackclub.com/hcb/) is our in-house financial software used by 1,500 Hack Clubbers to financially power their Hack Clubs, hackathons, and student-organized nonprofits. When teenagers use HCB, we act as their backing financial and legal entity, allowing them to leverage our 501(c)(3) nonprofit status to receive donations and use our beautiful in-house software to manage and spend their funds.
|
||||
[HCB](https://hackclub.com/fiscal-sponsorship/) is our in-house financial software used by 1,500 Hack Clubbers to financially power their Hack Clubs, hackathons, and student-organized nonprofits. When teenagers use HCB, we act as their backing financial and legal entity, allowing them to leverage our 501(c)(3) nonprofit status to receive donations and use our beautiful in-house software to manage and spend their funds.
|
||||
|
||||
As our 7th full-time staff member, your role will be to end-to-end own the human operations essential to HCB's success, including onboarding video calls with new Hack Clubbers signing up, daily backend transaction categorization and management through our custom software, and providing a world-class customer success experience over Slack and email to Hack Clubbers. In addition to yourself, in this role you will lead 3 current part-time Hack Clubbers - and grow this team - to help you accomplish HCB's human operations.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { Box, Card, Link, Text } from "theme-ui";
|
||||
import { Box, Card, Link, Text } from 'theme-ui'
|
||||
|
||||
export default function MailCard({ body, date, link }) {
|
||||
body = body.length > 130 ? body.substring(0, 130) + '...' : body
|
||||
return (
|
||||
<Card
|
||||
variant='interactive'
|
||||
variant="interactive"
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
padding: '0 !important',
|
||||
padding: '0 !important'
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`https://workshops.hackclub.com/leader-newsletters/${link}`}
|
||||
sx={{ textDecoration: 'none' }}
|
||||
target='_blank'
|
||||
rel='noopener norefferer'
|
||||
target="_blank"
|
||||
rel="noopener norefferer"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: '90%',
|
||||
color: 'black',
|
||||
textDecoration: 'none !important',
|
||||
textDecoration: 'none !important'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
|
@ -29,7 +29,7 @@ export default function MailCard({ body, date, link }) {
|
|||
height: '10px',
|
||||
backgroundRepeat: 'repeat-x',
|
||||
backgroundSize: '100%',
|
||||
backgroundImage: `url('/letter-pattern.svg')`,
|
||||
backgroundImage: `url('/letter-pattern.svg')`
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
|
|
@ -37,17 +37,17 @@ export default function MailCard({ body, date, link }) {
|
|||
placeItems: 'center',
|
||||
display: 'grid',
|
||||
height: '100%',
|
||||
paddingY: [3, 4, 0],
|
||||
paddingY: [3, 4, 0]
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: [3, 4] }}>
|
||||
<Text>
|
||||
{date}
|
||||
<Text sx={{ color: '#8492a6' }}>— From Hack Club, to You</Text>
|
||||
</Text>
|
||||
<Text as='h2' sx={{ fontWeight: 'normal' }}>
|
||||
{body}
|
||||
</Text>
|
||||
<Text>
|
||||
{date}
|
||||
<Text sx={{ color: '#8492a6' }}>— From Hack Club, to You</Text>
|
||||
</Text>
|
||||
<Text as="h2" sx={{ fontWeight: 'normal' }}>
|
||||
{body}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import styled from '@emotion/styled'
|
||||
import { css, keyframes } from '@emotion/react'
|
||||
import { Box, Container, Flex, Link, Text } from 'theme-ui'
|
||||
import { Box, Container, Flex, Link } from 'theme-ui'
|
||||
import theme from '../lib/theme'
|
||||
import Icon from './icon'
|
||||
import Flag from './flag'
|
||||
|
|
@ -52,18 +52,12 @@ const Root = styled(Box)`
|
|||
}
|
||||
`
|
||||
|
||||
export const Content = styled(Box)`
|
||||
export const Content = styled(Container)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 1024px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-left: ${theme.space[3]}px;
|
||||
@media (min-width: ${theme.breakpoints[2]}em) {
|
||||
padding: 0 ${theme.space[4]}px;
|
||||
}
|
||||
`
|
||||
|
||||
const hoverColor = name =>
|
||||
|
|
@ -99,7 +93,6 @@ const layout = props =>
|
|||
font-weight: bold;
|
||||
font-size: ${theme.fontSizes[2]}px;
|
||||
width: 100vw;
|
||||
max-width: 18rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(48, 48, 48, 0.125);
|
||||
}
|
||||
|
|
@ -114,8 +107,7 @@ const layout = props =>
|
|||
justify-content: flex-end;
|
||||
}
|
||||
a {
|
||||
font-size: ${theme.fontSizes[1]}px;
|
||||
text-transform: uppercase;
|
||||
font-size: 18px;
|
||||
&:hover {
|
||||
color: ${theme.colors[hoverColor(props.color)]};
|
||||
}
|
||||
|
|
@ -125,7 +117,7 @@ const NavBar = styled(Box)`
|
|||
display: none;
|
||||
${layout};
|
||||
a {
|
||||
margin-left: ${theme.space[3]}px;
|
||||
margin-left: ${theme.space[1]}px;
|
||||
padding: ${theme.space[3]}px;
|
||||
text-decoration: none;
|
||||
@media (min-width: 56em) {
|
||||
|
|
@ -135,11 +127,12 @@ const NavBar = styled(Box)`
|
|||
`
|
||||
|
||||
const Navigation = props => (
|
||||
// REMINDER: This should be no more than 7 links :)
|
||||
<NavBar role="navigation" {...props}>
|
||||
<NextLink href="/clubs" passHref>
|
||||
<Link>Clubs</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/hcb" passHref>
|
||||
<NextLink href="/fiscal-sponsorship" passHref>
|
||||
<Link>Fiscal Sponsorship</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/hackathons" passHref>
|
||||
|
|
@ -149,7 +142,7 @@ const Navigation = props => (
|
|||
<Link>Community</Link>
|
||||
</NextLink>
|
||||
<Link href="https://scrapbook.hackclub.com/">Scrapbook</Link>
|
||||
<Link href="https://jams.hackclub.com/">Workshops</Link>
|
||||
<Link href="https://toolbox.hackclub.com/">Toolbox</Link>
|
||||
<NextLink href="/onboard" passHref>
|
||||
<Link>OnBoard</Link>
|
||||
</NextLink>
|
||||
|
|
@ -205,13 +198,13 @@ function Header({ unfixed, color, bgColor, dark, fixed, flag, ...props }) {
|
|||
const baseColor = dark
|
||||
? color || 'white'
|
||||
: color === 'white' && scrolled
|
||||
? 'black'
|
||||
: color
|
||||
? 'black'
|
||||
: color
|
||||
const toggleColor = dark
|
||||
? color || 'snow'
|
||||
: toggled || (color === 'white' && scrolled)
|
||||
? 'slate'
|
||||
: color
|
||||
? 'slate'
|
||||
: color
|
||||
|
||||
return (
|
||||
<Root
|
||||
|
|
|
|||
155
components/onboard/gallery-paginated.js
Normal file
155
components/onboard/gallery-paginated.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import PaginationButtons from './pagination-buttons'
|
||||
import Meta from '@hackclub/meta'
|
||||
import Head from 'next/head'
|
||||
import { Box, Button, Flex, Grid, Heading, Text } from 'theme-ui'
|
||||
import Item from './item'
|
||||
import Nav from '../nav'
|
||||
import { Slide } from 'react-reveal'
|
||||
|
||||
const perPage = 10
|
||||
|
||||
export const GalleryPage = ({ currentPage, itemCount, currentProjects }) => {
|
||||
const spotlightRef = useRef()
|
||||
useEffect(() => {
|
||||
const handler = event => {
|
||||
spotlightRef.current.style.background = `radial-gradient(
|
||||
circle at ${event.pageX}px ${event.pageY}px,
|
||||
rgba(0, 0, 0, 0) 10px,
|
||||
rgba(0, 0, 0, 0.8) 80px
|
||||
)`
|
||||
}
|
||||
window.addEventListener('mousemove', handler)
|
||||
return () => window.removeEventListener('mousemove', handler)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
as={Head}
|
||||
title="Gallery"
|
||||
description="Check out the latest and greatest from the Onboard project."
|
||||
></Meta>
|
||||
<style>{`
|
||||
|
||||
@font-face {
|
||||
font-family: 'Phantom Sans';
|
||||
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff')
|
||||
format('woff'),
|
||||
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
`}</style>
|
||||
<Head></Head>
|
||||
<Nav />
|
||||
<Box
|
||||
as="header"
|
||||
sx={{
|
||||
bg: '#000000',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
|
||||
url('https://cloud-dst3a9oz5-hack-club-bot.vercel.app/0image.png')
|
||||
`,
|
||||
color: '#ffffff',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={spotlightRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
zIndex: 2,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
bg: '#000000',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
sx={{
|
||||
p: 4,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
zIndex: 5,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
p: 4,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
zIndex: 5,
|
||||
margin: '3vh auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Heading as="h1" variant="title" sx={{ textAlign: 'center' }}>
|
||||
Gallery
|
||||
</Heading>
|
||||
<Text as="p" variant="subtitle" sx={{ textAlign: 'center' }}>
|
||||
Check out the latest and greatest from the OnBoard project.
|
||||
</Text>
|
||||
<Flex sx={{ mt: 16, gap: 10, flexDirection: ['column', 'row'] }}>
|
||||
<Button
|
||||
variant="ctaLg"
|
||||
as="a"
|
||||
href="https://hackclub.com/onboard"
|
||||
target="_blank"
|
||||
sx={{
|
||||
background: t => t.util.gx('#60cc38', '#113b11')
|
||||
}}
|
||||
>
|
||||
Make your own!
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
bg: 'white',
|
||||
py: [4, 5],
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<PaginationButtons
|
||||
currentPage={currentPage}
|
||||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
/>
|
||||
<Grid
|
||||
gap={4}
|
||||
columns={[null, 2]}
|
||||
sx={{
|
||||
p: 4,
|
||||
maxWidth: 'copyPlus',
|
||||
mx: 'auto',
|
||||
mt: 4,
|
||||
mb: 5,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{currentProjects.map(project => (
|
||||
<Slide delay={10} up key={project.name}>
|
||||
<Item key={project.name} project={project} />
|
||||
</Slide>
|
||||
))}
|
||||
</Grid>
|
||||
<PaginationButtons
|
||||
currentPage={currentPage}
|
||||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
30
components/onboard/item.js
Normal file
30
components/onboard/item.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Heading, Card } from 'theme-ui'
|
||||
|
||||
const Item = ({ project }) => {
|
||||
const { name, imageTop, galleryURL } = project
|
||||
return (
|
||||
<Card
|
||||
as="a"
|
||||
href={galleryURL}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<img src={imageTop} alt={name} />
|
||||
<Heading
|
||||
as="h2"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
mt: 3
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Heading>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
||||
59
components/onboard/pagination-buttons.js
Normal file
59
components/onboard/pagination-buttons.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Box, Button, Text } from 'theme-ui'
|
||||
|
||||
const PaginationButtons = ({ currentPage, itemCount, perPage }) => {
|
||||
const showPreviousPage = currentPage > 1
|
||||
const showNextPage = itemCount > currentPage * perPage
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 5,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{showPreviousPage && (
|
||||
<Button
|
||||
as="a"
|
||||
href={`/onboard/gallery/${parseInt(currentPage) - 1}`}
|
||||
sx={{
|
||||
bg: 'black',
|
||||
color: 'white',
|
||||
':hover': {
|
||||
bg: 'white',
|
||||
color: 'black'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{'<'}
|
||||
</Button>
|
||||
)}
|
||||
<Text
|
||||
as="span"
|
||||
sx={{
|
||||
mx: 3,
|
||||
color: 'black'
|
||||
}}
|
||||
>
|
||||
{currentPage}
|
||||
</Text>
|
||||
{showNextPage && (
|
||||
<Button
|
||||
as="a"
|
||||
href={`/onboard/gallery/${parseInt(currentPage) + 1}`}
|
||||
sx={{
|
||||
bg: 'black',
|
||||
color: 'white',
|
||||
':hover': {
|
||||
bg: 'white',
|
||||
color: 'black'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{'>'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationButtons
|
||||
251
components/onboard/recap.js
Normal file
251
components/onboard/recap.js
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import usePrefersReducedMotion from '../../lib/use-prefers-reduced-motion'
|
||||
import { Box, Grid } from 'theme-ui'
|
||||
import sleep from '../../lib/sleep'
|
||||
|
||||
const dimBg = '#151515'
|
||||
|
||||
// "LET'S RECAP" pixel art (exported from Piskel)
|
||||
// Original: https://doggo.ninja/fiK0nk.piskel
|
||||
const recapWidth = 71
|
||||
const recapHeight = 10
|
||||
const recapPixels = [
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff,
|
||||
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff,
|
||||
0xffffffff, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0x00000000,
|
||||
0xffffffff, 0xffffffff, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0xffffffff, 0xffffffff
|
||||
]
|
||||
|
||||
const Recap = () => {
|
||||
const prefersReducedMotion = usePrefersReducedMotion()
|
||||
// Fancy lights animation
|
||||
const lightsScrollTrigger = useRef()
|
||||
const lightsAnimated = useRef(false)
|
||||
useEffect(() => {
|
||||
let canceled = false
|
||||
|
||||
const setAtIndex = (i, color) => {
|
||||
if (canceled) return
|
||||
|
||||
// Going outside of React for performance
|
||||
const el = document.getElementById(`pixel-${i}`)
|
||||
if (!el) return
|
||||
|
||||
if (recapPixels[i]) {
|
||||
el.style.background = color
|
||||
el.style.boxShadow = `0 0 10px ${color}`
|
||||
} else {
|
||||
el.style.background = dimBg
|
||||
el.style.boxShadow = 'none'
|
||||
}
|
||||
}
|
||||
const setAll = color => {
|
||||
for (let i = 0; i < recapPixels.length; i++) setAtIndex(i, color)
|
||||
}
|
||||
|
||||
const animate = async () => {
|
||||
if (lightsAnimated.current) return
|
||||
lightsAnimated.current = true
|
||||
|
||||
// Illuminate lights in diagonal lines starting with only top left.
|
||||
for (
|
||||
let curColumn = 0;
|
||||
curColumn < recapWidth + recapHeight;
|
||||
curColumn++
|
||||
) {
|
||||
for (
|
||||
let offset = curColumn;
|
||||
offset >= Math.max(0, curColumn - recapHeight);
|
||||
offset--
|
||||
) {
|
||||
const i = curColumn * recapWidth + offset - offset * recapWidth
|
||||
setAtIndex(i, '#ffffff')
|
||||
if (!recapPixels[i]) await sleep(4)
|
||||
if (canceled) return
|
||||
}
|
||||
// await sleep(2); if (canceled) return
|
||||
}
|
||||
|
||||
// Flash the lights twice
|
||||
await sleep(600)
|
||||
if (canceled) return
|
||||
|
||||
setAll(dimBg)
|
||||
await sleep(80)
|
||||
if (canceled) return
|
||||
|
||||
setAll('#aaaaaa')
|
||||
await sleep(20)
|
||||
if (canceled) return
|
||||
|
||||
setAll(dimBg)
|
||||
await sleep(30)
|
||||
if (canceled) return
|
||||
|
||||
setAll('#aaaaaa')
|
||||
await sleep(100)
|
||||
if (canceled) return
|
||||
|
||||
setAll(dimBg)
|
||||
await sleep(200)
|
||||
if (canceled) return
|
||||
|
||||
// Animate rainbow 2-column increments
|
||||
for (let x = 0; x < recapWidth; x++) {
|
||||
const color = `hsl(${(x * 360) / recapWidth}, 100%, 65%)`
|
||||
for (let y = 0; y < recapHeight; y++) {
|
||||
const i = y * recapWidth + x
|
||||
setAtIndex(i, color)
|
||||
}
|
||||
if (x % 2 === 1) await sleep(35)
|
||||
}
|
||||
}
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
if (!lightsAnimated.current) setAll('#ffffff')
|
||||
return () => (canceled = true)
|
||||
} else {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) animate()
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
)
|
||||
observer.observe(lightsScrollTrigger.current)
|
||||
return () => {
|
||||
canceled = true
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
}, [prefersReducedMotion])
|
||||
|
||||
return (
|
||||
<Grid
|
||||
ref={lightsScrollTrigger}
|
||||
gap={['2px', '3px', '4px']}
|
||||
columns={recapWidth}
|
||||
sx={{ width: '100%', maxWidth: 800 }}
|
||||
>
|
||||
{recapPixels.map((_, i) => (
|
||||
<Box id={`pixel-${i}`} key={i} sx={{ bg: dimBg, paddingTop: '100%' }} />
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Recap
|
||||
41
components/onboard/youtube-video.js
Normal file
41
components/onboard/youtube-video.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useMemo } from 'react'
|
||||
|
||||
const YoutubeVideo = ({
|
||||
'youtube-id': youtubeID = 'dQw4w9WgXcQ',
|
||||
list,
|
||||
title = 'YouTube video player',
|
||||
width,
|
||||
height
|
||||
}) => {
|
||||
const src = useMemo(() => {
|
||||
const url = new URL(`https://www.youtube.com/embed/${youtubeID}`)
|
||||
if (list) {
|
||||
url.searchParams.set('list', list)
|
||||
}
|
||||
return url
|
||||
}, [youtubeID, list])
|
||||
|
||||
const allowlist = [
|
||||
'accelerometer',
|
||||
'autoplay',
|
||||
'clipboard-write',
|
||||
'encrypted-media',
|
||||
'gyroscope',
|
||||
'picture-in-picture',
|
||||
'web-share',
|
||||
'fullscreen'
|
||||
].join('; ')
|
||||
|
||||
return (
|
||||
<iframe
|
||||
src={src}
|
||||
title={title}
|
||||
{...{ width, height }}
|
||||
frameborder="0"
|
||||
allow={allowlist}
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
)
|
||||
}
|
||||
|
||||
export default YoutubeVideo
|
||||
73
components/slack/arrows.js
Normal file
73
components/slack/arrows.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { VisibilityContext } from 'react-horizontal-scrolling-menu'
|
||||
import { useContext } from 'react'
|
||||
import Icon from '@hackclub/icons'
|
||||
import { Box, Link } from 'theme-ui'
|
||||
|
||||
function Arrow({ direction, disabled, onClick }) {
|
||||
return (
|
||||
<Link
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
borderRadius: 100,
|
||||
boxShadow: 'none',
|
||||
backgroundColor: 'black',
|
||||
padding: '8px',
|
||||
cursor: 'pointer',
|
||||
placeItems: 'center',
|
||||
display: 'flex',
|
||||
mr: 2,
|
||||
opacity: disabled ? '0.5' : '1',
|
||||
pointerEvents: disabled ? 'none' : 'auto',
|
||||
transition: 'opacity 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
glyph={direction === 'left' ? 'view-back' : 'view-forward'}
|
||||
size={32}
|
||||
color="white"
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function LeftArrow() {
|
||||
const { scrollPrev } =
|
||||
useContext(VisibilityContext)
|
||||
|
||||
const visibility = useContext(VisibilityContext)
|
||||
const isVisible = visibility.useIsVisible("first", false);
|
||||
|
||||
return (
|
||||
<Arrow direction="left" disabled={isVisible} onClick={() => scrollPrev()} />
|
||||
)
|
||||
}
|
||||
|
||||
export function RightArrow() {
|
||||
const { scrollNext } =
|
||||
useContext(VisibilityContext)
|
||||
|
||||
const visibility = useContext(VisibilityContext)
|
||||
const isVisible = visibility.useIsVisible("last", false);
|
||||
|
||||
return (
|
||||
<Arrow direction="right" disabled={isVisible} onClick={() => scrollNext()} />
|
||||
)
|
||||
}
|
||||
|
||||
export default function Arrows() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
marginBottom: 32,
|
||||
position: 'relative',
|
||||
// this is v janky please ignore, thank you.
|
||||
ml: ['1rem', '1rem', '1rem', 'calc(50vw - 36.5rem)']
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<LeftArrow /> <RightArrow />
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
218
components/slack/channels.js
Normal file
218
components/slack/channels.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
import { Badge, Box, Card, Flex, Grid, Heading, Image, Text } from 'theme-ui'
|
||||
import Icon from '@hackclub/icons'
|
||||
import NextLink from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import fetcher from '../../lib/fetcher'
|
||||
import SlackEvents from './slack-events'
|
||||
|
||||
const withCommas = x => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
|
||||
export default function Channels() {
|
||||
const { data: millionCount } = useSWR(
|
||||
'https://jia.haas.hackclub.com/api/currentNumber',
|
||||
fetcher,
|
||||
{ refreshInterval: 10_000 }
|
||||
)
|
||||
return (
|
||||
<Grid
|
||||
columns={[2, 9, 15]}
|
||||
gap={3}
|
||||
sx={{
|
||||
py: [3, 4],
|
||||
h3: { my: 0 },
|
||||
'> div': {
|
||||
px: [2, 3],
|
||||
py: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gridColumn: ['span 1', 'span 3']
|
||||
},
|
||||
a: {
|
||||
position: 'relative',
|
||||
':hover,:focus': {
|
||||
svg: {
|
||||
transform: 'translateX(28px) translateY(-28px)',
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
svg: {
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
fill: 'white',
|
||||
transition: 'transform 0.25s ease-in-out, opacity 0.25s ease-in-out'
|
||||
},
|
||||
h3: {
|
||||
variant: 'text.headline',
|
||||
color: 'white',
|
||||
lineHeight: 'title',
|
||||
m: 0,
|
||||
'+ p': {
|
||||
mt: 2,
|
||||
color: 'white',
|
||||
opacity: 0.75,
|
||||
fontSize: 2,
|
||||
lineHeight: 'caption'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
as="aside"
|
||||
sx={{
|
||||
gridRow: [null, 'span 2'],
|
||||
gridColumn: ['span 2', 'span 3'],
|
||||
maxHeight: '100%',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
as="h2"
|
||||
variant="subheadline"
|
||||
sx={{
|
||||
mt: 0,
|
||||
mb: 0,
|
||||
color: 'red',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'headline'
|
||||
}}
|
||||
>
|
||||
Live from our Slack <br />
|
||||
</Heading>
|
||||
<Text
|
||||
as="p"
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: 1,
|
||||
fontWeight: 300,
|
||||
fontStyle: 'italic',
|
||||
mb: '16px'
|
||||
}}
|
||||
>
|
||||
Waiting for more messages...
|
||||
</Text>
|
||||
<SlackEvents />
|
||||
</Box>
|
||||
<NextLink href="/ship" passHref>
|
||||
<Card
|
||||
as="a"
|
||||
variant="interactive"
|
||||
sx={{
|
||||
gridColumn: ['span 2', 'span 5'],
|
||||
bg: 'blue',
|
||||
backgroundImage: t => t.util.gx('cyan', 'blue')
|
||||
}}
|
||||
>
|
||||
<Icon glyph="external" size={24} />
|
||||
<Heading as="h3" variant="headline">
|
||||
#ship
|
||||
</Heading>
|
||||
<Text as="p">Launch your latest projects & get feedback</Text>
|
||||
</Card>
|
||||
</NextLink>
|
||||
<Card
|
||||
as="a"
|
||||
href="https://scrapbook.hackclub.com/"
|
||||
variant="interactive"
|
||||
sx={{
|
||||
gridColumn: ['span 2', 'span 5'],
|
||||
bg: 'dark',
|
||||
backgroundImage: t => t.util.gx('yellow', 'orange')
|
||||
}}
|
||||
>
|
||||
<Icon glyph="external" size={24} />
|
||||
<Heading as="h3" variant="headline">
|
||||
#scrapbook
|
||||
</Heading>
|
||||
<Text as="p">A daily diary of project updates</Text>
|
||||
</Card>
|
||||
<Card
|
||||
bg="red"
|
||||
sx={{
|
||||
gridColumn: ['span 2 !important', 'span 2 !important'],
|
||||
gridRow: ['span 1 !important', 'span 3 !important'],
|
||||
writingMode: ['lr-tb', 'tb-rl']
|
||||
}}
|
||||
>
|
||||
<Heading as="h3">#counttoamillion</Heading>
|
||||
<Text as="p" sx={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
We’re at{' '}
|
||||
<Badge
|
||||
variant="outline"
|
||||
as="span"
|
||||
sx={{ ml: [2, 0], mt: [0, 2], px: [2, 0], py: [0, 2] }}
|
||||
>
|
||||
{millionCount ? withCommas(millionCount.number) : '???'}
|
||||
</Badge>
|
||||
!
|
||||
</Text>
|
||||
</Card>
|
||||
<Card backgroundColor="green">
|
||||
<h3 sx={{ color: 'black' }}>#gamedev</h3>
|
||||
</Card>
|
||||
<Card
|
||||
sx={{
|
||||
backgroundImage:
|
||||
'url(https://cloud-n6i5i4zb9-hack-club-bot.vercel.app/12020-07-25_fqxym71bmqjr1d35btawn5q6ph1zt0mk.png)',
|
||||
backgroundColor: '#FEC62E',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% auto',
|
||||
gridColumn: ['span 2', 'span 3 !important', 'span 4 !important'],
|
||||
h3: { opacity: 0 }
|
||||
}}
|
||||
>
|
||||
<h3>#design</h3>
|
||||
</Card>
|
||||
<Card
|
||||
bg="dark"
|
||||
sx={{ h3: { color: 'green', textShadow: '0 0 4px currentColor' } }}
|
||||
>
|
||||
<h3>#code</h3>
|
||||
</Card>
|
||||
<Card
|
||||
sx={{
|
||||
bg: 'dark',
|
||||
backgroundImage:
|
||||
'url(https://cloud-jnfb0t781-hack-club-bot.vercel.app/02020-07-25_r6thfxwv1u0c71uw0qk94juv6fxxjygf.png)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
textShadow: 'text',
|
||||
gridColumn: ['span 2 !important', 'span 4 !important']
|
||||
}}
|
||||
>
|
||||
<h3>#photography</h3>
|
||||
</Card>
|
||||
<Card bg="purple">
|
||||
<Flex>
|
||||
<Text as="h3" sx={{ placeSelf: 'center' }}>
|
||||
#music
|
||||
</Text>
|
||||
<Image
|
||||
src="https://cloud-jd45ga0mv-hack-club-bot.vercel.app/0music.svg"
|
||||
alt="Music notes"
|
||||
sx={{ height: '50px', width: '50px', ml: 2 }}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card bg="orange">
|
||||
<Flex>
|
||||
<Text as="h3" sx={{ placeSelf: 'center' }}>
|
||||
#lounge
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card
|
||||
bg="red"
|
||||
sx={{
|
||||
backgroundImage: ({ colors }) =>
|
||||
`linear-gradient(-184deg, ${colors.red} 0%, ${colors.red} 16.6666%, ${colors.orange} 16.6666%, ${colors.orange} 33.333%, ${colors.yellow} 33.333%, ${colors.yellow} 50%, ${colors.green} 50%, ${colors.green} 66.6666%, ${colors.blue} 66.6666%, ${colors.blue} 83.3333%, ${colors.purple} 83.3333%, ${colors.purple} 100%)`
|
||||
}}
|
||||
>
|
||||
<h3>#lgbtq</h3>
|
||||
</Card>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Heading, Grid } from 'theme-ui'
|
||||
import { Box, Grid, Heading } from 'theme-ui'
|
||||
import SlideUp from '../slide-up'
|
||||
import JoinForm from './join-form'
|
||||
import usePrefersMotion from '../../lib/use-prefers-motion'
|
||||
|
|
@ -118,7 +118,7 @@ const Slack = () => {
|
|||
/>
|
||||
</Box>
|
||||
<Cover />
|
||||
<Content />
|
||||
<Content nameInputRef />
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import {
|
|||
Text,
|
||||
Textarea
|
||||
} from 'theme-ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import useForm from '../../lib/use-form'
|
||||
import Submit from '../submit'
|
||||
import { getCookie, hasCookie } from 'cookies-next'
|
||||
|
||||
const JoinForm = ({ sx = {} }) => {
|
||||
const router = useRouter()
|
||||
import { withRouter } from 'next/router'
|
||||
|
||||
const JoinForm = ({ sx = {}, router }) => {
|
||||
const useWaitlist = process.env.NEXT_PUBLIC_OPEN !== 'true'
|
||||
|
||||
const { status, formProps, useField } = useForm('/api/join/', null, {
|
||||
clearOnSubmit: 60000,
|
||||
method: 'POST',
|
||||
|
|
@ -29,8 +31,6 @@ const JoinForm = ({ sx = {} }) => {
|
|||
})
|
||||
|
||||
const eventReferrer = useField('event').value
|
||||
const isAdult = useField('educationLevel').value === 'tertiary'
|
||||
const useWaitlist = process.env.NEXT_PUBLIC_OPEN !== 'true'
|
||||
|
||||
return (
|
||||
<Card sx={{ maxWidth: 'narrow', mx: 'auto', label: { mb: 3 }, ...sx }}>
|
||||
|
|
@ -58,16 +58,17 @@ const JoinForm = ({ sx = {} }) => {
|
|||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Grid columns={[1, 2]} gap={1} sx={{ columnGap: 2 }}>
|
||||
<Grid columns={[1, 3]} gap={1} sx={{ columnGap: 2 }}>
|
||||
<Label>
|
||||
Full name
|
||||
<Input
|
||||
{...useField('name')}
|
||||
placeholder="Fiona Hackworth"
|
||||
required
|
||||
id="joiner_full_name"
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
<Label sx={{ width: '100%' }}>
|
||||
Email address
|
||||
<Input
|
||||
{...useField('email')}
|
||||
|
|
@ -76,39 +77,17 @@ const JoinForm = ({ sx = {} }) => {
|
|||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
Your home continent
|
||||
Education level
|
||||
<Select
|
||||
{...useField('continent')}
|
||||
{...useField('year')}
|
||||
required
|
||||
sx={{ color: useField('continent').value === '' ? 'muted' : '' }}
|
||||
>
|
||||
<option value="" selected disabled hidden>
|
||||
Select a continent...
|
||||
</option>
|
||||
<option>Africa</option>
|
||||
<option>Asia</option>
|
||||
<option>Europe</option>
|
||||
<option>North America</option>
|
||||
<option value="Australia">Oceania / Australia</option>
|
||||
<option>South America</option>
|
||||
</Select>
|
||||
</Label>
|
||||
<Label>
|
||||
Current education level
|
||||
<Select
|
||||
{...useField('educationLevel')}
|
||||
required
|
||||
sx={{
|
||||
color: useField('educationLevel').value === '' ? 'muted' : ''
|
||||
}}
|
||||
>
|
||||
<option value="" selected disabled hidden>
|
||||
Select a level...
|
||||
</option>
|
||||
<option value="middle">
|
||||
Middle School (approx. 11 to 14)
|
||||
</option>
|
||||
<option value="high">High School (approx. 14 to 18)</option>
|
||||
<option value="middle">Middle School</option>
|
||||
<option value="high">High School</option>
|
||||
<option value="tertiary">Tertiary Education (18+)</option>
|
||||
</Select>
|
||||
</Label>
|
||||
|
|
@ -121,8 +100,7 @@ const JoinForm = ({ sx = {} }) => {
|
|||
required
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{isAdult && (
|
||||
{/*{isAdult && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color="secondary"
|
||||
|
|
@ -136,44 +114,68 @@ const JoinForm = ({ sx = {} }) => {
|
|||
a email at{' '}
|
||||
<Link href="mailto:team@hackclub.com">team@hackclub.com</Link>.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isAdult && (
|
||||
<Box>
|
||||
<Submit
|
||||
status={status}
|
||||
mt={'0px!important'}
|
||||
labels={{
|
||||
default: useWaitlist ? 'Join Waitlist' : 'Get Invite',
|
||||
error: 'Something went wrong',
|
||||
success: useWaitlist
|
||||
? "We'll be in touch soon!"
|
||||
: 'Check your email for invite!'
|
||||
)}*/}
|
||||
<Box>
|
||||
<Submit
|
||||
status={status}
|
||||
mt={'0px!important'}
|
||||
labels={{
|
||||
default: useWaitlist ? 'Join Waitlist' : 'Get Invite',
|
||||
error: 'Something went wrong',
|
||||
success: useWaitlist
|
||||
? "You're on the Waitlist!"
|
||||
: 'Check your email for invite!'
|
||||
}}
|
||||
disabled={status === 'loading' || status === 'success'}
|
||||
/>
|
||||
{status === 'success' && !useWaitlist && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color="secondary"
|
||||
as="div"
|
||||
sx={{
|
||||
maxWidth: '600px',
|
||||
textAlign: 'center',
|
||||
mt: 3
|
||||
}}
|
||||
disabled={status === 'loading' || status === 'success'}
|
||||
/>
|
||||
{status === 'success' && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color="secondary"
|
||||
as="div"
|
||||
sx={{
|
||||
maxWidth: '600px',
|
||||
textAlign: 'center',
|
||||
mt: 3
|
||||
}}
|
||||
>
|
||||
Search for "Slack" in your mailbox! Not there?{' '}
|
||||
<Link href="mailto:slack@hackclub.com" sx={{ ml: 1 }}>
|
||||
Send us an email
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
Search for "Slack" in your mailbox! Not there?{' '}
|
||||
<Link href="mailto:slack@hackclub.com" sx={{ ml: 1 }}>
|
||||
Send us an email
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default JoinForm
|
||||
function AdultChecker() {
|
||||
return (
|
||||
<Label>
|
||||
Birthday
|
||||
<Select
|
||||
required
|
||||
onChange={handleYearChange}
|
||||
sx={{ color: useField('continent').value === '' ? 'muted' : '' }}
|
||||
>
|
||||
<option value="" selected disabled hidden>
|
||||
Year
|
||||
</option>
|
||||
<option value="middle" disabled hidden>
|
||||
Hi, I'm hidden!
|
||||
</option>
|
||||
{years
|
||||
.map(year => (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
))
|
||||
.reverse()}
|
||||
</Select>
|
||||
</Label>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(JoinForm)
|
||||
|
|
|
|||
62
components/slack/join.js
Normal file
62
components/slack/join.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Box, Image, Link, Text } from 'theme-ui'
|
||||
import Icon from '@hackclub/icons'
|
||||
|
||||
export default function Join() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: '#F9FAFC',
|
||||
mt: '2rem',
|
||||
borderRadius: 12,
|
||||
overflowX: 'hidden',
|
||||
height: ['', '30rem'],
|
||||
paddingTop: ['2rem', 0],
|
||||
display: ['grid', 'grid', 'flex']
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: ['100%', '100%', '50%'],
|
||||
paddingX: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Text
|
||||
as="h1"
|
||||
variant="title"
|
||||
sx={{ mb: 3, fontSize: ['36px', '48px', '56px'] }}
|
||||
>
|
||||
Discover the Hack Club Slack
|
||||
</Text>
|
||||
<Link
|
||||
href="#"
|
||||
sx={{
|
||||
mb: 3,
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
fontSize: '24px',
|
||||
fontWeight: 500,
|
||||
placeItems: 'center',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
I'm ready to join <Icon glyph="view-forward" size={24} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
<Image
|
||||
src="https://cloud-j0p07nviw-hack-club-bot.vercel.app/0image.png"
|
||||
sx={{
|
||||
width: ['100%', '100%', '50%'],
|
||||
height: ['100%', '100%', '30rem'],
|
||||
objectFit: 'cover',
|
||||
position: 'relative',
|
||||
top: 0
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
32
components/slack/preventScroll.js
Normal file
32
components/slack/preventScroll.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const preventDefault = ev => {
|
||||
if (ev.preventDefault) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
ev.returnValue = false
|
||||
}
|
||||
|
||||
const enableBodyScroll = () => {
|
||||
document && document.removeEventListener('wheel', preventDefault, false)
|
||||
}
|
||||
const disableBodyScroll = () => {
|
||||
document &&
|
||||
document.addEventListener('wheel', preventDefault, {
|
||||
passive: false
|
||||
})
|
||||
}
|
||||
|
||||
export default function usePreventScroll() {
|
||||
const [hidden, setHidden] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
hidden ? disableBodyScroll() : enableBodyScroll()
|
||||
|
||||
return enableBodyScroll
|
||||
}, [hidden])
|
||||
|
||||
const disableScroll = useCallback(() => setHidden(true), [])
|
||||
const enableScroll = useCallback(() => setHidden(false), [])
|
||||
return { disableScroll, enableScroll }
|
||||
}
|
||||
67
components/slack/project.js
Normal file
67
components/slack/project.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Box, Grid, Image, Text } from 'theme-ui'
|
||||
|
||||
export default function Project({ title, description, color, img, itemId }) {
|
||||
return (
|
||||
<Grid
|
||||
sx={{
|
||||
borderRadius: 12,
|
||||
gridTemplateColumns: 'auto ',
|
||||
my: '2rem',
|
||||
backgroundImage: t =>
|
||||
`linear-gradient(to bottom, ${color[0]}, ${color[1]})`,
|
||||
color: 'white',
|
||||
overflow: 'clip',
|
||||
width: ['100vw', '40rem', '50rem', '70rem'],
|
||||
height: ['25rem', '40rem'],
|
||||
transformOrigin: 'center',
|
||||
mr: 16,
|
||||
// this is v janky please ignore, thank you.
|
||||
ml: ['1rem', '1rem', '1rem', `${itemId === 0 && 'calc(50vw - 36.5rem)'}`]
|
||||
}}
|
||||
itemId={itemId}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
paddingX: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
placeItems: 'center',
|
||||
height: ['full', '12.5rem', '20rem'],
|
||||
placeSelf: 'center',
|
||||
placeContent: 'end'
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="h1"
|
||||
variant="title"
|
||||
sx={{
|
||||
width: ['full', 'copyUltra'],
|
||||
textAlign: 'center',
|
||||
fontSize: ['36px', 6]
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
as="p"
|
||||
variant="subtitle"
|
||||
sx={{
|
||||
width: ['full', 'copyPlus'],
|
||||
opacity: '75%',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</Box>
|
||||
<Image
|
||||
src={`/slack/${img}.png`}
|
||||
sx={{
|
||||
visibility: ['visible'],
|
||||
height: ['100%'],
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
87
components/slack/projects.js
Normal file
87
components/slack/projects.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
const projects = [
|
||||
{
|
||||
title: 'Brainwave device for thought-based computer interaction.',
|
||||
description:
|
||||
'The team of teens behind Monolith BCI is building both the hardware and software for a brainwave reading device to interact with computers using thoughts',
|
||||
img: 'bci',
|
||||
color: ['#ec3750', '#F58695'],
|
||||
itemId: 0
|
||||
},
|
||||
{
|
||||
title: 'A free domain service.',
|
||||
description:
|
||||
'The teenage hackers behind Oblong are building a free domain service and non-profit to break down the barriers of entry for building a website.',
|
||||
img: 'oblong',
|
||||
color: ['#ff8c37', '#F2A510'],
|
||||
itemId: 1
|
||||
},
|
||||
{
|
||||
title: 'An open source VPN.',
|
||||
description:
|
||||
'Lead by an ex-Apple engineer, the team behind Burrow is building an open source VPN to burrow through school firewalls and keep your data safe.',
|
||||
img: 'burrow',
|
||||
color: ['#f1c40f', '#FAE078'],
|
||||
itemId: 2
|
||||
},
|
||||
{
|
||||
title: 'Free compute infrastructure.',
|
||||
description:
|
||||
"The team behind Nest is building a free compute infrastructure for high schoolers to run their code on. It's like AWS, but free and for students.",
|
||||
img: 'nest',
|
||||
color: ['#33d6a6', '#51F5C5'],
|
||||
itemId: 3
|
||||
},
|
||||
{
|
||||
title: 'A chat app and cell phone carrier.',
|
||||
description:
|
||||
'The teenage PurpleBubble team are building a private, secure and open source chat app and cell phone carrier',
|
||||
img: 'purplebubble',
|
||||
color: ['#5bc0de', '#88e5f8'],
|
||||
itemId: 4
|
||||
}
|
||||
]
|
||||
|
||||
export default projects
|
||||
|
||||
/*
|
||||
Here lies the horizontal scroll menu. It's not currently in use, but it's here if anyone every wants it! - Toby
|
||||
const triggerRef = useRef(null)
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
useEffect(() => {
|
||||
const sections = gsap.utils.toArray('.project')
|
||||
|
||||
const projects = gsap.to(sections, {
|
||||
xPercent: -100 * (sections.length - 1),
|
||||
ease: 'none',
|
||||
duration: 1,
|
||||
scrollTrigger: {
|
||||
trigger: triggerRef.current,
|
||||
start: 'top top',
|
||||
end: () => '+=' + document.querySelector('.container').offsetWidth,
|
||||
scrub: 1.25,
|
||||
pin: true,
|
||||
anticipatePin: 1,
|
||||
invalidateOnRefresh: true,
|
||||
snap: 0.5 * (1 / (sections.length - 1))
|
||||
},
|
||||
onUpdate: function () {
|
||||
const progress = this.progress()
|
||||
if (progress < 1 / 6) {
|
||||
setColors(['red', '#F58695'])
|
||||
} else if (progress < 2 / 6) {
|
||||
setColors(['orange', '#F2A510'])
|
||||
} else if (progress < 3 / 6) {
|
||||
setColors(['yellow', '#FAE078'])
|
||||
} else if (progress < 4 / 6) {
|
||||
setColors(['green', '#51F5C5'])
|
||||
} else if (progress < 5 / 6) {
|
||||
setColors(['cyan', '#88e5f8'])
|
||||
} else {
|
||||
setColors(['purple', '#d786ff'])
|
||||
}
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
projects.kill()
|
||||
}
|
||||
}, [])*/
|
||||
24
components/slack/quote.js
Normal file
24
components/slack/quote.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Box, Flex, Image, Text } from 'theme-ui'
|
||||
|
||||
export default function Quote({ text, person, age, location, img }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: '32px',
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#F9FAFC',
|
||||
width: 'full'
|
||||
}}
|
||||
>
|
||||
<Text as="h3" variant="title" sx={{ mb: 3, fontSize: ['36px', 4, 5] }}>
|
||||
"{text}"
|
||||
</Text>
|
||||
<Flex sx={{ gap: '8px' }}>
|
||||
<Image src={img} sx={{ height: 24, width: 24, borderRadius: 100 }} />
|
||||
<Text as="h3" sx={{ fontWeight: 400 }}>
|
||||
{person}, {age} from {location}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ const Stat = ({
|
|||
as="span"
|
||||
sx={{
|
||||
color,
|
||||
fontSize: lg ? [5, 6, 7] : sm ? [3, 4] : [4, 5, 6],
|
||||
fontSize: lg ? [5, 6, 7] : sm ? [3, 4] : [5, 6],
|
||||
fontWeight: 'heading',
|
||||
letterSpacing: 'title',
|
||||
my: 0
|
||||
|
|
@ -78,7 +78,7 @@ const Stat = ({
|
|||
as="span"
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: lg ? [1, 2, 3] : [0, 1],
|
||||
fontSize: lg ? [1, 2, 3] : 1,
|
||||
letterSpacing: 'headline',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const Submit = ({
|
|||
}) => (
|
||||
<Button
|
||||
as="button"
|
||||
type="submit"
|
||||
type={'submit' || props.type}
|
||||
sx={{
|
||||
py: 2,
|
||||
px: 3,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import VanillaTilt from 'vanilla-tilt'
|
||||
import useMedia from '../lib/use-media'
|
||||
|
||||
// NOTE(@lachlanjc): only pass one child!
|
||||
const Tilt = ({ options = {}, children, ...props }) => {
|
||||
const root = useRef(null)
|
||||
|
||||
const { matches: enabled } = useMedia('(hover: hover)')
|
||||
|
||||
useEffect(() => {
|
||||
VanillaTilt.init(root.current, {
|
||||
max: 7.5,
|
||||
scale: 1.05,
|
||||
speed: 400,
|
||||
glare: true,
|
||||
'max-glare': 0.25,
|
||||
gyroscope: false,
|
||||
...options
|
||||
})
|
||||
}, [options])
|
||||
if (enabled) {
|
||||
VanillaTilt.init(root.current, {
|
||||
max: 7.5,
|
||||
scale: 1.05,
|
||||
speed: 400,
|
||||
glare: true,
|
||||
'max-glare': 0.25,
|
||||
gyroscope: false,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return () => root.current?.vanillaTilt?.destroy()
|
||||
}, [options, enabled])
|
||||
|
||||
return React.cloneElement(children, { ref: root })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export default function Signup() {
|
|||
const handleSubmit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
await fetch('/api/hcb/demo', {
|
||||
await fetch('/api/fiscal-sponsorship/demo', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
eventName,
|
||||
|
|
@ -109,7 +109,7 @@ export default function Signup() {
|
|||
<Base
|
||||
id="form"
|
||||
method="POST"
|
||||
action="/api/hcb/demo"
|
||||
action="/api/fiscal-sponsorship/demo"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Field
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
async function getOrRefreshToken() {
|
||||
// Get the token from localStorage
|
||||
const token = JSON.parse(localStorage.getItem('mapkit-token'))
|
||||
if (token) {
|
||||
// If the token is still valid, return it
|
||||
if (Date.now() < token.refreshBefore) return token.accessToken
|
||||
} else {
|
||||
// The token is invalid or doesn't exist, so get a new one
|
||||
|
||||
// This is a MapKit master token, restricted to https://hackclub.com
|
||||
const master =
|
||||
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkNSQkg2S1VLTEIifQ.eyJpc3MiOiJQNlBWMlI5NDQzIiwiaWF0IjoxNjc5NjY3NjIyLCJleHAiOjE3MTEyMzg0MDB9.E6g9QPdbEWLgF6tdcL0YfX8NescYnwKhQpXdiyRitNm7-Oot-3VH0ze9xUd8xkOzuS_-7KeWy4bfYTD-2yX7Sg'
|
||||
|
||||
// Get a MapKit server token
|
||||
const res = await fetch('https://maps-api.apple.com/v1/token', {
|
||||
headers: { Authorization: `Bearer ${master}` }
|
||||
})
|
||||
const resJson = await res.json()
|
||||
|
||||
// Set the token's expiration time to 10 seconds before the actual expiration time
|
||||
resJson.refreshBefore =
|
||||
Date.now() + resJson.expiresInSeconds * 1_000 - 10_000
|
||||
|
||||
// Save the token to localStorage
|
||||
localStorage.setItem('mapkit-token', JSON.stringify(resJson))
|
||||
|
||||
return resJson.accessToken
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Limit the number of retries
|
||||
|
||||
export async function search(query) {
|
||||
if (!query || !query.trim()) return
|
||||
|
||||
const token = await getOrRefreshToken()
|
||||
|
||||
const res = await fetch(`https://maps-api.apple.com/v1/search?q=${query}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
const resJson = await res.json()
|
||||
|
||||
if (resJson.error) {
|
||||
if (resJson.error.message === 'Not Authorized') {
|
||||
// The token is invalid, so remove it from localStorage
|
||||
localStorage.removeItem('mapkit-token')
|
||||
|
||||
// Try again
|
||||
console.warn('MapKit token expired, refreshing')
|
||||
return search(query)
|
||||
} else {
|
||||
throw new Error(resJson.error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return resJson
|
||||
}
|
||||
|
||||
export async function geocode(query) {
|
||||
if (!query || !query.trim()) return
|
||||
|
||||
const token = await getOrRefreshToken()
|
||||
|
||||
const res = await fetch(`https://maps-api.apple.com/v1/geocode?q=${query}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
const resJson = await res.json()
|
||||
|
||||
if (resJson.error) {
|
||||
if (resJson.error.message === 'Not Authorized') {
|
||||
// The token is invalid, so remove it from localStorage
|
||||
localStorage.removeItem('mapkit-token')
|
||||
|
||||
// Try again
|
||||
console.warn('MapKit token expired, refreshing')
|
||||
return geocode(query)
|
||||
} else {
|
||||
throw new Error(resJson.error.message)
|
||||
}
|
||||
}
|
||||
|
||||
return resJson
|
||||
}
|
||||
4
lib/sleep.js
Normal file
4
lib/sleep.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Beloved classic utility function :3
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export default sleep
|
||||
|
|
@ -54,12 +54,12 @@ const useForm = (
|
|||
if (callback) callback(r)
|
||||
if (options.clearOnSubmit) {
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
setStatus('default');
|
||||
}, options.clearOnSubmit);
|
||||
setData({})
|
||||
setStatus('default')
|
||||
}, options.clearOnSubmit)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
setData({})
|
||||
setStatus('default')
|
||||
}, 3500)
|
||||
}
|
||||
|
|
|
|||
16
lib/use-media.js
Normal file
16
lib/use-media.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function useMedia(query) {
|
||||
const [matches, setMatches] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = e => setMatches(e.matches)
|
||||
const mq = window.matchMedia(query)
|
||||
setMatches(mq.matches)
|
||||
mq.addEventListener('change', onChange)
|
||||
|
||||
return () => mq.removeEventListener('change', onChange)
|
||||
}, [query])
|
||||
|
||||
return { matches }
|
||||
}
|
||||
|
|
@ -14,9 +14,8 @@ export function middleware(request) {
|
|||
}
|
||||
|
||||
if (request.nextUrl.pathname === '/donate/') {
|
||||
return NextResponse.redirect('https://hackclub.com/philanthropy/');
|
||||
return NextResponse.redirect('https://hackclub.com/philanthropy/')
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const nextConfig = {
|
|||
'scrapbook.hackclub.com',
|
||||
'assets.hackclub.com',
|
||||
'v5.airtableusercontent.com',
|
||||
''
|
||||
'hcb.hackclub.com'
|
||||
],
|
||||
remotePatterns: [
|
||||
{
|
||||
|
|
@ -34,6 +34,16 @@ const nextConfig = {
|
|||
destination: '/hcb/:path*',
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: '/hcb/fiscal-sponsorship/',
|
||||
destination: '/fiscal-sponsorship/about/',
|
||||
permanent: false
|
||||
},
|
||||
{
|
||||
source: '/hcb/:path*',
|
||||
destination: '/fiscal-sponsorship/:path*',
|
||||
permanent: false
|
||||
},
|
||||
{ source: '/grant/', destination: '/hackathons/grant', permanent: false },
|
||||
{
|
||||
source: '/sprig/',
|
||||
|
|
@ -175,6 +185,11 @@ const nextConfig = {
|
|||
source: '/daysofservice/',
|
||||
destination: 'https://daysofservice.hackclub.com',
|
||||
permanent: true
|
||||
},
|
||||
{
|
||||
source: '/blot/',
|
||||
destination: 'https://blot.hackclub.com',
|
||||
permanent: false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -278,6 +293,15 @@ const nextConfig = {
|
|||
},
|
||||
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' }
|
||||
]
|
||||
},
|
||||
{
|
||||
source: '/api/onboard/svg/(.+)',
|
||||
headers: [
|
||||
{
|
||||
key: 'content-type',
|
||||
value: 'image/svg+xml'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
45
package.json
45
package.json
|
|
@ -12,29 +12,37 @@
|
|||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.8",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@apollo/client": "^3.9.9",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@github/time-elements": "^4.0.0",
|
||||
"@hackclub/icons": "^0.0.14",
|
||||
"@hackclub/markdown": "^0.0.43",
|
||||
"@hackclub/meta": "1.1.32",
|
||||
"@hackclub/theme": "^0.3.3",
|
||||
"@mdx-js/loader": "^1.6.22",
|
||||
"@next/mdx": "^14.0.3",
|
||||
"@next/mdx": "^14.1.0",
|
||||
"@octokit/auth-app": "^6.0.1",
|
||||
"@octokit/core": "^5.0.1",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@sendgrid/mail": "^8.1.1",
|
||||
"@tracespace/core": "^5.0.0-alpha.0",
|
||||
"@tracespace/identify-layers": "^5.0.0-alpha.0",
|
||||
"@tracespace/parser": "^5.0.0-next.0",
|
||||
"@tracespace/plotter": "^5.0.0-alpha.0",
|
||||
"@tracespace/renderer": "^5.0.0-alpha.0",
|
||||
"@tracespace/xml-id": "^4.2.7",
|
||||
"add": "^2.0.6",
|
||||
"airtable": "^0.12.2",
|
||||
"airtable-plus": "^1.0.4",
|
||||
"animated-value": "^0.2.4",
|
||||
"animejs": "^3.2.2",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.6.7",
|
||||
"cookies-next": "^4.0.0",
|
||||
"country-list": "^2.3.0",
|
||||
"country-list-js": "^3.1.8",
|
||||
"create-react-class": "^15.7.0",
|
||||
"cursor-effects": "^1.0.12",
|
||||
"cursor-effects": "^1.0.15",
|
||||
"date-fns": "^2.30.0",
|
||||
"devtools-detect": "^4.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
@ -42,33 +50,42 @@
|
|||
"geopattern": "^1.2.3",
|
||||
"globby": "^11.0.4",
|
||||
"graphql": "^16.8.1",
|
||||
"js-confetti": "^0.11.0",
|
||||
"js-confetti": "^0.12.0",
|
||||
"jszip": "^3.10.1",
|
||||
"jszip-utils": "^0.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"million": "^2.6.4",
|
||||
"next": "^12.3.1",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-transpile-modules": "^10.0.1",
|
||||
"nextjs-current-url": "^1.0.3",
|
||||
"pcb-stackup": "^4.2.8",
|
||||
"react": "^17.0.2",
|
||||
"react-before-after-slider-component": "^1.1.8",
|
||||
"react-datepicker": "^4.24.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-horizontal-scrolling-menu": "^6.0.2",
|
||||
"react-konami-code": "^2.3.0",
|
||||
"react-marquee-slider": "^1.1.5",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"react-page-visibility": "^7.0.0",
|
||||
"react-relative-time": "^0.0.9",
|
||||
"react-reveal": "^1.2.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-scrolllock": "^5.0.1",
|
||||
"react-snowfall": "^1.2.1",
|
||||
"react-tappable": "^1.0.4",
|
||||
"react-ticker": "^1.3.2",
|
||||
"react-tooltip": "^4.5.1",
|
||||
"react-tsparticles": "^2.12.2",
|
||||
"react-use-websocket": "^4.5.0",
|
||||
"react-type-animation": "^3.2.0",
|
||||
"react-use-websocket": "^4.7.0",
|
||||
"react-wrap-balancer": "^1.1.0",
|
||||
"recharts": "2.1.12",
|
||||
"styled-components": "^6.1.1",
|
||||
"recharts": "2.12.2",
|
||||
"remark": "^15.0.1",
|
||||
"remark-html": "^16.0.1",
|
||||
"styled-components": "^6.1.8",
|
||||
"swr": "^2.2.4",
|
||||
"theme-ui": "^0.14",
|
||||
"tinytime": "^0.2.6",
|
||||
|
|
@ -77,8 +94,8 @@
|
|||
"yarn": "^1.22.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.55.0",
|
||||
"eslint-config-next": "14.0.3",
|
||||
"prettier": "^3.0.3"
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"prettier": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
pages/api/bin/rsvp.js
Normal file
31
pages/api/bin/rsvp.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// https://airtable.com/appKjALSnOoA0EmPk/tblYYhxN9TaPPMMRV/viwJFvTlmRNHj0Toh?blocks=hide
|
||||
import AirtablePlus from 'airtable-plus'
|
||||
|
||||
const rsvpsTable = new AirtablePlus({
|
||||
apiKey: process.env.AIRTABLE_API_KEY,
|
||||
baseID: 'appKjALSnOoA0EmPk',
|
||||
tableName: 'RSVPs'
|
||||
})
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const { email, high_schooler, stickers, address_line_1, address_zip } =
|
||||
req.body
|
||||
|
||||
const fields = {
|
||||
Email: email,
|
||||
'High Schooler': '' + high_schooler,
|
||||
Stickers: '' + stickers,
|
||||
'Address (line 1)': address_line_1,
|
||||
'Address (zip code)': address_zip
|
||||
}
|
||||
|
||||
await rsvpsTable.create(fields)
|
||||
|
||||
res.status(200).json({ success: true })
|
||||
} else if (req.method == 'GET') {
|
||||
const result = await rsvpsTable.read()
|
||||
|
||||
res.status(200).json(result.length)
|
||||
}
|
||||
}
|
||||
12
pages/api/channels/get.js
Normal file
12
pages/api/channels/get.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default async function handler(req, res) {
|
||||
const channelDataReq = await fetch(
|
||||
`https://slack.com/api/conversations.info?channel=${req.body.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log(channelDataReq)
|
||||
return res.status(200).send(channelDataReq)
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import AirtablePlus from 'airtable-plus'
|
||||
import { getCode } from 'country-list'
|
||||
|
||||
const applicationsTable = new AirtablePlus({
|
||||
baseID: 'apppALh5FEOKkhjLR',
|
||||
|
|
@ -14,8 +15,8 @@ export default async function handler(req, res) {
|
|||
body: JSON.stringify({
|
||||
email: data.userEmail,
|
||||
name: data.eventName,
|
||||
country: getCode(data.eventLocation) || '',
|
||||
transparent: data.transparent,
|
||||
// country: data.eventCountryCode,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
export async function getGames() {
|
||||
let games = await fetch(
|
||||
'https://raw.githubusercontent.com/hackclub/sprig/main/games/metadata.json'
|
||||
'https://sprig.hackclub.com/api/gallery?new'
|
||||
).then(res => res.json())
|
||||
games = games
|
||||
.sort((a, b) => new Date(b.addedOn) - new Date(a.addedOn))
|
||||
.slice(-4)
|
||||
|
||||
return games
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const getMessage = (type, payload, repo) => {
|
|||
const getUrl = (type, payload, repo) => {
|
||||
switch (type) {
|
||||
case 'PushEvent':
|
||||
return payload.commits?.[0].url
|
||||
return payload.commits?.[0]?.url
|
||||
? normalizeGitHubCommitUrl(payload.commits[0].url)
|
||||
: 'https://github.com/hackclub'
|
||||
case 'PullRequestEvent':
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import AirtablePlus from 'airtable-plus'
|
||||
|
||||
const sgMail = require('@sendgrid/mail')
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
|
||||
const joinTable = new AirtablePlus({
|
||||
apiKey: process.env.AIRTABLE_API_KEY,
|
||||
baseID: 'appaqcJtn33vb59Au',
|
||||
|
|
@ -36,15 +40,29 @@ export default async function handler(req, res) {
|
|||
error: '*PUT that request away!* (Method not allowed, use POST)'
|
||||
})
|
||||
case 'POST':
|
||||
console.log('POST request received. WOO!')
|
||||
break
|
||||
default:
|
||||
return res.status(405).json({ error: 'Method not allowed, use POST' })
|
||||
}
|
||||
|
||||
const data = req.body || {}
|
||||
let data = req.body || {}
|
||||
const open = process.env.NEXT_PUBLIC_OPEN === 'true'
|
||||
const waitlist = !open
|
||||
const isAdult = data.educationLevel === 'tertiary'
|
||||
const isAdult = data.year === 'tertiary'
|
||||
|
||||
if (isAdult) {
|
||||
const mail = {
|
||||
to: data.email,
|
||||
from: 'Hack Club Slack <team@hackclub.com>',
|
||||
subject: 'Slack Waiting List update',
|
||||
text: 'Hello world',
|
||||
html: "Hey! Thanks for your interest in the Hack Club Slack. <br/> Our online community is for minors, and thus only pre-approved adults are permitted.\nTo find out more about what all we do, check out our <a href='https://github.com/hackclub'>Github</a>. If you're a parent or educator & want to talk to a member of our team, send us a email at <a href='mailto:team@hackclub.com'>team@hackclub.com</a>.",
|
||||
imageUrl: 'https://assets.hackclub.com/icon-rounded.png'
|
||||
}
|
||||
|
||||
sgMail.send(mail)
|
||||
}
|
||||
|
||||
const secrets = (process.env.NAUGHTY || '').split(',')
|
||||
|
||||
|
|
@ -58,7 +76,7 @@ export default async function handler(req, res) {
|
|||
const airtablePromise = joinTable.create({
|
||||
'Full Name': data.name,
|
||||
'Email Address': data.email,
|
||||
Student: !isAdult,
|
||||
Minor: !isAdult,
|
||||
Reason: data.reason,
|
||||
Invited: !waitlist,
|
||||
Club: data.club ? data.club : '',
|
||||
|
|
@ -76,7 +94,7 @@ export default async function handler(req, res) {
|
|||
const slackPromise = postData(
|
||||
'https://toriel.hackclub.com/slack-invite',
|
||||
{
|
||||
email: data.email,
|
||||
email: !isAdult ? data.email : null,
|
||||
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
|
||||
continent: data.continent,
|
||||
teen: !isAdult,
|
||||
|
|
@ -93,6 +111,7 @@ export default async function handler(req, res) {
|
|||
res.json({ status: 'success', message: 'You’ve been invited to Slack!' })
|
||||
)
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
res.status(500).json({ error })
|
||||
})
|
||||
}
|
||||
|
|
|
|||
57
pages/api/onboard/p/[project]/index.js
Normal file
57
pages/api/onboard/p/[project]/index.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// return a project's metadata
|
||||
|
||||
import { getAllOnboardProjects } from '..'
|
||||
|
||||
async function getReadmeData(url) {
|
||||
const readme = await fetch(url)
|
||||
const text = await readme.text()
|
||||
// parse YAML frontmatter
|
||||
const lines = text.split('\n')
|
||||
const frontmatter = {}
|
||||
let i = 0
|
||||
for (; i < lines.length; i++) {
|
||||
if (lines[i].startsWith('---')) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for (i++; i < lines.length; i++) {
|
||||
if (lines[i].startsWith('---')) {
|
||||
break
|
||||
}
|
||||
const [key, value] = lines[i].split(': ')
|
||||
frontmatter[key] = value || null
|
||||
}
|
||||
const description = lines.slice(i + 1).join('\n')
|
||||
return {
|
||||
frontmatter,
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
export const getOnboardProject = async name => {
|
||||
// this is not performant to call all projects every time, but we're doing it for now while things load quickly enough
|
||||
// TODO: Speed this up
|
||||
try {
|
||||
const project = (await getAllOnboardProjects()).find(p => p.name === name)
|
||||
const readmeData = await getReadmeData(project.readmeURL)
|
||||
|
||||
const result = { ...project, readmeData }
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { name } = req.query
|
||||
if (!name) {
|
||||
return res.status(400).json({ status: 400, error: 'Must provide name' })
|
||||
}
|
||||
const project = await getOnboardProject(name)
|
||||
if (!project) {
|
||||
return res.status(404).json({ status: 404, error: 'Project not found' })
|
||||
}
|
||||
return res.status(200).json(project)
|
||||
}
|
||||
12
pages/api/onboard/p/count.js
Normal file
12
pages/api/onboard/p/count.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { getAllOnboardProjects } from '.'
|
||||
|
||||
export async function onboardProjectCount() {
|
||||
const projects = await getAllOnboardProjects()
|
||||
return projects.length
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const count = await onboardProjectCount()
|
||||
|
||||
res.json({ count })
|
||||
}
|
||||
62
pages/api/onboard/p/index.js
Normal file
62
pages/api/onboard/p/index.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// returns a list of all projects
|
||||
|
||||
export const getAllOnboardProjects = async () => {
|
||||
const url = 'https://api.github.com/repos/hackclub/onboard/contents/projects'
|
||||
const fetchOptions = {}
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
// this field is optional because we do heavy caching in production, but nice to have for local development
|
||||
fetchOptions.headers = {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
|
||||
}
|
||||
}
|
||||
|
||||
let res;
|
||||
try { res = await fetch(url, fetchOptions).then(r => r.json()) }
|
||||
catch (e) {
|
||||
console.error('Failed to fetch projects from GitHub', e)
|
||||
return []
|
||||
}
|
||||
|
||||
if (res.message && res.message.startsWith('API rate limit exceeded')) {
|
||||
console.error('GitHub API rate limit exceeded')
|
||||
return []
|
||||
}
|
||||
if(!res) return []
|
||||
|
||||
const projects = []
|
||||
|
||||
res.forEach(p => {
|
||||
if (p.type !== 'dir') {
|
||||
return
|
||||
}
|
||||
if (p.name[0] === '.') {
|
||||
return
|
||||
}
|
||||
if (p.name[0] === '!') {
|
||||
return
|
||||
}
|
||||
|
||||
const projectData = {
|
||||
name: p.name,
|
||||
url: `https://github.com/hackclub/OnBoard/tree/main/projects/${p.name}/README.md`,
|
||||
galleryURL: `/onboard/board/${p.name}`,
|
||||
githubURL: p.html_url,
|
||||
readmeURL: `https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${p.name}/README.md`,
|
||||
schematicURL: `https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${p.name}/schematic.pdf`,
|
||||
gerberURL: `https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${p.name}/gerber.zip`
|
||||
}
|
||||
|
||||
projectData.imageTop = `/api/onboard/svg/${encodeURIComponent(projectData.gerberURL)}/top`
|
||||
projectData.imageBottom = `/api/onboard/svg/${encodeURIComponent(projectData.gerberURL)}/bottom`
|
||||
|
||||
projects.push(projectData)
|
||||
})
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const projects = await getAllOnboardProjects()
|
||||
|
||||
res.json(projects)
|
||||
}
|
||||
17
pages/api/onboard/svg/[board_url]/bottom.js
Normal file
17
pages/api/onboard/svg/[board_url]/bottom.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip/bottom
|
||||
|
||||
import { gerberToSvg } from '.'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { board_url } = req.query
|
||||
if (!board_url) {
|
||||
return res.status(404).json({ status: 404, error: 'Must provide file' })
|
||||
}
|
||||
// ensure valid file url is included
|
||||
const parsed_url = new URL(decodeURI(board_url))
|
||||
if (!parsed_url) {
|
||||
return res.status(404).json({ status: 404, error: 'Invalid file' })
|
||||
}
|
||||
const svg = await gerberToSvg(parsed_url)
|
||||
return res.status(200).send(svg.bottom)
|
||||
}
|
||||
98
pages/api/onboard/svg/[board_url]/index.js
Normal file
98
pages/api/onboard/svg/[board_url]/index.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import JSZip from 'jszip'
|
||||
import {
|
||||
read,
|
||||
plot,
|
||||
renderLayers,
|
||||
renderBoard,
|
||||
stringifySvg
|
||||
} from '@tracespace/core'
|
||||
import fs from 'fs'
|
||||
|
||||
export const gerberToSvg = async gerberURL => {
|
||||
const data = await fetch(gerberURL).then(res => {
|
||||
return { status: res.status, arrayBuffer: res.arrayBuffer()}
|
||||
})
|
||||
if (data.status !== 200) {
|
||||
return { status: data.status, error: 'Failed to fetch gerber file' }
|
||||
}
|
||||
const files = []
|
||||
const zip = new JSZip()
|
||||
|
||||
const zippedData = await new Promise((resolve, _reject) => {
|
||||
zip.loadAsync(data.arrayBuffer).then(resolve, e => {
|
||||
console.error(e)
|
||||
resolve({
|
||||
files: {} // TODO: actually handle this error (bad or nonexistent gerber.zip)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const allowedExtensions = [
|
||||
'gbr', // gerber
|
||||
'drl', // drillfile
|
||||
'gko', // gerber board outline
|
||||
'gbl', // gerber bottom layer
|
||||
'gbp', // gerber bottom paste
|
||||
'gbs', // gerber bottom solder mask
|
||||
'gbo', // gerber bottom silk
|
||||
'gtl', // gerber top layer
|
||||
'gto', // gerber top silk
|
||||
'gts' // gerber top soldermask
|
||||
]
|
||||
const unzipJobs = Object.entries(zippedData.files).map(
|
||||
async ([filename, file]) => {
|
||||
const extension = filename.split('.').pop().toLowerCase()
|
||||
if (allowedExtensions.includes(extension)) {
|
||||
const filePath = `/tmp/${filename}`
|
||||
await new Promise((resolve, _reject) => {
|
||||
file.async('uint8array').then(function (fileData) {
|
||||
fs.writeFileSync(filePath, fileData)
|
||||
files.push(filePath)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await Promise.all(unzipJobs)
|
||||
|
||||
let readResult
|
||||
try {
|
||||
readResult = await read(files)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return {}
|
||||
}
|
||||
const plotResult = plot(readResult)
|
||||
const renderLayersResult = renderLayers(plotResult)
|
||||
const renderBoardResult = renderBoard(renderLayersResult)
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file)) {
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
}
|
||||
return {
|
||||
top: stringifySvg(renderBoardResult.top),
|
||||
bottom: stringifySvg(renderBoardResult.bottom)
|
||||
// all: stringifySvg(renderLayersResult)
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { file, format } = req.query
|
||||
if (!file) {
|
||||
return res.status(400).json({ status: 400, error: 'Must provide file' })
|
||||
}
|
||||
// ensure valid file url is included
|
||||
const url = new URL(decodeURI(file))
|
||||
const svg = await gerberToSvg(url)
|
||||
if (format === 'top') {
|
||||
res.contentType('image/svg')
|
||||
return res.status(200).send(svg.top)
|
||||
}
|
||||
if (format === 'json') return res.status(200).json(svg)
|
||||
|
||||
return res.status(200).json(svg)
|
||||
}
|
||||
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip
|
||||
20
pages/api/onboard/svg/[board_url]/top.js
Normal file
20
pages/api/onboard/svg/[board_url]/top.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip/top
|
||||
|
||||
import { gerberToSvg } from '.'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { board_url } = req.query
|
||||
if (!board_url) {
|
||||
return res.status(404).json({ status: 404, error: 'Must provide file' })
|
||||
}
|
||||
// ensure valid file url is included
|
||||
const parsed_url = new URL(decodeURI(board_url))
|
||||
if (!parsed_url) {
|
||||
return res.status(404).json({ status: 404, error: 'Invalid file' })
|
||||
}
|
||||
const svg = await gerberToSvg(parsed_url)
|
||||
if (svg.error) {
|
||||
return res.status(svg.status).send(svg.error)
|
||||
}
|
||||
return res.status(200).send(svg.top)
|
||||
}
|
||||
|
|
@ -1,54 +1,70 @@
|
|||
export default async (req, res) => {
|
||||
const calendarId = "c_e7c63a427761b0f300ede97f432ba4af24033daad26be86da0551b40b7968f00@group.calendar.google.com";
|
||||
const apiKey = "AIzaSyD_8dEnTDle3WmaoOTvEW6L1GW540FU_wg"; // Replace with your API Key
|
||||
const steveApiHandler = async (req, res) => {
|
||||
const calendarId =
|
||||
'c_e7c63a427761b0f300ede97f432ba4af24033daad26be86da0551b40b7968f00@group.calendar.google.com'
|
||||
|
||||
let allBusyDays = new Set();
|
||||
//This API key is for google calendar and has only read access to Steve
|
||||
const apiKey = 'AIzaSyD_8dEnTDle3WmaoOTvEW6L1GW540FU_wg'
|
||||
|
||||
try {
|
||||
const currentDateTime = new Date();
|
||||
const adjustedDateTime = new Date(currentDateTime.getTime() + (currentDateTime.getTimezoneOffset() + 240) * 60 * 1000); // Adjust to GMT-04
|
||||
const startTime = adjustedDateTime.toISOString();
|
||||
const endTime = new Date(adjustedDateTime.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
let allBusyDays = new Set()
|
||||
|
||||
const response = await fetch(`https://www.googleapis.com/calendar/v3/freeBusy?key=${apiKey}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timeMin: startTime,
|
||||
timeMax: endTime,
|
||||
items: [{ id: calendarId }]
|
||||
})
|
||||
});
|
||||
try {
|
||||
const currentDateTime = new Date()
|
||||
const adjustedDateTime = new Date(
|
||||
currentDateTime.getTime() +
|
||||
(currentDateTime.getTimezoneOffset() + 240) * 60 * 1000
|
||||
) // Adjust to GMT-04
|
||||
const startTime = adjustedDateTime.toISOString()
|
||||
const endTime = new Date(
|
||||
adjustedDateTime.getTime() + 30 * 24 * 60 * 60 * 1000
|
||||
).toISOString()
|
||||
|
||||
const data = await response.json();
|
||||
const response = await fetch(
|
||||
`https://www.googleapis.com/calendar/v3/freeBusy?key=${apiKey}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timeMin: startTime,
|
||||
timeMax: endTime,
|
||||
items: [{ id: calendarId }]
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (data.error) {
|
||||
return res.status(400).json({ error: data.error.message });
|
||||
}
|
||||
const data = await response.json()
|
||||
|
||||
// Assuming there are time ranges where the calendar is busy:
|
||||
const busyTimes = data.calendars[calendarId].busy;
|
||||
|
||||
// For each busy time range, extract all days that are busy:
|
||||
for (let busy of busyTimes) {
|
||||
let startDate = new Date(busy.start);
|
||||
let endDate = new Date(busy.end);
|
||||
|
||||
// Adjust dates to GMT-04
|
||||
startDate = new Date(startDate.getTime() + (startDate.getTimezoneOffset() + 240) * 60 * 1000);
|
||||
endDate = new Date(endDate.getTime() + (endDate.getTimezoneOffset() + 240) * 60 * 1000);
|
||||
|
||||
while (startDate < endDate) {
|
||||
allBusyDays.add(startDate.toISOString().split('T')[0]);
|
||||
startDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000); // Adding 24 hours for the next date
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json([...allBusyDays]);
|
||||
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: "Failed to fetch busy times." });
|
||||
if (data.error) {
|
||||
return res.status(400).json({ error: data.error.message })
|
||||
}
|
||||
};
|
||||
|
||||
// Assuming there are time ranges where the calendar is busy:
|
||||
const busyTimes = data.calendars[calendarId].busy
|
||||
|
||||
// For each busy time range, extract all days that are busy:
|
||||
for (let busy of busyTimes) {
|
||||
let startDate = new Date(busy.start)
|
||||
let endDate = new Date(busy.end)
|
||||
|
||||
// Adjust dates to GMT-04
|
||||
startDate = new Date(
|
||||
startDate.getTime() + (startDate.getTimezoneOffset() + 240) * 60 * 1000
|
||||
)
|
||||
endDate = new Date(
|
||||
endDate.getTime() + (endDate.getTimezoneOffset() + 240) * 60 * 1000
|
||||
)
|
||||
|
||||
while (startDate < endDate) {
|
||||
allBusyDays.add(startDate.toISOString().split('T')[0])
|
||||
startDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000) // Adding 24 hours for the next date
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json([...allBusyDays])
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: 'Failed to fetch busy times.' })
|
||||
}
|
||||
}
|
||||
|
||||
export default steveApiHandler
|
||||
|
|
|
|||
447
pages/bin.js
Normal file
447
pages/bin.js
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
import {
|
||||
Box,
|
||||
Container,
|
||||
Text,
|
||||
Heading,
|
||||
Card,
|
||||
Flex,
|
||||
Image,
|
||||
Link,
|
||||
Grid,
|
||||
Button
|
||||
} from 'theme-ui'
|
||||
import Head from 'next/head'
|
||||
import Meta from '@hackclub/meta'
|
||||
import Nav from '../components/nav'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import Footer from '../components/footer'
|
||||
import { keyframes } from '@emotion/react'
|
||||
import RsvpForm from '../components/bin/rsvp-form'
|
||||
import { Fade } from 'react-reveal'
|
||||
import ForceTheme from '../components/force-theme'
|
||||
import JSConfetti from 'js-confetti'
|
||||
import Sparkles from '../components/sparkles'
|
||||
import Icon from "@hackclub/icons"
|
||||
import Announcement from '../components/announcement'
|
||||
import { TypeAnimation } from 'react-type-animation'
|
||||
|
||||
const RsvpCount = () => {
|
||||
const targetRSVPs = 500
|
||||
const [rsvpCount, setRsvpCount] = useState(0)
|
||||
useEffect(async () => {
|
||||
// const url = 'https://api2.hackclub.com/v0.1/The Bin/rsvp' <- switch to this once we have api2 back up and running
|
||||
const url = '/api/bin/rsvp'
|
||||
const results = await fetch(url).then(r => r.json())
|
||||
setRsvpCount(results)
|
||||
}, [])
|
||||
|
||||
if (rsvpCount < targetRSVPs) {
|
||||
return <Text>{targetRSVPs - rsvpCount} RSVPs until the start of...</Text>
|
||||
} else {
|
||||
return <Text>{rsvpCount} have already RSVP'd to...</Text>
|
||||
}
|
||||
}
|
||||
|
||||
const stickerImages = [
|
||||
'https://cloud-mt5wqf6f5-hack-club-bot.vercel.app/0rummaging.png',
|
||||
'https://cloud-mt5wqf6f5-hack-club-bot.vercel.app/1prototype.png',
|
||||
'https://cloud-i547pyt1f-hack-club-bot.vercel.app/0idea.png'
|
||||
]
|
||||
|
||||
|
||||
const PartPicker = () => {
|
||||
const parts = [
|
||||
{
|
||||
name: "Relay",
|
||||
description: "On/Off Switch",
|
||||
image: "https://cloud-4zl0ojqxq-hack-club-bot.vercel.app/0placeholder3.png"
|
||||
},
|
||||
{
|
||||
name: "Mic",
|
||||
description: "Record Sound",
|
||||
image: "https://cloud-4zl0ojqxq-hack-club-bot.vercel.app/0placeholder3.png"
|
||||
}
|
||||
]
|
||||
|
||||
const [currentParts, setCurrentParts] = useState(parts)
|
||||
|
||||
function randomizeParts() {
|
||||
const randomParts = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
randomParts.push(parts[Math.floor(Math.random() * parts.length)])
|
||||
}
|
||||
setCurrentParts(randomParts)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{currentParts.map((part, index) => (
|
||||
<Electronic
|
||||
key={index}
|
||||
name={part.name}
|
||||
description={part.description}
|
||||
imageUrl={part.imageUrl}
|
||||
/>
|
||||
))}
|
||||
<button onClick={randomizeParts}>Randomize</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const OnboardCount = () => {
|
||||
const [onboardCount, setOnboardCount] = useState(200)
|
||||
|
||||
useEffect(async () => {
|
||||
const url = '/api/onboard/p/count'
|
||||
const results = await fetch(url).then(r => r.json())
|
||||
setOnboardCount(results.count)
|
||||
}, [])
|
||||
|
||||
return <Text>{onboardCount}</Text>
|
||||
}
|
||||
|
||||
const Electronic = ({ imageUrl, name, description }) => {
|
||||
return (
|
||||
<Card sx={{ display: 'inline-flex', textAlign: 'center', my: 'auto' }}>
|
||||
<Flex
|
||||
sx={{ mx: 'auto', flexDirection: 'column', display: 'inline-flex' }}
|
||||
>
|
||||
<Image src={imageUrl} width="100" />
|
||||
<Heading as="span" variant="headline">
|
||||
{name}
|
||||
</Heading>
|
||||
<Text sx={{ whiteSpace: 'nowrap' }}>{description}</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const spin = keyframes({
|
||||
from: { transform: 'rotate(0deg)' },
|
||||
to: { transform: 'rotate(360deg)' }
|
||||
})
|
||||
const wobble = keyframes({
|
||||
'0%': { transform: 'rotate(15deg)' },
|
||||
'50%': { transform: 'scale(1.1)' },
|
||||
'100%': { transform: 'rotate(20deg)' }
|
||||
})
|
||||
|
||||
const bounce = keyframes({
|
||||
'0%': { transform: 'scaleX(100%) scaleY(100%)' },
|
||||
'50%': { transform: 'scaleX(115%) scaleY(90%)' },
|
||||
'100%': { transform: 'scaleX(100%) scaleY(100%)' }
|
||||
})
|
||||
|
||||
const slideIn = keyframes({
|
||||
'0%': { transform: 'translateX(-100%)', opacity: 0 },
|
||||
'100%': { transform: 'translateX(0);', opacity: 1 }
|
||||
})
|
||||
|
||||
const slideOut = keyframes({
|
||||
'100%': { transform: 'translateX(-100%)', opacity: 0 },
|
||||
'0%': { transform: 'translateX(0);', opacity: 1 }
|
||||
})
|
||||
|
||||
function crunch() {
|
||||
const crunchAudioUrls = [
|
||||
'https://cloud-fwf97jf44-hack-club-bot.vercel.app/0crunch_4_audio.mp4',
|
||||
'https://cloud-fwf97jf44-hack-club-bot.vercel.app/1crunch_3_audio.mp4',
|
||||
'https://cloud-fwf97jf44-hack-club-bot.vercel.app/2crunch_2_audio.mp4',
|
||||
'https://cloud-fwf97jf44-hack-club-bot.vercel.app/3crunch_1_audio.mp4',
|
||||
]
|
||||
const randomCrunch = crunchAudioUrls[Math.floor(Math.random() * crunchAudioUrls.length)]
|
||||
|
||||
const audio = new Audio(randomCrunch)
|
||||
audio.play()
|
||||
}
|
||||
|
||||
const ExpiresAt = ({ children, expirationDate = new Date() - 1 }) => {
|
||||
console.log(expirationDate, new Date())
|
||||
if (expirationDate > new Date()) {
|
||||
return children
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function spinIt(el) {
|
||||
el.classList.add("spin");
|
||||
setTimeout(() => el.classList.remove("spin"), 500);
|
||||
}
|
||||
export default function Bin() {
|
||||
const confettiInstance = useRef(null);
|
||||
function fireConfetti() {
|
||||
if (!confettiInstance.current) {
|
||||
confettiInstance.current = new JSConfetti()
|
||||
}
|
||||
confettiInstance.current.addConfetti({
|
||||
emojis: ['🔌', '⚡️', '💥', '🚨', '🔋', '🤖', '🛞', '🔊', '🎙️', '💿', '🖲️', '⚙️', '🛠️'],
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Meta as={Head}
|
||||
title="The Bin"
|
||||
description="Rummage around in The Bin to get a free electronics starter kit!"
|
||||
image="https://cloud-6902szs7o-hack-club-bot.vercel.app/0og_image.png"
|
||||
/>
|
||||
<Nav color="text" />
|
||||
<ForceTheme theme="light" />
|
||||
<Box as="main" sx={{ bg: '#ECE9E0', textAlign: 'center', backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%239C92AC' fill-opacity='0.2' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E")` }}>
|
||||
<Box sx={{ background: 'url(https://cloud-jxq5r0yyp-hack-club-bot.vercel.app/0bg.png)', backgroundSize: 'cover', py: '3em' }}>
|
||||
<Container sx={{ position: 'relative' }}>
|
||||
<Box as="section" sx={{ textAlign: 'center', pt: '4em', overflow: 'hidden' }}>
|
||||
<ExpiresAt expirationDate={new Date(2024, 3, 13)}>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Announcement
|
||||
copy="Please pardon our dust!"
|
||||
caption="You found us a little early! We're still building this page, but you can RSVP early."
|
||||
iconLeft="welcome"
|
||||
/>
|
||||
</Box>
|
||||
</ExpiresAt>
|
||||
<Box sx={{
|
||||
'@media (prefers-reduced-motion: no-preference)': {
|
||||
animation: `${wobble} 0.5s ease-in-out infinite alternate`
|
||||
},
|
||||
}}>
|
||||
<Image
|
||||
src="https://cloud-mt5wqf6f5-hack-club-bot.vercel.app/0rummaging.png"
|
||||
onClick={(e) => { fireConfetti(); crunch(); spinIt(e.target) }}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
':active': {
|
||||
animation: `${bounce} 0.125s`
|
||||
},
|
||||
'&.spin': {
|
||||
animation: `${spin} 0.25s`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<br />
|
||||
<RsvpCount />
|
||||
<Box id="rsvp">
|
||||
<Sparkles size="100px">
|
||||
<Image src="https://cloud-rdlz8he4l-hack-club-bot.vercel.app/0thebin.svg" sx={{ maxWidth: '250px' }} />
|
||||
</Sparkles>
|
||||
</Box>
|
||||
<Text sx={{ fontWeight: 'bold' }}>
|
||||
Build{' '}
|
||||
<em>
|
||||
|
||||
<TypeAnimation
|
||||
cursor={false}
|
||||
sequence={[
|
||||
// Same substring at the start will only be typed out once, initially
|
||||
'a laser guided nerf gun',
|
||||
1000, // wait 1s before replacing "Mice" with "Hamsters"
|
||||
'a clap activated lamp',
|
||||
1000,
|
||||
'a temperature activated Febreze can',
|
||||
1000,
|
||||
'a flame actuated speaker',
|
||||
1000,
|
||||
'a light dependant door',
|
||||
1000
|
||||
]}
|
||||
repeat={Infinity}
|
||||
/>
|
||||
</em>
|
||||
{' '}
|
||||
with parts you pick out.
|
||||
<br />
|
||||
Free for high schoolers.
|
||||
{/* with all the parts bought for you */}
|
||||
{/* An electronics starter kit, customized for <em>your</em> project */}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box as="section" sx={{ textAlign: 'center' }}>
|
||||
<Fade up delay={100}>
|
||||
<Card sx={{ p: 3, mt: 4, mx: 'auto', maxWidth: '50ch' }}>
|
||||
<Text as="p" sx={{ mb: 1, mt: 0, textWrap: 'pretty', fontSize: 2 }}>
|
||||
Running for only 2 months.
|
||||
{/* High schoolers can RSVP now! */}
|
||||
{/* High schoolers can get a kit of electronics parts for free to
|
||||
build their first project. */}
|
||||
</Text>
|
||||
<Text as="p" sx={{ color: 'muted', mb: 2, fontSize: 2, fontWeight: 800 }}>
|
||||
RSVP to get notified when orders open.
|
||||
</Text>
|
||||
<RsvpForm />
|
||||
</Card>
|
||||
</Fade>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
<Container sx={{ position: 'relative' }}>
|
||||
<Box
|
||||
as="section"
|
||||
sx={{
|
||||
textAlign: 'left',
|
||||
pt: '4em',
|
||||
maxWidth: 'narrow',
|
||||
mx: 'auto'
|
||||
}}
|
||||
>
|
||||
<Heading as="h2" variant="title" sx={{
|
||||
'> .hidden': {
|
||||
opacity: 0,
|
||||
animation: `${slideOut} 0.25s ease-in-out`,
|
||||
},
|
||||
":hover": {
|
||||
'> .hidden': {
|
||||
display: 'inline-block',
|
||||
animation: `${slideIn} 0.25s ease-in-out`,
|
||||
opacity: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}>
|
||||
Motors & lasers & mics,{' '}
|
||||
<Text as="span" sx={{ fontWeight: 400, fontStyle: 'italic' }}>
|
||||
oh my!
|
||||
</Text>
|
||||
<Text as="span" className="hidden" sx={{ fontWeight: 400, fontStyle: 'italic', ml: 2 }}>
|
||||
oh my!
|
||||
</Text>
|
||||
</Heading>
|
||||
<Box sx={{ textAlign: 'left' }}>
|
||||
<Flex sx={{ my: 4 }}>
|
||||
<Box>
|
||||
<Image src="https://cloud-mt5wqf6f5-hack-club-bot.vercel.app/0rummaging.png" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading as="p" variant="headline">
|
||||
<b>Rummage</b>
|
||||
</Heading>
|
||||
<Text>
|
||||
Dig through the bin to get a randomly generated set of parts
|
||||
(<em>or you can choose your own</em>). For example...
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Image src="https://cloud-2wkwrydc4-hack-club-bot.vercel.app/0parts.svg" sx={{ width: '100%' }} />
|
||||
<Flex sx={{ my: 4 }}>
|
||||
<Box>
|
||||
<Image src="https://cloud-h7vwjlwe3-hack-club-bot.vercel.app/0frame_1__50_.png" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text as="p" variant="headline">
|
||||
<b>Think!</b>
|
||||
</Text>
|
||||
<Text>
|
||||
With your parts picked out, <b>what will you make?</b> A portable disco party? A flashlight
|
||||
that only turns on in the daytime?
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex sx={{ my: 4 }}>
|
||||
<Box>
|
||||
<Text as="p" variant="headline">
|
||||
<b>Prototype</b>
|
||||
</Text>
|
||||
<Text>
|
||||
Turn your idea into something almost real: simulate your
|
||||
project in an online editor for beginners.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Image src="https://cloud-mt5wqf6f5-hack-club-bot.vercel.app/1prototype.png" />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box sx={{
|
||||
boxShadow: 'card',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<Box
|
||||
p={2}
|
||||
sx={{
|
||||
bg: 'dark',
|
||||
flexGrow: 1,
|
||||
textAlign: 'center',
|
||||
color: 'white',
|
||||
fontWeight: 800,
|
||||
borderBottom: '1px solid gray',
|
||||
display: 'flex',
|
||||
gap: 2
|
||||
}}>
|
||||
<Icon glyph='private-outline' height={24} />
|
||||
<Box sx={{ bg: 'darkless', borderRadius: 4, flexGrow: 1 }}>wokwi.com</Box>
|
||||
<Icon glyph='view-reload' height={24} />
|
||||
</Box>
|
||||
<Image src="https://cloud-ghggsmjwa-hack-club-bot.vercel.app/0image.png" alt="Screenshot" />
|
||||
</Box>
|
||||
<Flex sx={{ my: 4 }}>
|
||||
<Box>
|
||||
<Text as="p" variant="headline">
|
||||
<b>Build it!</b>
|
||||
</Text>
|
||||
<Text>
|
||||
If it works in simulation, <b>we’ll send you the parts to
|
||||
build it in real life.</b>
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Image
|
||||
src="https://cloud-ge8yutn2q-hack-club-bot.vercel.app/0image.png"
|
||||
width="100%"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
<Container>
|
||||
<Text as="h3">Turn some trash into your treasure.</Text>
|
||||
<br></br>
|
||||
<Button variant="ctaLg" as="a" href="#rsvp" >
|
||||
RSVP</Button><br></br><br></br>
|
||||
</Container>
|
||||
</Box>
|
||||
<Footer>
|
||||
<Box sx={{ a: { color: 'blue' }, pb: 4 }}>
|
||||
<Heading as="h3" variant="subheadline" mb={2}>
|
||||
A project by <a href="https://hackclub.com/">Hack Club</a>.
|
||||
</Heading>
|
||||
<Text
|
||||
as="p"
|
||||
variant="caption"
|
||||
mb={3}
|
||||
sx={{ width: ['85%', '75%', '60%'] }}
|
||||
>
|
||||
Hack Club is a registered 501(c)3 nonprofit organization that
|
||||
supports a network of 20k+ technical high schoolers. We believe you
|
||||
learn best by building so we're removing barriers to hardware access
|
||||
so any teenager can explore. In the past few years, we{' '}
|
||||
<Link href="https://onboard.hackclub.com" target="_blank">
|
||||
fabricated custom PCBs designed by <OnboardCount /> teenagers
|
||||
</Link>
|
||||
,{' '}
|
||||
<Link
|
||||
href="https://github.com/hackclub/the-hacker-zephyr"
|
||||
target="_blank"
|
||||
>
|
||||
hosted the world's longest hackathon on land
|
||||
</Link>
|
||||
, and{' '}
|
||||
<Link href="https://hackclub.com/winter" target="_blank">
|
||||
gave away $75k of hardware
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Box>
|
||||
</Footer>
|
||||
<style>
|
||||
{
|
||||
`
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
`
|
||||
}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue