Merge branch 'main' into hw

This commit is contained in:
Caleb Denio 2024-04-23 11:05:10 -04:00
commit a53220c3f9
No known key found for this signature in database
195 changed files with 8185 additions and 5879 deletions

32
.github/workflows/caniuse-update.yml vendored Normal file
View 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
View file

@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid",
"printWidth": 80,
"semi": false
}

View file

@ -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'] } }}

View file

@ -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 dont blame someone else: we try to take them on to solve. Elon is very selective about the nonprofits he supports and Im proud Hack&nbsp;Club is one of them.
So…how will Hack&nbsp;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&nbsp;Club, we look to you to help us make a higher-quality experience. We plan to continue much of what were 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&nbsp;Club better (video game designers, software engineers, media producers, and more). We are pushing hard to try and make the [Hack&nbsp;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&nbsp;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&nbsp;Club, we look to you to help us make a higher-quality experience. We plan to continue much of what were 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&nbsp;Club better (video game designers, software engineers, media producers, and more). We are pushing hard to try and make the [Hack&nbsp;Club Slack](https://hackclub.com/) the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/fiscal-sponsorship/).
Well be fully transparent in how we spend this money. One thing weve been working toward after winning the [Frank Grant](https://grant.frank.ly/) is open sourcing our finances. Hack&nbsp;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 Elons gift. Soon, well also launch [Franks](https://frank.ly/) transparency tools on [hackclub.com](https://hackclub.com/).

View file

@ -4,7 +4,7 @@ In 2014, Hack Club was founded, and Tom joined as Hack Clubs 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.

View file

@ -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.

View 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>
</>
)
}

View file

@ -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">

View file

@ -27,7 +27,7 @@ a lot of what weve already been doing (and [what I wrote about at the beginni
of the year](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)):
well spend as little money as possible at all times, and well 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.
Well have a proper announcement in a few weeks, but one thing were doing after

View file

@ -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 (
<>

View 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>
)
}

View 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

View 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 youre looking to set up a for-profit entity, consider{' '}
<Link href="https://stripe.com/atlas" target="_blank">
Stripe Atlas
</Link>
.
</p>
</Box>
)
}

View file

@ -1,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="24 sentences will suffice."
requiredFields={requiredFields}
>
<Textarea
@ -73,9 +79,7 @@ export default function OrganizationInfoForm({ requiredFields }) {
id="eventDescription"
rows={3}
sx={{
resize: 'vertical',
width: '100%',
...AutofillColourFix
resize: 'vertical'
}}
/>
</Field>

View file

@ -1,18 +1,9 @@
import { Input, Flex, Label, Radio } from 'theme-ui'
import Checkbox from './checkbox'
import AddressInput from './address-input'
import { Input, Flex, Label, Radio, Grid, Select } from 'theme-ui'
import Field from './field'
import AutofillColourFix from './autofill-colour-fix'
import { useState } from 'react'
export default function PersonalInfoForm({
setValidationResult,
requiredFields
}) {
export default function PersonalInfoForm({ requiredFields }) {
const [selectedContactOption, setSelectedContactOption] = useState('Email')
const [email, setEmail] = useState(
window.sessionStorage.getItem('bank-signup-userEmail')
) // For display only, is not used for data submission.
return (
<>
@ -22,24 +13,14 @@ export default function PersonalInfoForm({
label="First name"
requiredFields={requiredFields}
>
<Input
name="firstName"
id="firstName"
placeholder="Fiona"
sx={{ ...AutofillColourFix }}
/>
<Input name="firstName" id="firstName" placeholder="Fiona" />
</Field>
<Field
name="lastName"
label="Last name"
requiredFields={requiredFields}
>
<Input
name="lastName"
id="lastName"
placeholder="Hacksworth"
sx={{ ...AutofillColourFix }}
/>
<Input name="lastName" id="lastName" placeholder="Hacksworth" />
</Field>
</Flex>
<Field name="userEmail" label="Email" requiredFields={requiredFields}>
@ -48,10 +29,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>
</>

View file

@ -0,0 +1,65 @@
async function sendApplication() {
// Get the form data from sessionStorage
const data = {}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i)
if (key.startsWith('bank-signup-')) {
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
}
}
console.dir('Sending data:', data)
// Send the data
try {
return fetch('/api/fiscal-sponsorship/apply', {
method: 'POST',
cors: 'no-cors',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
} catch (error) {
console.error(error)
}
}
export function onSubmit({
event,
router,
form,
requiredFields,
formError,
setFormError,
setIsSubmitting
}) {
event.preventDefault()
/* Don't return from inside the loop since
we want all input values to be saved every time */
let wasError = false
const formData = new FormData(form.current)
// Save form data
formData.forEach((value, key) => {
sessionStorage.setItem('bank-signup-' + key, value)
// Check if there are empty required fields.
if (
((!value || value.trim() === '') && requiredFields.includes(key)) ||
(formData.get('contactOption') === 'slack' &&
(!formData.get('slackUsername') != null ||
formData.get('slackUsername') === '')) // I'm so sorry for this
) {
setFormError('Please fill out all required fields.')
wasError = true
}
})
if (wasError) return
if (!formError) {
setIsSubmitting(true)
sendApplication().then(() => {
router.push('/fiscal-sponsorship/apply/success')
})
}
return
}

View file

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

View file

@ -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&nbsp;call <Link href={`tel:${phoneNumberUri}`}>{phoneNumber}</Link>
</Text>
</Flex>
)
}

View file

@ -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'

View 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 dont license software from
for-profit entities. Since day one, weve 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 Reboots 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>
)
}

View 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>
)
}

View file

@ -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

View file

@ -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' }}>

View file

@ -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 }}
>

View file

@ -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,

View file

@ -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>

View file

@ -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&nbsp;
<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'
}}
>

View file

@ -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.
</>
}
/>

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

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

View file

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

View file

@ -1,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>
)
}

View file

@ -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

View file

@ -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 youre a for-profit entity, then HCB is not for you.
Consider setting up a business.
</li>
</ul>
</Text>
</FlexCol>
</FlexCol>
</FlexCol>
</FlexCol>
</Box>
)
}

View file

@ -1,178 +0,0 @@
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { Button, Flex, Text, Spinner } from 'theme-ui'
async function sendApplication() {
// Get the form data from sessionStorage
const data = {}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i)
if (key.startsWith('bank-signup-')) {
data[key.replace('bank-signup-', '')] = sessionStorage.getItem(key)
}
}
console.dir('Sending data:', data)
// Send the data
try {
const res = await fetch('/api/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>
)
}

View file

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

View file

@ -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 youll&nbsp;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&nbsp;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
}

View file

@ -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 Circuits 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 &amp; 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 youd 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="Well 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>
)
}

View file

@ -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>
)
}

View file

@ -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 techs 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>
)
}

View file

@ -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 doesnt 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 youre 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>
)
}

View file

@ -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 couldnt 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>
)
}

View file

@ -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>
)
}

View file

@ -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 = () => (

View file

@ -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>
);
)
}

View file

@ -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>

View file

@ -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&apos;ll send you an email no more than once a month, when we work
on something cool for you. Check out our{' '}
We&apos;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>
)

View file

@ -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>
)
}
}

View file

@ -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>

View file

@ -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'

View file

@ -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]}

View 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>
)
}

View file

@ -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>
)
}
}

View file

@ -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.

View file

@ -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>

View file

@ -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&nbsp;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

View 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>
</>
)
}

View 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

View 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
View 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

View 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

View 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>
)
}

View 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&nbsp;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' }}>
Were 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>
)
}

View file

@ -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 {

View file

@ -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)&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;
</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!&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
</option>
{years
.map(year => (
<option key={year} value={year}>
{year}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</option>
))
.reverse()}
</Select>
</Label>
)
}
export default withRouter(JoinForm)

62
components/slack/join.js Normal file
View 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&apos;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>
)
}

View 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 }
}

View 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>
)
}

View 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
View 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>
)
}

View file

@ -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'
}}

View file

@ -41,7 +41,7 @@ const Submit = ({
}) => (
<Button
as="button"
type="submit"
type={'submit' || props.type}
sx={{
py: 2,
px: 3,

View file

@ -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 })
}

View file

@ -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

View file

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

4
lib/sleep.js Normal file
View file

@ -0,0 +1,4 @@
// Beloved classic utility function :3
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
export default sleep

View file

@ -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
View 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 }
}

View file

@ -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()
}

View file

@ -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'
}
]
}
]
}

View file

@ -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
View 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
View 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)
}

View file

@ -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: {

View file

@ -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
}

View file

@ -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':

View file

@ -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: 'Youve been invited to Slack!' })
)
.catch(error => {
console.error(error)
res.status(500).json({ error })
})
}

View 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)
}

View 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 })
}

View 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)
}

View 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)
}

View 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

View 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)
}

View file

@ -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
View 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>&nbsp;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&nbsp;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&nbsp;my!
</Text>
<Text as="span" className="hidden" sx={{ fontWeight: 400, fontStyle: 'italic', ml: 2 }}>
oh&nbsp;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>well 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