Hack Club's new website. This codebase is what runs on [hackclub.com](https://hackclub.com). For new developers getting started, run the following in your terminal:
1. Download the code to your computer:
```bash
git clone https://github.com/hackclub/site && cd site
```
2. Install dependencies:
```bash
bun install
```
3. Start running the website on your computer:
```bash
bun run dev
```
4. Open up your web browser and go to [localhost:3000](http://localhost:3000)
> [!NOTE]
> There are a number of redirects and rewrites essential to the website's functionality, which you can see in [next.config.ts](./next.config.ts).
Powered by [Next.js] with [MDX], [Theme UI], & [Hack Club Theme].
Code under MIT License, assets may not be re-used or re-distributed.
---
Join us in building Hack Club's homepage and show new hackers what Hack Club could be for them 💖.
See something that could be better? Make a PR! Have an easter egg idea? Make a PR! Is the site missing something? Make a PR! _(Do you see a trend? :))_
If you need to add content to the site, here's how you can:
Create a new card
Most things on the homepage are cards, modular components that can easily be added and removed according to relevancy to Hack Clubbers. There are 3 main sections: connection, open-source, and IRL community. Most new cards will likely fall within the first two sections!
First, you can create a new file under [components/index/cards](components/index/cards/) with the name of your new event/project. Next add `import CardModel from './card-model'` and add whatever you want :) Finally, use a `` component (`import Buttons from './button'`) to highlight call-to-action buttons. If it's the main button, use the primary prop to add a background color!
Your challenge: try and make the card as unique as possible, like a mini poster! Not sure where to start? Look at other cards on the page :)
Add to the carousel
If there's a Hack Club or Hack Club community-led project (past or present) that Hack Clubbers can get involved in, please add it to [public/carousel.json](public/carousel.json) and add your card to the end of the json file. An example looks like this:
```json
{
"background": "dark",
"titleColor": "white",
"descriptionColor": "white",
"title": "Hackers Wanted",
"description": "Our open love letter to hackers",
"img": "https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f4bb@2x.png",
"link": "/hackers-wanted"
}
```
Every week, thousands of people visit hackclub.com. What story do you want to tell?
_Have questions? Join us in [#hackclub-site-dev](https://hackclub.slack.com/archives/C036BTDGP43) and to learn more about the style guide at Hack Club check [this](https://hackclub.com/brand/) out_
---
Hack Club, 2026. MIT License.
[next.js]: https://nextjs.org
[mdx]: https://mdxjs.com
[theme ui]: https://theme-ui.com
[hack club theme]: https://theme.hackclub.com
================================================
FILE: components/AButton.ts
================================================
import { Button } from 'theme-ui'
export const AButton = Button as any
================================================
FILE: components/analytics.tsx
================================================
import Script from 'next/script'
const Analytics = () => (
)
export default Analytics
================================================
FILE: components/announcement.tsx
================================================
import { keyframes } from '@emotion/react'
import Image from 'next/image'
import { Box, Card, Text } from 'theme-ui'
import Icon from './icon'
export const unfold = keyframes({
from: { transform: 'scaleY(0)' },
to: { transform: 'scaleY(100%)' }
})
type AnnouncementProps = {
caption?: string
copy: string
iconLeft?: string
iconRight?: string
imgSrc?: string
imgAlt?: string
color?: string
textColor?: string
sx?: any
width?: number | string
href?: string
}
const Announcement = ({
caption,
copy,
iconLeft,
iconRight,
imgSrc,
imgAlt,
color = 'accent',
textColor = 'secondary',
sx = {},
width,
...props
}: AnnouncementProps) => (
{iconLeft && (
)}
{imgSrc && (
)}
{copy}
{caption && (
{' '}
{caption}
)}
{iconRight && }
)
export default Announcement
================================================
FILE: components/announcements/amount.tsx
================================================
import Sparkles from '../sparkles'
const Amount = ({ amount }) => (
{amount}
)
export default Amount
================================================
FILE: components/announcements/cta.tsx
================================================
import { Box, Button, Grid, Heading, Text } from 'theme-ui'
import Icon from '@hackclub/icons'
import NextLink from 'next/link'
import { thousands } from '../../lib/members'
export default function SlackCTA() {
return (
t.util.gx('yellow', 'orange'),
color: 'white',
py: [4, 5]
}}
>
Teenager? New here? Welcome!
Hack Club is a global community of high school makers & student-led
coding clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+
teenagers learning to code & building amazing projects, & you’ll fit
right in.
)
}
================================================
FILE: components/announcements/elon.mdx
================================================
import Amount from './amount'
import Signature from '../signature'
# Today, I’m proud to share: Elon Musk is donating to [Hack Club](https://hackclub.com).
Elon Musk is one of the most prolific and ambitious hackers of the last decade.
It was a huge honor last month to have Elon [spend an hour in an ask-me-anything call with our community of high schoolers](https://youtu.be/riru9OzScwk)—at one point he remarked we were “asking better questions than all the mainstream media” and called our community “very wholesome.”
**Afterwards, Elon wanted to support Hack Club further.**
When hackers see problems in the world, we don’t blame someone else: we try to take them on to solve. Elon is very selective about the nonprofits he supports and I’m proud Hack Club is one of them.
So…how will Hack Club invest $500,000? We want to use this to help 1,000 more students start and join Hack Clubs in their towns ([see the worldwide map](https://hackclub.com/map/)). For those already in Hack Club, we look to you to help us make a higher-quality experience. We plan to continue much of what we’re already doing (and [what I wrote about in January](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)): spending as little money as possible at all times, growing slowly, adding diverse staff to make Hack Club better (video game designers, software engineers, media producers, and more). We are pushing hard to try and make the [Hack Club Slack](https://hackclub.com/) the best place to be a teenager on the internet and expanding [HCB](https://hackclub.com/fiscal-sponsorship/).
We’ll be fully transparent in how we spend this money. One thing we’ve been working toward after winning the [Frank Grant](https://grant.frank.ly/) is open sourcing our finances. Hack Club HQ has been running on HCB since February, and starting today, [**you can see our finances publicly**](https://hcb.hackclub.com/hq). Through HCB, you can track how we spend every dollar of Elon’s gift. Soon, we’ll also launch [Frank’s](https://frank.ly/) transparency tools on [hackclub.com](https://hackclub.com/).
Hack Club’s mission is to build a new generation of hackers. This starts in high school, where Hack Club students learn to be technically proficient, build their friend network, learn to raise and spend money, and develop into kind, curious, thoughtful, optimistic, and honest leaders. And now Elon Musk is one of our largest supporters.
To every Hack Clubber: Elon is now supporting you and your work, so go forth and do amazing things. We can’t wait to show Elon what you make.
Zach Latta, Founder
================================================
FILE: components/announcements/hcb-mobile.mdx
================================================
I’m Mohamad, a 17-year-old from the SF Bay Area, and I just shipped the official mobile app for HCB.
If you haven't heard of it, HCB is the financial backbone for over **6,500 teenager-led nonprofits**, clubs, and hackathons. We provide **501(c)(3) nonprofit** status, access to a bank account, a donation collection platform, and debit cards for thousands of young people trying to do good in their communities.
HCB is currently processing an average of **$6 million per month** (over $80M in its lifetime).[^1] For the last year, I’ve led the project to build the first-ever mobile app for this entire community.
_The entire project is open source on [GitHub](https://github.com/hackclub/hcb-mobile) (we'd love a ⭐️!)._
## Why build this?
These teenagers are running run robotics teams, hackathons, and nonprofit projects that improve their community. They need a way to manage their organization's finances from their pocket.
With HCB Mobile, they'll be able to:
- Track their **organization's balance** and transactions on the go.
- Accept in-person **tap-to-pay donations**, perfect for an in-person fundraiser or event! No extra hardware required. Just tap any credit/debit card against your phone.
- **Issue new debit cards**, add them to **Apple / Google Wallet**, and freeze or cancel them directly from their phone.
- **Upload receipts** directly from their device or match existing receipts in Receipt Bin to transactions with a tap.
## The Technical Stuff
When I started working on this app, I wanted to build in native code like **SwiftUI** for iOS and **Kotlin/Jetpack Compose** for Android. However, I realized that it would be a pain for me, a **full-time student** with classes, to handle two codebases. I'd have to duplicate every feature I created for one OS to the other and deal with all the integration issues along the way. Then, I discovered **Expo** (a React Native framework) which allowed me to write one app that worked on multiple devices. Working with Expo, I learned about creating my own Expo Modules (to bridge native code features to Typescript) and optimization methods like memoization and component recycling.
The non-code side of this app was _no joke_, either. I had to work with the Apple and Google app review teams to obtain **restricted entitlements** for features like mobile **tap-to-pay terminal provisioning** (made possible by Stripe) and **push provisioning** (which allows users to add cards to their payment wallet directly from HCB Mobile). It took several months and many back-and-forth email chains to finally get the entitlements we needed.
After over 250 hours of development work, I can say that I'm incredibly proud of HCB Mobile because it's **built by teenagers** to make it easier for teenagers like me to run nonprofit organizations and projects with HCB. Beyond teenagers, HCB also supports hundreds of adult-ran organizations such as mutual aid groups, open source projects, and community spaces.
## Download the app!
[^1]: _This amount is excluding HQ (our own) [finances](https://hcb.hackclub.com/hq)._
================================================
FILE: components/announcements/hcb-open-source.mdx
================================================
Hack Club launched HCB in 2018 to enable hackathons to raise and spend money
through [fiscal sponsorship](https://hackclub.com/fiscal-sponsorship/). Since
then, we’ve expanded to all nonprofit projects; our 12,000 users have transacted
$50 million.
Local student-ran hackathons, robotics teams, and community groups use HCB as a
nonprofit neobank to collect donations, send payments, issue debit cards, and
gain 501(c)(3) status.
When we started HCB, it was developed in private for security reasons. That
said, one of Hack Club’s core principles has always been transparency - we [open
source](https://github.com/hackclub/ledger) our
[finances](https://hcb.hackclub.com/hq), [document how we run
events](https://github.com/hackclub/assemble), and have 500+ public repositories
on [GitHub](https://github.com/hackclub).
**_[github.com/hackclub/hcb](https://github.com/hackclub/hcb) is now public -
check it out and star it._**
Paired with our technical documentation, it’s a great resource for anyone
interested in building financial software or applications with Ruby on Rails.
Our engineering work is also entirely public; the world can learn from our
successes and mistakes.
Since 2018, over fifty people have made 10k+ commits to HCB (thank you!); we
can’t wait for more contributors to join us:
PS: if you’re looking to start a nonprofit, we’re accepting applications! Head
over to [nonprofit.new](https://nonprofit.new/) and we’ll be in touch.
We're launching this live from SF!
================================================
FILE: components/announcements/hcb_cta.tsx
================================================
import { Box, Button, Grid, Heading, Text } from 'theme-ui'
import Icon from '@hackclub/icons'
import NextLink from 'next/link'
export default function HCBCTA() {
return (
t.util.gx('yellow', 'orange'),
color: 'white',
py: [4, 5]
}}
>
Looking to start a nonprofit?
We're accepting applications! No startup fees, no minimum balance,
and no long wait time.
)
}
================================================
FILE: components/announcements/holder.tsx
================================================
import { Container, BaseStyles } from 'theme-ui'
export default function AnnouncementHolder({ children }) {
return (
{children}
)
}
================================================
FILE: components/announcements/pills.tsx
================================================
import { Avatar, Badge, Flex } from 'theme-ui'
export function PillHolder({ children }) {
return (
{children}
)
}
export function AuthorPill({ tag, image, firstName }) {
return (
{tag}
)
}
export function DatePill({ tag }) {
return (
{tag}
)
}
================================================
FILE: components/announcements/preston-werner-2022.mdx
================================================
This gift means a lot to the Hack Club community, and we are grateful for Tom and Theresa’s continued support.
In 2014, Hack Club was founded, and Tom joined as Hack Club’s first board member. In the years since, he has contributed open source code, mentored Hack Clubbers 1:1, donated dozens of laptops to teenagers who didn't have access to computers, and been a constant advisor on the Hack Club community, strategy, and product.
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/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.
We thank Tom and Theresa for their generous gift and will carefully use each cent to advance our mission to create a new generation of young, highly-technical teen leaders capable of solving our world’s greatest problems. Every penny will be spent [transparently](https://hcb.hackclub.com/hq).
— Christina Asquith, COO, and Zach Latta, founder
================================================
FILE: components/announcements/preston-werner.mdx
================================================
import Amount from './amount'
# Today, we're proud to share: Tom and Theresa Preston-Werner are donating to [Hack Club](https://hackclub.com).
We are deeply grateful for this gift.
In the coming months, we hope you’ll share our excitement as we make 2 new hires directly serving Hack Clubbers. This gift increases Hack Club’s budget by 60%, and helps us build a diverse foundation at the leadership level of Hack Club as we grow.
We are so honored to be included among the many gifts the Preston-Werners make each year. This is the 3rd year the Preston-Werners have given a gift to Hack Club, and they've supported the organization from the beginning.
Tom and Theresa inspire Hack Club's values. They are self-made hackers, passionate about constructing a better world in creative ways. They are deeply committed to environmental protection, women’s rights, ending global poverty and injustice; and are tremendous collaborators in making Hack Club a place where all young people can build and create their own solutions through coding and technology, regardless of their background. They apply rigorous-thinking, curiosity, humility, transparency, and deep expertise in academia and coding to the problems that need solving in the world, and inspire us to do the same.
The Preston-Werners generously donate dozens of hours of their time each year to Hack Club HQ and Hack Clubbers. Theresa has been a steady champion of Hack Club, supporting us with feedback, advice, editing, and meeting with Hack Club students. Tom is a founding board member and a personal mentor of Zach’s for the last 5 years. Just some of the ways they support Hack Club is that they inspired the idea to launch Hack Club’s “Ask Me Anything” series, and Tom was our first speaker last April. In December 2019, they threw an amazing Christmas party at their San Francisco home for Hack Club.
Their incredible and generous gift ushers Hack Club into a big new year in which we get closer to our vision to build a new cultural institution for the 21st century akin to the Boy and Girl Scouts, in which we support high schoolers to gain critical computer science skills, healthy, fun and wholesome friendships, and a set of modern values that honor kindness, integrity, inclusivity, curiosity, optimism, and building and doing.
We send them a huge thank you. To every Hack Clubber: Tom and Theresa are now supporting you and your work, so go forth and do amazing things. We can’t wait to show them what you make.
—Christina Asquith, COO, and Zach Latta, founder
================================================
FILE: components/announcements/relon.mdx
================================================
import Signature from '../signature'
import Signatures from '../signatures'
import Image from 'theme-ui'
In March 2020, Elon Musk [spent an hour hanging out with Hack Clubbers](https://www.youtube.com/watch?v=riru9OzScwk). He [donated $500,000 to build our team](https://hackclub.com/elon/), [tweeted Hack Club was a cool group](https://twitter.com/elonmusk/status/1263275969743216640), and said that Hack Club makes him more optimistic about the future. This year, Hack Clubbers met SpaceX engineers and demoed projects on SpaceX's factory floor in Hawthorne, California.
This summer, Elon reached back out.
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://slack.hackclub.com), 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/fiscal-sponsorship/).
Elon is very selective about the nonprofits he supports and we're proud Hack Club is one of them.
Hack Club will be fully transparent in how we spend this money. Hack Club HQ has been running on HCB since February 2020, and [you can see our finances publicly here](https://hcb.hackclub.com/hq).
Hack Club's mission is to help foster a new generation of hackers. This starts in high school, where Hack Clubbers learn to be technically proficient, build their friend network, learn to raise and spend money, and develop into kind, curious, thoughtful, optimistic, and honest leaders. And now Elon Musk is one of our largest supporters.
To every Hack Clubber: Elon continues to support you and your work, so go forth and do amazing things. We can't wait to share what you make.
Zach Latta, Founder & Executive Director
Christina Asquith, COO
================================================
FILE: components/arcade/footer.tsx
================================================
import { Box, Heading, Text, Link } from 'theme-ui'
import Footer from '../footer'
const Description = () => (
A project by Hack Club.
Previous edition: Power Hour
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 creating community and providing grants so you can make.
In the past few years, we've{' '}
given away 100k+ in hardware grants
,{' '}
hosted the world's longest hackathon on land
, and{' '}
brought 183 teenagers to SF for a hackathon
.
)
const ArcadeFooter = () => {
return (
)
}
export default ArcadeFooter
================================================
FILE: components/arcade/projects.tsx
================================================
/** @jsxImportSource theme-ui */
import React, { useState } from 'react'
import styled from '@emotion/styled'
import {
Box,
Button,
Container,
Flex,
Heading,
Card,
Grid,
Text,
Avatar
} from 'theme-ui'
import NextImage from 'next/image'
import Marquee from '../marquee'
import Photo1 from '../../public/winter/1.jpeg'
import Photo2 from '../../public/winter/2.png'
import Photo3 from '../../public/winter/3.jpeg'
import Photo4 from '../../public/winter/4.jpeg'
import Photo5 from '../../public/winter/5.jpeg'
import Photo6 from '../../public/winter/6.jpeg'
import Photo7 from '../../public/winter/7.jpeg'
import Photo8 from '../../public/winter/8.jpeg'
import Photo9 from '../../public/winter/9.jpeg'
import Photo10 from '../../public/winter/10.jpeg'
import Photo12 from '../../public/winter/12.jpeg'
import Photo13 from '../../public/winter/13.jpeg'
import Photo14 from '../../public/winter/14.jpeg'
import Photo15 from '../../public/winter/15.jpeg'
import Photo16 from '../../public/winter/16.jpeg'
import Photo17 from '../../public/winter/17.jpeg'
import Photo18 from '../../public/winter/18.jpeg'
import Photo19 from '../../public/winter/19.jpeg'
import Photo20 from '../../public/winter/20.jpeg'
import Photo21 from '../../public/winter/21.jpeg'
const Header = styled(Box)`
background: url('/pattern.svg');
`
const PhotoRow = ({ photos }) => (
)
const Cards = ({ avatar, username, description, image }) => {
return (
@{username}
div': { width: 18, height: 18 }
}}
>
{description}
{/* */}
)
}
export default function Projects() {
const [count, setCount] = useState(0)
const list = [
'Drawing robot',
'3D printer',
'DIY Electric Skateboard',
'Pixel art display',
'Smart Garden',
'CNC machine',
'Interactive LED Art',
'VR Escape Room',
'Image Recognition App',
'DIY Camera',
'Multiplayer AR Game',
'Drone Swarm Choreography'
]
if (count === list.length - 1) {
setCount(0)
}
const project_idea = list[count]
return (
You could be building a setCount(count + 1)}
>
{project_idea}
)
}
================================================
FILE: components/background-image.tsx
================================================
import { Box } from 'theme-ui'
import Image, { StaticImageData } from 'next/image'
/*
* Use this component inside a container with CSS:
* `position: relative; overflow: hidden;`
* then pass width/height/alt/src as usual
* (you can pass `gradient` valueless to avoid gradient)
*/
type BGImgProps = {
gradient?: string | boolean
alt?: string
src: string | StaticImageData
placeholder?: 'blur' | 'empty'
}
export default function BGImg({
gradient = 'linear-gradient(rgba(0,0,0,0.25), rgba(0,0,0,0.5))',
alt = '',
...props
}: BGImgProps) {
return (
)
}
================================================
FILE: components/bin/GalleryPosts.tsx
================================================
/** @jsxImportSource theme-ui */
import Image from 'next/image'
import styles from '../../public/bin/style/gallery.module.css'
import PartTag from './PartTag'
type BinPostProps = {
title: string
desc: string
slack: string
link: string
id: string
date: string
parts?: string[]
}
const BinPost = ({
title = 'Bin Post',
desc = 'Bin Project',
slack = '',
link = '',
id,
date,
parts
}: BinPostProps) => {
link = link.trim()
if (!/^https?:\/\//i.test(link)) {
link = 'https://' + link
}
const projectID = link.split('/')[4]
const imgLink = `https://thumbs.wokwi.com/projects/${projectID}/social/bin.png`
function handleClick() {
if (typeof window !== 'undefined') {
window.open(link, '_blank')
}
}
function formatDate(dateString) {
const inputDate = new Date(dateString)
const now = new Date()
const oneDay = 24 * 60 * 60 * 1000 // Number of milliseconds in one day
// Check if the input date is within the last 24 hours
if (now.getTime() - inputDate.getTime() < oneDay) {
const hours = inputDate.getHours().toString().padStart(2, '0')
const minutes = inputDate.getMinutes().toString().padStart(2, '0')
return `Today at ${hours}:${minutes}`
} else {
// Format the date to "Month day, year"
const options = {
year: 'numeric',
month: 'long',
day: 'numeric'
} as const
return inputDate.toLocaleDateString(undefined, options)
}
}
if (parts) {
parts = parts.filter(
part => part !== 'recvK14pXAY1tn3HQ' && part !== 'rec5TQNvkGkscsGuQ'
) //Filter out breadboards and raspberry pi
}
return (
)
}
export default BinPost
================================================
FILE: components/bin/PartTag.module.css
================================================
.tag {
color: e1e1e1;
padding: 4px 10px;
border-radius: 20px;
width: fit-content;
max-width: 300px;
display: flex;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: transform 0.3s ease;
box-sizing: border-box;
}
.tag:hover {
cursor: pointer;
transform: scale(1.1);
}
.outlined {
border: 5px dotted #c5c5c5;
}
================================================
FILE: components/bin/PartTag.tsx
================================================
import React from 'react'
import styles from './PartTag.module.css'
import { useState } from 'react'
const PartTag = ({ partID, search = false, addFilter, removeFilter }) => {
const [isOutlined, setIsOutlined] = useState(false)
const handleClick = () => {
if (search) {
setIsOutlined(prevState => !prevState)
if (isOutlined) {
removeFilter(partID)
} else {
addFilter(partID)
}
}
}
let backgroundColor: string
let text: string
switch (partID) {
case 'recltWikgPdLvpJfe':
backgroundColor = '#0000FF' // Vibrant blue
text = 'Servo'
break
case 'recRzllr0dui91NLd':
backgroundColor = '#008000' // Vibrant green
text = 'LED'
break
case 'recM7OOofV9Bp7AM9':
backgroundColor = '#FF0000' // Vibrant red
text = 'ESP32'
break
case 'recALoD1CCKt3CxKE':
backgroundColor = '#800080' // Vibrant purple
text = 'Buzzer'
break
case 'rechtwyljZ5WR8DtR':
backgroundColor = '#FF4500' // Vibrant orange
text = 'Slider'
break
case 'recry1GsMO6QLakzw':
backgroundColor = '#8B4513' // Dark brown
text = 'Photoresistor'
break
case 'recjRu1vTAU3qDanE':
backgroundColor = '#FF1493' // Vibrant pink
text = 'LCD'
break
case 'recrgS7NnxS42tkmg':
backgroundColor = '#A52A2A' // Vibrant brown
text = 'LED Screen'
break
case 'recocuypi4xP0UgAj':
backgroundColor = '#000000' // Black
text = 'Joystick'
break
case 'recgLUxtFZHufN70W':
backgroundColor = '#1E90FF' // Dodger blue
text = 'LED Bar Graph'
break
case 'recKBAnftT9PgppUC':
backgroundColor = '#00FFFF' // Vibrant cyan
text = 'Shift Register'
break
case 'recibIXNCSdhDHjXD':
backgroundColor = '#FF00FF' // Vibrant magenta
text = 'Thermistor'
break
case 'recwSKHd3anpKqNbg':
backgroundColor = '#00FF00' // Vibrant lime
text = 'IR Receiver'
break
case 'recLRovQNumB1Et8B':
backgroundColor = '#008080' // Vibrant teal
text = 'Range Finder'
break
case 'recMVBkeJ4KQdZihl':
backgroundColor = '#808000' // Vibrant olive
text = 'Keypad'
break
case 'recGrj5GpSExI18Ff':
backgroundColor = '#000080' // Vibrant navy
text = 'Humidity'
break
case 'rec9G0CAXM0kdp7HY':
backgroundColor = '#800000' // Vibrant maroon
text = 'RTC'
break
case 'rec4vTiJIx4UP8Thl':
backgroundColor = '#DAA520' // Goldenrod
text = 'Motion Sensor'
break
case 'reczWN9rZOY95VXOT':
backgroundColor = '#FF8C00' // Dark orange
text = 'LED Matrix'
break
case 'recNjAmh8gF0gZNtI':
backgroundColor = '#FF6347' // Tomato red
text = 'Accelerometer'
break
case 'recPmyV5b8cvaMtTk':
backgroundColor = '#4B0082' // Vibrant indigo
text = 'Neopixel LED'
break
case 'recj5b4DKez4GNa8i':
backgroundColor = '#87CEEB' // Vibrant sky blue
text = 'Relay'
break
case 'rec5TQNvkGkscsGuQ':
backgroundColor = '#9932CC' // Vibrant orchid
text = 'Pico W'
break
case 'recqffGd1j1jRh56m':
backgroundColor = '#DDA0DD' // Vibrant plum
text = 'Multicolor LED'
break
case 'recJUolkJURydamzG':
backgroundColor = '#CD5C5C' // Vibrant light coral
text = 'Encoder'
break
case 'rec7lggt0DsgrWHzc':
backgroundColor = '#20B2AA' // Vibrant light sea green
text = 'Temp Sensor'
break
case 'rectVgu4kWbbaqccc':
backgroundColor = '#FFA07A' // Vibrant light salmon
text = 'Button'
break
case 'recWKEXSaByRvl68t':
backgroundColor = '#4682B4' // Vibrant light steel blue
text = '4 Digit Display'
break
default:
backgroundColor = 'gray' // Default gray
text = 'Invalid Tag'
console.log('invalid', partID)
}
return (
{text}
)
}
export default PartTag
================================================
FILE: components/bin/nav.tsx
================================================
import React from 'react'
import styles from '../../public/bin/style/gallery.module.css'
const Nav = () => {
return (
)
}
export default Nav
================================================
FILE: components/bin/rsvp-form.tsx
================================================
/** @jsxImportSource theme-ui */
import { Checkbox, Input, Label, Text, Box } from 'theme-ui'
import useForm from '../../lib/use-form'
import Submit from '../submit'
import { Slide } from '../react-reveal-compat'
export default function RsvpForm() {
const { status, formProps, useField } = useForm('/api/bin/rsvp', null, {
clearOnSubmit: 5000,
method: 'POST',
initData: {},
bearer: null
})
return (
<>
>
)
}
================================================
FILE: components/bio.tsx
================================================
/** @jsxImportSource theme-ui */
import Icon from '@hackclub/icons'
import { useState } from 'react'
import { Box, Card, Flex, Text } from 'theme-ui'
export default function Bio({ popup = true, spanTwo = false, ...props }) {
const { name, teamRole, pronouns, text, subrole, email, href, video, img } =
props
const [expand, setExpand] = useState(false)
return (
<>
{
if (text && popup) {
setExpand(true)
}
}}
>
{img && (
)}
{name}
{teamRole}
{subrole && (
<>
{subrole}
>
)}
{pronouns && (
({pronouns})
)}
{!popup &&
email &&
(email.includes('@') ? (
{email}
) : (
{email}@hackclub.com
))}
{!popup && (
<>
{text}
{video && (
)}
>
)}
{!popup && href && (
{href}
)}
{popup && expand && (
setExpand(false)}
/>
)}
>
)
}
================================================
FILE: components/boardbio.tsx
================================================
/** @jsxImportSource theme-ui */
import Icon from '@hackclub/icons'
import { useState } from 'react'
import { Avatar, Box, Card, Flex, Text } from 'theme-ui'
export default function BoardBox({ popup = true, ...props }) {
const { img, name, teamRole, pronouns, text, subrole, email, href, video } =
props
const [expand, setExpand] = useState(false)
return (
<>
{
if (text && popup) {
setExpand(true)
}
}}
>
{popup ? (
<>
{name}
{teamRole}
{subrole && (
{subrole}
)}
>
) : (
<>
{name}
{teamRole}
{subrole && (
<>
{subrole}
>
)}
{pronouns && (
({pronouns})
)}
{email &&
(email.includes('@') ? (
{email}
) : (
{email}@hackclub.com
))}
{text}
{video && (
)}
{href && (
{href}
)}
>
)}
{popup && expand && (
setExpand(false)}
/>
)}
>
)
}
================================================
FILE: components/color-switcher.tsx
================================================
import { IconButton, useColorMode } from 'theme-ui'
const ColorSwitcher = props => {
const [mode, setMode] = useColorMode()
return (
setMode(mode === 'dark' ? 'light' : 'dark')}
title={`Switch to ${mode === 'dark' ? 'light' : 'dark'} mode`}
sx={{
position: 'absolute',
top: [2, 3],
right: [2, 3],
color: 'primary',
cursor: 'pointer',
borderRadius: 'circle',
transition: 'box-shadow .125s ease-in-out',
':hover,:focus': {
boxShadow: '0 0 0 3px',
outline: 'none'
}
}}
{...props}
>
)
}
export default ColorSwitcher
================================================
FILE: components/comma.ts
================================================
export default function Comma({ children }) {
return children
? children.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
: ''
}
================================================
FILE: components/directoryModal.tsx
================================================
/** @jsxImportSource theme-ui */
export const badges = [
{
label: 'Transparent',
id: 'Transparent',
tooltip: 'Transparent',
color: 'purple',
icon: 'explore',
match: org => org.isTransparent
}
]
export function getBadgesForOrg(org: { [key: string]: any }): typeof badges {
return badges.filter(badge => badge.match?.(org))
}
import { Badge as ThemeBadge, Box, Flex } from 'theme-ui'
import { Text, Button, Card } from 'theme-ui'
import Icon from '@hackclub/icons'
import Image from 'next/image'
type OrganizationModalProps = {
organization: {
name: string
location: {
country: string
}
branding: {
backgroundImage: string
logo?: string
description?: string
}
links: {
website?: string
financials?: string
donations: string
}
raw: {
transparent?: boolean
}
}
onClose: () => void
}
export function OrganizationModal({
organization,
onClose
}: OrganizationModalProps) {
return (
{
e.stopPropagation()
}}
>
{organization.branding.logo && (
)}
{organization.name}
{organization.location.country}
{/* Badges */}
{/* hardcoded "nonprofit" badge */}
Nonprofit
{organization.raw.transparent && (
Transparent
)}
{/* info & buttons */}
{organization.branding.description && (
{organization.branding.description}
)}
{organization.links.website && (
Website
)}
{organization.links.financials && (
Transparent Finances
)}
All donations are tax-deductible.
)
}
================================================
FILE: components/donate/donors.json
================================================
{
"1517": "http://www.1517fund.com/",
"Aakash Adesara": null,
"Abby Fischler": null,
"Achal Srinivasan": null,
"Adora Svitak": null,
"Adrienne Tran": null,
"Alex Koren": "https://twitter.com/alexekoren",
"Alex Peña": "http://www.alexaaronpena.com/home.html",
"Alexander Turin": null,
"Alishaan Ali": "https://alishaan.io",
"Allyson Dias": "https://twitter.com/AllysonDias",
"Amanda Mae-an Wofford": null,
"Amanda Southworth": "https://twitter.com/amndasuthwrth",
"Amogh Chaubey": "https://amogh.sh",
"Amritha Jayanti": null,
"Amy Sorto": null,
"Andrew Breckenridge": "https://github.com/AndrewSB",
"Andrew Downing": null,
"Andrew Ninh": null,
"Andrew Zoerb": null,
"Andy Haden": "https://github.com/andyh2",
"Angus Jyu": null,
"Ankit Ranjan": "http://www.ankit.io/",
"Ankit Shah": "https://twitter.com/AnkitShah",
"Ann Mazuk": null,
"Arjun Dileep": null,
"Asta Lasf": null,
"Athul Blesson": "https://www.linkedin.com/in/athul-blesson-92ab3b115/",
"Avi Romanoff": null,
"Ben Yu": "https://twitter.com/intenex",
"Beyang Liu": "https://www.linkedin.com/in/beyang-liu-07651227",
"Bhargav Yadavalli": "https://www.linkedin.com/in/bhargavy/",
"Bigglesworth Family Foundation": "https://www.bigglesworthff.org/",
"Brayden McLean": null,
"Brett Neese": null,
"Brian Nguyen": null,
"Cayce Beames": null,
"Chaleb Pommells": null,
"Changbai Li": "https://changbai.li/",
"Chaoyi Zha": null,
"Chris Van Pelt": "https://twitter.com/vanpelt",
"Chris Walker": "https://twitter.com/EnDimensions",
"Christian Zenaty": null,
"Christina Kim": null,
"Christina Lewis Halpern": null,
"Clara Tsao": null,
"Connie Liu": null,
"Daniel Sinclair": null,
"David C Farnan-Williams": null,
"Dennis Ashendorf": null,
"Dhruv Maheshwari": null,
"Doris Capet": null,
"Edward Jiang": null,
"Elon Musk": "https://en.wikipedia.org/wiki/Elon_Musk",
"Emily Pries": null,
"Emily Tseng": null,
"Erik Batista": null,
"Ethan Resnick": null,
"Evan Shui": null,
"Fast Forward": "https://www.ffwd.org/",
"Fern Ray": null,
"Ferran Arricivita": null,
"Fiona Carty": "https://dribbble.com/nonafiona",
"Garrett Wesley": null,
"Gautam Bhargava": null,
"Gautam Mittal": null,
"Gemma Busoni": "https://twitter.com/gemmabusoni",
"Geoff Ralston": null,
"Gisela Kottmeier": null,
"Google.org": "https://www.google.org/",
"Gordon Smith": null,
"Guilherme de Souza": null,
"Hallie Lomax": "http://lomax.ninja/",
"Hamza bnr Bellucci": null,
"Hortensia Gomez-tirella": null,
"Ishaan Parikh": null,
"Jack Chak": null,
"Jackcheal Dang": null,
"Jacob Haap": "https://jacobhaap.com",
"Jake Brownson": null,
"Jamsheed Mistri": null,
"Jason Marmon": null,
"Jay Freeman": "http://www.saurik.com/",
"Jeff Hilnbrand": null,
"Jeffrey Owens": null,
"Jennifer Kakuske": null,
"Jevin Sidhu": null,
"Jim Latta": null,
"Joe Lonsdale": null,
"Joseph Douglas": "https://twitter.com/JosephRDouglas",
"Josh & Michelle Weatherspoon": null,
"Julie Latta": null,
"Junius Sim": "https://www.linkedin.com/in/juniussim/",
"Justin Brezhnev": null,
"Justin Harris": "https://envisionwithjustin.com/",
"Kartik Talwar": null,
"Katie Latta": null,
"Keala Lusk": "http://kea.la/",
"Kelly Peng": null,
"Kevin Chu": null,
"Kevin Conner": null,
"Kevin Wang": "https://twitter.com/kevinverse",
"Kunal Batra": null,
"Kyle Emile": null,
"Lachlan Campbell": "https://lachlanjc.com",
"LạiDuy": null,
"Lan Paje": "https://twitter.com/lanpaje",
"Larry Weiss": null,
"Lia Stanciu Gregory": null,
"Liam Horne": "https://twitter.com/liamihorne",
"Mackenzie Burnett": null,
"Maria Choi": null,
"Mark Prideaux": null,
"Matthew Stanciu": "https://matthewstanciu.me",
"Megan Cui": "https://megancui.com/",
"Michael Akilian": null,
"Michael Copley": null,
"Michael Hulet": null,
"Michael Yoo": null,
"Michelle Leveille": null,
"Mick Donahue": null,
"Mike Swift": "https://twitter.com/swiftalphaone",
"Mingjie Jiang": "https://mingjie.info",
"Mohit Bhatia": "https://github.com/mohitbhatia1994",
"Myles Byrne": "https://twitter.com/quackingduck",
"Nate Wienert": "https://github.com/natew",
"Nelson Gomez": null,
"Nick Quinlan": null,
"Nikhil Srinivasan": null,
"Oliver Belanger": null,
"Patrick Pistor": "https://github.com/yogert96",
"Paul Cichocki": null,
"Phat Le": "https://www.phatle.com/",
"Phil Hedayatnia": "http://www.hedayatnia.com/",
"Polly Schneider": null,
"Prisma Data, Inc.": "https://prismic.io/",
"Quinn Slack": "https://qslack.com/",
"Rashid Al-Abri": null,
"Reid Workman": null,
"Robert Gregory": null,
"Rohith Varanasi": null,
"Rush Wofford": null,
"Ryan Orbuch": "https://twitter.com/orbuch",
"Saharsh Yeruva": "https://saharsh.tech",
"Samay Shamdasani": "https://shamdasani.org/",
"Samuel Escapa": "https://github.com/saescapa",
"Santiago Siri": "https://twitter.com/santisiri",
"Scott Chow": null,
"Scott Motte": "https://www.scottmotte.com/",
"Sean Kim": null,
"Selynna Sun": "https://www.selynnasun.com/",
"Shariq Hashme": "https://shar.iq/",
"Sheryl Kern-Jones": null,
"Shrav Mehta": null,
"Shriya Nevatia": null,
"Shriyash Jalukar": "https://www.linkedin.com/in/shriyashjalukar/",
"Siddhartha Desai": null,
"Sina Hamedian": null,
"Sourcegraph": "https://sourcegraph.com/",
"Spencer Yen": null,
"Stephanie He": null,
"Steven Duong": null,
"Tanya Latta": null,
"Taylor Otwell": null,
"Tejas Manohar": "https://tejas.io/",
"The Reva & David Logan Foundation": "http://www.loganfdn.org/",
"The Thiel Fellowship": "http://thielfellowship.org/",
"Thiel Foundation": "http://www.thielfoundation.org/",
"Tiffany Yin": null,
"Tom Preston-Werner": "http://tom.preston-werner.com/",
"Truman Chan": null,
"Tyler Hilliard": null,
"Victor Truong": "https://victortruong.net",
"Waj Waj": null,
"Will Gaybrick": null,
"William Wold": null,
"Xavier Shay": "https://xaviershay.com/",
"Yacoub Oulad Daoud": null,
"Yev Barkalov": "http://www.yev.sh/",
"Zach Holman": "https://zachholman.com/",
"Zane Davis-Barrs": "https://zane.sh/",
"Zane Sindhu": null
}
================================================
FILE: components/donate/sponsors.tsx
================================================
/** @jsxImportSource theme-ui */
import styled from '@emotion/styled'
import { Box, Image, Link } from 'theme-ui'
const Base = styled(Box)`
display: grid;
grid-gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
align-items: center;
justify-content: center;
img {
margin: auto;
max-width: 12rem;
}
`
const Sponsor = ({ name, href, img, ...props }) => (
)
const Sponsors = props => (
{[
'Vercel',
'Slack',
'Netlify',
'FullStory',
'BrowserStack',
'Stripe',
'Segment',
'Bugsnag',
'Google',
'Dialpad'
].map(name => (
))}
)
export default Sponsors
================================================
FILE: components/dot.tsx
================================================
import { Text } from 'theme-ui'
import { keyframes } from '@emotion/react'
const flashing = keyframes({
from: { opacity: 0 },
'50%': { opacity: 1 },
to: { opacity: 0 }
})
export default function Dot({ hideOnMobile }) {
return (
)
}
================================================
FILE: components/elon.mdx
================================================
Elon Musk holds a special place amongst hackers. After growing up in a difficult
family situation in South Africa, working his way in small jobs until reaching
Los Angeles, teaching himself to code, and making hundreds of millions
co-founding PayPal, he kept on building.
It was a huge honor last month to have Elon [spend an hour in a Hack Club
AMA](https://youtu.be/riru9OzScwk)—at one point he remarked we were “asking
better questions than all the mainstream media.”
Afterwards, Elon wanted to support Hack Club further.
# Today, I’m proud to share: Elon Musk is donating $500,000 to Hack Club.
In so many ways, this is a milestone for every Hack Clubber. 6 years ago, I
started Hack Club as a 16-year-old programmer living on my own, scraping by,
barely able to make rent. Now Elon Musk is one of our largest supporters.
Elon is supporting us because we are a community of builders. When hackers see
problems in the world, we don’t blame someone else: we try to take them on
ourselves to solve. Elon is very selective about the nonprofits he supports and
I’m proud Hack Club is one of them.
So…how is Hack Club going to invest $500k? We want to use this to help 1000 more
students start and join Hack Clubs in their communities. For those already in
Hack Clubs, we look to you to help us make a more high-quality experience. We’re
a lot of what we’ve already been doing (and [what I wrote about at the beginning
of the year](https://zachinto2020.wordpress.com/2019/12/31/as-midnight-approaches/)):
we’ll spend as little money as possible at all times, and we’ll hire a small
number of diverse staff from video game engineers to media producers to make
Hack Club better. We are pushing hard now to expand users of [HCB](https://hackclub.com/fiscal-sponsorship/),
and continuing to try and make the Hack Club Slack the best place to be a teenager on the intenet.
We’ll have a proper announcement in a few weeks, but one thing we’re doing after
winning the [Frank Grant](https://grant.frank.ly/) and now receiving Elon’s
gift, is open sourcing our finances. Hack Club HQ has been running on HCB
since February and starting today, you can see our account publicly at
https://hcb.hackclub.com/hq. You can track how we spend every single dollar of
Elon’s gift. Soon, we will also launch https://frank.ly/ on Hack Club’s
website.
Hack Club’s mission is to build a new generation of hackers. This starts in high
school, where Hack Club students learn to be technically proficient, build their
friend network, learn to raise and spend money, and develop into kind, curious,
thoughtful, optimistic, and honest leaders.
Elon Musk is now supporting you and your work, so go out and do amazing things.
Elon can’t wait to see what you make.
—Zach
================================================
FILE: components/fade-in.tsx
================================================
import React from 'react'
import { Box } from 'theme-ui'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })
const Wrapper = styled(Box)`
@media (prefers-reduced-motion: no-preference) {
animation-name: ${fadeIn};
animation-fill-mode: backwards;
}
`
const FadeIn = ({ duration = 300, delay = 0, ...props }) => (
)
export default FadeIn
================================================
FILE: components/fiscal-sponsorship/contact.tsx
================================================
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 (
Questions? Email {email}{' '}
or call {phoneNumber}
)
}
================================================
FILE: components/fiscal-sponsorship/directory/card.tsx
================================================
import { Card, Badge as ThemeBadge, Box, Heading, Text, Image } from 'theme-ui'
import { Organization } from '../../../lib/organization'
import Tilt from '../../tilt'
import Icon from '@hackclub/icons'
import Tooltip from '../tooltip'
export const Badge = ({ badge }) =>
badge.image ? (
.
) : (
)
type OrganizationCardProps = {
organization: Organization
openModal: (organization: Organization) => void
badges: any[]
}
/**
*
* @param {{
* organization: Organization,
* showTags: boolean
* }} props
* @returns
*/
export const OrganizationCard = ({
openModal,
badges,
organization
}: OrganizationCardProps) => (
openModal(organization)}
rel="noopener noreferrer"
itemScope
itemType="http://schema.org/Event"
variant="event"
sx={{
justifyContent: 'center',
alignItems: 'center',
minHeight: 128,
color: 'white',
cursor: 'pointer',
textShadow: '0 1px 4px rgba(0, 0, 0, 0.375)',
textDecoration: 'none',
backgroundColor: 'black',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
borderRadius: 'extra',
overflow: 'hidden',
position: 'relative',
p: 3,
height: '100%',
display: 'flex',
px: 3,
backgroundImage: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,0.375) 75%), url('${organization.branding.backgroundImage}')`,
textAlign: 'center',
flexDirection: 'column'
}}
>
{badges.map((badge, i) => (
))}
{organization.branding.logo && (
)}
{organization.name}
<>
{organization.raw.location.continent}
>
{organization.links.website}
)
export default OrganizationCard
================================================
FILE: components/fiscal-sponsorship/features.tsx
================================================
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 (
Powerful financial tools built by our nonprofit, for yours.
Since day one, we’ve built beautiful, self-serve software to empower
you to raise and spend money without administrative hassle. We’re also
open source!
{/* Send money & reimburse via check, ACH, bank wire, & 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. " */}
See our open source on GitHub
)
}
function Module({ icon, name, body }) {
return (
{name}
{' '}
{body}
)
}
function Laptop({ href, title }) {
return (
See Reboot’s finances in Transparency Mode
)
}
================================================
FILE: components/fiscal-sponsorship/first/apply-button.tsx
================================================
import { Button, Text, Flex } from 'theme-ui'
import Icon from '../../icon'
import Link from 'next/link'
export default function ApplyButton() {
return (
)
}
================================================
FILE: components/fiscal-sponsorship/first/features.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Heading, Link, Text, Container, Card, Image } from 'theme-ui'
import Icon from '../../icon'
import Masonry from 'react-masonry-css'
import NextImage from 'next/image'
import { Fade } from '../../react-reveal-compat'
export default function Features() {
return (
Everything you'll need.
Organize your team's finances in real time, receive grants, gain
nonprofit status, & more.
Use features engineered by FIRST alumni to help you run a
successful team.
{/* */}
{/*
Date
10-10-2020
Pay to the
order of
Hack Club
$
1000
One thousand only
Dollars
Memo
{' '}
Grant for Poseidon Robotics
⑆ 00000000000 ⑆ 123456789 ⑆
*/}
{/* */}
Hack Club does not directly provide banking services. Banking services
are provided by FDIC-certified financial institutions.
)
}
type ModuleProps = {
icon: string
name: string
body: string
iconColor?: string
}
function Module({ icon, name, body, iconColor }: ModuleProps) {
return (
{name}
{body}
)
}
function ModuleDetails({ children }) {
return (
{children}
)
}
function Document({ name, cost }) {
return (
{name}
{cost && (
{cost}
)}
)
}
function Laptop({ href, title, sx }) {
return (
)
}
================================================
FILE: components/fiscal-sponsorship/first/start.tsx
================================================
import { Box, Link, Text, Heading, Flex } from 'theme-ui'
import Stats from './stats'
import ApplyButton from './apply-button'
export default function Start({ stats }) {
return (
<>
Sign up for HCB.
Open to Hack Clubs, hackathons, and charitable organizations in
the US and Canada.
We run Hack Club HQ on HCB!{' '}
See our finances.
>
)
}
================================================
FILE: components/fiscal-sponsorship/first/stats.tsx
================================================
import { Text, Box, Flex } from 'theme-ui'
import { useEffect, useState } from 'react'
const easeInOutExpo = x =>
x === 0
? 0
: x === 1
? 1
: x < 0.5
? Math.pow(2, 20 * x - 10) / 2
: (2 - Math.pow(2, -20 * x + 10)) / 2
function startMoneyAnimation(
setBalance,
amount,
duration = 2_000,
moneyFormatter
) {
const startTime = performance.now()
function animate() {
const time = performance.now() - startTime
const progress = time / duration
const easedProgress = easeInOutExpo(progress)
setBalance(moneyFormatter(amount * easedProgress))
if (progress < 1) {
requestAnimationFrame(animate)
} else {
setBalance(moneyFormatter(amount))
}
}
requestAnimationFrame(animate)
}
function formatMoney(amount) {
const normalisedAmount = amount / 100
return normalisedAmount
.toLocaleString('en-US', { style: 'currency', currency: 'USD' })
.split('.')
}
const Stats = ({ stats }) => {
const [balance, setBalance] = useState(0) // A formatted balance string, split by decimal
useEffect(() => {
const observer = new IntersectionObserver(
e => {
if (e[0].isIntersecting) {
console.info('intersecting')
startMoneyAnimation(
setBalance,
stats.transactions_volume,
2_500,
formatMoney
)
}
},
{ threshold: 1.0 }
)
observer.observe(document.querySelector('#parent'))
return () => observer.disconnect()
}, [stats.transactions_volume])
if (stats.transactions_volume === undefined) {
return null
}
return (
So far we have enabled
{stats ? (
<>
{balance[0]}
.{balance[1]}
>
) : (
...
)}
in transactions
)
}
export async function getStaticProps() {
const res = await fetch(`https://hcb.hackclub.com/stats`)
try {
const stats = await res.json()
return {
props: {
stats
},
revalidate: 10
}
} catch (e) {
return {
props: {
stats: {}
},
revalidate: 10
}
}
}
export default Stats
================================================
FILE: components/fiscal-sponsorship/first/testimonials.tsx
================================================
import {
Box,
Image,
Text,
Heading,
Container,
Grid,
Link,
Avatar,
Button
} from 'theme-ui'
import { Slide } from '../../react-reveal-compat'
export default function Testimonials() {
return (
<>
FIRST teams all over the country run on HCB.
Everywhere from San Jose to Boston to New York, HCB powers teams of
all sizes.
>
)
}
function Organization({
logo,
name,
website,
teamNum,
teamLocation,
budget: _budget,
budgetLabel: _budgetLabel,
url,
imgSrc,
quote,
hackerName,
hackerAvatarUrl,
hackerRole,
transparency = undefined
}) {
return (
{name}
{teamNum}
{' '}
• {teamLocation}
"{quote}"
{hackerName}
{hackerRole}
{transparency && (
)}
)
}
================================================
FILE: components/fiscal-sponsorship/open-source.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Heading, Button, Text, Container, Grid, Flex } from 'theme-ui'
import Icon from '../icon'
import Photo from '../photo'
import HCBGource from '../../public/fiscal-sponsorship/hcb-gource.gif'
export default function OpenSource() {
return (
Open source infrastructure for fiscally sponsored organizations.
HCB is open source and built in public, like many other Hack Club
projects. Join us in building the infrastructure powering fiscally
sponsored organizations around the world.
)
}
================================================
FILE: components/fiscal-sponsorship/organization-spotlight.tsx
================================================
/** @jsxImportSource theme-ui */
import Tilt from '../tilt'
import { Card, Heading, Text } from 'theme-ui'
import Image from 'next/image'
import { Balancer } from 'react-wrap-balancer'
export default function OrganizationSpotlight({ organization }) {
return (
>
)
}
function Slide({ children }) {
return (
{children}
)
}
function BlueGradientFilter() {
return (
)
}
================================================
FILE: components/hackathons/overview.tsx
================================================
import { Box, Heading, Container, Text, Grid } from 'theme-ui'
export default function Overview() {
return (
<>
A hackathon is a social coding marathon where teenagers{' '}
come together to{' '}
build projects for a weekend and{' '}
share them with the world.
The best way to learn is by building.
A hackathon is a space that helps give makers everything they need
to start building–mentors, collaborators, inspiration, and a goal
to work towards. Hackers will leave a hackathon with a project of
their own, ready and excited to keep hacking once they get home.
We're at our best when we're making.
Hack Club is a global community of thousands of high school
makers. We're organizers, coders, hackers, painters, engineers,
musicians, writers, volunteers. We make things. We want others to
make things too.
>
)
}
function Highlight({ children }) {
return (
{children}
)
}
================================================
FILE: components/hackathons/recap.tsx
================================================
/** @jsxImportSource theme-ui */
import { Card, Box, Heading, Grid, Text } from 'theme-ui'
import Stage from '../stage'
export default function Recap() {
return (
<>
Get started today
Resources so you can organize an{' '}
amazing
{' '}
hackathon.
a, > div': {
borderRadius: 'extra',
boxShadow: 'elevated',
px: [3, null, 4],
py: [4, null, 5]
},
span: {
boxShadow:
'-2px -2px 6px rgba(255,255,255,0.125), inset 2px 2px 6px rgba(0,0,0,0.1), 2px 2px 8px rgba(0,0,0,0.0625)'
},
svg: { fill: 'currentColor' }
}}
>
>
)
}
================================================
FILE: components/hackathons/scrolling-hackathons.tsx
================================================
/** @jsxImportSource theme-ui */
import Ticker from 'react-ticker'
import {
Box,
Card,
Text,
Heading,
Badge,
Container,
Image,
Link
} from 'theme-ui'
import { useState } from 'react'
import { keyframes } from '@emotion/react'
import Tilt from '../tilt'
import PageVisibility from 'react-page-visibility'
import { formatAddress } from '../../lib/helpers'
export default function ScrollingHackathons({
eventData,
mode,
title,
...props
}) {
const [pageIsVisible, setPageIsVisible] = useState(true)
const handleVisibilityChange = isVisible => {
setPageIsVisible(isVisible)
}
return (
<>
{title ? (
Join other high-schoolers at an upcoming hackathon.
from{' '}
hackathons.hackclub.com
, last updated just now.
) : (
<>>
)}
{pageIsVisible && (
{() => (
{eventData.map((event: any) => (
))}
)}
)}
>
)
}
const flashing = keyframes({
from: { opacity: 0 },
'50%': { opacity: 1 },
to: { opacity: 0 }
})
function Dot() {
return (
)
}
type EventCardProps = {
name: string
website: string
start: string
end: string
city?: string
state?: string
country?: string
countryCode?: string
banner: string
logo?: string
virtual?: boolean
hybrid?: boolean
footer?: React.ReactNode
}
function EventCard({
name,
website,
start,
end,
city,
state,
country,
countryCode,
banner,
logo,
virtual,
hybrid,
footer
}: EventCardProps) {
return (
{virtual ? 'Online' : hybrid ? 'Hybrid' : 'In-Person'}
{logo && (
)}
{name}
{footer ? (
footer
) : (
<>
{!virtual && (
{formatAddress(city, state, country, countryCode)}
)}
>
)}
{virtual
? 'https://schema.org/OnlineEventAttendanceMode'
: 'https://schema.org/OfflineEventAttendanceMode'}
{website}
{start}
{end}
)
}
================================================
FILE: components/icon.tsx
================================================
import React from 'react'
import Icon from '@hackclub/icons'
export default function IconComponent(props: any): React.ReactElement {
return
}
================================================
FILE: components/index/cards/beest.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Image, Text } from 'theme-ui'
import { keyframes } from '@emotion/react'
const slideIn = keyframes({
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(-70%)' }
})
export default function Beest() {
return (
Spend 40 hours building projects, fly to the Netherlands, build a
mechanical animal!
Get building!
)
}
================================================
FILE: components/index/cards/button.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Button, Text } from 'theme-ui'
import ReactTooltip from '../../react-tooltip'
import Icon from '@hackclub/icons'
type ButtonsProps = {
children: React.ReactNode
icon?: string
customIcon?: React.ReactNode
id: string
content?: React.ReactNode
link?: string
primary?: boolean | string
overrideColor?: string
zIndex?: number
sx?: any
}
export default function Buttons({
children,
icon,
customIcon,
id,
content,
link,
primary,
overrideColor,
zIndex,
sx,
...props
}: ButtonsProps) {
const fontWeight = primary ? '700' : '400'
return (
{
return null
}}
className="custom-tooltip-radius custom-arrow-radius"
arrowRadius="2"
tooltipRadius="10"
>
{content}
)
}
================================================
FILE: components/index/cards/card-model.tsx
================================================
import Icon from '../../icon'
import { Box, Card, Flex, Image, Link, Text } from 'theme-ui'
import ReactTooltip from '../../react-tooltip'
import Comma from '../../comma'
/** @jsxImportSource theme-ui */
const CardModel = ({
background,
children,
image,
image_fit,
link,
highlight,
github_link,
badge,
text,
color,
stars,
delay,
position,
filter,
visible = false,
...props
}: {
[x: string]: any
background?: any
children?: any
image?: any
image_fit?: any
link?: any
highlight?: any
github_link?: any
badge?: any
text?: any
color?: any
stars?: any
delay?: any
position?: any
filter?: any
visible?: boolean
}) => (
//
{badge && (
{text || 'Happening now'}
)}
{github_link && (
{position === 'bottom' ? (
{stars ? (
⭐️ {stars}
) : (
<>>
)}
) : (
{stars ? (
⭐️ {stars}
) : (
<>>
)}
)}
)}
{image && (
)}
{children}
//
)
export default CardModel
================================================
FILE: components/index/cards/clubs.tsx
================================================
/** @jsxImportSource theme-ui */
import Buttons from './button'
import CardModel from './card-model'
import { Box, Grid, Flex, Image, Text } from 'theme-ui'
const Cover = () => (
)
export default function Clubs() {
// let [fooRef, setFooRef] = useState('')
// let [toggle, setToggle] = useState(true)
return (
A Network of 1000+ Coding Clubs
Join or start a Hack Club and be part of a network of high
quality coding clubs where you learn to code entirely through
building things.
You can start with no experience and build and ship a project every
meeting.
Start a club
)
}
================================================
FILE: components/index/cards/fallout.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text, Image } from 'theme-ui'
import CardModel from './card-model'
import Buttons from './button'
export default function Fallout() {
return (
Build hardware projects, track your hours, then{' '}
attend a hardware hackathon in Shenzhen!
Start Building
)
}
================================================
FILE: components/index/cards/flavortown.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Image, Text } from 'theme-ui'
import Buttons from './button'
export default function Flavortown() {
return (
Make a website, game, hardware project, or anything your heart
desires, share your project for others to experience and to get
cookies - our virtual currency, and exchange your cookies for iPads,
MacBooks, Raspberry Pis and so many more things - all for free!
Start Cooking
)
}
================================================
FILE: components/index/cards/hackathons.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Image, Link, Text } from 'theme-ui'
import Buttons from './button'
import Dot from '../../dot'
import { formatDate } from '../../../lib/dates'
const Cover = () => (
)
export default function Hackathons({ data, stars }) {
return (
High School Hackathons
We support the largest network of high school hackathons in the
world. From an online community of organizers to free stickers and
more!{' '}
Attend a hackathon
Organizer? Learn more.
Upcoming Hackathons
{data.slice(0, 5).map(data => (
{data.logo && (
)}
{data.name}
{formatDate({ format: 'mmmm d', date: data.start })}
))}
Upcoming Hackathons:
{data.slice(0, 2).map(data => (
{data.logo && (
)}
{data.name}
))}
)
}
================================================
FILE: components/index/cards/haxidraw.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Text } from 'theme-ui'
import Buttons from './button'
export default function Haxidraw({ stars }) {
return (
Blot
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.
Learn more
Create something in the editor
Share your creations and chat on Slack
)
}
================================================
FILE: components/index/cards/hcb.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Grid, Heading, Text } from 'theme-ui'
import Buttons from './button'
export default function Bank({ data }) {
return (
HCB
Become a 501(c)3 nonprofit and join 700+ teams using HCB to run
world-class events.
This platform is built and maintained by the Hack Club team.
Start fundraising!
{/* */}
{' '}
{/* */}
)
}
================================================
FILE: components/index/cards/hctg.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Image, Text } from 'theme-ui'
export default function HackClubTheGame() {
return (
Hack Club: The Game
Build any type of project you want! Submit them, and eventually you'll
be able to compete in a scavenger hunt adventure game across
Manhattan!
Join the Game!
)
}
================================================
FILE: components/index/cards/horizons.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Text, Box, Image, Button } from 'theme-ui'
import localFont from 'next/font/local'
const hypebuzz = localFont({
src: '../../../public/horizons/Hypebuzz.otf'
})
export default function Horizons() {
return (
HIGH SCHOOL FLAGSHIP HACKATHONS ACROSS THE WORLD
{[
{ color: '#ffa936', height: '15px' },
{ color: '#f86d95', height: '15px' },
{ color: '#46467c', height: '15px' }
].map(({ color, height }) => (
))}
)
}
================================================
FILE: components/index/cards/jackpot.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text, Image, Grid, Flex } from 'theme-ui'
import CardModel from './card-model'
export default function Jackpot() {
return (
No hours required...enjoy a weekend hackathon in Las Vegas!
)
}
================================================
FILE: components/index/cards/macondo.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text, Image } from 'theme-ui'
import CardModel from './card-model'
import Buttons from './button'
export default function Macondo() {
return (
Build software or hardware projects (with up to $1k in funding for
hardware), win prizes, and{' '}
fly to Bogotá, Colombia for a hackathon!
Start Building
)
}
================================================
FILE: components/index/cards/mailing-list.tsx
================================================
import Icon from '@hackclub/icons'
import { useEffect, useRef, useState } from 'react'
import { Box, Button, Card, Flex, Grid, Input, Link, Text } from 'theme-ui'
import { format, parse } from 'date-fns'
import BGImg from '../../background-image'
import background from '../../../public/home/footer.png'
import MailCard from '../../mail-card'
const Loading = () => (
)
const MailingList = () => {
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [data, setData] = useState({ finalHtml: [], names: [] })
const formRef = useRef(null)
const handleSubmit = async e => {
e.preventDefault()
setSubmitting(true)
const res = await fetch('/api/mailing-list', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: e.target.name.value,
email: e.target.email.value
})
})
formRef.current.reset()
if (res.ok) {
setSubmitted(true)
}
setSubmitting(false)
}
// This lovely concoction of JavaScript basically fetches the last two newsletters from the GitHub repo,
// converts them to HTML, gets rid of those HTML tags, the sets all of that as the state of the component.
// Then, It makes a second fetch request to get the filename, so that can be used to determine the link.
// After that, it removes the file extension, so we can use that as the date.
// Finally, it sets the state of data to the final HTML and the names of the files, so we can map that later on!
useEffect(() => {
Promise.all([
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 =>
import('@hackclub/markdown').then(mod =>
Promise.all(markdown.map(md => mod.default(md)))
)
)
.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 }))
}, [])
return (
Join the newsletter
We'll send you an email no more than once a month, when we
work on something cool for you. Check out our{' '}
previous issues
.
{data.finalHtml
.map((html, index) => (
))
.reverse()}
)
}
export default MailingList
================================================
FILE: components/index/cards/sinerider.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Image, Text } from 'theme-ui'
import Buttons from './button'
export default function Sinerider({ stars }) {
return (
SineRider is a game about love and graphing, powered by teenage
hackers of all kinds: artists, musicians, programmers, storytellers…
so if that’s you, come join us! We can always use help keeping
everything up to date and running smoothly.
Play now
Join the development
)
}
================================================
FILE: components/index/cards/slack.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Heading, Image, Link, Text } from 'theme-ui'
import Buttons from './button'
import Event from '../events'
import Comma from '../../comma'
const Cover = () => (
)
const Stats = ({ data, subheading, nonMobile = false }) => (
{data}
{subheading}
)
export default function Slack({ data, events }) {
return (
t.util.gx('cyan', 'purple'),
minHeight: ['500px', '400px'],
py: [3, 3, 4]
}}
>
Our Online Community
Coding doesn't have to be a solitary activity. At Hack Club, we
make remarkable things together, and in our Slack you'll find
awesome people to hang out with too. Code together, find your
programming community, dream up something wild, or just #lounge.
Occasionally we invite someone we really want to speak to (like Sal
Khan, George Hotz, and Lady Ada) and host an{' '}
AMA
{' '}
with them.{' '}
Join our Slack
0 ? 'block' : 'none'
}}
>
)
}
================================================
FILE: components/index/cards/sleepover.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Image, Text } from 'theme-ui'
import CardModel from './card-model'
export default function Sleepover() {
return (
{/* Numbered list on the right */}
{[
'Spend 30 hours learning to code',
'Earn prizes like plushies, iPads, and more!',
'Fly to a slumber party themed hackathon in Chicago this April!'
].map((item, i) => (
{i + 1}.
{item}
))}
{/* Logo on the left */}
{/* Button below logo - tablet/desktop only */}
{/* Button at bottom center - mobile only */}
)
}
================================================
FILE: components/index/cards/sprig-console.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Grid, Image, Text } from 'theme-ui'
import Buttons from './button'
import CardModel from './card-model'
import Tilt from '../../tilt'
export default function SprigConsole({ stars, consoleCount }) {
return (
Join the other {consoleCount} teenagers with Sprigs!
Play your own Sprig games on this console, which you can assemble
and disassemble. Each kit includes parts needed for getting
started with hardware engineering and embedded systems
programming.{' '}
Build a game and get your console
)
}
================================================
FILE: components/index/cards/sprig.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Flex, Grid, Image, Link, Text } from 'theme-ui'
import Buttons from './button'
import RelativeTime from 'react-relative-time'
function Game({ game, gameImage, gameImage1, ...props }) {
return (
')`,
borderImageOutset: '2',
boxShadow: '0 8px 8px rgba(0, 0, 0, 0.2)',
'&:hover': {
transform: 'scale(1.05)',
background: 'rgba(77, 90, 114, 0.8)'
}
}}
{...props}
>
')`,
borderImageOutset: '2',
height: '100%',
textDecoration: 'none'
}}
>
{/* */}
{game?.title}
by {game?.author}
)
}
export default function Sprig({ stars, game, gameImage, gameImage1 }) {
return (
Draw, make music, and craft games in our web-based JavaScript game
editor, which has been used by 7k+ makers around the world.
Build a Sprig game
Review games / build the engine
Connect with other game devs
New from{' '}
the gallery
...
)
}
================================================
FILE: components/index/cards/stasis.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Flex, Image, Text } from 'theme-ui'
import CardModel from './card-model'
export default function Stasis() {
return (
We're bringing 100+ hack clubbers from all over the world to Austin,
TX for a 4-day hardware hackathon, and we're funding your next biggest
hardware projects.
)
}
================================================
FILE: components/index/cards/workshops.tsx
================================================
/** @jsxImportSource theme-ui */
import CardModel from './card-model'
import { Box, Card, Flex, Grid, Heading, Image, Text } from 'theme-ui'
import Buttons from './button'
const WorkshopCard = ({
slug,
name,
description,
img,
height,
section,
...props
}) => (
{name}
{description}
)
export default function Workshops({ stars }) {
return (
Workshops
100+ community-contributed, self-guided coding tutorials and ideas.
Learn to code by building, one project at a time.
Browse The Workshops
Build A Workshop
)
}
================================================
FILE: components/index/carousel-cards.tsx
================================================
import { Box, Card, Image, Link, Text } from 'theme-ui'
import Icon from '../icon'
export default function CarouselCards({
background,
backgroundImage,
backgroundSize,
titleColor,
descriptionColor,
title,
description,
img,
link
}) {
return (
{title}
{description}
)
}
================================================
FILE: components/index/carousel.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text } from 'theme-ui'
import CarouselCards from './carousel-cards'
import React, { useState } from 'react'
import Ticker from 'react-ticker'
import PageVisibility from 'react-page-visibility'
export default function Carousel({ cards }) {
const [speed, setSpeed] = useState(5)
const [pageIsVisible, setPageIsVisible] = useState(true)
const handleVisibilityChange = isVisible => {
setPageIsVisible(isVisible)
}
return (
{pageIsVisible && (
Here are a few projects you could get involved in:
{() => (
setSpeed(2)}
onMouseOut={() => setSpeed(6)}
>
{cards.map((card, idx) => (
))}
)}
)}
)
}
================================================
FILE: components/index/ctas.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text, Image, Card } from 'theme-ui'
import React, { useState } from 'react'
import PageVisibility from 'react-page-visibility'
export default function CTAS({ cards }) {
const [pageIsVisible, setPageIsVisible] = useState(true)
const handleVisibilityChange = isVisible => {
setPageIsVisible(isVisible)
}
return (
{pageIsVisible && (
{cards.map((card, idx) => {
const {
background,
backgroundImage,
backgroundSize,
gridBackground,
stickerImage,
stickerImageScale,
buttonImage,
animatedStickers,
description,
descriptionColor,
title,
logo,
logoScale,
link
} = card
return (
{animatedStickers && (
{animatedStickers.map((sticker, i) => (
))}
)}
{stickerImage && !animatedStickers && (
)}
{description}
{buttonImage && (
)}
)
})}
)}
)
}
================================================
FILE: components/index/events.tsx
================================================
import { Box, Text, Grid, Badge, Flex, Avatar, Heading } from 'theme-ui'
import tt from 'tinytime'
import Link from 'next/link'
const past = dt => new Date(dt) < new Date()
const now = (start, end) =>
new Date() > new Date(start) && new Date() < new Date(end)
const Event = ({ id, slug, title, desc, leader, avatar, start, end, cal }) => (
{tt('{MM} {Do}').render(new Date(start))}{' '}
{tt('{h}:{mm}').render(new Date(start))}–
{tt('{h}:{mm} {a}').render(new Date(end))}
{title}
{now(start, end)}
{!avatar.includes('emoji') && (
)}
{leader}
{/* {now(start, end) && (
)} */}
)
export default function Events({ events }) {
return (
{events
.slice(0, 3)
.map(event =>
!past(event.end) ? : <>>
)}
)
}
================================================
FILE: components/index/github.tsx
================================================
import { Badge, Image, Text } from 'theme-ui'
import RelativeTime from 'react-relative-time'
type GitHubProps = {
type: 'commit' | 'issue' | 'pull_request'
img: string
user: string
time: string
message: string
opacity?: number
url?: string
}
export default function GitHub({
type,
img,
user,
time,
message,
opacity,
url,
...props
}: GitHubProps) {
return (
{user}
{message}
)
}
================================================
FILE: components/letterhead.tsx
================================================
/** @jsxImportSource theme-ui */
import { Avatar, Badge, Box, Container, Flex, Heading } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from './nav'
import Footer from './footer'
import ForceTheme from './force-theme'
const Authored = ({ name, avatar, url, date, ...props }) => (
{name}
{date}
)
const Letterhead = ({
title,
desc,
author = { name: null, avatar: null, url: null },
date,
img,
path,
includeMeta = true,
hideGitHub = false,
children,
...props
}) => (
<>
{title}
{desc}
{author?.name && }
{children}
>
)
export default Letterhead
================================================
FILE: components/mail-card.tsx
================================================
import { Box, Card, Link, Text } from 'theme-ui'
export default function MailCard({ body, date, link, issue }) {
body = body.length > 130 ? body.substring(0, 130) + '...' : body
return (
{date}
— From Hack Club, to You
{body}
)
}
================================================
FILE: components/marquee.tsx
================================================
import {
useRef,
useEffect,
useState,
Children,
cloneElement,
isValidElement,
type ReactNode,
type ReactElement
} from 'react'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
const scroll = keyframes`
from { transform: translateX(0); }
to { transform: translateX(-50%); }
`
const Track = styled.div<{ duration: number }>`
display: flex;
width: max-content;
animation: ${scroll} ${props => props.duration}s linear infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
`
type Props = {
velocity?: number
children: ReactNode
onInit?: () => void
onFinish?: () => void
}
export default function Marquee({
velocity = 30,
children,
onInit,
onFinish
}: Props) {
const trackRef = useRef(null)
const [duration, setDuration] = useState(20)
useEffect(() => {
if (trackRef.current) {
const width = trackRef.current.scrollWidth / 2
setDuration(width / velocity)
}
}, [velocity])
const childArray = Children.toArray(children)
const dupes = childArray.map((child, i) =>
isValidElement(child)
? cloneElement(child as ReactElement, { key: `dup-${i}` })
: child
)
return (
}
if (chunk?.startsWith('`')) {
return {chunk.replace(/`/g, '')}
}
if (chunk?.startsWith('*')) {
return {chunk.replace(/\*/g, '')}
}
if (chunk?.startsWith('_')) {
return {chunk.replace(/_/g, '')}
}
return {chunk?.replace(/&/g, '&')}
})
const Post = ({
user = {
username: 'abc',
avatar: '',
streakDisplay: false,
streakCount: 0
},
text,
attachments = [],
postedAt
}) => (
@{user.username}
{formatDate({ format: 'mmmm d yyyy', date: postedAt })}
div': { width: 18, height: 18 }
}}
>
{formatText(text)}
{attachments.length > 0 && (
<>
{filter(attachments, a =>
['jpg', 'jpeg', 'png'].includes(
a.split('.')[a.split('.').length - 1]
)
).map(img => (
))}
>
)}
)
export default function Posts({ data = [] }) {
return (
{data.map(post => (
))}
These are just a few posts…
)
}
================================================
FILE: components/posts/mention.tsx
================================================
import { Link, Avatar } from 'theme-ui'
import { memo, useState, useEffect } from 'react'
import { trim } from 'lodash'
const Mention = memo(function Mention({ username }: { username: string }) {
const [img, setImg] = useState(null)
useEffect(() => {
fetch(`https://scrapbook.hackclub.com/api/profiles/${trim(username)}`)
.then(r => r.json())
.then(profile => setImg(profile.avatar))
.catch(console.error)
}, [username])
return (
{img && (
)}
@{username}
)
})
export default Mention
================================================
FILE: components/press.mdx
================================================
import { Grid } from 'theme-ui'
Hack Club is a global nonprofit network of high school makers & student-led coding clubs where young people build the agency, the network, & the technical talent to think big & do big things in the world. Founded in 2014 by 16-year-old Zach Latta, Hack Clubs are now in nearly 400 high schools with 10,000 students each year.
Hack Club has been profiled on the TODAY Show, in the Wall Street Journal, and in many other publications around the country. If you are writing a story or have other press inquires, please contact Hack Club Co-founder Christina Asquith: [christina@hackclub.com](mailto:christina@hackclub.com).
## Photos






================================================
FILE: components/react-reveal-compat.tsx
================================================
import React from 'react'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
const kFade = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })
const kFadeLeft = keyframes({
from: { opacity: 0, transform: 'translateX(-30px)' },
to: { opacity: 1, transform: 'translateX(0)' }
})
const kFadeRight = keyframes({
from: { opacity: 0, transform: 'translateX(30px)' },
to: { opacity: 1, transform: 'translateX(0)' }
})
const kFadeUp = keyframes({
from: { opacity: 0, transform: 'translateY(30px)' },
to: { opacity: 1, transform: 'translateY(0)' }
})
const kFadeDown = keyframes({
from: { opacity: 0, transform: 'translateY(-30px)' },
to: { opacity: 1, transform: 'translateY(0)' }
})
const kSlideLeft = keyframes({
from: { opacity: 0, transform: 'translateX(-80px)' },
to: { opacity: 1, transform: 'translateX(0)' }
})
const kSlideRight = keyframes({
from: { opacity: 0, transform: 'translateX(80px)' },
to: { opacity: 1, transform: 'translateX(0)' }
})
const kSlideUp = keyframes({
from: { opacity: 0, transform: 'translateY(60px)' },
to: { opacity: 1, transform: 'translateY(0)' }
})
const kSlideDown = keyframes({
from: { opacity: 0, transform: 'translateY(-60px)' },
to: { opacity: 1, transform: 'translateY(0)' }
})
const kZoom = keyframes({
from: { opacity: 0, transform: 'scale(0.85)' },
to: { opacity: 1, transform: 'scale(1)' }
})
const Wrapper = styled.div<{
$anim: ReturnType
$delay: number
$duration: number
}>`
animation: ${p => p.$anim} ${p => p.$duration}ms ease-out ${p => p.$delay}ms
both;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
`
type RevealProps = {
children?: React.ReactNode
left?: boolean
right?: boolean
up?: boolean
down?: boolean
top?: boolean
bottom?: boolean
delay?: number | string
duration?: number | string
cascade?: boolean
style?: React.CSSProperties
className?: string
}
function RevealWrap({
children,
anim,
delay,
duration
}: {
children: React.ReactNode
anim: ReturnType
delay: number
duration: number
}) {
return (
{children}
)
}
export function Fade({
children,
left,
right,
up,
down,
delay = 0,
duration = 500,
cascade
}: RevealProps) {
const anim = left
? kFadeLeft
: right
? kFadeRight
: up
? kFadeUp
: down
? kFadeDown
: kFade
const d = Number(delay)
const dur = Number(duration)
if (cascade) {
return (
<>
{React.Children.map(children, (child, i) => (
{child}
))}
>
)
}
return (
{children}
)
}
export function Slide({
children,
left,
right,
top,
up,
delay = 0,
duration = 500
}: RevealProps) {
const anim = left
? kSlideLeft
: right
? kSlideRight
: top
? kSlideDown
: kSlideUp
return (
{children}
)
}
export function Zoom({ children, delay = 0, duration = 500 }: RevealProps) {
return (
{children}
)
}
export default Fade
================================================
FILE: components/react-tooltip.ts
================================================
import dynamic from 'next/dynamic'
const ReactTooltip = dynamic(() => import('react-tooltip'), {
ssr: false
})
export default ReactTooltip
================================================
FILE: components/replit/scale-up.tsx
================================================
import React, { useState, useEffect, useRef } from 'react'
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
const ScaleUp = ({ number, from = 0 }) => {
const [displayNumber, setDisplayNumber] = useState(from)
const previousNumberRef = useRef(from)
useEffect(() => {
const startValue = previousNumberRef.current
const duration = 2000
const startTime = performance.now()
const animate = () => {
const time = performance.now() - startTime
const progress = time / duration
const easedProgress = easeInOutExpo(progress)
const currentValue = startValue + (number - startValue) * easedProgress
setDisplayNumber(Math.round(currentValue))
if (progress < 1) {
requestAnimationFrame(animate)
} else {
setDisplayNumber(number)
previousNumberRef.current = number
}
}
if (startValue !== number) {
requestAnimationFrame(animate)
}
}, [number])
return {displayNumber}
}
export default ScaleUp
================================================
FILE: components/replit/token-instructions.tsx
================================================
'use client'
import Icon from '@hackclub/icons'
import { useState } from 'react'
import { Box, Card, Text, Image, Heading, Button } from 'theme-ui'
const TokenInstructions = () => {
const [currentStep, setCurrentStep] = useState(0)
const tokenSteps = [
{
image: '/replit/replit-homepage.png',
desc: 'Go to replit.com, and sign in.'
},
{
image: '/replit/aarc1.gif',
desc: "Open your browser's developer tools. You can do this by right-clicking on the page and selecting 'Inspect' or by pressing F12 on your keyboard."
},
{
image: '/replit/aarc2.gif',
desc: 'Select the application tab in the devtools'
},
{
image: '/replit/aarc3.gif',
desc: "Make sure replit.com cookies are selected, then scroll down to find 'connect.sid'. Copy the entire token & paste it in the form at the top of this page."
}
]
return (
Step {currentStep + 1} of {tokenSteps.length}
{tokenSteps[currentStep].desc}
)
}
export default TokenInstructions
================================================
FILE: components/scroll-hint.tsx
================================================
import { Box } from 'theme-ui'
import { animate } from 'animejs'
const handleClick = () => {
const scroll = { x: document.scrollingElement.scrollTop }
animate(scroll, {
x: window.innerHeight,
ease: 'outExpo',
duration: 800,
onUpdate: () => {
document.scrollingElement.scrollTop = scroll.x
}
})
}
const ScrollHint = ({ ...props }) => (
)
export default ScrollHint
================================================
FILE: components/secret.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Text } from 'theme-ui'
import { useState, useEffect } from 'react'
import Image from 'next/image'
export default function Secret({ reveal, ...props }) {
const [img, setImage] = useState('')
useEffect(() => {
setImage('https://github.com/hackclub/dinosaurs/raw/main/club_dinosaur.png')
}, [])
return (
.lid-one': {
transform: 'rotateX(90deg)',
transitionDelay: '0s'
},
'&:hover > .lid-two': {
transform: 'rotateX(180deg)',
transitionDelay: '0.25s'
},
'&:hover > .letter': {
transform: 'translateY(-50px)',
transitionDelay: '0.5s'
}
}}
>
print kc
)
}
// credits: https://codepen.io/Coding-Star/pen/WNpbvwB
================================================
FILE: components/ship/why.mdx
================================================
import { Card } from 'theme-ui'
## Your first ship your first day.
Students in many traditional computer science classes are lucky to make a single project. At Hack Clubs, every member makes & ships their first website their very first meeting.
## Keeping your eyes on the prize.
Instead of learning programming concepts in isolation, learning by shipping means you focus on what you need to build real projects. It’s more fun & leads to better learning.
================================================
FILE: components/signature.tsx
================================================
import { Image, useColorMode } from 'theme-ui'
const Signature = ({ fname, lname, width }) => {
// enforce a sane color mode (typescript should do this in the future)
let [colorMode] = useColorMode()
colorMode = colorMode === 'dark' ? 'light' : 'dark'
return (
)
}
export default Signature
================================================
FILE: components/signatures.tsx
================================================
import { Image, useColorMode } from 'theme-ui'
const Signatures = ({ fileName, width }) => {
// enforce a sane color mode (typescript should do this in the future)
let [colorMode] = useColorMode()
colorMode = colorMode === 'dark' ? 'light' : 'dark'
return (
)
}
export default Signatures
================================================
FILE: components/slack.tsx
================================================
import { Button, Box, Container, Heading, Text } from 'theme-ui'
import styled from '@emotion/styled'
import usePrefersMotion from '../lib/use-prefers-motion'
import useHasMounted from '../lib/use-has-mounted'
import { formatted } from '../lib/members'
import Link from 'next/link'
let Highlight = styled(Text)`
color: inherit;
border-radius: 1em 0 1em 0;
background: linear-gradient(
-100deg,
rgba(250, 247, 133, 0.33),
rgba(250, 247, 133, 0.66) 95%,
rgba(250, 247, 133, 0.1)
);
`
Highlight = Highlight.withComponent('mark')
const Content = () => (
~ The Hack Club Slack ~
Come for the skills, stay for the people.
Communication and planning for our open source projects happen in the
Slack. Coding is often seen as an isolating activity. Plenty of groups
exist for kids who are interested in sports, theater, or chess, but the
stereotype of a programmer is a person who sits alone in a dark room.{' '}
It doesn't have to be this way—in the Hack Club Slack
(Discord-style online groupchat), you'll find a group of {formatted}+{' '}
fabulous people to talk to, active at all hours.
)
const Cover = () => (
t.util.gx('cyan', 'purple'),
opacity: 0.825,
zIndex: 1
}}
/>
)
const Static = ({
img = 'https://cloud-r4rrjh2z8-hack-club-bot.vercel.app/02020-07-25_a1tcva4ch6mmr6j2cfmcb4e9ync3yhar.png'
// img="https://cloud-r4rrjh2z8-hack-club-bot.vercel.app/12020-07-25_hn13qhejqrzu4n1jy9yacxxgrgp3wf5u.png"
}) => (
)
const Slack = () => {
const hasMounted = useHasMounted()
const prefersMotion = usePrefersMotion()
if (hasMounted && prefersMotion) {
return (
)
} else {
return
}
}
export default Slack
================================================
FILE: components/slide-down.tsx
================================================
import React from 'react'
import { Box } from 'theme-ui'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
const slideDown = keyframes({
from: { transform: 'translateY(-100%)', opacity: 0 },
to: { transform: 'translateY(0)', opacity: 1 }
})
const Wrapper = styled(Box)`
@media (prefers-reduced-motion: no-preference) {
animation-name: ${slideDown};
animation-fill-mode: backwards;
}
`
const SlideDown = ({ duration = 500, delay = 0, ...props }) => (
)
export default SlideDown
================================================
FILE: components/slide-up.tsx
================================================
import React from 'react'
import { Box } from 'theme-ui'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
const slideUp = keyframes({
from: { transform: 'translateY(100%)', opacity: 0 },
to: { transform: 'translateY(0)', opacity: 1 }
})
const Wrapper = styled(Box)`
@media (prefers-reduced-motion: no-preference) {
animation-name: ${slideUp};
animation-fill-mode: backwards;
}
`
const SlideUp = ({ duration = 500, delay = 0, ...props }) => (
)
export default SlideUp
================================================
FILE: components/sparkles/index.tsx
================================================
// Full credit to https://joshwcomeau.com/react/animated-sparkles-in-react/
import { useState } from 'react'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
import { range, sample, random } from 'lodash'
import { Text } from 'theme-ui'
import theme from '@hackclub/theme'
import useRandomInterval from '../../lib/use-random-interval'
import usePrefersReducedMotion from '../../lib/use-prefers-reduced-motion'
const generateSparkle = color => {
const sparkle = {
id: String(random(10000, 99999)),
createdAt: Date.now(),
color,
size: random(10, 20),
style: {
top: random(0, 100) + '%',
left: random(0, 100) + '%'
}
}
return sparkle
}
type SparklesProps = {
colors?: string[]
children: React.ReactNode
sx?: any
props?: any
}
const Sparkles = ({
colors = ['orange', 'yellow', 'green'],
children,
sx,
props,
...delegated
}: SparklesProps) => {
const allColors = colors.map(n => theme.colors[n])
const getColor = () => sample(allColors)
const [sparkles, setSparkles] = useState(() => {
return range(3).map(() => generateSparkle(getColor()))
})
const prefersReducedMotion = usePrefersReducedMotion()
useRandomInterval(
() => {
const sparkle = generateSparkle(getColor())
const now = Date.now()
const nextSparkles = sparkles.filter(sp => {
const delta = now - sp.createdAt
return delta < 750
})
nextSparkles.push(sparkle)
setSparkles(nextSparkles)
},
prefersReducedMotion ? null : 50,
prefersReducedMotion ? null : 450
)
return (
{sparkles.map(sparkle => (
))}
{children}
)
}
const Sparkle = ({ size, color, style }) => {
const path =
'M26.5 25.5C19.0043 33.3697 0 34 0 34C0 34 19.1013 35.3684 26.5 43.5C33.234 50.901 34 68 34 68C34 68 36.9884 50.7065 44.5 43.5C51.6431 36.647 68 34 68 34C68 34 51.6947 32.0939 44.5 25.5C36.5605 18.2235 34 0 34 0C34 0 33.6591 17.9837 26.5 25.5Z'
return (
)
}
const comeInOut = keyframes`
0% {
transform: scale(0);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0);
}
`
const spin = keyframes`
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(180deg);
}
`
const Wrapper = styled.span`
display: inline-block;
position: relative;
`
const SparkleWrapper = styled.span`
position: absolute;
display: block;
@media (prefers-reduced-motion: no-preference) {
animation: ${comeInOut} 1000ms forwards;
}
`
const SparkleSvg = styled.svg`
display: block;
@media (prefers-reduced-motion: no-preference) {
animation: ${spin} 1250ms linear;
}
`
const ChildWrapper = styled(Text)`
position: relative;
z-index: 1;
font-weight: bold;
`
export default Sparkles
================================================
FILE: components/sparkles/money.tsx
================================================
// Full credit to https://joshwcomeau.com/react/animated-sparkles-in-react/
import { useState } from 'react'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
import { range, sample, random } from 'lodash'
import { Text } from 'theme-ui'
import theme from '@hackclub/theme'
import useRandomInterval from '../../lib/use-random-interval'
import usePrefersReducedMotion from '../../lib/use-prefers-reduced-motion'
const generateSparkle = color => {
const sparkle = {
id: String(random(10000, 99999)),
createdAt: Date.now(),
color,
size: random(10, 25),
style: {
top: random(-10, 100) + '%',
left: random(-10, 100) + '%'
}
}
return sparkle
}
type MSparklesProps = {
colors?: string[]
children: React.ReactNode
sx?: any
path?: string
viewBox?: string
props?: any
}
const MSparkles = ({
colors = ['orange', 'yellow', 'green'],
children,
sx,
path,
viewBox,
props,
...delegated
}: MSparklesProps) => {
const allColors = colors.map(n => theme.colors[n])
const getColor = () => sample(allColors)
const [sparkles, setSparkles] = useState(() => {
return range(3).map(() => generateSparkle(getColor()))
})
const prefersReducedMotion = usePrefersReducedMotion()
useRandomInterval(
() => {
const sparkle = generateSparkle(getColor())
const now = Date.now()
const nextSparkles = sparkles.filter(sp => {
const delta = now - sp.createdAt
return delta < 750
})
nextSparkles.push(sparkle)
setSparkles(nextSparkles)
},
prefersReducedMotion ? null : 250,
prefersReducedMotion ? null : 150
)
return (
{sparkles.map(sparkle => (
))}
{children}
)
}
const Sparkle = ({ size, color, style, path, viewBox }) => {
if (!path)
path =
'M27 0H18V7.44119C8.56638 8.96454 2.608 15.2023 2.608 24.168C2.608 34.4553 10.2396 38.3636 18 41.2862V60.3901C14.2364 58.7919 11.8724 55.3359 11.536 50.088H0.591999C0.999374 61.0056 7.97574 68.051 18 69.933V80H27V70.2565C37.6237 69.0685 44.656 62.1891 44.656 51.912C44.656 40.3121 35.4657 36.7574 27 33.8641V16.9739C30.363 18.3007 32.5185 21.3983 32.656 26.76H43.312C43.228 15.2519 36.6759 8.81537 27 7.38614V0ZM18 16.871C14.8637 17.9425 13.072 20.2358 13.072 23.304C13.072 26.6134 15.0429 28.7127 18 30.3352V16.871ZM27 44.7132V61.0707C31.4416 60.1212 34.192 57.2657 34.192 53.16C34.192 48.9744 31.1721 46.6064 27 44.7132Z'
return (
)
}
const comeInOut = keyframes`
0% {
transform: scale(0);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0);
}
`
const spin = keyframes`
0% {
transform: rotate(0deg);
}
50% {
transform: rotate(20deg);
}
100% {
transform: rotate(0deg)
}
`
const Wrapper = styled.span`
display: inline-block;
position: relative;
`
const SparkleWrapper = styled.span`
position: absolute;
display: block;
@media (prefers-reduced-motion: no-preference) {
animation: ${comeInOut} 1000ms forwards;
}
`
const SparkleSvg = styled.svg`
display: block;
@media (prefers-reduced-motion: no-preference) {
animation: ${spin} 2250ms linear;
}
`
const ChildWrapper = styled(Text)`
position: relative;
z-index: 1;
font-weight: bold;
`
export default MSparkles
================================================
FILE: components/stage.tsx
================================================
import { Box, Heading, Text, ThemeUIStyleObject } from 'theme-ui'
import Icon from './icon'
type StageProps = {
icon: string
color: string
name: string
desc: string
children?: React.ReactNode
sx?: ThemeUIStyleObject
}
export default function Stage({
icon,
color,
name,
desc,
children,
...props
}: StageProps) {
return (
{children || (
)}
{name}
{desc}
)
}
================================================
FILE: components/stat.tsx
================================================
import { Flex, Text } from 'theme-ui'
import { isEmpty } from 'lodash'
type StatProps = {
value: string | number
label?: string
unit?: string
color?: string
of?: string | number
center?: boolean
reversed?: boolean
half?: boolean
lg?: boolean
sm?: boolean
sx?: any
}
const Stat = ({
value,
label,
unit = '',
color = 'text',
of,
center = false,
reversed = false,
half = false,
lg = false,
sm = false,
...props
}: StatProps) => (
{value || '—'}
{!isEmpty(unit) && (
{unit}
)}
{!isEmpty(of) && (
{of}
)}
{!isEmpty(label) && (
{label}
)}
)
export default Stat
================================================
FILE: components/submit.tsx
================================================
import { Button } from 'theme-ui'
import theme from '../lib/theme'
const bg = {
default: {
bg: 'blue',
backgroundImage: theme.util.gx('cyan', 'blue')
},
submitting: {
bg: 'blue',
backgroundImage: theme.util.gx('cyan', 'blue')
},
success: {
bg: 'green',
backgroundImage: theme.util.gx('yellow', 'green')
},
error: {
bg: 'orange',
backgroundImage: theme.util.gx('orange', 'red'),
boxShadow: `0 0 0 1px ${theme.colors.white}, 0 0 0 4px ${theme.colors.primary}`
},
disabled: {
bg: 'gray',
backgroundImage: theme.util.gx('gray', 'gray')
}
}
const submitting = {
...bg.default,
opacity: 0.5,
pointerEvents: 'none',
cursor: 'not-allowed'
}
const Submit = ({
status,
labels = {
default: 'Submit',
error: 'Error!',
success: 'Check your email!'
},
width = '100%',
sx,
disabled,
...props
}) => (
)
export default Submit
================================================
FILE: components/tilt.tsx
================================================
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(() => {
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 })
}
export default Tilt
================================================
FILE: components/winter/breakdown-box.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Card, Heading, Text } from 'theme-ui'
import { Zoom } from '../react-reveal-compat'
import Icon from '@hackclub/icons'
function BreakdownBox({
subtitle,
icon,
text,
description,
delay,
href,
color,
bg
}) {
return (
{subtitle ? (
{subtitle}
) : (
)}
{text}
{description}
)
}
export default BreakdownBox
================================================
FILE: components/winter/breakdown.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Container, Heading, Grid } from 'theme-ui'
import { Fade, Slide } from '../react-reveal-compat'
import BreakdownBox from './breakdown-box'
function Breakdown() {
return (
<>
Dear high school hacker, we have a challenge for you:What will you make this winter?
Join Hack Clubbers in a winter of making with
From Feb 14-23, work on your project, share
short photo/video updates each day.
>
}
delay="100"
href="https://scrapbook.hackclub.com/r/10daysinpublic"
/>
>
)
}
export default Breakdown
================================================
FILE: components/winter/footer.tsx
================================================
import { Box, Heading, Text, Link } from 'theme-ui'
import Footer from '../footer'
const Description = () => (
A project by Hack Club.
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've{' '}
partnered with GitHub to give away $50k of hardware
,{' '}
hosted the world's longest hackathon on land
, and{' '}
brought 183 teenagers to SF for a hackathon
.
Illustrations by Ripley.
)
const WinterFooter = () => {
return (
)
}
export default WinterFooter
================================================
FILE: components/winter/info.tsx
================================================
/** @jsxImportSource theme-ui */
import {
Card,
Grid,
Box,
Container,
Heading,
Text,
Flex,
Avatar
} from 'theme-ui'
import Icon from '../icon'
import Tilt from '../tilt'
import { Zoom } from '../react-reveal-compat'
export default function InfoGrid() {
return (
A deeper look at
Free hardware for your project
To qualify:
Be a high schooler (or younger)
If you qualify, share your idea! We're giving out as many grants
as possible!
Once you have a project idea,
figure out the hardware you need and where you can buy it. Share
that with us and we'll give you a grant of up to $250.
It could be your first ever electronics project or your tenth,
we want to support you in building whatever you want.
Receive and spend the grant through HCB.
Full history and balance, viewed on a powerful web dashbaord
Issue yourself a debit card to spend the funds
Use transparency mode to spend it in public
)
}
function BulletItem({ children, iconGlyph, iconColor, iconSize }) {
return (
{children}
)
}
================================================
FILE: components/winter/landing.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Heading, Button, Link, Text, Container } from 'theme-ui'
import Snowfall from 'react-snowfall'
import { Fade } from '../react-reveal-compat'
export default function Landing() {
return (
a hacker's
Winter Hardware
Wonderland
This event has ended. Hundreds of amazing projects were built
together by the{' '}
Hack Club Slack
.
)
}
================================================
FILE: components/winter/projects.tsx
================================================
/** @jsxImportSource theme-ui */
import { useState } from 'react'
import styled from '@emotion/styled'
import {
Box,
Button,
Container,
Flex,
Heading,
Card,
Grid,
Link as A,
Text,
Avatar,
Image
} from 'theme-ui'
import NextImage from 'next/image'
import Marquee from '../marquee'
import Photo1 from '../../public/winter/1.jpeg'
import Photo2 from '../../public/winter/2.png'
import Photo3 from '../../public/winter/3.jpeg'
import Photo4 from '../../public/winter/4.jpeg'
import Photo5 from '../../public/winter/5.jpeg'
import Photo6 from '../../public/winter/6.jpeg'
import Photo7 from '../../public/winter/7.jpeg'
import Photo8 from '../../public/winter/8.jpeg'
import Photo9 from '../../public/winter/9.jpeg'
import Photo10 from '../../public/winter/10.jpeg'
import Photo12 from '../../public/winter/12.jpeg'
import Photo13 from '../../public/winter/13.jpeg'
import Photo14 from '../../public/winter/14.jpeg'
import Photo15 from '../../public/winter/15.jpeg'
import Photo16 from '../../public/winter/16.jpeg'
import Photo17 from '../../public/winter/17.jpeg'
import Photo18 from '../../public/winter/18.jpeg'
import Photo19 from '../../public/winter/19.jpeg'
import Photo20 from '../../public/winter/20.jpeg'
import Photo21 from '../../public/winter/21.jpeg'
/** @jsxImportSource theme-ui */
const Header = styled(Box)`
background: url('/pattern.svg');
`
const Sheet = styled(Card)`
position: relative;
overflow: hidden;
border-radius: 8px;
width: 100%;
color: white;
max-width: 52rem;
font-size: 20px;
padding: 32px;
color: white;
`
const PhotoRow = ({ photos }) => (
)
const Cards = ({ avatar, username, description, image }) => {
return (
@{username}
div': { width: 18, height: 18 }
}}
>
{description}
{/* */}
)
}
export default function Projects() {
const [count, setCount] = useState(0)
const list = [
'drawing robot',
'drone',
'CNC machine',
'pixel art display',
'camera',
'3D printer'
]
if (count === list.length - 1) {
setCount(0)
}
const project_idea = list[count]
return (
You could be building a setCount(count + 1)}
>
{project_idea}
)
}
================================================
FILE: components/winter/recap.tsx
================================================
/** @jsxImportSource theme-ui */
import { Button, Container, Heading, Grid, Card, Text } from 'theme-ui'
import { Slide, Zoom } from '../react-reveal-compat'
import BreakdownBox from './breakdown-box'
function Recap() {
return (
<>
Make your ideas real this winter, with electronics and Hack Club
friends.
This event has ended
Winter Hardware Wonderland is no longer accepting signups. Check
out the projects that were built on GitHub!
>
)
}
export default Recap
================================================
FILE: components/winter/timeline.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Flex, Container, Text, Badge, Link } from 'theme-ui'
import { Slide } from '../react-reveal-compat'
import Icon from '../icon'
function TimelineStep({ children }) {
return (
{children}
)
}
function Circle({ children }) {
return (
{children}
)
}
function Step({ icon, name, duration, href }) {
return (
{href ? (
) : (
)}
{duration}
{name}
)
}
export default function RealTimeline() {
return (
RSVPs are closed. Have a question? Here are the{' '}
FAQs
.
>
}
duration="RSVP"
/>
)
}
================================================
FILE: eslint.config.mts
================================================
import { defineConfig, globalIgnores } from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTs from 'eslint-config-next/typescript'
export default defineConfig([
...nextVitals,
...nextTs,
{
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off',
'react/no-unknown-property': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-require-imports': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-hooks/purity': 'off',
'react-hooks/immutability': 'off'
}
},
globalIgnores([
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
'public/**'
])
])
================================================
FILE: lib/cached-hcb-orgs.ts
================================================
const CACHE_FILENAME = 'hcb-orgs-cache.json'
export async function fetchAllOrganizations() {
const fs = require('fs')
const path = require('path')
const cacheFile = path.join(process.cwd(), '.next', CACHE_FILENAME)
try {
return JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
} catch (e) {
// Cache miss
}
let lastLength = 100
let total = []
let page = 1
while (lastLength >= 100) {
const json = await fetch(
'https://hcb.hackclub.com/api/v3/directory/organizations?per_page=100&page=' +
page
).then(res => res.json())
lastLength = json.length
page++
total = [...total, ...json]
}
try {
fs.mkdirSync(path.dirname(cacheFile), { recursive: true })
fs.writeFileSync(cacheFile, JSON.stringify(total))
} catch (e) {
console.error('Failed to write cache file:', e)
}
return total
}
================================================
FILE: lib/countries.json
================================================
{
"countries": [
"United States of America (US)",
"Canada (CA)",
"United Kingdom of Great Britain and Northern Ireland (GB)",
"Afghanistan (AF)",
"Åland Islands (AX)",
"Albania (AL)",
"Algeria (DZ)",
"American Samoa (AS)",
"Andorra (AD)",
"Angola (AO)",
"Anguilla (AI)",
"Antarctica (AQ)",
"Antigua and Barbuda (AG)",
"Argentina (AR)",
"Armenia (AM)",
"Aruba (AW)",
"Australia (AU)",
"Austria (AT)",
"Azerbaijan (AZ)",
"Bahamas (BS)",
"Bahrain (BH)",
"Bangladesh (BD)",
"Barbados (BB)",
"Belarus (BY)",
"Belgium (BE)",
"Belize (BZ)",
"Benin (BJ)",
"Bermuda (BM)",
"Bhutan (BT)",
"Bolivia (BO)",
"Bonaire (BQ)",
"Bosnia and Herzegovina (BA)",
"Botswana (BW)",
"Bouvet Island (BV)",
"Brazil (BR)",
"British Indian Ocean Territory (IO)",
"Brunei Darussalam (BN)",
"Bulgaria (BG)",
"Burkina Faso (BF)",
"Burundi (BI)",
"Cabo Verde (CV)",
"Cambodia (KH)",
"Cameroon (CM)",
"Cayman Islands (KY)",
"Central African Republic (CF)",
"Chad (TD)",
"Chile (CL)",
"China (CN)",
"Christmas Island (CX)",
"Cocos (Keeling) Islands (CC)",
"Colombia (CO)",
"Comoros (KM)",
"Congo (the Democratic Republic of the) (CD)",
"Congo (the) (CG)",
"Cook Islands (CK)",
"Costa Rica (CR)",
"Côte d'Ivoire (CI)",
"Croatia (HR)",
"Cuba (CU)",
"Curaçao (CW)",
"Cyprus (CY)",
"Czechia (CZ)",
"Denmark (DK)",
"Djibouti (DJ)",
"Dominica (DM)",
"Dominican Republic (DO)",
"Ecuador (EC)",
"Egypt (EG)",
"El Salvador (SV)",
"Equatorial Guinea (GQ)",
"Eritrea (ER)",
"Estonia (EE)",
"Eswatini (SZ)",
"Ethiopia (ET)",
"Falkland Islands (FK)",
"Faroe Islands (FO)",
"Fiji (FJ)",
"Finland (FI)",
"France (FR)",
"French Guiana (GF)",
"French Polynesia (PF)",
"French Southern Territories (TF)",
"Gabon (GA)",
"Gambia (GM)",
"Georgia (GE)",
"Germany (DE)",
"Ghana (GH)",
"Gibraltar (GI)",
"Greece (GR)",
"Greenland (GL)",
"Grenada (GD)",
"Guadeloupe (GP)",
"Guam (GU)",
"Guatemala (GT)",
"Guernsey (GG)",
"Guinea (GN)",
"Guinea-Bissau (GW)",
"Guyana (GY)",
"Haiti (HT)",
"Heard Island and McDonald Islands (HM)",
"Holy See (VA)",
"Honduras (HN)",
"Hong Kong (HK)",
"Hungary (HU)",
"Iceland (IS)",
"India (IN)",
"Indonesia (ID)",
"Iran (IR)",
"Iraq (IQ)",
"Ireland (IE)",
"Isle of Man (IM)",
"Israel (IL)",
"Italy (IT)",
"Jamaica (JM)",
"Japan (JP)",
"Jersey (JE)",
"Jordan (JO)",
"Kazakhstan (KZ)",
"Kenya (KE)",
"Kiribati (KI)",
"Korea (the Democratic People's Republic of) (KP)",
"Korea (the Republic of) (KR)",
"Kuwait (KW)",
"Lao People's Democratic Republic (LA)",
"Latvia (LV)",
"Lebanon (LB)",
"Lesotho (LS)",
"Liberia (LR)",
"Libya (LY)",
"Liechtenstein (LI)",
"Lithuania (LT)",
"Luxembourg (LU)",
"Macao (MO)",
"Macedonia (the former Yugoslav Republic of) (MK)",
"Madagascar (MG)",
"Malawi (MW)",
"Malaysia (MY)",
"Maldives (MV)",
"Mali (ML)",
"Malta (MT)",
"Marshall Islands (MH)",
"Martinique (MQ)",
"Mauritania (MR)",
"Mauritius (MU)",
"Mayotte (YT)",
"Mexico (MX)",
"Micronesia (FM)",
"Moldova (MD)",
"Monaco (MC)",
"Mongolia (MN)",
"Montenegro (ME)",
"Montserrat (MS)",
"Morocco (MA)",
"Mozambique (MZ)",
"Myanmar (MM)",
"Namibia (NA)",
"Nauru (NR)",
"Nepal (NP)",
"Netherlands (NL)",
"New Caledonia (NC)",
"New Zealand (NZ)",
"Nicaragua (NI)",
"Niger (NE)",
"Nigeria (NG)",
"Niue (NU)",
"Norfolk Island (NF)",
"Northern Mariana Islands (MP)",
"Norway (NO)",
"Oman (OM)",
"Pakistan (PK)",
"Palau (PW)",
"Palestine (PS)",
"Panama (PA)",
"Papua New Guinea (PG)",
"Paraguay (PY)",
"Peru (PE)",
"Philippines (PH)",
"Pitcairn (PN)",
"Poland (PL)",
"Portugal (PT)",
"Puerto Rico (PR)",
"Qatar (QA)",
"Réunion (RE)",
"Romania (RO)",
"Russian Federation (RU)",
"Rwanda (RW)",
"Saint Barthélemy (BL)",
"Saint Helena, Ascension and Tristan da Cunha (SH)",
"Saint Kitts and Nevis (KN)",
"Saint Lucia (LC)",
"Saint Martin (MF)",
"Saint Pierre and Miquelon (PM)",
"Saint Vincent and the Grenadines (VC)",
"Samoa (WS)",
"San Marino (SM)",
"Sao Tome and Principe (ST)",
"Saudi Arabia (SA)",
"Senegal (SN)",
"Serbia (RS)",
"Seychelles (SC)",
"Sierra Leone (SL)",
"Singapore (SG)",
"Sint Maarten (SX)",
"Slovakia (SK)",
"Slovenia (SI)",
"Solomon Islands (SB)",
"Somalia (SO)",
"South Africa (ZA)",
"South Georgia and the South Sandwich Islands (GS)",
"South Sudan (SS)",
"Spain (ES)",
"Sri Lanka (LK)",
"Sudan (the) (SD)",
"Suriname (SR)",
"Svalbard (SJ)",
"Sweden (SE)",
"Switzerland (CH)",
"Syrian Arab Republic (SY)",
"Taiwan (TW)",
"Tajikistan (TJ)",
"Tanzania, United Republic of (TZ)",
"Thailand (TH)",
"Timor-Leste (TL)",
"Togo (TG)",
"Tokelau (TK)",
"Tonga (TO)",
"Trinidad and Tobago (TT)",
"Tunisia (TN)",
"Turkey (TR)",
"Turkmenistan (TM)",
"Turks and Caicos Islands (TC)",
"Tuvalu (TV)",
"Uganda (UG)",
"Ukraine (UA)",
"United Arab Emirates (AE)",
"United States Minor Outlying Islands (UM)",
"Uruguay (UY)",
"Uzbekistan (UZ)",
"Vanuatu (VU)",
"Venezuela (VE)",
"Viet Nam (VN)",
"Virgin Islands (British) (VG)",
"Virgin Islands (U.S.) (VI)",
"Wallis and Futuna (WF)",
"Western Sahara (EH)",
"Yemen (YE)",
"Zambia (ZM)",
"Zimbabwe (ZW)"
]
}
================================================
FILE: lib/cta.json
================================================
[
{
"title": "Flavortown",
"logo": "https://cdn.hackclub.com/019c76b8-4f54-7de9-ae34-90b2190c2440/TeQ27w.png",
"background": "#7B4942",
"stickerImage": "https://cdn.hackclub.com/019c76b5-b513-7f5a-8718-bea38d4abb80/DM6Ztg.avif",
"description": "Cook tasty personal projects, win free prizes!",
"descriptionColor": "white",
"buttonText": "JOIN NOW",
"buttonColor": "#AD7858",
"link": "https://flavortown.hackclub.com/?ref=site-0"
},
{
"title": "Stasis",
"logo": "/stasis-logo-centered.png",
"logoScale": 1.1,
"background": "#dad2bf",
"backgroundImage": "url(/stasis-grid.png)",
"backgroundSize": "64px 64px",
"description": "Design hardware projects, get them funded, and come to a hackathon in Austin, TX!",
"descriptionColor": "white",
"buttonText": "JOIN NOW",
"buttonColor": "#D95D39",
"link": "https://stasis.hackclub.com/?utm_source=cta"
},
{
"title": "Macondo",
"logo": "https://cdn.hackclub.com/019dc215-69df-7087-9b63-73db5a7126fd/logo_macondo.png",
"logoScale": 1.2,
"stickerImage": "https://cdn.hackclub.com/019dc21b-d702-72d2-9930-487a4f7cfd9a/android-chrome-512x512.webp",
"stickerImageScale": 0.5,
"background": "#684D3A",
"backgroundImage": "url(https://cdn.hackclub.com/019dc218-1e6a-7562-800c-d79da12bc5d1/background_logo_2.png)",
"backgroundSize": "480px 240px",
"description": "Make hardware or software projects and win free prizes! Fly to Bogotá, Colombia for a hackathon!",
"descriptionColor": "#EACFB3",
"buttonText": "JOIN NOW",
"buttonColor": "#EACFB3",
"link": "https://macondo.hackclub.com/?utm_source=cta"
},
{
"title": "Sleepover",
"logo": "https://cdn.hackclub.com/019c76b7-644a-7ef7-b855-63253c99d2f8/UpZIvQ.png",
"background": "#B5AAE7",
"stickerImage": "https://cdn.hackclub.com/019c76b5-f328-725e-9d6d-4f5368685ab5/tNACWw.png",
"description": "Learn to code and fly to an all girls slumber party hackathon!",
"descriptionColor": "white",
"buttonText": "JOIN NOW",
"buttonColor": "#9DC9F7",
"link": "https://sleepover.hackclub.com/?utm_source=site_cta"
},
{
"title": "Hack Club: The Game",
"logo": "https://cdn.hackclub.com/019d0899-f270-7530-b145-19d1e53f113f/hctg-text-logo.png",
"description": "Build projects, then join a scavenger hunt in Manhattan!",
"background": "#FECB0D",
"descriptionColor": "#000",
"buttonText": "JOIN NOW",
"buttonColor": "black",
"link": "https://game.hackclub.com/?utm_source=site_cta"
},
{
"title": "Jackpot",
"logo": "https://cdn.hackclub.com/019d01dc-d676-746d-9fd9-794df0b50399/logo_draft.png",
"background": "#5F1212",
"logoScale": 1,
"stickerImage": "https://cdn.hackclub.com/019d01dd-6b56-748a-8386-8f77bad07d46/meow.png",
"stickerImageScale": 0.7,
"description": "No hours required...\nenjoy a weekend hackathon\nin Las Vegas!",
"descriptionColor": "white",
"buttonText": "JOIN NOW",
"buttonColor": "#FAD10B",
"link": "https://jackpot.hackclub.com"
},
{
"title": "Fallout",
"logo": "https://cdn.hackclub.com/019cdfd0-6f09-7c8c-bd01-d0349e421c32/logo2.svg",
"background": "#39c9ff",
"logoScale": 1,
"stickerImage": "https://cdn.hackclub.com/019d36f6-2170-792f-9487-db64bce4cf65/koifish.webp",
"stickerImageScale": 0.7,
"description": "Build hardware projects, get build funding, and fly to a Shenzhen hackathon!",
"descriptionColor": "white",
"buttonText": "JOIN NOW",
"buttonColor": "#FAD10B",
"link": "https://fallout.hackclub.com"
},
{
"title": "Beest",
"logo": "https://cdn.hackclub.com/019d87e3-965d-75b0-83dd-c73469f47911/beest-cropped.png",
"logoScale": 1,
"background": "#A7C1D6",
"backgroundImage": "url(https://cdn.hackclub.com/019d87bd-fdf5-725a-a552-7207ebceaf06/bg.png)",
"stickerImage": "https://cdn.hackclub.com/019d87b5-6865-7985-b88d-12a4c528cc86/beest-sticker.webp",
"stickerImageScale": 0.65,
"description": "Code a project, fly to the Netherlands, build a mechanical animal!",
"backgroundSize": "auto 131%",
"descriptionColor": "#4C483C",
"buttonText": "JOIN NOW",
"buttonColor": "#D95D39",
"link": "https://beest.hackclub.com/?utm_source=cta"
}
]
================================================
FILE: lib/dates.ts
================================================
export const dt = d => new Date(d).toLocaleDateString()
const year = new Date().getFullYear()
export const tinyDt = d => dt(d).replace(`/${year}`, '').replace(`${year}-`, '')
// based on https://github.com/withspectrum/spectrum/blob/alpha/src/helpers/utils.js#L146
export const timeSince = (
previous,
absoluteDuration = false,
longForm = false,
current = new Date().toISOString()
) => {
const msPerMinute = 60 * 1000
const msPerHour = msPerMinute * 60
const msPerDay = msPerHour * 24
const msPerWeek = msPerDay * 7
const msPerMonth = msPerDay * 30 * 2
const msPerYear = msPerDay * 365
const future = new Date(previous).getTime() - new Date(current).getTime()
const past = new Date(current).getTime() - new Date(previous).getTime()
const elapsed = [future, past].sort((a, b) => a - b)[1]
let humanizedTime
if (elapsed < msPerMinute) {
humanizedTime = '< 1m'
} else if (elapsed < msPerHour) {
const now = Math.round(elapsed / msPerMinute)
humanizedTime = longForm ? `${now} minutes` : `${now}m`
} else if (elapsed < msPerDay) {
const now = Math.round(elapsed / msPerHour)
humanizedTime = longForm ? `${now} hours` : `${now}h`
} else if (elapsed < msPerWeek) {
const now = Math.round(elapsed / msPerDay)
humanizedTime = longForm ? `${now} days` : `${now}d`
} else if (elapsed < msPerMonth) {
const now = Math.round(elapsed / msPerWeek)
humanizedTime = longForm ? `${now} weeks` : `${now}w`
} else if (elapsed < msPerYear) {
const now = Math.round(elapsed / msPerMonth)
humanizedTime = longForm ? `${now} months` : `${now}mo`
} else {
const now = Math.round(elapsed / msPerYear)
humanizedTime = longForm ? `${now} years` : `${now}y`
}
if (absoluteDuration) {
return humanizedTime
} else {
return elapsed > 0 ? `${humanizedTime} ago` : `in ${humanizedTime}`
}
}
function formatChunk(type, date) {
const days = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
]
const months = [
'January',
'Febuary',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
switch (type) {
case 'dddd':
return days[date.getDay()]
case 'ddd':
return formatChunk('dddd', date).slice(0, 3)
case 'dd':
return ('00' + formatChunk('d', date)).slice(-2)
case 'd':
return date.getDate()
case 'mmmm':
return months[date.getMonth()]
case 'mmm':
return formatChunk('mmmm', date).slice(0, 3)
case 'mm':
return ('00' + formatChunk('m', date)).slice(-2)
case 'm':
return (date.getMonth() + 1).toString()
case 'yyyy':
return date.getFullYear().toString()
case 'yy':
return formatChunk('yyyy', date).slice(-2)
default:
return null
}
}
type FormatDate = {
format?: string
date: string
divider?: string
}
export const formatDate = ({ format, date, divider = ' ' }: FormatDate) => {
return format
.split(divider)
.map(chunk => formatChunk(chunk, new Date(date)))
.join(divider)
}
================================================
FILE: lib/fetcher.ts
================================================
/**
* useSWR() fetcher
*/
export default async function fetcher(...args: Parameters) {
const res = await fetch(...args)
return await res.json()
}
================================================
FILE: lib/git-info.ts
================================================
export const getGitSha = (): string => {
return process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'dev'
}
export const getGitShaShort = (): string => {
const sha = getGitSha()
return sha === 'dev' ? 'dev' : sha.substring(0, 7)
}
================================================
FILE: lib/helpers.ts
================================================
export const dt = d => new Date(d).toLocaleDateString()
const year = new Date().getFullYear()
export const tinyDt = d => dt(d).replace(`/${year}`, '').replace(`${year}-`, '')
export const formatAddress = (city, stateCode, country, countryCode) => {
const firstHalf = city
const secondHalf = countryCode === 'US' ? stateCode : country
// Handle case where city or country is null
const final = [firstHalf, secondHalf].filter(e => e).join(', ')
// Handle case where an event's location is outside the US and is so long that
// it overflows the card when rendering. If the total length of the location
// is over 16 characters and outside the US, then just show the country name.
if (countryCode !== 'US' && final.length > 16) {
return country
} else {
return final
}
}
const humanizedMonth = date =>
date.toLocaleString('en', { month: 'long', timeZone: 'UTC' })
const shortHumanizedMonth = date =>
date.toLocaleString('en', { month: 'short', timeZone: 'UTC' })
export const humanizedDateRange = (start, end) => {
let result
// the substrings make sure that the dates aren't affected by dumb time zone bs
const startDate = new Date(start.substr(0, 10))
const endDate = new Date(end.substr(0, 10))
if (startDate.getUTCMonth() === endDate.getUTCMonth()) {
if (startDate.getUTCDate() === endDate.getUTCDate()) {
// Same day and month means we can return the date
result = `${humanizedMonth(startDate)} ${startDate.getUTCDate()}`
} else {
result = `${humanizedMonth(
startDate
)} ${startDate.getUTCDate()}–${endDate.getUTCDate()}`
}
} else {
result = `${shortHumanizedMonth(
startDate
)} ${startDate.getUTCDate()}–${shortHumanizedMonth(
endDate
)} ${endDate.getUTCDate()}`
}
if (new Date().getUTCFullYear() !== startDate.getUTCFullYear()) {
result += `, ${startDate.getUTCFullYear()}`
}
return result
}
// based on https://github.com/withspectrum/spectrum/blob/alpha/src/helpers/utils.js#L146
export const timeSince = (
previous,
absoluteDuration = false,
longForm = false,
current = new Date()
) => {
const msPerMinute = 60 * 1000
const msPerHour = msPerMinute * 60
const msPerDay = msPerHour * 24
const msPerWeek = msPerDay * 7
const msPerMonth = msPerDay * 30 * 2
const msPerYear = msPerDay * 365
const elapsed = new Date(current).getTime() - new Date(previous).getTime()
let humanizedTime
if (elapsed < msPerMinute) {
humanizedTime = '< 1m'
} else if (elapsed < msPerHour) {
const now = Math.round(elapsed / msPerMinute)
humanizedTime = longForm ? `${now} minute${now > 1 ? 's' : ''}` : `${now}m`
} else if (elapsed < msPerDay) {
const now = Math.round(elapsed / msPerHour)
humanizedTime = longForm ? `${now} hour${now > 1 ? 's' : ''}` : `${now}h`
} else if (elapsed < msPerWeek) {
const now = Math.round(elapsed / msPerDay)
humanizedTime = longForm ? `${now} day${now > 1 ? 's' : ''}` : `${now}d`
} else if (elapsed < msPerMonth) {
const now = Math.round(elapsed / msPerWeek)
humanizedTime = longForm ? `${now} week${now > 1 ? 's' : ''}` : `${now}w`
} else if (elapsed < msPerYear) {
const now = Math.round(elapsed / msPerMonth)
humanizedTime = longForm ? `${now} month${now > 1 ? 's' : ''}` : `${now}mo`
} else {
const now = Math.round(elapsed / msPerYear)
humanizedTime = longForm ? `${now} year${now > 1 ? 's' : ''}` : `${now}y`
}
if (absoluteDuration) {
return humanizedTime
} else {
return elapsed > 0 ? `${humanizedTime} ago` : `in ${humanizedTime}`
}
}
// NOTE(@lachlanjc): I know this is bad, I’m trying to get it out the door okay???
export const timeTo = (time, current = new Date(), longForm = true) => {
const msPerMinute = 60 * 1000
const msPerHour = msPerMinute * 60
const msPerDay = msPerHour * 64 // getting close to a day
const msPerYear = msPerDay * 365
const elapsed = new Date(time).getTime() - new Date(current).getTime()
let humanizedTime
if (elapsed < msPerMinute) {
humanizedTime = '< 1m'
} else if (elapsed < msPerHour) {
const now = Math.round(elapsed / msPerMinute)
humanizedTime = longForm ? `${now} more minutes` : `${now}m`
} else if (elapsed < msPerDay) {
const now = Math.round(elapsed / msPerHour)
humanizedTime = longForm ? `${now} more hours` : `${now}h`
} else if (elapsed < msPerYear) {
const now = Math.round(elapsed / msPerDay)
humanizedTime = longForm ? `${now} days` : `${now}d`
} else {
const now = Math.round(elapsed / msPerYear)
humanizedTime = longForm ? `${now} years` : `${now}y`
}
return humanizedTime
}
function formatChunk(type, date) {
const days = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
]
const months = [
'January',
'Febuary',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
switch (type) {
case 'dddd':
return days[date.getDay()]
case 'ddd':
return formatChunk('dddd', date).slice(0, 3)
case 'dd':
return ('00' + formatChunk('d', date)).slice(-2)
case 'd':
return date.getDate()
case 'mmmm':
return months[date.getMonth()]
case 'mmm':
return formatChunk('mmmm', date).slice(0, 3)
case 'mm':
return ('00' + formatChunk('m', date)).slice(-2)
case 'm':
return (date.getMonth() + 1).toString()
case 'yyyy':
return date.getFullYear().toString()
case 'yy':
return formatChunk('yyyy', date).slice(-2)
default:
return null
}
}
export const formatDate = (format, date, divider = ' ') => {
return format
.split(divider)
.map(chunk => formatChunk(chunk, new Date(date)))
.join(divider)
}
export const normalizeGitHubCommitUrl = url => {
return url
.replace('api.', '')
.replace('/repos', '')
.replace('commits', 'commit')
}
export const decodeHtmlEntities = str =>
str
?.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&/g, '&')
================================================
FILE: lib/members.ts
================================================
// this could use the slackData lib, but apparently top level awaits are risky
export const count: number = 103897
export const formatted = count.toLocaleString('en-US')
export const thousands = Math.round(count / 1000)
================================================
FILE: lib/organization.ts
================================================
import GeoPattern from 'geopattern'
/**
* Represents an organization.
*/
export class Organization {
public raw: any
/**
* Creates an instance of Organization.
* @param {object} rawOrganization - The raw organization data.
*/
constructor(rawOrganization: any) {
/**
* The raw organization data.
* @type {object}
*/
this.raw = rawOrganization
}
/**
* Gets the ID of the organization.
* @type {string}
*/
get id() {
return this.raw.id
}
/**
* Gets the name of the organization.
* @type {string}
*/
get name() {
return this.raw.name
}
/**
* Gets the slug of the organization.
* @type {string}
*/
get slug() {
return this.raw.slug
}
/**
* Checks if the organization is transparent.
* @type {boolean}
*/
get isTransparent() {
return this.raw.transparent
}
/**
* Checks if the organization is in demo mode.
* @type {boolean}
*/
get isDemo() {
return this.raw.demo_mode
}
/**
* Gets the number of users in the organization.
* @type {number}
*/
get users() {
return this.raw.users.length
}
/**
* Checks if the organization accepts donations.
* @type {boolean}
*/
get acceptsDonations() {
return this.raw.donation_link !== null
}
/**
* Gets the branding information of the organization.
* @type {object}
* @property {string} logo - The logo of the organization.
* @property {string} donationHeader - The donation header of the organization.
* @property {string} backgroundImage - The background image of the organization.
* @property {string} publicMessage - The public message of the organization.
*/
get branding() {
return {
logo: this.raw.logo,
donationHeader: this.raw.donation_header,
backgroundImage:
this.raw.background_image ||
GeoPattern.generate(this.raw.name).toDataUri(),
description: this.raw.description
}
}
/**
* Gets the tags of the organization.
* @type {object}
* @property {string} type - The type of the organization.
* @property {string} category - The category of the organization.
* @property {string[]} badges - The badges of the organization.
*/
get tags() {
return {
type: this.raw.category,
category: 'Coding',
badges: []
}
}
/**
* Gets the creation date of the organization.
* @type {Date}
*/
get createdAt() {
return new Date(this.raw.created_at)
}
/**
* Gets the links associated with the organization.
* @type {object}
* @property {string} website - The website link of the organization.
* @property {string} donations - The donation link of the organization (if it accepts donations).
* @property {string} financials - The financials link of the organization (if it is transparent).
*/
get links() {
const links: { website: string; donations?: string; financials?: string } =
{
website: this.raw.website
}
if (this.acceptsDonations) links.donations = this.raw.donation_link
if (this.isTransparent)
links.financials = `https://hcb.hackclub.com/${this.slug}`
return links
}
/**
* Gets the location of the organization.
* @type {object}
* @property {string} country - The country of the organization.
* @property {string} continent - The continent of the organization.
* @property {string} countryCode - The country code of the organization.
*/
get location() {
return {
country: this.raw.location.country,
continent: this.raw.location.continent,
countryCode: this.raw.location.country_code
}
}
/**
* Updates the organization data by making an API call.
* @returns {Organization} The updated Organization instance.
*/
async update() {
const response = await fetch(this.raw.href)
const json = await response.json()
this.raw = json
return this
}
}
================================================
FILE: lib/slackData.ts
================================================
type SlackData = {
active_users_28d?: number
full_members_count?: number
total_members_count?: number
ds?: string
}
export const slackData = async () =>
(await fetch('https://slack-data.hackclub.dev/full')
.then(r => r.json())
.then(d => d.stats?.sort((a, b) => b.ds.localeCompare(a.ds))[0] ?? {})
.catch(() => ({}))) as SlackData
================================================
FILE: lib/sleep.ts
================================================
// Beloved classic utility function :3
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
export default sleep
================================================
FILE: lib/theme.ts
================================================
import base from '@hackclub/theme'
import { merge } from 'lodash'
const theme = base
theme.config.useColorSchemeMediaQuery = false
theme.buttons.primary = merge(theme.buttons.primary, {
textTransform: 'uppercase'
})
theme.layout.copy.maxWidth = [null, null, 'copyPlus']
theme.text.title.fontSize = [5, 6]
export default theme
================================================
FILE: lib/use-form.ts
================================================
import { useState, useEffect } from 'react'
const useForm = (
submitURL = '/',
callback,
options = { clearOnSubmit: 5000, method: 'POST', initData: {}, bearer: null }
) => {
const [status, setStatus] = useState('default')
const [data, setData] = useState({ ...options.initData })
const [touched, setTouched] = useState({})
const onFieldChange = (e, name, type) => {
e.persist()
const value = e.target[type === 'checkbox' ? 'checked' : 'value']
setData(data => ({ ...options.initData, ...data, [name]: value }))
}
useEffect(() => {
setTouched(Object.keys(data))
}, [data])
const useField = (name, type = 'text', ...props) => {
const checkbox = type === 'checkbox'
const empty = checkbox ? false : ''
const onChange = e => onFieldChange(e, name, type)
const value = data[name] || options.initData[name]
return {
name,
type: name === 'email' ? 'email' : type,
[checkbox ? 'checked' : 'value']: value || empty,
onChange,
...props
}
}
const { method = 'POST' } = options
const action = submitURL
const onSubmit = e => {
e.preventDefault()
setStatus('submitting')
let header = {}
if (options.bearer) {
header = {
Authorization: `Bearer ${options.bearer}`
}
}
fetch(action, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...header
},
body: JSON.stringify(data)
})
.then(async r => {
const response = await r.json()
if (r.ok) {
setStatus('success')
if (callback) callback(response)
if (options.clearOnSubmit) {
setTimeout(() => {
setData({})
setStatus('default')
}, options.clearOnSubmit)
}
} else {
setStatus('error')
console.error(response)
}
})
.catch(e => {
console.error(e)
setStatus('error')
})
}
const formProps = { onSubmit }
return { status, data, touched, useField, formProps }
}
export default useForm
================================================
FILE: lib/use-has-mounted.ts
================================================
// Full credit to https://joshwcomeau.com/snippets/react-hooks/use-has-mounted
import React from 'react'
function useHasMounted() {
const [hasMounted, setHasMounted] = React.useState(false)
React.useEffect(() => {
setHasMounted(true)
}, [])
return hasMounted
}
export default useHasMounted
================================================
FILE: lib/use-media.ts
================================================
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 }
}
================================================
FILE: lib/use-prefers-motion.ts
================================================
// Inspired by https://joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion
import React from 'react'
const QUERY = '(prefers-reduced-motion: no-preference)'
const isRenderingOnServer = typeof window === 'undefined'
const getInitialState = () => {
// For our initial server render, we won't know if the user
// prefers reduced motion, but it doesn't matter. This value
// will be overwritten on the client, before any animations
// occur.
return isRenderingOnServer ? false : window.matchMedia(QUERY).matches
}
function usePrefersMotion() {
const [prefersMotion, setPrefersMotion] = React.useState(getInitialState)
React.useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY)
const listener = (event: MediaQueryListEvent) => {
setPrefersMotion(!event.matches)
}
mediaQueryList.addEventListener('change', listener)
return () => {
mediaQueryList.removeEventListener('change', listener)
}
}, [])
return prefersMotion
}
export default usePrefersMotion
================================================
FILE: lib/use-prefers-reduced-motion.ts
================================================
// Full credit to https://joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion
import React from 'react'
const QUERY = '(prefers-reduced-motion: no-preference)'
const isRenderingOnServer = typeof window === 'undefined'
const getInitialState = () => {
// For our initial server render, we won't know if the user
// prefers reduced motion, but it doesn't matter. This value
// will be overwritten on the client, before any animations
// occur.
return isRenderingOnServer ? true : !window.matchMedia(QUERY).matches
}
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] =
React.useState(getInitialState)
React.useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY)
const listener = (event: MediaQueryListEvent) => {
setPrefersReducedMotion(!event.matches)
}
mediaQueryList.addEventListener('change', listener)
return () => {
mediaQueryList.removeEventListener('change', listener)
}
}, [])
return prefersReducedMotion
}
export default usePrefersReducedMotion
================================================
FILE: lib/use-random-interval.ts
================================================
// Full credit to https://joshwcomeau.com/snippets/react-hooks/use-random-interval
import React from 'react'
// Utility helper for random number generation
const random = (min, max) => Math.floor(Math.random() * (max - min)) + min
const useRandomInterval = (callback, minDelay, maxDelay) => {
const timeoutId = React.useRef(null)
const savedCallback = React.useRef(callback)
React.useEffect(() => {
savedCallback.current = callback
})
React.useEffect(() => {
const isEnabled =
typeof minDelay === 'number' && typeof maxDelay === 'number'
if (isEnabled) {
const handleTick = () => {
const nextTickAt = random(minDelay, maxDelay)
timeoutId.current = window.setTimeout(() => {
savedCallback.current()
handleTick()
}, nextTickAt)
}
handleTick()
}
return () => window.clearTimeout(timeoutId.current)
}, [minDelay, maxDelay])
const cancel = React.useCallback(function () {
window.clearTimeout(timeoutId.current)
}, [])
return cancel
}
export default useRandomInterval
================================================
FILE: next.config.ts
================================================
import withMDX from '@next/mdx'
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactStrictMode: true,
trailingSlash: true,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'hackclub.com' },
{ protocol: 'https', hostname: 'dl.airtable.com' },
{ protocol: 'https', hostname: 'emoji.slack-edge.com' },
{ protocol: 'https', hostname: 'cdn.glitch.com' },
{ protocol: 'https', hostname: 'scrapbook.hackclub.com' },
{ protocol: 'https', hostname: 'assets.hackclub.com' },
{ protocol: 'https', hostname: 'v5.airtableusercontent.com' },
{ protocol: 'https', hostname: 'hcb.hackclub.com' },
{ protocol: 'https', hostname: 'cdn.hackclub.com' },
{ protocol: 'https', hostname: 'hc-cdn.hel1.your-objectstorage.com' },
{ protocol: 'https', hostname: 'cdn.prod.website-files.com' },
{ protocol: 'https', hostname: 'cloud-*-hack-club-bot.vercel.app' },
{ protocol: 'https', hostname: 'cdn.hack.pet' },
{ protocol: 'https', hostname: 'cdn.hackclubber.dev' },
{ protocol: 'https', hostname: 'github.com' },
{ protocol: 'https', hostname: 'avatars1.githubusercontent.com' },
{ protocol: 'https', hostname: 'ca.slack-edge.com' },
{
protocol: 'https',
hostname: 'scrapbook-into-the-redwoods.s3.us-east-1.amazonaws.com'
},
{ protocol: 'https', hostname: 'cloud-5v0kfmsva.vercel.app' }
]
},
async redirects() {
return [
{
source: '/bank/:path*',
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: '/fiscal-sponsorship/apply/',
destination: 'https://hcb.hackclub.com/applications/new',
permanent: false
},
{ source: '/grant/', destination: '/hackathons/grant', permanent: false },
{
source: '/privacy/',
destination: '/privacy-and-terms/',
permanent: true
},
{
source: '/sprig/',
destination: 'https://sprig.hackclub.com',
permanent: true
},
{ source: '/start/', destination: '/', permanent: false },
{ source: '/repl/', destination: '/', permanent: true },
{ source: '/c9/', destination: '/deprecated/cloud9/', permanent: true },
{
source: '/cloud9_setup/',
destination: '/deprecated/cloud9/',
permanent: true
},
{
source: '/redeem_tech_domain/',
destination: '/deprecated/tech_domains/',
permanent: true
},
{
source: '/challenge/',
destination: '/deprecated/challenge/',
permanent: true
},
{
source: '/slack_invite/',
destination: 'https://slack.hackclub.com',
permanent: true
},
{
source: '/first/',
destination: '/bank/first/',
permanent: false
},
{
source: '/bank/frc/',
destination: '/bank/first/',
permanent: false
},
{
source: '/bank/ftc/',
destination: '/bank/first/',
permanent: false
},
{
source: '/bank/fll/',
destination: '/bank/first/',
permanent: false
},
{
source: '/wom/',
destination: '/winter/',
permanent: false
},
{
source: '/workshops/slack/',
destination: 'https://slack.hackclub.com',
permanent: true
},
{
source: '/community/',
destination: 'https://slack.hackclub.com',
permanent: true
},
{ source: '/hack_camp/', destination: '/camp/', permanent: true },
{ source: '/branding/', destination: '/brand/', permanent: true },
{ source: '/ama/', destination: '/amas/', permanent: false },
{ source: '/geohot', destination: '/amas/geohot/', permanent: false },
{ source: '/sal', destination: '/amas/sal/', permanent: false },
{ source: '/vitalik', destination: '/amas/vitalik/', permanent: false },
{
source: '/open-source/',
destination: '/opensource/',
permanent: false
},
{ source: '/coc/', destination: '/conduct/', permanent: true },
{
source: '/code_of_conduct/',
destination: '/conduct/',
permanent: true
},
{
source: '/finder/',
destination: 'https://finder.hackclub.com',
permanent: true
},
{
source: '/camp/',
destination: 'https://camp.hackclub.com',
permanent: true
},
{
source: '/apply/',
destination: 'https://apply.hackclub.com',
permanent: true
},
{
source: '/icons/',
destination: 'https://icons.hackclub.com',
permanent: true
},
{
source: '/updates/',
destination:
'https://www.youtube.com/playlist?list=PLbNbddgD-XxEC5-_KQTye6nFPBLtI_mds',
permanent: false
},
{
source: '/workshops/',
destination: 'https://workshops.hackclub.com/',
permanent: false
},
{
source: '/workshops/([a-z_]+)/',
destination: 'https://workshops.hackclub.com/$1/',
permanent: true
},
{
source: '/daysofservice/',
destination: 'https://daysofservice.hackclub.com',
permanent: true
},
{
source: '/blot/',
destination: 'https://blot.hackclub.com',
permanent: false
},
{
source: '/donate',
destination: '/philanthropy',
permanent: false
},
{
source: '/github',
destination: 'https://github.com/hackclub',
permanent: true
},
{
source: '/nest',
destination: 'https://hackclub.app',
permanent: true
},
{
source: '/security',
destination: 'https://security.hackclub.com',
permanent: true
},
{
source: '/congressional-app-challenge',
destination: 'https://finalist.hackclub.com',
permanent: true
},
{
source: '/hardware',
destination: 'https://blueprint.hackclub.com',
permanent: true
},
{
source: '/slack',
destination: 'https://slack.hackclub.com',
permanent: true
}
]
},
async rewrites() {
return [
{
source: '/fiscal-sponsorship/mobile-app/',
destination: '/fiscal-sponsorship/mobile/'
},
{
source: '/clubs/leaders-letters',
destination: 'https://leaders-letters.vercel.app/'
},
{
source: '/letters',
destination: 'https://leaders-letters.vercel.app/'
},
{
source: '/clubs/leaders-letters/:path*',
destination: 'https://leaders-letters.vercel.app/:path*'
},
{
source: '/letter/:path*',
destination: 'https://leaders-letters.vercel.app/letter/:path*'
},
{
source: '/clubs/leaders-letters/_next/:path*',
destination: 'https://leaders-letters.vercel.app/_next/:path*'
},
{
source: '/workshops/_next/:path*',
destination: 'https://workshops.hackclub.com/_next/:path*'
},
{
source: '/summer/_next/:path*',
destination: 'https://summer.hackclub.com/_next/:path*'
},
{
source: '/sponsorship/',
destination: '/content/sponsorship/'
},
{
source: '/bin/beta',
destination: '/bin/landing-new/'
},
{
source: '/covid19/',
destination: '/content/covid19/'
},
{
source: '/it-admins/',
destination: '/content/it-admins/'
},
{
source: '/sunsetting-som/',
destination: '/content/sunsetting-som/'
},
{
source: '/banner/',
destination: 'https://workshops.hackclub.com/banner/'
},
{
source: '/conduct/',
destination: 'https://workshops.hackclub.com/conduct/'
},
{
source: '/privacy-and-terms/',
destination: 'https://workshops.hackclub.com/privacy-and-terms/'
},
{
source: '/safeguarding-policy/',
destination: 'https://workshops.hackclub.com/safeguarding-policy/'
},
{
source: '/workshop-bounty/',
destination: 'https://workshops.hackclub.com/workshop-bounty/'
},
{
source: '/vip-newsletters/',
destination: 'https://workshops.hackclub.com/vip-newsletters/'
},
{
source: '/vip-newsletters/(.*)',
destination: 'https://workshops.hackclub.com/vip-newsletters/$1'
},
{
source: '/newsletter/',
destination: 'https://workshops.hackclub.com/newsletter/'
},
{
source: '/newsletter/(.*)',
destination: 'https://workshops.hackclub.com/newsletter/$1'
},
{
source: '/transparency/may-2020/',
destination: '/content/transparency/may-2020/'
},
{
source: '/map/',
destination: 'https://map.hackclub.dev/'
},
{
source: '/map/(.*)',
destination: 'https://map.hackclub.dev/$1'
},
{
source: '/how-to-organize-a-hackathon',
destination: 'https://expandables.hackclub.dev/organizing.html'
},
{
source: '/how-to-organize-a-hackathon/style.css',
destination: 'https://expandables.hackclub.dev/style.css'
},
{
source: '/bin/',
destination: '/bin/index.html'
},
{
source: '/bin/:path*',
destination: '/bin/:path*'
},
{
source: '/bin/selector/',
destination: '/bin/selector/index.html'
},
{
source: '/arcade/:path+',
destination: '/arcade'
}
]
},
async headers() {
return [
{
source: '/banners/(.*)',
headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }]
},
{
source: '/fonts/(.*)',
headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }]
},
{
source: '/api/(.+)',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, OPTIONS'
},
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' }
]
},
{
source: '/api/bin/wokwi/(.+)',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, OPTIONS'
},
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' }
]
},
{
source: '/api/onboard/svg/(.+)',
headers: [
{
key: 'content-type',
value: 'image/svg+xml'
}
]
}
]
}
}
export default withMDX({ extension: /\.mdx?$/ })(nextConfig)
================================================
FILE: package.json
================================================
{
"name": "@hackclub/v3",
"version": "0.0.1",
"author": "Lachlan Campbell ",
"license": "MIT",
"private": true,
"packageManager": "bun@1.3.8",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"lint:errors": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hackclub/icons": "^0.2.1",
"@hackclub/markdown": "^0.1.3",
"@hackclub/meta": "^1.2.0",
"@hackclub/theme": "^1.0.0",
"@next/mdx": "^16.1.6",
"@octokit/auth-app": "^8.2.0",
"@octokit/rest": "^20.1.2",
"@sendgrid/mail": "^8.1.6",
"@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",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"airtable-plus": "^1.0.4",
"animejs": "^4.3.6",
"camelcase": "^9.0.0",
"cookies-next": "^4.3.0",
"date-fns": "^4.1.0",
"fuzzysort": "^2.0.4",
"geopattern": "^1.2.3",
"js-confetti": "^0.13.1",
"jszip": "^3.10.1",
"lodash": "^4.17.23",
"next": "^16.1.6",
"react": "^19.2.4",
"react-before-after-slider-component": "^1.1.8",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.4",
"react-masonry-css": "^1.0.16",
"react-page-visibility": "^7.0.0",
"react-relative-time": "^0.0.9",
"react-responsive-carousel": "^3.2.23",
"react-scrolllock": "^5.0.1",
"react-snowfall": "^2.4.0",
"react-ticker": "^1.3.2",
"react-tooltip": "^4.5.1",
"react-tsparticles": "^2.12.2",
"react-type-animation": "^3.2.0",
"react-wrap-balancer": "^1.1.1",
"recharts": "^3.8.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"theme-ui": "^0.14.7",
"tinytime": "^0.2.6",
"vanilla-tilt": "^1.8.1"
},
"devDependencies": {
"@mdx-js/loader": "^3.1.1",
"@types/node": "^25.4.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "^16.1.6",
"jiti": "^2.6.1",
"prettier": "^3.8.1",
"typescript": "^5.9.3"
}
}
================================================
FILE: pages/404.tsx
================================================
import React from 'react'
import styled from '@emotion/styled'
import { keyframes } from '@emotion/react'
import { Heading, Container, Button, Text, Image } from 'theme-ui'
import NextLink from 'next/link'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import theme from '../lib/theme'
import ForceTheme from '../components/force-theme'
import Nav from '../components/nav'
import Icon from '../components/icon'
import Footer from '../components/footer'
// Credit for animation from https://codepen.io/igli/pen/mPMqGz?html-preprocessor=none
const animation1 = keyframes`
0% {
clip: rect(62px, 9999px, 68px, 0);
}
5% {
clip: rect(45px, 9999px, 9px, 0);
}
10% {
clip: rect(9px, 9999px, 76px, 0);
}
15% {
clip: rect(89px, 9999px, 83px, 0);
}
20% {
clip: rect(44px, 9999px, 8px, 0);
}
25% {
clip: rect(59px, 9999px, 24px, 0);
}
30% {
clip: rect(96px, 9999px, 51px, 0);
}
35% {
clip: rect(38px, 9999px, 28px, 0);
}
40% {
clip: rect(92px, 9999px, 1px, 0);
}
45% {
clip: rect(63px, 9999px, 80px, 0);
}
50% {
clip: rect(1px, 9999px, 49px, 0);
}
55% {
clip: rect(7px, 9999px, 49px, 0);
}
60% {
clip: rect(35px, 9999px, 16px, 0);
}
65% {
clip: rect(93px, 9999px, 72px, 0);
}
70% {
clip: rect(55px, 9999px, 52px, 0);
}
75% {
clip: rect(58px, 9999px, 68px, 0);
}
80% {
clip: rect(94px, 9999px, 95px, 0);
}
85% {
clip: rect(81px, 9999px, 24px, 0);
}
90% {
clip: rect(98px, 9999px, 12px, 0);
}
95% {
clip: rect(2px, 9999px, 96px, 0);
}
100% {
clip: rect(95px, 9999px, 47px, 0);
}
`
const animation2 = keyframes`
0% {
clip: rect(57px, 9999px, 7px, 0);
}
5% {
clip: rect(61px, 9999px, 22px, 0);
}
10% {
clip: rect(34px, 9999px, 47px, 0);
}
15% {
clip: rect(92px, 9999px, 40px, 0);
}
20% {
clip: rect(6px, 9999px, 40px, 0);
}
25% {
clip: rect(39px, 9999px, 46px, 0);
}
30% {
clip: rect(33px, 9999px, 17px, 0);
}
35% {
clip: rect(5px, 9999px, 17px, 0);
}
40% {
clip: rect(40px, 9999px, 70px, 0);
}
45% {
clip: rect(14px, 9999px, 34px, 0);
}
50% {
clip: rect(26px, 9999px, 30px, 0);
}
55% {
clip: rect(15px, 9999px, 100px, 0);
}
60% {
clip: rect(10px, 9999px, 32px, 0);
}
65% {
clip: rect(49px, 9999px, 61px, 0);
}
70% {
clip: rect(61px, 9999px, 22px, 0);
}
75% {
clip: rect(85px, 9999px, 36px, 0);
}
80% {
clip: rect(38px, 9999px, 59px, 0);
}
85% {
clip: rect(21px, 9999px, 88px, 0);
}
90% {
clip: rect(46px, 9999px, 16px, 0);
}
95% {
clip: rect(13px, 9999px, 35px, 0);
}
100% {
clip: rect(75px, 9999px, 13px, 0);
}
`
const Blinking = styled(Heading)`
position: relative;
display: inline-block;
line-height: 1;
&:before,
&:after {
content: '404!';
position: absolute;
top: 0;
color: ${theme.colors.smoke};
background: ${theme.colors.dark};
overflow: hidden;
clip: rect(0, 512px, 0, 0);
}
&:after {
left: 2px;
text-shadow: -2px 0 ${theme.colors.red};
animation: ${animation1} 2s infinite steps(2, jump-end) alternate-reverse;
}
&:before {
left: -2px;
text-shadow: -2px 0 ${theme.colors.cyan};
animation: ${animation2} 4s infinite steps(2, jump-end) alternate-reverse;
}
`
const Spinning = styled(Image)`
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
animation-name: spin;
animation-duration: 10000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
@media (prefers-reduced-motion) {
animation: none;
}
aspect-ratio: 1;
`
const NotFoundPage = () => (
<>
404!
We couldn’t find that page.
>
)
export default NotFoundPage
================================================
FILE: pages/_app.tsx
================================================
import Head from 'next/head'
import Analytics from '../components/analytics'
import { Analytics as VercelAnalytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/react'
import Meta from '@hackclub/meta'
import '@hackclub/theme/fonts/reg-bold.css'
import theme from '../lib/theme'
import { ThemeProvider, Theme } from 'theme-ui'
import { Provider as BalancerProvider } from 'react-wrap-balancer'
const App = ({ Component, pageProps }) => (
)
export default App
================================================
FILE: pages/_document.tsx
================================================
import Document, { Html, Head, Main, NextScript } from 'next/document'
const org = {
'@context': 'http://schema.org',
'@type': 'Organization',
name: 'Hack Club',
url: 'https://hackclub.com/',
logo: 'https://hackclub.com/social.png',
sameAs: [
'https://twitter.com/hackclub',
'https://github.com/hackclub',
'https://www.instagram.com/starthackclub',
'https://www.facebook.com/Hack-Club-741805665870458',
'https://www.youtube.com/channel/UCQzO0jpcRkP-9eWKMpJyB0w'
],
contactPoint: [
{
'@type': 'ContactPoint',
email: 'team@hackclub.com',
contactType: 'customer support',
url: 'https://hackclub.com/'
}
]
}
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
)
}
}
================================================
FILE: pages/acknowledged.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Container, Grid, Text } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import Nav from '../components/nav'
import Footer from '../components/footer'
import Bio from '../components/bio'
import ForceTheme from '../components/force-theme'
import { fetchAcknowledged } from './api/team'
export default function Acknowleged({ team }) {
return (
<>
By the students,
for the students.
We believe in a world where every young person is empowered to be
the change they want to see around them. At Hack Club, we’re
working hard to make it reality.
Acknowledgements
Thank you to everyone who helped shape Hack Club into what it is
today...
{team.acknowledged?.map(member => (
))}
>
)
}
export const getStaticProps = async () => {
try {
const acknowledged = await fetchAcknowledged()
return { props: { team: { acknowledged } } }
} catch (e) {
return { props: { team: {} } }
}
}
================================================
FILE: pages/amas/geohot.tsx
================================================
import { Box, Button, Image, Grid, Text, Link } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import React, { useEffect, useState } from 'react'
import tt from 'tinytime'
import { thousands } from '../../lib/members'
export default function Geohot() {
const minutes = 1
const milliseconds = minutes * 60000
if (typeof window !== 'undefined') {
setTimeout(function () {
window.location.reload()
}, milliseconds)
}
const calculateTimeLeft = () => {
const difference = +new Date(`2022-11-11T23:00:00.000Z`) - +new Date()
let timeLeft = {}
if (difference > 0) {
timeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
min: Math.floor((difference / 1000 / 60) % 60),
sec: Math.floor((difference / 1000) % 60)
}
}
return timeLeft
}
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft())
useEffect(() => {
const timer = setTimeout(() => {
setTimeLeft(calculateTimeLeft())
}, 1000)
return () => clearTimeout(timer)
})
const timer = []
Object.keys(timeLeft).forEach(e => {
if (!timeLeft[e]) {
if (e === 'days') {
return
} else if (e === 'hours') {
if (!timeLeft['days']) {
return
}
} else if (e === 'min') {
if (!timeLeft['days'] && !timeLeft['hours']) {
return
}
} else {
if (!timeLeft['days'] && !timeLeft['hours'] && !timeLeft['min']) {
return
}
}
}
let name: string
if (e === 'days') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'day'
} else {
name = 'days'
}
} else if (e === 'hours') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'hour'
} else {
name = 'hours'
}
} else if (e === 'min') {
name = 'min'
} else {
name = 'sec'
}
timer.push(
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15'),
position: 'relative',
width: ['16vw', '15vw', '15vw'],
height: ['15vh', '20vh', '35vh'],
borderRadius: '5px',
border: ['none', '1.5px solid'],
borderColor: t.util.gxText('#00FF15', '#00FF15'),
fontSize: [4, 5, 7],
fontWeight: 'bold',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center'
})}
>
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15')
})}
>
{timeLeft[e]}{' '}
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15'),
position: 'relative',
fontSize: [1, 3, 4],
fontWeight: 'bold',
display: 'block',
pb: '15px'
})}
>
{name}
)
})
return (
<>
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15'),
mt: [3, 4],
px: '10px',
py: '5px',
borderRadius: '5px',
border: '1px solid',
fontSize: [1, 2],
display: 'block',
fontWeight: 'bold'
})}
>
{tt('{MM} {Do} {h}:{mm} {a}').render(
new Date(`2022-11-11T23:00:00.000Z`)
)}
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15'),
display: 'block'
})}
>
in your local date/time
{timer.length ? (
{timer}
) : (
({
color: 'primary',
...t.util.gxText('#00FF15', '#00FF15'),
fontSize: [3, 4, 5],
fontWeight: 'bold'
})}
>
The AMA has ended. Thank you to George Hotz and everyone for
joining us!
)}
{timer.length ? (
Teenager? New here? Welcome!{' '}
Hack Club
{' '}
is a global community of high school makers & student-led coding
clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+
teenagers learning to code & building amazing projects, & you’ll
fit right in.
) : (
Teenager? New here? Welcome!{' '}
Hack Club
{' '}
is a global community of high school makers & student-led coding
clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+
teenagers learning to code & building amazing projects, & you’ll
fit right in.
)}
{timer.length ? (
<>
>
) : (
<>
>
)}
>
)
}
================================================
FILE: pages/amas/index.tsx
================================================
import { Box, Button, Card, Flex, Grid, Heading, Link, Text } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../components/force-theme'
import BGImg from '../../components/background-image'
import NextLink from 'next/link'
import Image from 'next/image'
import Nav from '../../components/nav'
import SlideDown from '../../components/slide-down'
import Footer from '../../components/footer'
import { dt } from '../../lib/dates'
import Sal from '../../public/ama/sal.png'
const Page = ({ upcoming, past }) => (
<>
Lights, webcam…
AMAs
We call someone we’ve always wanted to talk to—and the entire
Hack Club Slack community is invited to ask questions & chat with
the guest live. No vetting questions. No endorsements. Conversations
are streamed live publicly on{' '}
YouTube.
{upcoming.length > 0 && (
<>
Upcoming guests
{upcoming
.sort((x, y) => {
return new Date(y.start).getTime() - new Date(x.start).getTime()
})
.map(event => (
{event.title.replace('AMA with ', '')}
{dt(event.start)}
))}
>
)}
See all upcoming events »
Past AMAs
{past
.sort((x, y) => {
return new Date(y.start).getTime() - new Date(x.start).getTime()
})
.map(event => (
{event.title.replace('AMA with ', '')}
{' '}
{/* hydration ignored cause different date formats */}
{dt(event.start)}
{event.youtube && (
Watch now »
)}
))}
>
)
export default Page
export const getStaticProps = async () => {
const { filter } = require('lodash')
let upcoming = []
let past = []
const d = dt => new Date(new Date(dt).toISOString().substring(0, 10))
const today = d(new Date())
await fetch('https://events.hackclub.com/api/amas')
.then(r => r.json())
.then(events => {
upcoming = filter(events, e => d(e.start) >= today)
past = filter(events, e => d(e.start) < today)
})
.catch(e => console.error(e, 'Failed to fetch AMAs'))
return { props: { upcoming, past }, revalidate: 10 }
}
================================================
FILE: pages/amas/sal.tsx
================================================
import { Box, Button, Image, Grid, Text, Link } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import React, { useEffect, useState } from 'react'
import tt from 'tinytime'
import { thousands } from '../../lib/members'
export default function Sal() {
const minutes = 1
const milliseconds = minutes * 60000
if (typeof window !== 'undefined') {
setTimeout(function () {
window.location.reload()
}, milliseconds)
}
const calculateTimeLeft = () => {
const difference = +new Date(`2022-10-28T23:00:00.000Z`) - +new Date()
let timeLeft = {}
if (difference > 0) {
timeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
min: Math.floor((difference / 1000 / 60) % 60),
sec: Math.floor((difference / 1000) % 60)
}
}
return timeLeft
}
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft())
useEffect(() => {
const timer = setTimeout(() => {
setTimeLeft(calculateTimeLeft())
}, 1000)
return () => clearTimeout(timer)
})
const timer = []
Object.keys(timeLeft).forEach(e => {
if (!timeLeft[e]) {
if (e === 'days') {
return
} else if (e === 'hours') {
if (!timeLeft['days']) {
return
}
} else if (e === 'min') {
if (!timeLeft['days'] && !timeLeft['hours']) {
return
}
} else {
if (!timeLeft['days'] && !timeLeft['hours'] && !timeLeft['min']) {
return
}
}
}
let name: string
if (e === 'days') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'day'
} else {
name = 'days'
}
} else if (e === 'hours') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'hour'
} else {
name = 'hours'
}
} else if (e === 'min') {
name = 'min'
} else {
name = 'sec'
}
timer.push(
({
color: 'primary',
...t.util.gxText('#ffffff', '#ffffff'),
position: 'relative',
width: '100%',
height: ['125%', '125%', '150%'],
borderRadius: '5px',
border: ['none', '1.5px solid'],
borderColor: t.util.gxText('#14BF96', '#14BF96'),
fontSize: [4, 5, 7],
fontWeight: 'bold',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center'
})}
>
({
color: 'primary',
...t.util.gxText('#14BF96', '#14BF96')
})}
>
{timeLeft[e]}{' '}
({
color: 'primary',
...t.util.gxText('#14BF96', '#14BF96'),
position: 'relative',
fontSize: [1, 3, 4],
fontWeight: 'bold',
display: 'block',
pb: '15px'
})}
>
{name}
)
})
return (
<>
({
color: 'primary',
...t.util.gxText('#14BF96', '#14BF96'),
mt: [3, 4],
px: '10px',
py: '5px',
borderRadius: '5px',
border: '1px solid #14BF96',
fontSize: [1, 2],
display: 'block',
fontWeight: 'bold'
})}
>
{tt('{MM} {Do} {h}:{mm} {a}').render(
new Date(`2022-10-28T23:00:00.000Z`)
)}
({
color: 'primary',
...t.util.gxText('#14BF96', '#14BF96'),
display: 'block'
})}
>
in your local date/time
{timer.length ? (
{timer}
) : (
({
color: 'primary',
...t.util.gxText('#ffffff', '#ffffff'),
fontSize: [3, 4, 5],
fontWeight: 'bold'
})}
>
The livestream has ended. Thank you to everyone for joining us!
)}
Teenager? New here? Welcome!{' '}
Hack Club
{' '}
is a global community of high school makers & student-led coding
clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+ teenagers
learning to code & building amazing projects, & you’ll fit right
in.
{timer.length ? (
<>
>
) : (
<>
>
)}
>
)
}
================================================
FILE: pages/amas/vitalik.tsx
================================================
import { Box, Button, Image, Grid, Text, Link } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import React, { useEffect, useState } from 'react'
import tt from 'tinytime'
import Particle from '../../components/particles'
import { thousands } from '../../lib/members'
export default function Vitalik() {
const calculateTimeLeft = () => {
const difference = +new Date(`2022-02-04T01:00:00.000Z`) - +new Date()
let timeLeft = {}
if (difference > 0) {
timeLeft = {
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
min: Math.floor((difference / 1000 / 60) % 60),
sec: Math.floor((difference / 1000) % 60)
}
}
return timeLeft
}
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft())
useEffect(() => {
const timer = setTimeout(() => {
setTimeLeft(calculateTimeLeft())
}, 1000)
return () => clearTimeout(timer)
})
const timer = []
Object.keys(timeLeft).forEach(e => {
if (!timeLeft[e]) {
if (e === 'days') {
return
} else if (e === 'hours') {
if (!timeLeft['days']) {
return
}
} else if (e === 'min') {
if (!timeLeft['days'] && !timeLeft['hours']) {
return
}
} else {
if (!timeLeft['days'] && !timeLeft['hours'] && !timeLeft['min']) {
return
}
}
}
let name: string
if (e === 'days') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'day'
} else {
name = 'days'
}
} else if (e === 'hours') {
if (timeLeft[e] === 1 || timeLeft[e] === 0) {
name = 'hour'
} else {
name = 'hours'
}
} else if (e === 'min') {
name = 'min'
} else {
name = 'sec'
}
timer.push(
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9'),
position: 'relative',
width: ['16vw', '15vw', '15vw'],
height: ['15vh', '20vh', '35vh'],
borderRadius: '5px',
border: ['none', '1.5px solid'],
borderColor: t.util.gxText('#CDAEFB', '#82A9F9'),
fontSize: [4, 5, 7],
fontWeight: 'bold',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center'
})}
>
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9')
})}
>
{timeLeft[e]}{' '}
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9'),
position: 'relative',
fontSize: [1, 3, 4],
fontWeight: 'bold',
display: 'block',
pb: '15px'
})}
>
{name}
)
})
return (
<>
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9'),
mt: [3, 4],
px: '10px',
py: '5px',
borderRadius: '5px',
border: '1px solid',
fontSize: [1, 2],
display: 'block',
fontWeight: 'bold'
})}
>
{tt('{MM} {Do} {h}:{mm} {a}').render(
new Date(`2022-02-04T01:00:00.000Z`)
)}
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9'),
display: 'block'
})}
>
in your local date/time
{timer.length ? (
{timer}
) : (
({
color: 'primary',
...t.util.gxText('#CDAEFB', '#82A9F9'),
fontSize: [3, 4, 5],
fontWeight: 'bold'
})}
>
The AMA has ended. Thank you to Vitalik and everyone for joining
us!
)}
{timer.length ? (
Teenager? New here? Welcome!{' '}
Hack Club
{' '}
is a global community of high school makers & student-led coding
clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+
teenagers learning to code & building amazing projects, & you’ll
fit right in.
) : (
Teenager? New here? Welcome!{' '}
Hack Club
{' '}
is a global community of high school makers & student-led coding
clubs. We’ve got a 24/7 Slack chatroom of {thousands}k+
teenagers learning to code & building amazing projects, & you’ll
fit right in.
)}
{timer.length ? (
<>
>
) : (
<>
>
)}
>
)
}
================================================
FILE: pages/api/arcade/hack-hour/inventory.ts
================================================
import AirtablePlus from 'airtable-plus'
const flavorText = async () => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app3ODCEuTL5iGjb3',
tableName: 'Flavor Text'
})
const records = airtable.read()
return records
}
const inventoryParts = async () => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app3ODCEuTL5iGjb3',
tableName: 'Inventory'
})
const records = await airtable.read()
return records
}
type InventoryRecord = {
fields: {
Name: string
'Name Small Text': string
Hours: number
'Image URL': string
'Order Form URL': string
Description: string
'Flavor text'?: string[]
Enabled: boolean
}
}
type FlavorRecord = {
id: string
fields: {
Message: string
Character: string
characterURL: string
'Image URL': string
'Self Click': boolean
'Character (from Shopkeepers)'?: string[]
'Image Link (from Shopkeepers)'?: string[]
}
}
export default async function handler(req, res) {
const data: { inventory?: InventoryRecord[]; flavor?: FlavorRecord[] } = {}
await Promise.all([
inventoryParts().then((d: InventoryRecord[]) => {
data.inventory = d
}),
flavorText().then((d: FlavorRecord[]) => {
data.flavor = d
})
])
const inventoryResults = data.inventory
.filter(record => record.fields['Enabled'])
.map(record => {
return {
name: record.fields['Name'],
smallName: record.fields['Name Small Text'],
hours: record.fields['Hours'],
imageURL: record.fields['Image URL'],
formURL: record.fields['Order Form URL'],
description: record.fields['Description'],
flavorText: record?.fields['Flavor text']?.map(recordID => {
const flavorRecord = data.flavor.find(f => f.id === recordID)
const result = {
message: flavorRecord.fields['Message'],
character: flavorRecord.fields['Character'],
imageURL: flavorRecord.fields['Image URL'],
characterURL: ''
}
if (flavorRecord.fields['Shopkeepers']) {
result.characterURL =
flavorRecord.fields['Image Link (from Shopkeepers)'][0]
result.character =
flavorRecord.fields['Character (from Shopkeepers)'][0]
}
return result
})
}
})
const selfClicks = {}
data.flavor
.filter(f => f.fields['Self Click'])
.forEach(record => {
const char = record.fields['Character (from Shopkeepers)'][0]
const charURL = record.fields['Image Link (from Shopkeepers)'][0]
const charMsg = record.fields['Message']
selfClicks[char] = selfClicks[char] || []
selfClicks[char].push({
message: charMsg,
characterURL: charURL,
character: char
})
})
res.status(200).json({ inventory: inventoryResults, selfClicks })
}
================================================
FILE: pages/api/arcade/shop.ts
================================================
import AirtablePlus from 'airtable-plus'
export const shopParts = async () => {
const baseID = 'app4kCWulfB02bV8Q'
const shopItemsTable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID,
tableName: 'Shop Items'
})
const records = await shopItemsTable.read()
const newRecordsPromise = records.map(async record => {
const fields = record.fields
let stock = fields['Stock']
if (stock && fields['Count of Orders Fulfilled']) {
stock -= fields['Count of Orders Fulfilled']
}
return {
id: record.id,
...record.fields,
Stock: stock === null ? null : stock >= 0 ? stock : 0
}
})
const newRecords = await Promise.all(newRecordsPromise)
return newRecords
}
export default async function handler(req, res) {
const data = await shopParts()
const filteredData = data
.filter(record => record['Enabled'])
.map(record => {
return {
name: record['Name'],
smallName: record['Small Name'],
description: record['Description'],
hours: record['Cost Hours'],
imageURL: record['Image URL'],
stock: record['Stock']
}
})
return res.json(filteredData)
}
================================================
FILE: pages/api/bin/gallery/posts.ts
================================================
import AirtablePlus from 'airtable-plus'
const fetchPosts = async () => {
try {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'Main'
})
const records = await airtable.read()
const posts = records.map(record => {
return {
ID: record.id,
title: record.fields.Title,
desc: record.fields['What will you be building?'],
slack: record.fields['Slack Handle'],
link: record.fields['Wokwi Share link'],
status: record.fields.Status,
hide: record.fields['Hide From Gallery'],
created: record.fields['Created At'],
parts: record.fields['Parts List']
}
})
return posts
} catch (error) {
console.error('Error fetching posts:', error)
throw error
}
}
export default async function handler(req, res) {
try {
const data = await fetchPosts()
res.status(200).json(data)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch posts' })
}
}
================================================
FILE: pages/api/bin/gallery/tags.ts
================================================
import AirtablePlus from 'airtable-plus'
const fetchTags = async () => {
try {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'Supported Parts'
})
const records = await airtable.read()
const tags = records.map(record => {
return {
ID: record.id,
hide: record.fields['Hide From Gallery']
}
})
console.log('tags', tags)
return tags
} catch (error) {
console.error('Error fetching tags:', error)
throw error
}
}
export default async function handler(req, res) {
try {
const data = await fetchTags()
res.status(200).json(data)
} catch (error) {
res.status(500).json({ error: 'Failed to fetch tags' })
}
}
================================================
FILE: pages/api/bin/rsvp.ts
================================================
// https://airtable.com/appKjALSnOoA0EmPk/tblYYhxN9TaPPMMRV/viwJFvTlmRNHj0Toh?blocks=hide
import AirtablePlus from 'airtable-plus'
let rsvpsTable
// only fetch if apiKey present
if (process.env.AIRTABLE_WRITE_API_KEY) {
rsvpsTable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'RSVPs'
})
} else {
console.warn(
'No AIRTABLE_WRITE_API_KEY environment variable found, the bin RSVP will not be fetched'
)
}
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') {
if (!rsvpsTable) return res.status(200).json(0)
const result = await rsvpsTable.read()
res.status(200).json(result.length)
}
}
================================================
FILE: pages/api/bin/wokwi/new/[parts].ts
================================================
import { findOrCreateProject } from '.'
export default async function handler(req, res) {
const parts = req.query.parts.split('|')
const shareLink = await findOrCreateProject(parts)
res.redirect(shareLink)
}
================================================
FILE: pages/api/bin/wokwi/new/index.ts
================================================
import AirtablePlus from 'airtable-plus'
export const findOrCreateProject = async (partsList = []) => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'Cached Projects'
})
const cacheName = partsList.sort().join(',')
const existingProject = await airtable.read({
filterByFormula: `{Name}="${cacheName}"`,
maxRecords: 1
})
if (existingProject.length > 0) {
return existingProject[0].fields['Share Link']
} else {
const shareLink = await createProject(partsList)
if (shareLink) {
await airtable.create({
Name: cacheName,
'Share Link': shareLink
})
return shareLink
} else {
return null
}
}
}
const createProject = async (partsList = []) => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'Supported Parts'
})
// adjust these to taste:
const PADDING = 30
const MAX_WIDTH = 320 // big question mark on this one
const ROW_HEIGHT = 215 // close enough for jazz, keypad is too big for this but ¯\_(ツ)_/¯
const parts = [
{ type: 'board-pi-pico-w', id: 'pico', top: 0, left: 0, attrs: {} }
]
let x = 88 + PADDING // for already included Pico
let y = 0
await Promise.all(
partsList.map(async part => {
const airPart = await airtable.read({
filterByFormula: `{Wokwi Name}= "${part}"`,
maxRecords: 1
})
return airPart[0].fields['Wokwi Name'].split(',').forEach((name, i) => {
const width = airPart[0].fields['Wokwi X-Offset']
const attrs = airPart[0].fields['attrs']
if (x + width + PADDING > MAX_WIDTH) {
x = 0
y += ROW_HEIGHT
}
parts.push({
type: name,
id: name + '--' + i,
left: x,
top: y,
attrs: attrs
})
x += width + PADDING
})
})
)
const body = JSON.stringify({
name: 'The Bin!',
unlisted: true,
files: [
{
name: 'help.md',
content: `# Welcome to The Bin! 🦝
Now that you've thrown some parts into The Bin, it's time to turn that trash into treasure! 🗑️➡️💎
Wire up your parts and write some code to make them work together. If you need
help with a part, click the "?" above it.
If you want to see examples, check here:
https://hack.club/bin-example
You can get help by chatting with other high schoolers on the Hack Club Slack in
the #electronics channel:
👉 https://slack.hackclub.com 👈
Once you're ready build your design IRL, click the "Share" button and submit
your design:
https://hack.club/bin-submit
`
},
{
name: 'sketch.ino',
content: `// Now turn this trash into treasure!
void setup() {
// put your setup code here, to run once:
Serial1.begin(115200);
Serial1.println("Hello, Raspberry Pi Pico W!");
}
void loop() {
// put your main code here, to run repeatedly:
delay(1); // this speeds up the simulation
}`
},
{
name: 'diagram.json',
content: JSON.stringify(
{
version: 1,
author: 'The Bin - Hack Club',
editor: 'wokwi',
parts: parts,
connections: [
['pico:GP0', '$serialMonitor:RX', '', []],
['pico:GP1', '$serialMonitor:TX', '', []]
],
dependencies: {}
},
null,
2
)
}
]
})
const response = await fetch('https://wokwi.com/api/projects/save', {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
Referer: 'https://wokwi.com/projects/new/pi-pico-w',
'User-Agent': 'Hack Club - contact max@hackclub.com for any complaints!'
},
body
}).catch(e => {
console.log(e)
})
if (!response) return null
const data = await response.json()
const { projectId } = data
return `https://wokwi.com/projects/${projectId}`
}
export default async function handler(req, res) {
if (req.method === 'POST') {
const { parts } = req.body
const shareLink = await findOrCreateProject(parts)
if (shareLink) {
res.status(200).json({ shareLink })
} else {
res.status(500).json({ error: 'Failed to create project' })
}
}
}
================================================
FILE: pages/api/bin/wokwi/parts.ts
================================================
import AirtablePlus from 'airtable-plus'
import camelcase from 'camelcase'
const camelizeObject = obj => {
Object.keys(obj).forEach(key => {
obj[camelcase(key)] = obj[key]
if (key !== camelcase(key)) {
delete obj[key]
}
})
return obj
}
const wokwiParts = async () => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'appKjALSnOoA0EmPk',
tableName: 'Supported Parts'
})
const records = await airtable.read()
const parts = records.map(record => camelizeObject(record.fields))
return parts
}
export default async function handler(req, res) {
const data = await wokwiParts()
res.status(200).json(data)
}
================================================
FILE: pages/api/bucky.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const result = await fetch('https://bucky.hackclub.com', {
method: 'POST',
body: req.body,
headers: {
'Content-Type': req.headers['content-type']
}
}).then(r => r.text())
res.status(200).json({ result })
}
================================================
FILE: pages/api/channels/resolve.ts
================================================
export default async function handler(req, res) {
// get a public channel name by id
const channelDataReq = await fetch(
`https://slack.com/api/conversations.info?channel=${req.query.id}`,
{
headers: {
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`
}
}
)
if (!channelDataReq.ok) {
console.warn(await channelDataReq.text())
return res.status(503).end()
}
const channelData = await channelDataReq.json()
if (!channelData.ok) {
console.warn(channelData)
return res.status(400).end()
}
res.status(200).send({ name: channelData.channel.name })
}
================================================
FILE: pages/api/contribute.ts
================================================
import { graphql } from '@octokit/graphql'
import { createAppAuth } from '@octokit/auth-app'
import { NextApiRequest, NextApiResponse } from 'next'
interface OrgQueryResponse {
organization: Record
}
const auth = createAppAuth({
appId: process.env.GITHUB_APP_ID as string,
privateKey: process.env.GITHUB_PRIVATE_KEY as string,
installationId: process.env.GITHUB_INSTALLATION_ID as string
})
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { organization } = await graphql(
!req.query.admin
? `
query orgQuery($login: String!) {
organization(login: $login) {
repositories(first: 50, privacy: PUBLIC, orderBy: {
field: PUSHED_AT, direction: DESC
}){
nodes {
name
description
languages(first: 1) {
nodes {
name
}
}
pushedAt
url
issues(states: OPEN) {
totalCount
}
}
}
}
}`
: `query orgQuery($login: String!) {
organization(login: $login) {
repositories(first: 100, privacy: PUBLIC, orderBy: {
field: PUSHED_AT, direction: DESC
}){
nodes {
name
description
isArchived
languages(first: 1) {
nodes {
name
}
}
pushedAt
url
pullRequests(first: 50, states: OPEN) {
nodes {
title,
url,
number,
repository {
id,
name
}
}
}
}
}
}
}`,
{
login: 'hackclub',
request: {
hook: auth.hook
}
}
)
res.status(200).json(organization)
}
================================================
FILE: pages/api/first-team.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const response = await fetch(
`https://thebluealliance.com/api/v3/team/frc${encodeURIComponent(
Array.isArray(req.query.teamNumber)
? req.query.teamNumber[0]
: req.query.teamNumber
)}`,
{ headers: { 'X-TBA-Auth-Key': process.env.TBA_API_KEY } }
)
const data = await response.json()
res.json(data)
} catch (e) {
res.status(404).json({ ok: false })
}
}
================================================
FILE: pages/api/games.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
export async function getGames() {
try {
const games = await fetch(
'https://sprig.hackclub.com/api/gallery?new'
).then(res => res.json())
return games
} catch (e) {
console.error(e)
return []
}
}
export default async function Games(
_req: NextApiRequest,
res: NextApiResponse
) {
const games = await getGames()
res.json(games)
}
================================================
FILE: pages/api/github.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'
import { normalizeGitHubCommitUrl } from '../../lib/helpers'
const isRelevantEventType = type =>
['PushEvent', 'PullRequestEvent', 'WatchEvent'].includes(type)
const getMessage = (type, payload, repo) => {
switch (type) {
case 'PushEvent':
return payload.commits?.[0]?.message || 'No commit message'
case 'PullRequestEvent':
return payload.pull_request.title
case 'WatchEvent':
return `starred ${repo.name}`
default:
return null
}
}
const getUrl = (type, payload, repo) => {
switch (type) {
case 'PushEvent':
return payload.commits?.[0]?.url
? normalizeGitHubCommitUrl(payload.commits[0].url)
: 'https://github.com/hackclub'
case 'PullRequestEvent':
return payload.pull_request.html_url
case 'WatchEvent':
return `https://github.com/${repo.name}`
default:
return `https://github.com/hackclub`
}
}
export async function fetchGitHub() {
try {
const initialGitHubData = await fetch(
'https://api.github.com/orgs/hackclub/events'
).then(r => r.json())
const gitHubData = initialGitHubData
.filter(({ type }) => isRelevantEventType(type))
.map(({ type, actor, payload, repo, created_at }) => ({
type,
user: actor.login,
userImage: actor.avatar_url,
url: getUrl(type, payload, repo),
message: getMessage(type, payload, repo),
time: created_at
}))
return gitHubData
} catch (error) {
console.error(error)
return []
}
}
export default async function github(
_req: NextApiRequest,
res: NextApiResponse
) {
const git = await fetchGitHub()
res.json(git)
}
================================================
FILE: pages/api/join.ts
================================================
import AirtablePlus from 'airtable-plus'
import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
const joinTable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'appaqcJtn33vb59Au',
tableName: 'Join Requests'
})
async function postData(url = '', data = {}, headers = {}) {
const response = await fetch(url, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
...headers
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(data)
})
return response.text()
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
switch (req.method) {
case 'OPTIONS':
return res.status(200).send('YIPPE YAY. YOU HAVE CLEARANCE TO PROCEED.')
case 'GET':
return res
.status(405)
.json({ error: '*GET outta here!* (Method not allowed, use POST)' })
case 'PUT':
return res.status(405).json({
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 || {}
const open = process.env.NEXT_PUBLIC_OPEN === 'true'
const waitlist = !open
const isAdult = data.year === 'tertiary'
if (isAdult) {
const mail = {
to: data.email,
from: 'Hack Club Slack ',
subject: 'Slack Waiting List update',
text: 'Hello world',
html: "Hey! Thanks for your interest in the Hack Club Slack. 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 Github. If you're a parent or educator & want to talk to a member of our team, send us a email at team@hackclub.com.",
imageUrl: 'https://assets.hackclub.com/icon-rounded.png'
}
sgMail.send(mail)
}
const secrets = (process.env.NAUGHTY || '').split(',')
const forwardedFor = Array.isArray(req.headers['x-forwarded-for'])
? req.headers['x-forwarded-for'][0]
: req.headers['x-forwarded-for']
if (secrets.includes(forwardedFor)) {
return res.json({
status: 'success',
message: 'You’ve been invited to Slack!'
})
}
const airtablePromise = joinTable.create({
'Full Name': data.name,
'Email Address': data.email,
Minor: !isAdult,
Reason: data.reason,
Invited: !waitlist,
Club: data.club ? data.club : '',
Event: data.event ? data.event : '',
IP: req.headers['x-forwarded-for'] || req.socket.remoteAddress
})
if (waitlist) {
return res.json({
status: 'success',
message: 'Your request will be reviewed soon.'
})
}
const slackPromise = postData(
'https://toriel.hackclub.com/slack-invite',
{
email: !isAdult ? data.email : null,
ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
continent: data.continent,
teen: !isAdult,
educationLevel: data.educationLevel,
reason: data.reason,
event: data.event,
userAgent: req.headers['user-agent']
},
{ authorization: `Bearer ${process.env.TORIEL_KEY}` }
)
Promise.all([airtablePromise, slackPromise])
.then(() =>
res.json({ status: 'success', message: 'You’ve been invited to Slack!' })
)
.catch(error => {
console.error(error)
res.status(500).json({ error })
})
}
================================================
FILE: pages/api/mailing-list.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
export default async function submit(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const data = req.body
const resp = await fetch('https://postal.hackclub.com/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
api_key: process.env.POSTAL_API_KEY,
name: data.name,
email: data.email,
list: process.env.POSTAL_LIST_ID,
boolean: 'true'
}).toString()
}).then(r => r.text())
res.json(resp)
}
}
================================================
FILE: pages/api/onboard/p/[project]/index.ts
================================================
// 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)
}
================================================
FILE: pages/api/onboard/p/count.ts
================================================
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 })
}
================================================
FILE: pages/api/onboard/p/index.ts
================================================
// returns a list of all projects
export const getAllOnboardProjects = async () => {
const url = 'https://api.github.com/repos/hackclub/onboard/contents/projects'
const fetchOptions = {
headers: {}
}
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 || !Array.isArray(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`,
imageTop: `/api/onboard/svg/${encodeURIComponent(`https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${p.name}/gerber.zip`)}/top`,
imageBottom: `/api/onboard/svg/${encodeURIComponent(`https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${p.name}/gerber.zip`)}/bottom`
}
projects.push(projectData)
})
return projects
}
export default async function handler(req, res) {
const projects = await getAllOnboardProjects()
res.json(projects)
}
================================================
FILE: pages/api/onboard/svg/[board_url]/bottom.ts
================================================
// 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)
}
================================================
FILE: pages/api/onboard/svg/[board_url]/index.ts
================================================
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(new JSZip()) // 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
================================================
FILE: pages/api/onboard/svg/[board_url]/top.ts
================================================
// 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)
}
================================================
FILE: pages/api/replit/signup.ts
================================================
export default async function handler(req, res) {
if (req.method === 'POST') {
try {
const { token, email } = req.body
const url = new URL('http://takeout.hackclub.com/signup')
url.searchParams.append('token', token)
url.searchParams.append('email', email)
const response = await fetch(url, { method: 'POST' })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
// Send the response from the replit-takeout service back to the client
res.status(200).json(data)
} catch (error) {
console.error('Error processing signup:', error)
res
.status(500)
.json({ message: 'Error processing signup', error: error.message })
}
} else {
// Handle any non-POST requests
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
}
================================================
FILE: pages/api/sprig-console.ts
================================================
import { NextApiRequest, NextApiResponse } from 'next'
function check(val: any) {
return val === 'Pending' || val === 'Approved'
}
export async function getConsoles() {
try {
const response = await fetch(
'https://airbridge.hackclub.com/v0.1/Sprig%20Waitlist/Requests'
)
if (!response.ok) {
return 100
}
const data = await response.json()
const consoleCount = Array.isArray(data)
? data.filter(console => check(console.fields.Status)).length
: 100
return consoleCount
} catch (error) {
console.error('Error fetching console data:', error)
return 100
}
}
export default async function SprigConsoles(
_req: NextApiRequest,
res: NextApiResponse
) {
let consoleCount = 100
try {
consoleCount = await getConsoles()
} catch (error) {
console.error(error)
}
res.json(consoleCount)
}
================================================
FILE: pages/api/stars.ts
================================================
import { graphql } from '@octokit/graphql'
import { NextApiRequest, NextApiResponse } from 'next/dist/shared/lib/utils'
interface GitHubStarsResponse {
organization: {
blot: { stargazerCount: number }
sinerider: { stargazerCount: number }
sprig: { stargazerCount: number }
hackclub: { stargazerCount: number }
hackathons: { stargazerCount: number }
sprigHardware: { stargazerCount: number }
onboard: { stargazerCount: number }
}
}
export async function fetchStars() {
const emptyStats = {
sprig: { stargazerCount: 0 },
sinerider: { stargazerCount: 0 },
sprigHardware: { stargazerCount: 0 },
hackclub: { stargazerCount: 0 },
hackathons: { stargazerCount: 0 },
blot: { stargazerCount: 0 },
onboard: { stargazerCount: 0 }
}
if (!process.env.GITHUB_TOKEN) {
console.warn(
'Note - GITHUB_TOKEN not defined, stars will not be fetched from github'
)
return emptyStats
}
try {
const { organization } = await graphql(
`
{
organization(login: "hackclub") {
blot: repository(name: "blot") {
stargazerCount
}
sinerider: repository(name: "sinerider") {
stargazerCount
}
sprig: repository(name: "sprig") {
stargazerCount
}
hackclub: repository(name: "hackclub") {
stargazerCount
}
hackathons: repository(name: "hackathons") {
stargazerCount
}
sprigHardware: repository(name: "sprig-hardware") {
stargazerCount
}
onboard: repository(name: "onboard") {
stargazerCount
}
}
}
`,
{
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`
}
}
)
return organization
} catch (error) {
console.error('Error fetching stars:', error)
return emptyStats
}
}
export default async function Stars(
_req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json(await fetchStars())
}
================================================
FILE: pages/api/steve.ts
================================================
import type { NextApiRequest, NextApiResponse } from 'next'
const steveApiHandler = async (_req: NextApiRequest, res: NextApiResponse) => {
const calendarId =
'c_e7c63a427761b0f300ede97f432ba4af24033daad26be86da0551b40b7968f00@group.calendar.google.com'
//This API key is for google calendar and has only read access to Steve
const apiKey = 'AIzaSyD_8dEnTDle3WmaoOTvEW6L1GW540FU_wg'
const allBusyDays = new Set()
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 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 }]
})
}
)
const data = await response.json()
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 (const 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(Array.from(allBusyDays))
} catch (error) {
return res.status(500).json({ error: 'Failed to fetch busy times.' })
}
}
export default steveApiHandler
================================================
FILE: pages/api/stickers.ts
================================================
import AirtablePlus from 'airtable-plus'
import { NextApiRequest, NextApiResponse } from 'next'
const peopleTable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'apptEEFG5HTfGQE7h',
tableName: 'People'
})
const addressesTable = new AirtablePlus({
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
baseID: 'apptEEFG5HTfGQE7h',
tableName: 'Addresses'
})
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const data = req.body
let personRecord = (
await peopleTable.read({
filterByFormula: `{Email} = '${data.email}'`
})
)[0]
if (!personRecord) {
personRecord = await peopleTable.create({
'Full Name': data.name,
Email: data.email
})
}
let address = (
await addressesTable.read({
filterByFormula: `AND({Email} = '${data.email}', {Is Valid?} = '1', {Club} = '')`
})
)[0]
if (!address) {
address = await addressesTable.create({
'Street (First Line)': data.addressFirst,
'Street (Second Line)': data.addressSecond,
City: data.city,
'State/Province': data.state,
'Postal Code': data.zipCode,
Country: data.country,
Person: [personRecord.id]
})
}
if (
!(
address.fields['Street (First Line)'].toLowerCase() ===
data.addressFirst.toLowerCase()
)
) {
address = await addressesTable.create({
'Street (First Line)': data.addressFirst,
'Street (Second Line)': data.addressSecond,
City: data.city,
'State/Province': data.state,
'Postal Code': data.zipCode,
Country: data.country,
Person: [personRecord.id]
})
}
const url = process.env.MAIL_MISSION_WEBHOOK
const body = JSON.stringify({
test: false,
scenarioRecordID: 'recNDwjb7Zr04Szix',
receiverAddressRecordID: address.id,
missionNotes: 'Requested via hackclub.com'
})
fetch(url, {
body,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(r => {
res.json({ status: 'success' })
})
.catch(error => {
console.error(error)
res.json({ status: 'error', error })
})
} else {
res.status(405).json({ status: 'error', error: 'Must send POST request' })
}
}
================================================
FILE: pages/api/stuff.ts
================================================
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function stuff(
_req: NextApiRequest,
res: NextApiResponse
) {
const formData = new FormData()
formData.append(
'token',
'xoxc-2210535565-1329510668482-3738018363764-a06090b7e70cef57099ae10c6d18f80013869ef9be48fcc389e5a40c90df2624'
)
formData.append('date_range', '30d')
const data = await fetch(
'https://hackclub.slack.com/api/team.stats.timeSeries',
{
method: 'POST',
body: formData,
headers: {
Cookie:
'd=xoxd-52SpoJa3LK%2BF%2FZA3OwTuxIhUHfsXCx%2Fq1hpcu1VdiH2OUhPuonSeXAYYGJefTNiJUZE8SjAIEfASlHsdYHeGkg%2FFZ584%2B7JbekY8Mz%2FbOEgEJxhGjRW7miVyqQvbPq3oQlSfwNoXb507TnD5VYOCLYUh3OuK2tc2GnfwC0MgPl9ZsAoDc1caaA%3D%3D'
}
}
).then(r => r.json())
res.json(
data.stats
.sort((a: { ds: number }, b: { ds: number }) => a.ds - b.ds)
.reverse()[0]
)
}
================================================
FILE: pages/api/team.ts
================================================
import teamMembers from '../../public/team.json'
import acknowledgedMembers from '../../public/acknowledged.json'
import type { NextApiRequest, NextApiResponse } from 'next'
export interface TeamMember {
name: string
department: string
role: string | string[]
acknowledged?: boolean
bio: string
slackId: string
overrideAvatar: string
email: string
website: string
pronouns: string
avatar: string
staff?: boolean
gapyear?: boolean
}
export async function fetchTeam() {
return teamMembers as TeamMember[]
}
export async function fetchAcknowledged() {
return acknowledgedMembers as TeamMember[]
}
export default async function handler(
_req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json(await fetchTeam())
}
================================================
FILE: pages/api/winter-rsvp.ts
================================================
import type { NextApiRequest, NextApiResponse } from 'next'
import AirtablePlus from 'airtable-plus'
const airtable = new AirtablePlus({
baseID: 'app1o9tRo6XulLnsr',
apiKey: process.env.AIRTABLE_WRITE_API_KEY,
tableName: 'rsvp'
})
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const rsvp = await airtable.create({
Name: req.body.Name,
Email: req.body.Email,
Age: req.body.Age,
IP: req.headers['x-forwarded-for'] || req.socket.remoteAddress
})
const url = process.env.WOM_SLACK_WEBHOOK_URL
const body = JSON.stringify({
rsvp
})
fetch(url, {
body,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(() => res.status(200).json({ success: true }))
.catch(error => {
console.error(error)
res.json({ status: 'Something went wrong', error })
})
} else {
res.status(405).json({ status: 'error', error: 'Must send POST request' })
}
}
================================================
FILE: pages/arcade/index.tsx
================================================
/** @jsxImportSource theme-ui */
import React, { useState } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'
import Nav from '../../components/nav'
import Meta from '@hackclub/meta'
import { Box, Text, Flex, Grid, Card, Close, Divider, Heading } from 'theme-ui'
import Image from 'next/image'
import fs from 'fs'
import path from 'path'
import { startCase } from 'lodash'
import Projects from '../../components/arcade/projects'
import Ticker from 'react-ticker'
import PageVisibility from 'react-page-visibility'
import ArcadeFooter from '../../components/arcade/footer'
import Balancer from 'react-wrap-balancer'
import { Fade } from '../../components/react-reveal-compat'
import Announcement from '../../components/announcement'
import Link from 'next/link'
import { shopParts } from '../api/arcade/shop'
const styled = `
@import url(https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap);
body, html {
overflow-x: hidden;
}
.slackey {
font-family: "Slackey", sans-serif;
}
.arcade {
text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6,
-2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6,
-1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6,
1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6,
3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4,
-5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4,
-2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4,
5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4,
6px 6px #09AFB4, 8px 8px #09AFB4, -8px -8px #09AFB4, 9px 9px #09AFB4, -9px -9px #09AFB4, 10px 10px #09AFB4, -10px -10px #09AFB4;
}
.arcade2 {
text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6,
-2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6,
-1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6,
1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6,
3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4,
-5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4,
-2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4,
5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4,
6px 6px #09AFB4;
}
.arcade3 {
text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6,
-2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6,
-1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6,
1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6,
3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4,
-5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4,
-2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4,
5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4,
6px 6px #09AFB4;
}
.emblema {
font-family: "Emblema One", system-ui;
}
.gaegu {
font-family: "Gaegu", sans-serif;
}
body {
background-color: #FAEFD6;
}
/* CSS from https://codepen.io/quadbaup/details/rKOKQv */
.thought {
display: flex;
background-color: #F8F3F3;
padding: 20px;
border-radius: 30px;
min-width: 40px;
max-width: 180px;
opacity: 0;
height: 100px;
margin: 20px;
margin-left: -10px;
position: relative;
align-items: center;
justify-content: center;
font-size: 12px;
/* text-align:center; */
}
.thought:before,
.thought:after {
content: "";
background-color: #F8F3F3;
border-radius: 50%;
display: block;
position: absolute;
z-index: -1;
}
.thought:before {
width: 44px;
height: 44px;
top: -12px;
left: 28px;
box-shadow: -50px 30px 0 -12px #F8F3F3;
}
.thought:after {
bottom: -10px;
right: 26px;
width: 30px;
height: 30px;
box-shadow: 40px -34px 0 0 #F8F3F3,
-28px -6px 0 -2px #F8F3F3,
-24px 17px 0 -6px #F8F3F3,
-5px 25px 0 -10px #F8F3F3;
}
#generate-project-idea {
margin-top: -100px;
}
.talking {
animation: talking 1s infinite;
}
@keyframes talking {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
.floaty {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
from,
to {
transform: translate(0%, -37%) rotate(-2deg);
}
25% {
transform: translate(-2%, -40%) rotate(2deg);
}
50% {
transform: translate(0%, -43%) rotate(-1deg);
}
75% {
transform: translate(-1%, -40%) rotate(-1deg);
}
}
a {
color: inherit;
}
`
const Powerups = ({
img,
text,
subtext,
fullName,
cost,
description,
fulfillmentDescription,
polaroidRotation,
ticketRotation,
extraTags,
...props
}) => {
const parsedFulfillmentDesc = fulfillmentDescription?.replace(
/\[(.*?)\]\((.*?)\)/g,
'$1'
)
return (
{text}
{subtext}
{cost} {cost === 1 ? 'ticket' : 'tickets'}
{extraTags?.map((tag, i) => {
if (tag === 'Limited Supply') {
return (
Limited!
)
}
})}
{
;(
document.getElementById(`${text}-info`) as HTMLDialogElement
).showModal()
}}
>
📦
)
}
const Intro = ({ title, num, text, img, third, ...props }) => {
return (
{title}
{text}
{num}
)
}
const Tickets = ({ title, num, text, link, bugEater, ...props }) => {
return (
{title}
{text}
{bugEater && (
<>
Click me for ideas!
Click me for ideas!
>
)}
)
}
const Sticker = ({ st }) => {
return (
{startCase(st.replace(/\.(svg|png)/, ''))}
)
}
const Item = ({ name, img, cost }) => {
return (
{cost}h
)
}
const FAQ = ({ question, answer }) => {
const parsedAnswer = answer?.replace(
/\[(.*?)\]\((.*?)\)/g,
'$1'
)
return (
{question}
)
}
async function generateProjectIdea() {
if (
document
.querySelector('#generate-project-idea')
.classList.contains('disabled')
) {
return
}
;(
document.querySelector('#generate-project-idea') as HTMLElement
).style.marginTop = '0px'
;(document.querySelector('#console') as HTMLElement).style.marginTop = '-50px'
;(document.querySelector('#console2') as HTMLElement).style.opacity = '0'
;(document.querySelector('#project-idea') as HTMLElement).style.opacity = '1'
document.querySelector('#project-idea').innerHTML =
'Arcade has ended! Thanks for playing.'
}
const Arcade = ({ stickers = [], carousel = [], highlightedItems = [] }) => {
const [showComponent, setShowComponent] = useState(false)
const [showNum, setNum] = useState(0)
const [showForm, setForm] = useState(false)
const [formSent, setFormSent] = useState(false)
const [isRevealed, setIsRevealed] = useState(false)
const router = useRouter()
const { query } = router
const slack = query.param
const generateRandomNumber = () => {
const newRandomNumber = Math.floor(Math.random() * stickers.length) // Generate a random number between 0 and 99
setNum(newRandomNumber)
}
const handleMouseEnter = () => {
setShowComponent(true)
}
const handleMouseLeave = () => {
setShowComponent(false)
}
const mouseEnter = () => {
handleMouseEnter()
generateRandomNumber()
}
const [pageIsVisible, setPageIsVisible] = useState(true)
const handleVisibilityChange = isVisible => {
setPageIsVisible(isVisible)
}
return (
<>
{slack === 'slack' ? (
) : (
<>>
)}
ARCADE
{/*
Build something cool.
*/}
The summer is yours for the making
{/* Get free tools
& build something cool */}
{/* Get free tools to build something cool. */}
{/* */}
{/* This summer is yours. */}
{/*
This summer is yours.
*/}
{showForm ? (
<>>
) : (
The Arcade closed September 1st, but you can still join the{' '}
Hack Club Slack
!
)}
{/*
Calling high school makers: Join{' '}
ARCADE
.
*/}
{/* What are you waiting for? */}
How to play
{/*
Hack. Rinse. Repeat.
*/}
Join the{' '}
Hack Club Slack
{' '}
and use /hack in #arcade to log your hours! You earn a
ticket for each hour spent!
>
}
num="2"
img="/arcade/o1.png"
/>
Make stuff. Get stuff. Repeat.
{pageIsVisible && (
{() => (
{carousel.map((item, i) => (
))}
)}
)}
{/* */}
Get{' '}
{showComponent && }
free stickers
{' '}
and code with other high schoolers!
One hour at a time,
What will{' '}
you
{' '}
make this summer?
Any technical project counts. You could build an AR game,
pixel art display, drawing robot, and more! Anytime you work
on your project, start the hack hour timer. You earn a
ticket for every hour you spend on your project.
Don't know where to start?
Boba drops:
{' '}
Build a website, get boba!
Wizard Orpheus:
{' '}
Build a text-based game with AI
The Bin:
{' '}
Build an online circuit, get the parts for free!
Sprig:
{' '}
Build a JS game, play it on your own console
OnBoard:
{' '}
Design a PCB, get a $100 grant
Hackaccino:
{' '}
Build a 3D Website and get a free frappuccino! ☕
Blot: Write code.
Make art. Get a drawing machine.
Cider: Make a
mobile app, get an Apple Developer account
Easel:
{' '}
Write a programming language, receive fudge!
>
}
num="Infinite"
sx={{
gridColumn: ['', 'span 2', 'span 2', 'span 2'],
h1: {
fontSize: [3, 4, 5]
},
p: {
fontSize: [2, 2, 3],
display: 'block',
pb: 2
},
minHeight: ['700px', '700px', '700px', 'auto']
}}
/>
Click me for ideas!>}
sx={{
'&ul>li': {
color: 'inherit'
},
gridColumn: ['span 2', 'span 2', 'span 2', 'span 1'],
minHeight: 'auto'
}}
/>
Prizes
{' '}
to powerup your next project!
Redeem these with your tickets! For high schoolers (or younger)
only.
{/*
All physical items only fulfillable where Amazon can be shipped
unless otherwise stated.
*/}
{highlightedItems.map((item, i) => (
0.5 ? 1 : -1)
}
key={i}
/>
))}
See all prizes!
This is just a{' '}
sneak peek
...new items will be added over the summer!{' '}
F.A.Q.
{/* */}
Join{' '}
ARCADE
.
Build real projects. Share it with friends.
>
)
}
export default Arcade
export async function getStaticProps() {
const stickersDir = path.join(process.cwd(), 'public', 'stickers')
const stickers = fs
.readdirSync(stickersDir)
.filter(sticker => sticker !== 'hero.jpg')
let carousel = []
let highlightedItems = []
// Only fetch shop data if API key is available
if (process.env.AIRTABLE_API_KEY) {
try {
const items = await shopParts()
carousel = items
.map(record => ({
hours: record['Cost Hours'] || 0,
imageURL: record['Image URL'] || '',
enabledCarousel: record['Enabled Carousel'] || false
}))
.filter(item => item.enabledCarousel)
.filter(item => item.imageURL !== '')
highlightedItems = items
.filter(item => item['Enabled Highlight'])
.sort((_a, _b) => Math.random() - 0.5 > 0)
.map(record => ({
'Image URL': record['Image URL'] || null,
Name: record['Name'] || null,
'Small Name': record['Small Name'] || null,
'Full Name': record['Full Name'] || null,
'Cost Hours': record['Cost Hours'] || null,
Description: record['Description'] || null,
'Fulfillment Description': record['Fulfillment Description'] || null,
'Extra tags': record['Extra tags'] || []
}))
} catch (error) {
console.error('Airtable fetch failed:', error)
}
} else {
console.log('AIRTABLE_API_KEY not defined, using empty shop data')
}
return {
props: {
stickers,
highlightedItems,
carousel
}
}
}
================================================
FILE: pages/bin/gallery.tsx
================================================
/** @jsxImportSource theme-ui */
import BinPost from '../../components/bin/GalleryPosts'
import styles from '../../public/bin/style/gallery.module.css'
import Script from 'next/script'
import Nav from '../../components/bin/nav'
import Footer from '../../components/footer'
import PartTag from '../../components/bin/PartTag'
import { useEffect, useState } from 'react'
export async function getStaticProps() {
const host =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: 'https://hackclub.com'
const res = await fetch(`${host}/api/bin/gallery/posts/`)
const posts = await res.json()
const filteredPosts = Array.isArray(posts)
? posts.filter(
post => post.status === 'Accepted' && post.parts && !post.hide
)
: []
//Tags
const resTag = await fetch(`${host}/api/bin/gallery/tags/`)
const tags = await resTag.json()
const filteredTags = Array.isArray(tags) ? tags.filter(tag => !tag.hide) : []
return {
props: { posts: filteredPosts, tags: filteredTags }
}
}
function Gallery({ posts = [], tags = [] }) {
const [allPosts, setAllPosts] = useState([])
const [filterPosts, setFilterPosts] = useState([])
const [filterParts, setFilterParts] = useState([])
useEffect(() => {
setAllPosts(posts)
setFilterParts([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setFilterPosts(
allPosts.filter(
post =>
post.parts && filterParts.every(part => post.parts.includes(part))
)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterParts])
const addFilter = partID => {
setFilterParts(prevParts => {
if (!prevParts.includes(partID)) {
return [...prevParts, partID]
}
return prevParts
})
}
const removeFilter = partID => {
setFilterParts(prevParts => {
return prevParts.filter(id => id !== partID)
})
}
return (
Bin Gallery
A display of all of bin's projects
{' '}
Want to add to the gallery? (window.location.href = '/bin')}
>
Create a bin project in wokwi{' '}
and your project will be added to the gallery!
Search By Tag:
{tags.map(tag => {
return (
)
})}
{filterPosts.map(post => {
return (
)
})}
)
}
export default Gallery
================================================
FILE: pages/bin/prelaunch.tsx
================================================
/** @jsxImportSource theme-ui */
import {
Box,
Container,
Text,
Heading,
Card,
Flex,
Image,
Link,
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 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(() => {
const fetchRsvpCount = 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'
let results = 0
try {
results = await fetch(url).then(r => r.json())
} catch (e) {
console.error('Failed to fetch bin RSVP count', e)
}
setRsvpCount(results)
}
fetchRsvpCount()
}, [])
if (rsvpCount < targetRSVPs) {
return {targetRSVPs - rsvpCount} RSVPs until the start of...
} else {
return {rsvpCount} have already RSVP'd to...
}
}
const OnboardCount = () => {
const [onboardCount, setOnboardCount] = useState(200)
useEffect(() => {
const fetchOnboardCount = async () => {
const url = '/api/onboard/p/count'
const results = await fetch(url).then(r => r.json())
setOnboardCount(results.count)
}
fetchOnboardCount()
}, [])
return {onboardCount}
}
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 }
})
const fadeUp = keyframes({
'0%': { transform: 'translateY(30px)', opacity: 0 },
'100%': { transform: 'translateY(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(Date.now() - 1) }) => {
console.log(expirationDate, new Date())
if (expirationDate.getTime() > Date.now()) {
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 (
<>
{
fireConfetti()
crunch()
spinIt(e.target)
}}
sx={{
cursor: 'pointer',
':active': {
animation: `${bounce} 0.125s`
},
'&.spin': {
animation: `${spin} 0.25s`
}
}}
/>
Build{' '}
{' '}
with parts you pick out.
Free for high schoolers.
{/* with all the parts bought for you */}
{/* An electronics starter kit, customized for your project */}
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. */}
RSVP to get notified when orders open.
.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,{' '}
oh my!
oh my!
Rummage
Dig through the bin to get a randomly generated set of parts
(or you can choose your own). For example...
Think!
With your parts picked out, what will you make? A
portable disco party? A flashlight that only turns on in the
daytime?
Prototype
Turn your idea into something almost real: simulate your
project in an online editor for beginners.
wokwi.com
Build it!
If it works in simulation,{' '}
we’ll send you the parts to build it in real life.Turn some trash into your treasure.
>
)
}
================================================
FILE: pages/brand.tsx
================================================
import {
Box,
Card,
Container,
Flex,
Grid,
Heading,
Image as ThemeImage,
Input,
Link as A,
Text
} from 'theme-ui'
import theme from '@hackclub/theme'
import Meta from '@hackclub/meta'
import Icon from '@hackclub/icons'
import Head from 'next/head'
import ForceTheme from '../components/force-theme'
import Nav from '../components/nav'
import Footer from '../components/footer'
import { startCase } from 'lodash'
import { AButton } from '../components/AButton'
import Image from 'next/image'
export const Logo = ({ name }: { name: string }) => (
{startCase(name)
.replace('Flag Orpheus', 'Orpheus Flag –')
.replace('Bw', ' (B/W)')
.replace('Hcb', 'HCB')}
SVG
PNG
PDF
)
const HTML = ({ file, html }) => (
Hack Club must always be written as Hack Club, not hackclub / Hackclub
/ HackClub / hackClub
Same with Hack Clubber or Hack Clubbers. It's never hackclubbers or
Hackclubbers
Important / should not be missable by anyone who is designing a
sticker: All sticker designs must have the text Hack Club somewhere on
the design. It can be subtle, but "Hack Club" must appear somewhere on
the design
Logos
{[
'flag-orpheus-top',
'flag-orpheus-left',
'flag-standalone',
'flag-orpheus-top-bw',
'flag-orpheus-left-bw',
'flag-standalone-bw',
'flag-standalone-wtransparent',
'icon-rounded',
'icon-square',
'icon-progress-rounded',
'icon-progress-square'
].map(key => (
))}
Download all →
HCB Logos
See all HCB logos →
HTML banners
Preview
HTML code
`}
/>
`}
/>
`}
/>
React component →
Colors
{[
'red',
'orange',
'yellow',
'green',
'cyan',
'blue',
'purple',
'muted'
].map(key => (
))}
FontsPhantom Sans
is our brand font.
Webfont CSS (for HQ sites only)
{css}
Icons
We have a custom iconset, published as{' '}
@hackclub/icons.
{[
'clubs',
'bank-circle',
'event-code',
'home',
'transactions',
'bolt',
'photo',
'emoji'
].map(k => (
))}
Explore Hack Club Icons →
UI components
Want to make a Hack Club themed site? Use our pre-made CSS and UI
components to hackify your site.
Explore Hack Club Theme →
Theme Starter on GitHub →
CSS Theme on GitHub →
>
)
export default Page
export const getStaticProps = () => {
const fs = require('fs')
const css = fs.readFileSync(
'./node_modules/@hackclub/theme/fonts/reg-ital-bold.css',
'utf8'
)
return { props: { css } }
}
================================================
FILE: pages/clubs.tsx
================================================
import {
Badge,
Box,
Button,
Card,
Container,
Grid,
Heading,
Link,
Text
} from 'theme-ui'
import styled from '@emotion/styled'
import Head from 'next/head'
import NextLink from 'next/link'
import Meta from '@hackclub/meta'
import Nav from '../components/nav'
import Icon from '../components/icon'
import BGImg from '../components/background-image'
import ForceTheme from '../components/force-theme'
import SlideDown from '../components/slide-down'
import FadeIn from '../components/fade-in'
import Footer from '../components/footer'
import AssembleImgFile from '../public/home/assemble.jpg'
import Slack from '../components/slack'
import Stage from '../components/stage'
import { AButton } from '../components/AButton'
import type { ReactNode } from 'react'
let Highlight = styled(Text)`
color: inherit;
border-radius: 1em 0 1em 0;
background: linear-gradient(
-100deg,
rgba(250, 247, 133, 0.33),
rgba(250, 247, 133, 0.66) 95%,
rgba(250, 247, 133, 0.1)
);
`
Highlight = Highlight.withComponent('mark')
const CardAsAny = Card as any
const Feature = ({
icon = 'star',
color = 'blue',
name = '',
desc,
children = null,
sx = {},
...props
}: {
icon?: string
color?: string
name?: string
desc?: ReactNode
children?: ReactNode
sx?: any
[key: string]: any
} = {}) => (
{children || (
)}
{name}
{desc}
)
const Page = () => (
<>
Don’t run your coding club alone.{' '}
Make it a{' '}
Hack Club
.
Hack Club is a nonprofit network of high school computer
science clubs and makers around the world.
Apply now
t.util.gx('green', 'blue'),
ml: [0, 3],
mt: [3, 0]
}}
>
Join the Slack
Hackers at Assemble in SF
The rundown
Clubs discovering the{' '}
joy of code
.
Hack Clubs typically meet for 1 hour each week in high schools,
makerspaces, community centers, churches, and any other venue where
teenagers can gather. As a club leader, you get members (mostly
beginners) started on something to learn/create, then members work at
their own pace, building websites, apps, & games, and presenting them
at the end.
{/* */}
1
A group of teens, many beginners, gather to start coding.
The leader (that’s you!) presents for a few minutes, getting the
group started building something new.
2
Everyone gets hacking, individually. Not hacking
bank accounts, but rather being creative and{' '}
making something awesome.
3
To end, everyone demos their work.
As a leader, you’re cultivating a community of makers. Each member
showing off their work builds momentum & motivation.
Go beyond club meetings.
Hack Clubs attend and run{' '}
hackathons like{' '}
Daydream &{' '}
Scrapyard, take part
in year long programs like{' '}
Blueprint
, and compete in events like the{' '}
Congressional App Challenge
. The hack’s the limit.
~ Welcome to the club ~
By the students, for the students.
Learning to code is like gaining a superpower — turning you from a
consumer of technology into a creator. It shouldn’t be taught like a
class — it should be a creative, inclusive space. To foster this
environment,{' '}
every Hack Club is student-led &
members make self-directed projects.
Hit the ground running
Get your club{' '}
going & growing
with Hack Club.
In our{' '}
Slack community,
you’ll be invited to a space for Hack Club leaders to ask
questions & chat, share projects, & attend weekly live events.
>
}
/>
We build tools, such as{' '}
Sprig, that your
members can use to make projects with in meetings! Build more of
them with us in our{' '}
Slack community.
>
}
>
Come prepared to every meeting with over 100{' '}
workshops (3 years’
worth!) and 19 Jams that
guide your club members through making fun, creative projects.
>
}
>
{/*
Need help getting started? Watch real club leaders run meetings, and
learn how to run them in your own club, with{' '}
Hack Club Meetings.
>
}
>
*/}
Get amazing stickers and posters
for marketing your club shipped directly to you & your club
members.
>
}
color="purple"
icon="sticker"
>
Use our 501(c)(3) status and a restricted fund with{' '}
HCB to fundraise, accept
donations, and buy things!
>
}
/>
From{' '}
Lock-in calls{' '}
to AMAs
{' to '}
weirder events
, the Slack community has live events for leaders & members
alike every week.
>
}
icon="event-code"
color="blue"
>
We're always building new tools for leaders, such as the{' '}
Leaders Portal! A
place to manage your club! We've also got free subscriptions to
Brilliant Premium, Code Crafters, and more for running a great
computer science club.
>
}
/>
When established Computer Science clubs join, they get all the
Hack Club benefits: Zoom Pro, stickers, our Slack
community, workshops
, the works. They’re welcome to use the “Hack Club” name or
keep their existing one.
>
}
as="aside"
sx={{
display: 'grid',
gridGap: [0, 4],
gridTemplateColumns: [null, 'auto 1fr'],
alignItems: 'start',
justifyContent: 'start',
bg: 'rgba(255,88,88,0.125)',
p: [3, 4],
mt: [3, 4],
borderRadius: 'extra',
span: { transform: 'none', width: 'min-intrinsic' },
svg: { color: 'white' }
}}
/>
Next steps
Apply today to{' '}
start your club
.
It’s all-online, free, & takes under an hour. We’ll help from there!
a, > div': {
borderRadius: 'extra',
boxShadow: 'elevated',
px: [3, null, 4],
py: [4, null, 5]
},
span: {
boxShadow:
'-2px -2px 6px rgba(255,255,255,0.125), inset 2px 2px 6px rgba(0,0,0,0.1), 2px 2px 8px rgba(0,0,0,0.0625)'
},
svg: { fill: 'currentColor' }
}}
>
>
)
export default Page
================================================
FILE: pages/content/covid19.mdx
================================================
import Letterhead from '../../components/letterhead'
This is a very difficult message for me to write and I want to get straight to the point: **Hack Club HQ is officially recommending postponing all events slated to happen in March, April, and May**.
- If you are running a hackathon in March, April, or May, we are officially and strongly recommending that you work with your team to postpone it ASAP.
- Over the next few days, you and your team should identify a new month for your event. We are recommending August at the earliest.
- By early next week, the first emails to your venue, sponsors, and attendees should go out. We are providing language to help with these emails.
We have shared resources with organizers to support communicating postponement and making sure sponsors don't withdraw their support. All of Hack Club's staff will also be on call in the Slack to support organizers through this difficult moment.
COVID-19 has passed the point of containment and now **necessitates decisive action on all levels**—including communities like Hack Club. While most who contract it become only mildly ill, it is extremely deadly for the elderly and people with chronic health problems. For young people like us who are mostly low risk, we can still contract and spread it, which threatens those who are at higher risk. All of us have a stake in this and need to be part of the solution.
Our hearts break with you in this moment. Hackathons changed my life and they have changed many, many lives in this community. Each of you has put in a superhuman effort to make your events happen. We know firsthand how devastating postponement is, but ultimately it’s the right thing to do in this situation. The world needs leadership from people exactly like all of you right now.
We are crossing our fingers that the spread of COVID-19 will slow down in the summer months, which is why we are limiting our recommendation to events happening through May. We will revisit our advice and provide updates as needed in the months to come.
My inbox is open. Please do not hesitate to reach out to me via email ([zach@hackclub.com](mailto:zach@hackclub.com)) or on Slack ([@zrl](https://app.slack.com/team/U0266FRGP)) if I can be of support. I also encourage everyone to make their thinking around this public in the Slack so we can all learn and act together. You can join at https://slack.hackclub.com if you're not already on it.
Best,
Zach Latta ([@zrl](https://app.slack.com/team/U0266FRGP))
## Some suggested reading
- https://twitter.com/MarkJHandley/status/1237119688578138112
- https://docs.google.com/presentation/u/3/d/1uG8g8AW4PEevKSESfnXUaKgH-bi8O-4PVOHBcdPMp9A/edit
- https://www.facebook.com/gweninvestor/posts/10158147740444169
- https://slatestarcodex.com/2020/03/02/coronavirus-links-speculation-open-thread/
[](https://twitter.com/MarkJHandley/status/1237119688578138112)
================================================
FILE: pages/content/it-admins.mdx
================================================
import Letterhead from '../../components/letterhead'
import Announcement from '../../components/announcement'
If you are reading this, you are probably an IT administrator at your organization. **Here's a quick overview of what we need from you.**
Please whitelist or unblock the following domains:
- `*.hackclub.com` - Hack Club's primary domain, hosts workshops, tools and other content
- `hack.af` - link shortener used for miscellanous links
- `*.replit.com`, `*.cdn.replit.com`, and `*.repl.co` - Replit, a browser-based IDE used for workshops
- `*.github.com`, `github.dev` - GitHub, an industry standard for code hosting and collaboration, owned by Microsoft.
- `hackclub.slack.com` - Hack Club's primary communication platform
Feel free to read on for more details and potential alternatives.
## Hack Club Sites
If you are unable to whitelist all subdomains, please whitelist the following:
- `hackclub.com`
- `workshops.hackclub.com`
- `jams.hackclub.com`
- `sprig.hackclub.com`
## GitHub
Besides the typical HTTP(S) ports, ports 22 and 9418 need to be open for SSH and Git respectively.
## Inspect Element / Developer Tools
Browser developer tools (eg. Inspect Element) are required for web development projects, so that sites can be debugged and styled.
## Replit
Replit provides their own [toolkit](https://docs.replit.com/teams-edu/it-administrators-toolkit) for IT administrators with further options for providing student access to Replit, including a firewalled domain.
Thank you for supporting your students and their interests. If you have any questions, please reach out to us at [clubs@hackclub.com](mailto:clubs@hackclub.com) and we'd be happy to address your concerns.
================================================
FILE: pages/content/sponsorship.mdx
================================================
import Letterhead from '../../components/letterhead'
import Announcement from '../../components/announcement'
_(To summarize: we’re a donor-supported nonprofit so outside of planned grant programs, we don't provide financial support to hackathons/other nonprofits. However, there are all kinds of ways we’d love to help out your project, so read on.)_
[Hack Club](https://hackclub.com/) is an independent nonprofit, supported by donors, with a responsibility to our supporters to spend our budget directly on our core programs. Therefore, as much as we’d like to, **we’re not in a position to provide financial support to hackathons/other nonprofits**. We occasionally run grant programs, like the [$500 grant for IRL high school hackathons](https://hackclub.com/hackathons/grant) which has ended as of December 31st, 2024. We wish you the best of luck in your sponsorship search—we recommend [Megan Cui’s “Meginar”](https://youtu.be/tOmXzA4reTY) and [Lachlan Campbell’s Flagship talk](https://notebook.lachlanjc.com/2020-01-19_how_to_start_your_first_hackathon/) if you’re looking for advice.
If you’re looking for [fiscal sponsorship](https://en.wikipedia.org/wiki/Fiscal_sponsorship) (aka becoming a legal nonprofit), we’ve got your back—[HCB](https://hackclub.com/fiscal-sponsorship/) will set you up with a nonprofit fund, legal entity, debit cards for your team, automated taxes/accounting, G Suite, an online donation form, ACH/check sending/receiving, discounts on stickers & software for your team, and great support. There are no upfront fees. Sign up at [https://hackclub.com/fiscal-sponsorship/](https://hackclub.com/fiscal-sponsorship/).
## If you’re running a hackathon…
1. Link your event on [https://hackathons.hackclub.com/](https://hackathons.hackclub.com/). There’s an application form on the website. The site has the largest email list of high school hackathon-goers, and will email all the subscribers who live within driving distance of your event. It’s also the first Google result for “high school hackathons,” so it’ll help your SEO.
2. We’d be happy to send some Hack Club stickers to your event. Email us [team@hackclub.com](mailto:team@hackclub.com) with the name of your event, expected attendance, and the shipping address and we’ll get them in the mail.
3. [Join our Slack community](https://slack.hackclub.com), and ask for access to the `#hackathon-organizers` channel. You can ask questions and talk to 500+ hackathon organizers from all over.
## If you’re running a club…
We’re a network of [hundreds](https://hackclub.com/map/) of high school clubs around the world. We provide mentorship/coaching, stickers, curriculum, Zoom Pro access for virtual meetings/events, and more. [Learn more here.](https://hackclub.com/clubs/)
---
Thanks for your interest, and we’re sorry not to be able to make donations. If you’ve got any questions, you can reach us here: [team@hackclub.com](mailto:team@hackclub.com). Best of luck with your project!
================================================
FILE: pages/content/sunsetting-som.mdx
================================================
import Letterhead from '../../components/letterhead'
import Link from 'next/link'
import { memo, useState, useEffect } from 'react'
import StaticMention from '../../components/mention'
_Sam Poder proposed the initial idea for Summer of Making, led logistics for Hardware Party, and is a 15-year-old Hack Club leader from Singapore._
**Make something amazing this summer.** At its core, that’s what [Summer of Making](https://summer.hackclub.com/landing) was. We were there to support Hack Clubbers and it was all of you who made this summer special. We set a challenge and teen hackers from 129 countries took it on!
We partnered with GitHub to fund $50,000 in hardware projects. With GitHub providing the financial backing and both Adafruit and Arduino providing discounts and community support, we funded hundreds of hardware projects, making 1,950 electronics purchases on behalf of 268 makers, the average age of whom was 16. You can see many of [their journeys on Scrapbook](http://scrapbook.hackclub.com/r/hardware).
Did someone mention Scrapbook? 414 Hack Clubbers shared 5,628 unique creations on [Scrapbook](https://scrapbook.hackclub.com/), a place for sharing project updates with the Hack Club community. It kept Snapchat-like streaks, and posted for 74 consecutive days! The most common post time was 11 PM. (If you’re interested, [wrote about how Scrapbook works](http://notebook.lachlanjc.com/2020-07-30_how_scrapbook_works/).)
Beyond Scrapbook & our hardware grants, so much more happened this summer. We ran [AMAs](https://hackclub.com/amas/)—[chatting](https://youtu.be/DvROZ-OBszU) [with](https://www.youtube.com/watch?v=IWFtj9cCaB0) [six](https://www.youtube.com/watch?v=fDKYjX37cbo) [absolutely](https://www.youtube.com/watch?v=tDtBCcLJ2xU&list=PLbNbddgD-XxFFLLLaODUtif9v99eFXub3) [remarkable](https://youtu.be/thXsjHVcxx4) [guests](https://youtu.be/KKEYTSUvsS8)—the Scrapbook CSS contest, the first ever Hack Club CTF, Open Source Fiesta, a chess tournament, a scavenger hunt on Slack, and weekly [Hack Nights](https://hackclub.com/night/). Personally, I learned Next.js, MDX, & Theme UI to make [a recap site, which is now temporarily Hack Club’s homepage](https://hackclub.com/).

_One of my favourite projects: & ’s [Smart Album Cover](https://github.com/phultquist/smart-album-cover)_
### Today (Monday, August 31st) we’re drawing the Summer of Making to a close. Here’s what’s next.
## Hardware Party
Through the summer, we’ve grown a substantial community of hardware hackers on the Slack and we want to keep the spirit alive. **To celebrate, we’ll be shipping custom, Orpheus-themed [Arduino Leonardos](https://www.arduino.cc/en/Main/Arduino_BoardLeonardo), which we’ve named the [Orpheus Leap](https://github.com/kevin200617/Orpheus-Leap-Micro).** These are fully-functional Arduino clones, packed into a dino shape, shipped for free.

A few weeks ago, , a 9th grade Hack Clubber from Massachusetts, designed the board. Then found a factory in China, got a quote, and is now managing the pre-fab process to verify that the board works before we finalize the order and start final production. Since we’re manufacturing in China, it will be many months before the final boards are shipped to everyone who participated.
Last hardware thing: we’re hosting a show & tell with Adafruit & the wonderful [John Park](https://en.wikipedia.org/wiki/John_Edgar_Park). More details will be shared in the next few weeks.
## Slack - Some Clean-Up Items
Today we’ve archived all the SoM channels (those prefixed with `#som-`), to shift conversation into the standard Slack channels like `#lounge`, `#code`, and `#welcome`. We’ve kept `#hardware-party` and its affiliated channels open for now, since we’re still shipping out the final grants & want to continue support as folks complete their projects. In the future, we’ll redirect folks to `#hardware`.
A handful of folks are yet to be promoted. We’ll be finding many of you who are active on Scrapbook & promoting you, but message anyone on `@summer` if you need a promotion.
**New users:** We’ve redesigned the flow for joining the Slack. New users now join as multi-channel guests, added only to `#welcome` and a private channel where they’re introduced to the Slack. Once they complete the tutorial, they’re automatically promoted to full users & unlock the rest of the community.
**Stickers:** Everyone who filled out the form at [https://hack.af/som-stickers](https://hack.af/som-stickers) will receive stickers in the upcoming 4-8 weeks! Over 7,000 people from 122 countries requested stickers and we're still trying to figure out how to ship them all. Hang tight.
## Scrapbook from Anywhere
While originally Scrapbook was merely a summer project, shutting it down didn’t feel right. Thanks to , today we’re launching **“Scrapbook from Anywhere”**. When you post in a public channel with the Scrappy bot, add a `:scrappy:` emoji reaction & your message will be posted to your Scrapbook. We’ve added this to help Scrapbook mix with the rest of Slack more, and while the `#scrapbook` channel will remain as-is, it allows you to Scrapbook the projects you `#ship`, get feedback in `#design` or `#wip`, show off your photo in `#photography`, or share with your club without cross-posting.
[](https://scrapbook.hackclub.com/amogh)
---
In summary, thank you—to everyone who helped run parts of the Summer, who made a Scrapbook post, built something epic with a hardware grant, attended a live event or just dropped by the Slack. Back in April, I posted my original summer idea in `#hq`. It’s been an experience of a lifetime helping put it together.
With gratitude,
Sam Poder, the Summer of Making team, & Hack Club HQ
================================================
FILE: pages/content/transparency/may-2020.mdx
================================================
import Letterhead from '../../../components/letterhead'
In 2014, after dropping out of high school at 16 to become a programmer, I started Hack Club. I had so many questions. How would finances work? How did other organizations get donations? How did they budget and spend their money? How much does it cost to run a program that reaches 1,000 people? What is an appropriate monthly salary for an employee? How much do lawyers and CPAs cost?
For me, learning to program was largely possible because of open source: the code of so much software written by both professionals and hobbyists is available publicly on GitHub. When you see under the hood at how software is made, you learn yourself. But nonprofits don't work that way. They are enigmas to outsiders. While top-level information is available to the public via [IRS Form 990](https://en.wikipedia.org/wiki/Form_990) ([example](https://990s.foundationcenter.org/990_pdf_archive/946/946069890/946069890_201712_990.pdf)), the actual budgets and details of spending are closely guarded secrets—often not even donors, staff members, or board members are privy to how nonprofits spend their money.
That lack of transparency creates an ivory tower of nonprofits that is near-impossible enter as an outsider. Since it's impossible to learn from what other organizations are doing, this results in a world where you have to be born into a high-priest class to successfully start a nonprofit and receive major gifts.
We’d like to do things differently at Hack Club.
A totally transparent nonprofit not only shows others how to do it. It also increases accountability. This is a goal of the Liberman family, who have inspired and supported Hack Club's transparency goals over the last year. In 2019, Hack Club won the [Frank Prize](https://grant.frank.ly/) of $1M to support growing Hack Club's programs and to support Hack Club in increasing transparency. Two weeks ago, [we announced we were publicly opening Hack Club's bank account](https://hackclub.com/elon/).
While a bunch of transactions in a bank account is great, I want to summarize Hack Club's spending in May 2020 publicly. This is something that I wish other nonprofits did when Hack Club was first getting started.
The below summary was calculated from HQ's export from [HCB](https://hackclub.com/fiscal-sponsorship/). You can see our full current account at https://hcb.hackclub.com/hq/ using HCB's new [Transparency Mode](https://twitter.com/hackclub/status/1262471150963130374).
### Revenue (total $20,621.78)
- **Donations ($15,621.78)**
- $15,000 - Ron Conway
- $244.20 - Samuel Escapa
- $200 - Tim and Kate Clem
- $97.50 - Jake Brownson
- $20 - Jeffrey Owens
- $19.30 - David C Farnan-Williams
- $19.30 - Reid Workman
- $10 - Chaleb Pommells
- $9.48 - Tyler Hilliard
- $2 - Hamza bnr Bellucci
- **Consulting Income ($5,000)**
_This was a one-time project to advise Think Together on their September 2020 school re-opening plans._
- $5,000 - Think Together
Thank you to all the new donors to Hack Club this month. You make Hack Club possible. We rely on donations to keep Hack Club free for students. [Donate here.](https://hackclub.com/philanthropy/)
Please note that [Elon Musk also donated $500K this month](https://twitter.com/hackclub/status/1263153557945159680), but the gift didn't hit our account until June 3rd, so it will be included in the June finance update.
### Expenses ($61,729.35 - nearly entirely salaries)
- **People including taxes ($57,625.25)**
_For the first time, Hack Club staff is growing beyond 3 people. When we were still tiny, we all took essentially the same salary of $4K/month. Now that we're growing, we still need to figure out how we want to think about salaries as an organization._
- $13,333.33 - Christina Asquith - COO, leads donor side
- $9,962.34 - Net pay
- $3,370.99 - Employee taxes
- $1,020 - Employer taxes
- $6,666.67 - Chris Walker - Clubs
- $5,268.17 - Net pay
- $1,398.50 - Employee taxes
- $510 - Employer taxes
- $6,500 - Zach Latta - Executive Director
- No taxes pre-withdrawn due to 1099, net pay above
- $5,166.67 - Max Wofford - Clubs, working with Chris, recently owning HCB too
- No taxes pre-withdrawn due to 1099, net pay above
- $4,761.90 - Lachlan Campbell - Design, https://hackclub.com
- No taxes pre-withdrawn due to 1099, net pay above
- $4,425 - Hack Club India Team
- There are 117 Hack Clubs in India overseen by Athul and his team. We provide monthly funding to cover salaries for the team, an office, and operational expenses like travel and food.
- $4,166.67 - Matthew Stanciu - Clubs, working with Chris and Max
- No taxes pre-withdrawn due to 1099, net pay above
- $3,500 - Theo Bleier - Donor side, working with Christina
- No taxes pre-withdrawn due to 1099, net pay above
- $3,100 - Michael Destefanis - Runs operations on HCB
- $2,444.13 - Net pay
- $655.87 - Employee taxes
- $237.15 - Employer taxes
- $2,000 - Melody - Community and running [our Twitter](https://twitter.com/hackclub)
- Note: Melody is paid $4K/month. I've been paying them weekly out of my personal bank account and getting reimbursed by Hack Club. Not all the reimbursements for May have gone through yet, which is why this number is $2K instead of $4K.
- $1,642.86 - Sean Victory (filled in for Michael during medical leave, this was his final payment + $500 bonus)
- $495 - James Click (contractor on AMA video editing, ~$150/each)
- $100 - Sammi Brie (contractor on graphic design for Elon AMA)
- **Hardware Chris Purchased for Simone and future AMAs ($1,201.11)**
- $938.08 - Bass Pro
- $256.51 - Lowe's
- $6.52 - Amazon
- **Software ($1,121.31)**
- $260.25 - Rippling (payroll processor)
- $249.90 - Zoom
- $184.66 - Airtable
- $99 - Zapier
- $82.83 - Calendly
- $59.99 - LinkedIn Premium
- $45 - Figma
- $40 - QuickBooks
- $39 - Mountain Duck
- $26.99 - Dialpad
- $14.99 - Castr.io
- $9 - Unity
- $8.50 - DNSimple
- $1.20 - Mailgun
- **HCB ($515.25)**
- $207 - Expensify
- $181.60 - Earth Class Mail (mail processing service)
- $120 - DocuSign
- $6.65 - Lob (check printer)
- **Hosting ($464.81)**
- $425.12 - Heroku
- $18.50 - Compose.io
- $9.11 - Digital Ocean
- $6.54 - Linode
- $3.50 - Vultr
- $2.04 - Backblaze
- **Food ($434.71)**
- $409.78 - Groceries
- $24.93 - Staff lunch in Philly
- **Office ($287.46)**
- $207.79 - Standing whiteboard
- $42.39 - White noise machine
- $21.39 - Video call lighting supplies
- $15.89 - Kitchen supplies
- **News ($67.94)**
- $38.99 - Wall Street Journal
- $12.95 - Business Insider
- $12 - Stratechery
- $4 - New York Times
- **Minecraft Server ($59)**
- $59 - Wholesale Internet
- **Mail Team ($117.49)**
- $59.49 - Supplies
- $49 - ShipStation
- $9 - Aftership
- **Misc ($42.81)**
- $42.81 - Medicine
The above numbers are on a cash basis, meaning they only include transactions that hit our bank account or cards this month.
In some cases, there are expenses that we have committed to in May that have not yet hit our accounts, like rent for the space that the team is currently staying in. I've done my best to list all known missing transactions below:
- $500,000 donation from Elon Musk
- $5,000 in rent for May 17th - June 14th (4 weeks) for team in Vermont
- $1,800 paid to Melody for work done in April and March 2020 (I paid them at the time from my personal account and have not been reimbursed yet)
- $3,670 paid to Lewis Mudge for rent the 2nd 1/2 April and the first 1/2 of May (place Max and I stayed in). Similarly, I paid Lewis at the time from my personal account and have not yet been reimbursed.
- ~$100 gas for staff trip to Vermont
Please note: there may be errors in this post. While I have pulled the numbers directly from HCB, I did mess around a bit in a spreadsheet and have not double checked my work. I believe that all numbers below are approximately correct.
If you’re interested in seeing the Google Sheet I used to calculate the above numbers, you can see it at https://docs.google.com/spreadsheets/d/1UDw7YewsS5wJIVm0Uh5wOGlM2Ddv-kZVrD3QqIypvRQ/edit.
Please note that while the above encompasses all of HQ's spending in our HCB account, it does not include GitHub grants to clubs, postage bought by Mail Team, or grants made from our internal "Discretionary Fund" to students in need that is funded by Ron Conway.
_Thanks to Christina, Melody, and Lachlan for their help writing this post._
================================================
FILE: pages/deprecated/[deprecated].tsx
================================================
import { Box, Button, Container, Heading, Text } from 'theme-ui'
import Meta from '@hackclub/meta'
import Icon from '@hackclub/icons'
import Head from 'next/head'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
type DeprecatedPageProps = {
page: {
name: string
desc: string
icon: string
link: string
}
}
const DeprecatedPage = ({
page: { name, desc, icon, link }
}: DeprecatedPageProps) => (
<>
We no longer offer {name}.
{desc}
>
)
export default DeprecatedPage
const pages = {
cloud9: {
name: 'Cloud9',
desc: 'Cloud9 has been replaced by repl.it, a newer online IDE.',
link: 'https://repl.it/?ref=hackclub',
icon: 'terminal'
},
challenge: {
name: 'Challenge',
desc: 'If you miss Hack Club Challenge, check out Scrapbook!',
link: 'https://scrapbook.hackclub.com/',
icon: 'sticker'
},
tech_domains: {
name: '.TECH domains',
desc: 'If you’re looking for a domain, you can get one via the Hack Pack.',
link: 'https://hack.af/pack',
icon: 'web'
},
pack: {
name: 'the Hack Pack',
desc: 'GitHub still offers the GitHub Student Developer Pack through the standard application flow.',
link: 'https://education.github.com/pack',
icon: 'github'
}
}
export const getStaticPaths = () => {
const paths = Object.keys(pages).map(key => ({ params: { deprecated: key } }))
return { paths, fallback: false }
}
export const getStaticProps = ({ params }) => {
const page = pages[params.deprecated]
return { props: { page } }
}
================================================
FILE: pages/elon.tsx
================================================
import { Box, Container, Heading, Text } from 'theme-ui'
import {
PillHolder,
AuthorPill,
DatePill
} from '../components/announcements/pills'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from '../components/nav'
import ForceTheme from '../components/force-theme'
import Footer from '../components/footer'
import Elon from '../components/announcements/elon.mdx'
import SlackCTA from '../components/announcements/cta'
import AnnouncementHolder from '../components/announcements/holder'
const ElonPage = () => (
<>
t.util.gx('yellow', 'green')
}}
>
Hack Club “makes me feel much more optimistic{' '}
about the future.”
—Elon Musk
>
)
export default ElonPage
================================================
FILE: pages/events.tsx
================================================
import {
Box,
Button,
Container,
Heading,
Card,
Grid,
Flex,
Image as Img,
Link
} from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import ForceTheme from '../components/force-theme'
import Nav from '../components/nav'
import Footer from '../components/footer'
import Image from 'next/image'
import OuternetPic from '../public/outernet/hack.jpg'
import theme from '@hackclub/theme'
const events = [
{
name: 'Haunted House',
description: `Where Fright Meets Byte: A Haunted House Hackathon Experience in Downtown Chicago.`,
logo: 'https://emoji.slack-edge.com/T0266FRGM/hauntedhouse/427353c4bd656767.png',
location: 'Chicago, Illinois',
season: 'Fall',
year: '2023',
// repo: 'outernet',
image: 'https://cloud-6vo1bh2dg-hack-club-bot.vercel.app/0image.png',
link: 'https://haunted.hackclub.com'
},
{
name: 'Outernet',
description: `An out-of-doors, make-it-yours programming and camping adventure in Vermont's Northeast Kingdom.`,
logo: 'https://emoji.slack-edge.com/T0266FRGM/outernet/522a19d904a627e6.png',
location: 'Cabot, Vermont',
season: 'Summer',
year: '2023',
video: 'https://www.youtube.com/embed/O1s5HqSqKi0',
repo: 'outernet'
},
{
name: 'Epoch',
logo: `https://emoji.slack-edge.com/T0266FRGM/epoch/1337c0f7f3c8341d.png`,
description: `A magical New Year's spent hacking in New Delhi, our first flagship event abroad and in India.`,
location: 'Delhi NCR, India',
season: 'Winter',
year: '2022/23',
video: 'https://www.youtube.com/embed/KLx4NZZPzMc',
repo: 'epoch'
},
{
name: 'Assemble',
logo: 'https://emoji.slack-edge.com/T0266FRGM/assemble/4f9465eb00175463.png',
description:
'The first high school hackathon since the pandemic! Hosted by a team of Hack Clubbers to kick off a hackathon renaissance.',
location: 'San Francisco, California',
season: 'Summer',
year: '2022',
video: 'https://youtube.com/embed/PnK4gzO6S3Q',
repo: 'assemble'
},
{
name: 'The Hacker Zephyr',
logo: 'https://hackclub.com/stickers/zephyr.svg',
description:
'A cross-country hacker adventure on a train and the longest hackathon (by miles) on land.',
location: 'Burlington (VT) to Los Angeles (CA)',
season: 'Summer',
year: '2021',
video: 'https://youtube.com/embed/2BID8_pGuqA',
repo: 'the-hacker-zephyr'
},
{
name: 'Summer of Making',
logo: 'https://hackclub.com/stickers/summer_of_making.svg',
description:
'$50k in hardware donations to teen hackers around the world and the creation of Scrapbook:',
location: 'Online (thanks COVID-19!)',
season: 'Summer',
year: '2020',
image:
'https://cdn.sanity.io/images/2ejqxsnu/production/ed144128afb78a7095d6c77945efdd2c38078ecf-1637x990.png?w=3840&q=75&fit=clip&auto=format',
link: 'https://scrapbook.hackclub.com/r/summer-of-making',
ghTag: 'summer-of-making'
},
{
name: 'Flagship',
logo: 'https://hackclub.com/stickers/ship.png',
description:
'An IRL meetup of high school hackathon organizers and coding club leaders. Our first "flagship" event.',
location: 'San Francisco, California',
season: 'Summer',
year: '2019',
image:
'https://github.com/hackclub/www-assemble/blob/main/public/hackers-assemble.jpg?raw=true',
link: 'https://hack.af/flagship-album'
}
]
const Event = ({
name,
logo,
location,
season,
description,
year,
video,
repo,
ghTag,
image,
link
}) => (
{name}{description}
{video ? (
) : (
)}
{season}, {year} - {location}
{' '}
{' '}
{repo && (
<>github.com/hackclub/{repo}>
)}
{ghTag && (
<>github.com/topics/{ghTag}>
)}
{link && !repo && !ghTag && (
<>{link.replace('https://', '')}>
)}
)
const Page = () => (
<>
Hack Club's Events
Every summer and now every winter, Hack Club does something special
to bring the community together. Let's take a trip down memory lane.
{events.map((event, i) => (
))}
Looking for more? Hack Clubbers often organise their own hackathons!
Check them out at{' '}
hackathons.hackclub.com
. Hack Club is also behind a series of{' '}
Day of Service
{' '}
events and{' '}
frequent virtual events
.
>
)
export default Page
================================================
FILE: pages/fiscal-sponsorship/about.tsx
================================================
import { Box, Container, Flex, Link, Text } from 'theme-ui'
import { useEffect, useRef } from 'react'
import { keyframes } from '@emotion/react'
import FlexCol from '../../components/flex-col'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import Icon from '../../components/icon'
import Tilt from '../../components/tilt'
type BulletProps = {
glow?: boolean
icon: string
href?: string
children: React.ReactNode
}
function Bullet({ glow = true, icon, href, children }: BulletProps) {
const effectColours = [
'#ec3750',
'#ff8c37',
'#f1c40f',
'#33d6a6',
'#5bc0de',
'#338eda',
'#a633d6'
]
function keyframeGenerator(spread, blur, colours, opacity = 0.5) {
const hexOpacity = Math.max(Math.min(Math.round(opacity * 255), 255), 0)
.toString(16)
.padStart(2, '0')
const final = {}
for (let i = 0; i <= 100; i++) {
let baseX = Math.sin((i * Math.PI) / 50) // 50 keyframes for each pi radians
let baseY = -Math.cos((i * Math.PI) / 50)
// Ensure no scientific notation
const roundFactor = 1_000_000
baseX = Math.round(baseX * roundFactor) / roundFactor
baseY = Math.round(baseY * roundFactor) / roundFactor
let boxShadow = ''
for (let c = 0; c < colours.length; c++) {
// Rotate by 2pi / colours.length * c radians
const x =
baseX * Math.cos((2 * Math.PI * c) / colours.length) -
baseY * Math.sin((2 * Math.PI * c) / colours.length)
const y =
baseX * Math.sin((2 * Math.PI * c) / colours.length) +
baseY * Math.cos((2 * Math.PI * c) / colours.length)
boxShadow += `${x * spread}px ${y * spread}px ${blur}px ${
colours[c]
}${hexOpacity},`
}
// Remove trailing comma
boxShadow = boxShadow.slice(0, -1)
final[i + '%'] = { boxShadow }
}
return final
}
const shadowSpread = glow ? 5 : 0
const shadowBlur = glow ? 10 : 0
const animatedBoxShadow = keyframes(
keyframeGenerator(shadowSpread, shadowBlur, effectColours)
)
const borderWidth = '2px'
return (
{children}
{href && (
)}
)
}
function BulletBox({ padding = '2rem', children }) {
return (
{children}
)
}
function Section({ id, children }) {
return (
{children}
)
}
export default function FiscalSponsorship() {
const gridRef = useRef(null)
const glowRef = useRef(null)
const scrollPos = useRef(0)
const mousePos = useRef([0, 0])
const setGlowMaskPosition = () => {
const finalPos = [
-mousePos.current[0],
-mousePos.current[1] + scrollPos.current
]
glowRef.current.style.maskPosition = `${finalPos[0]}px ${finalPos[1]}px`
glowRef.current.style.WebkitMaskPosition = `${finalPos[0]}px ${finalPos[1]}px`
}
useEffect(() => {
const handleScroll = e => {
scrollPos.current = -window.scrollY / 10
gridRef.current.style.transform = `translateY(${scrollPos.current}px)`
setGlowMaskPosition()
}
const handleMouseMove = e => {
const x = e.clientX
const y = e.clientY
glowRef.current.style.left = x + 'px'
glowRef.current.style.top = y + 'px'
mousePos.current = [x, y]
setGlowMaskPosition()
}
window.addEventListener('scroll', handleScroll)
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])
return (
Fiscal Sponsorship | HCB
Fiscal sponsorship:
501(c)(3) nonprofit{' '}
status in just 24 hours
Organizing an event, project, or organization to serve the public good
or your community? Consider fiscal sponsorship before the pain of
paperwork distracts you from your goals.
Jump to:
Costs and perks of 501(c)(3) status
How fiscal sponsorship works
Requirements for fiscal sponsorship
HCB, the #1 fiscal sponsor
Why organizers go after 501(c)(3) status
Every year, 1.6 million nonprofits in the U.S. apply for and renew
501(c)(3) status through the IRS for charitable recognition and tax
exemption for their funding. It can take anywhere from 2-12 months
to hear a decision back from the IRS, and in general, nonprofit
organizers should be prepared for:
$3,000 in up-front costs, from
{' '}
forms{' '}
to state fees to support from legal counsel
The potential for the IRS to reject an application
Hiring bookkeepers and accountants to prepare taxes and
provide upkeep annually to stay in good standing
Closing costs averaging around $5,000 if you lose or
terminate status
Though it’s expensive and time consuming to apply, being a
legally-recognized 501(c)(3) nonprofit in the U.S., your organization
gains:
The ability to receive tax deductible donations from
sponsors.
Reduced taxable income for your U.S. supporters, which
incentivizes giving.
Exemption from U.S. federal income tax and unemployment tax.
Potential exemption from state income, sales, and employment taxes.
Potential for reduced rates on postage, marketing, advertising,
legal counsel, and more.
Unfortunately between the costs and time needed to organize a
nonprofit, many charitable initiatives are prevented from exiting an
idea phase or progressing at a pace originally hoped. Imagine how much
more valuable impact could happen on the world if these barriers
didn’t exist.
As it turns out, there’s an alternative route for startups,
student-led initiatives, or anyone looking to avoid a headache with
the IRS to obtain all the benefits of 501(c)(3) status. That’s where
fiscal sponsorship comes in.
Fiscal sponsorship?
By legally working with an existing nonprofit offering fiscal
sponsorship, projects and events can claim most of the legal
benefits of individual 501(c)(3) status. Piggy-backing off this
existing status, organizations also gain access to resources from
their fiscal sponsor like:
Bookkeeping and administration to ensure that all paperwork and
taxes are filed
Fully established HR and benefits, which can vary by the fiscal
sponsor
Waived responsibility to organize a board of directors
Fully transparent operational fees, typically ranging from 7-12%
that prevent you from paying typical operating costs.
The ability to terminate your fiscal sponsorship agreement and
file for separate tax-exempt status at any point.
If you’re brand new to nonprofit organizing or unsure where your
project will take you, fiscal sponsorship is a great tool to help
manage your finances and gauge whether becoming an independent
nonprofit down the line is practical or financially feasible.
Requirements for fiscal sponsorship
Depending on the fiscal sponsor you choose, requirements for working
together can vary. Fiscal sponsors generally ask that your
nonprofit’s goals be similar to theirs. They also usually ask that
your organization or event commits to remaining charitable in nature
and refrains from activities that may result in loss of 501(c)(3)
status.
HCB, the #1 fiscal sponsor
While many fiscal sponsors require that their partners relate to
their mission in similar ways, at HCB, we’ve built our
infrastructure to support hundreds of causes in all areas of
charitability.
Check out some of the resources we’ve built our fiscal sponsorship
foundation on:
A beautiful web interface to manage finances
Fee-free invoicing, ACH or check transfers, and reimbursements
A customizable and embeddable donations URL
An account & routing number to connect to external platforms, like
Shopify and GoFundMe
Perks like PVSA certification, newsletter software, and 1Password
credits
Looking for nonprofit status and not a religious or political
organization? We’d love to meet you and chat about working together.
Feel free to apply
here
or
email our team if you
have more questions about fiscal sponsorship!
At its core, Hack Club is a nonprofit encouraging students to learn
how to code by building and making cool things. HCB was built out by
teenagers at Hack Club and continues to be a real-world space
that high schoolers can contribute to every day.
)
}
================================================
FILE: pages/fiscal-sponsorship/climate/[region].tsx
================================================
import ClimateDirectory, {
regions,
fetchRawClimateOrganizations
} from './index'
import { map, find, kebabCase, startCase } from 'lodash'
const regionsWithIds = map(regions, region => ({
id: kebabCase(region.label),
...region
}))
export default function ClimateRegionalPage({ rawOrganizations, pageRegion }) {
return (
)
}
export const getStaticPaths = () => {
const paths = map(map(regionsWithIds, 'id'), id => ({
params: { region: `organizations-in-${id}` }
}))
return { paths, fallback: false }
}
export const getStaticProps = async ({ params }) => {
let { region } = params
region = find(regionsWithIds, ['id', region.replace('organizations-in-', '')])
const { fetchAllOrganizations } = await import('../../../lib/cached-hcb-orgs')
const total = await fetchAllOrganizations()
const orgs = total
.filter(org => org.climate)
.filter(org => org.location.continent === region.label)
return {
props: {
rawOrganizations: orgs,
pageRegion: region
},
revalidate: 60 // seconds
}
}
================================================
FILE: pages/fiscal-sponsorship/climate/index.tsx
================================================
import {
Badge as ThemeBadge,
Box,
Container,
Flex,
Grid,
Heading,
Input,
Image as ThemeImage
} from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../../components/force-theme'
import Nav from '../../../components/nav'
import Footer from '../../../components/footer'
import MSparkles from '../../../components/sparkles/money'
import { Text, Button, Card } from 'theme-ui'
import Icon from '@hackclub/icons'
import OrganizationCard, {
Badge
} from '../../../components/fiscal-sponsorship/directory/card'
import fuzzysort from 'fuzzysort'
import { useEffect, useState } from 'react'
/** @jsxImportSource theme-ui */
import NextLink from 'next/link'
import { kebabCase, intersection } from 'lodash'
import theme from '@hackclub/theme'
import Tooltip from '../../../components/fiscal-sponsorship/tooltip'
import { Organization } from '../../../lib/organization'
import Image from 'next/image'
const styles = `
html {
scroll-behavior: smooth;
}
`
export const badges = [
{
label: 'Transparent',
id: 'Transparent',
tooltip: 'Transparent',
color: 'purple',
icon: 'explore',
match: org => org.isTransparent
}
]
export function getBadgesForOrg(org: Organization): typeof badges {
return badges.filter(badge => badge.match?.(org))
}
export const tags = [
{
label: 'Climate',
id: 'Climate',
color: '#1eb36d',
match: (org: Organization) => true
},
{
label: 'Nonprofit',
id: 'Nonprofit',
color: 'blue',
match: (org: Organization) => true
}
]
export function getTagsForOrg(org: Organization): typeof tags {
return tags.filter(tag => tag.match?.(org))
}
export const regions = [
{
label: 'North America',
color: 'secondary',
iconColor: 'red',
icon: 'photo',
image:
'https://cloud-cberabu5z-hack-club-bot.vercel.app/3north_america.png',
ogImage: '/fiscal-sponsorship/climate/NorthAmerica.png'
},
{
label: 'South America',
color: 'secondary',
iconColor: 'orange',
icon: 'photo',
image:
'https://cloud-cberabu5z-hack-club-bot.vercel.app/4south_america.png',
ogImage: '/fiscal-sponsorship/climate/SouthAmerica.png'
},
{
label: 'Africa',
color: 'secondary',
iconColor: 'purple',
icon: 'explore',
image: 'https://cloud-cberabu5z-hack-club-bot.vercel.app/0africa.png',
ogImage: '/fiscal-sponsorship/climate/Africa.png'
},
{
label: 'Europe',
color: 'secondary',
iconColor: 'blue',
icon: 'explore',
image: 'https://cloud-oax3m4v0t-hack-club-bot.vercel.app/1europe.png',
ogImage: '/fiscal-sponsorship/climate/Europe.png'
},
{
label: 'Asia & Oceania',
color: 'secondary',
iconColor: 'green',
icon: 'explore',
image:
'https://cloud-oax3m4v0t-hack-club-bot.vercel.app/0asia___oceania.png',
ogImage: '/fiscal-sponsorship/climate/Asia+Oceania.png'
}
]
const FilterPanel = ({ filter, mobile }) => {
const [hiddenOnMobile, setHiddenOnMobile] = useState(mobile)
const setStateVariable = filter[0]
const currentSelections = filter[1]
const title = filter[2]
const baseData = filter[3]
if (!baseData?.length) return <>>
return (
<>
setHiddenOnMobile(!hiddenOnMobile)}
>
{mobile && 'FILTER BY '} {title}{' '}
{mobile && (hiddenOnMobile ? '▶︎' : '▼')}
setStateVariable([...baseData.map(x => x.id)])}
>
All
{baseData?.map((item, idx) => (
{
if (currentSelections.length === baseData.length) {
setStateVariable([item.id])
} else if (currentSelections.includes(item.id)) {
let temp = currentSelections
temp = temp.filter(selection => selection !== item.id)
if (temp.length === 0) {
setStateVariable([...baseData.map(x => x.id)])
} else {
setStateVariable(temp)
}
} else {
setStateVariable([...currentSelections, item.id])
}
}}
>
{item.image ? (
) : (
)}
{item.label}
))}
>
)
}
type RegionPanelProps = {
currentRegion: (typeof regions)[number] | null
mobile?: boolean
}
const RegionPanel = ({ currentRegion, mobile }: RegionPanelProps) => {
const [hiddenOnMobile, setHiddenOnMobile] = useState(mobile)
return (
<>
setHiddenOnMobile(!hiddenOnMobile)}
>
{mobile && 'FILTER BY '} REGION{' '}
{mobile && (hiddenOnMobile ? '▶︎' : '▼')}
All
{regions?.map((item, idx) => (
{item.image ? (
) : (
)}
{item.label}
))}
>
)
}
type FilteringProps = {
mobile?: boolean
region: (typeof regions)[number] | null
[key: string]: any
}
const Filtering = ({ mobile, region, ...props }: FilteringProps) => {
return (
<>
{Object.values(props).map((filter, i) => (
))}
>
)
}
export default function ClimatePage({ rawOrganizations, pageRegion }) {
const [searchValue, setSearchValue] = useState('')
// const [region, setRegion] = useState(pageRegion);
const region = pageRegion
// useEffect(() => {
// // history.pushState(null, null, `/fiscal-sponsorship/climate/organizations-in-${region.toLowerCase().split(' ').join('-')}`);
// }, [region]);
const [modalOrganization, setModalOrganization] = useState(null)
useEffect(() => {
const handle = e => {
if (e.key === 'Escape') {
closeModal()
}
}
window.addEventListener('keydown', handle)
return () => window.removeEventListener('keydown', handle)
})
let organizations = rawOrganizations
if (searchValue.length) {
const search = fuzzysort.go(searchValue, rawOrganizations, {
keys: ['name', 'description'],
threshold: -1000
})
organizations = search.map(({ obj }) => obj)
}
const [currentBadges, setBadges] = useState([...badges.map(x => x.id)])
const openModal = organization => {
setModalOrganization(organization)
}
const closeModal = () => {
setModalOrganization(null)
}
return (
)
}
export async function fetchRawOrganizations() {
let lastLength = 100
let total = []
let page = 1
while (lastLength >= 100) {
const json = await fetch(
'https://hcb.hackclub.com/api/v3/directory/organizations?per_page=100&page=' +
page
).then(res => res.json())
lastLength = json.length
page++
total = [...total, ...json]
}
return [
...total.filter(a => a.logo !== null),
...total.filter(a => a.logo === null)
]
}
export const getStaticProps = async () => {
const { fetchAllOrganizations } = await import('../../../lib/cached-hcb-orgs')
const total = await fetchAllOrganizations()
const rawOrganizations = [
...total.filter(a => a.logo !== null),
...total.filter(a => a.logo === null)
]
return {
props: { rawOrganizations },
revalidate: 60 // seconds
}
}
================================================
FILE: pages/fiscal-sponsorship/first.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Heading, Container, Text, Button, Badge } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import Icon from '../../components/icon'
import Features from '../../components/fiscal-sponsorship/first/features'
import Testimonials from '../../components/fiscal-sponsorship/first/testimonials'
import Start from '../../components/fiscal-sponsorship/first/start'
import theme from '@hackclub/theme'
import { Balancer } from 'react-wrap-balancer'
import { setCookie } from 'cookies-next'
import { useEffect } from 'react'
export default function First({ stats }) {
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const tubProgram = params.get('tub_program')
const referral = params.get('referral')
if (referral) {
setCookie('referral', referral)
setCookie('tub_program', 'GFGS')
} else if (tubProgram) {
setCookie('tub_program', tubProgram)
setCookie('referral', '')
}
}, [])
return (
<>
Financial Toolkit for FIRST Teams | HCB
The ultimate booster club for{' '}
theme.colors.blue,
WebkitTextStrokeWidth: '1px',
WebkitTextFillColor: theme => theme.colors.white,
textShadow: theme => `0 0 12px ${theme.colors.blue}`
}}
>
FRC, FTC, and FLL teams
.
Nonprofit statusReceive grantsDebit cards
Built by FIRST alumni for FIRST teams, HCB is a
comprehensive financial platform used by hundreds of clubs, teams
and hackathons.
>
)
}
export async function getStaticProps(context) {
const res = await fetch(`https://hcb.hackclub.com/stats`)
try {
const stats = await res.json()
return {
props: {
stats
},
revalidate: 60 * 60 // once an hour
}
} catch (e) {
return {
props: {
stats: {}
},
revalidate: 60 * 60 // once an hour
}
}
}
================================================
FILE: pages/fiscal-sponsorship/index.tsx
================================================
/** @jsxImportSource theme-ui */
import Meta from '@hackclub/meta'
import Head from 'next/head'
import Link from 'next/link'
import { Balancer } from 'react-wrap-balancer'
import {
Box,
Button,
Card,
Container,
Flex,
Grid,
Heading,
Link as UILink,
Text
} from 'theme-ui'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import Photo from '../../components/photo'
import Stat from '../../components/stat'
import ContactBanner from '../../components/fiscal-sponsorship/contact'
import Features from '../../components/fiscal-sponsorship/features'
import OuternetImgFile from '../../public/home/outernet-110.jpg'
import SignIn from '../../components/fiscal-sponsorship/sign-in'
import OrganizationSpotlight from '../../components/fiscal-sponsorship/organization-spotlight'
import { setCookie, getCookie } from 'cookies-next'
import { useEffect, useState } from 'react'
import { unfold } from '../../components/announcement'
import OpenSource from '../../components/fiscal-sponsorship/open-source'
import 'react-responsive-carousel/lib/styles/carousel.min.css'
import Sparkles from '../../components/sparkles'
import Icon from '../../components/icon'
import Image from 'next/image'
const organizations = [
{
id: 'org_MpJurQ',
name: 'Reboot',
description:
'Publishing techno-optimism, through newsletters, magazines, and events.',
slug: 'reboot',
location: { readable: 'Bay Area, CA, USA' },
logo: '/fiscal-sponsorship/reboot.png',
background_image: '/fiscal-sponsorship/reboot-bg.jpg'
},
{
id: 'org_AluOql',
name: 'Apocalypse',
description: "Canada's largest in-person high school hackathon.",
slug: 'apocalypse',
location: { readable: 'Toronto, Canada' },
logo: '/fiscal-sponsorship/apocalypse.png',
background_image: '/fiscal-sponsorship/apocalypse-bg.png'
},
{
id: 'org_BbVuWN',
name: 'Green Mountain Robotics',
description: 'Spreading STEM interest, one robot at a time.',
slug: 'green-mountain-robotics',
location: { readable: 'Chittenden County, VT, USA' },
logo: '/fiscal-sponsorship/green-mountain-robotics.png',
background_image: 'green-mountain-robotics-bg.png'
},
{
id: 'org_a29uVj',
name: 'Hack Club HQ',
description: 'This is us! We run our operations on HCB.',
slug: 'hq',
location: { readable: 'Vermont, USA' },
logo: '/fiscal-sponsorship/hq.png',
background_image: '/fiscal-sponsorship/hq-bg.jpg'
}
]
function MobileAppAlert() {
return (
New!
HCB Mobile is here!
Manage your HCB organizations on the go. Issue cards, view
transactions, and more!
)
}
export default function Page() {
const [hasReferral, setHasReferral] = useState(false)
const [mobileInstalls, setMobileInstalls] = useState(0)
useEffect(() => {
fetch('https://hcb.hackclub.com/stats')
.then(res => res.json())
.then(data => {
setMobileInstalls(data.mobile_installs)
})
}, [])
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const tubProgram = params.get('tub_program')
const referral = params.get('referral')
const referralCookie = getCookie('referral')
if (referral) {
setCookie('referral', referral)
setCookie('tub_program', 'GFGS')
} else if (tubProgram) {
setCookie('tub_program', tubProgram)
setCookie('referral', '')
}
setHasReferral(!!referral || !!referralCookie)
}, [])
return (
<>
The foundation of your nonprofit.
Start your nonprofit with{' '}
our fiscal sponsorship program, HCB: a 501(c)(3)
legal entity, bank account, automatic taxes & accounting, and
best-in-class software.
{hasReferral && (
Apply by April 16th using referral code (
{getCookie('referral')}) and get stickers + fiscal sponsorship
fees waived for the month of May.
*
)}
HCB in your pocket
The official mobile app lets you manage your organization's
finances, issue cards, and more!
See your organization's spending
Stay up to date on your organization's balance and
transactions.
Accept Tap to Pay donations
No extra hardware required! Tap any card against your phone.
Great for in-person fundraisers.
Issue, manage, and
add cards
You can directly add cards to Apple Wallet and
Google Wallet. No more forgetting your card!
Upload receipts the easy way
Quickly snap a photo or upload a file!
{mobileInstalls.toLocaleString()} installs
Powering nonprofits at every scale
{organizations.map(org => (
))}
One simple, transparent fee:
7% of revenue.
This fee goes directly to Hack Club's operations staff,
including teen interns working under mentors. This allows us to
deliver best-in-class software and support, grow sustainably,
while also providing paid career training for young people from
diverse backgrounds.
No legal fees.
No startup fees.
No transaction fees.
No card issuing fees.
No subscription fees.
No check deposit fees.
No credit card processing fees.
{/** removed for now
The fiscal sponsor of choice for the best funders.
{['ycjf.png', 'first.png'].map(file => (
))}
As{' '}
Hack Club
{' '}
grew, we needed a way to empower our members. We currently have
over 60,000 high schoolers involved in Hack Club with over 400
clubs around the world.
We started HCB in 2018 to support teen-led clubs and hackathons.
After showing it to our educational partners, we knew we had
tapped into something much larger. Today, HCB removes financial
and legal barriers for thousands doing good in their community.
As part of our commitment to the environment, funding for
HCB's operations and staff will never come from the{' '}
fossil fuel industry
.
>
)
}
================================================
FILE: pages/fiscal-sponsorship/mobile/index.tsx
================================================
import { Box, Container, Heading } from 'theme-ui'
import {
PillHolder,
AuthorPill,
DatePill
} from '../../../components/announcements/pills'
import Head from 'next/head'
import NextLink from 'next/link'
import styled from '@emotion/styled'
import theme from '../../../lib/theme'
import Meta from '@hackclub/meta'
import Nav from '../../../components/nav'
import ForceTheme from '../../../components/force-theme'
import Footer from '../../../components/footer'
import Copy from '../../../components/announcements/hcb-mobile.mdx'
import HCBCTA from '../../../components/announcements/hcb_cta'
import AnnouncementHolder from '../../../components/announcements/holder'
import Balancer from 'react-wrap-balancer'
const StyledLink = styled(NextLink)`
text-decoration: underline;
color: ${theme.colors.white};
`
const Link = props => {
const { href } = props
return {props.children}
}
const MobilePage = () => (
<>
t.util.gx('purple', 'orange')
}}
>
HCB Mobile is here!
Manage your HCB organizations on the go. Issue cards, view
transactions, and more!
>
)
export default MobilePage
================================================
FILE: pages/fiscal-sponsorship/open-source.tsx
================================================
import { Box, Container, Heading } from 'theme-ui'
import {
PillHolder,
AuthorPill,
DatePill
} from '../../components/announcements/pills'
import Head from 'next/head'
import NextLink from 'next/link'
import styled from '@emotion/styled'
import theme from '../../lib/theme'
import Meta from '@hackclub/meta'
import Nav from '../../components/nav'
import ForceTheme from '../../components/force-theme'
import Footer from '../../components/footer'
import Copy from '../../components/announcements/hcb-open-source.mdx'
import SlackCTA from '../../components/announcements/cta'
import AnnouncementHolder from '../../components/announcements/holder'
import Balancer from 'react-wrap-balancer'
const StyledLink = styled(NextLink)`
text-decoration: underline;
color: ${theme.colors.white};
`
const Link = props => {
const { href } = props
return {props.children}
}
const RelonPage = () => (
<>
t.util.gx('purple', 'orange')
}}
>
HCB is now open source!
Our fiscal sponsorship platform’s{' '}
codebase is now
publicly available under the AGPL license and we’re continuing to
encourage transparency amongst nonprofits.
>
)
export default RelonPage
================================================
FILE: pages/hackathons/grant.tsx
================================================
import { Box, Container, Flex, Grid, Heading } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import MSparkles from '../../components/sparkles/money'
import NextLink from 'next/link'
import { Link, Text, Button, Card } from 'theme-ui'
import Icon from '@hackclub/icons'
import { Zoom } from '../../components/react-reveal-compat'
/** @jsxImportSource theme-ui */
const styles = `
html {
scroll-behavior: smooth;
}
`
import type { glyphs } from '@hackclub/icons'
type RequirementProps = {
title: React.ReactNode
checkmark: keyof typeof glyphs
background: string
size: number
children: React.ReactNode
}
const Requirement = ({ title, children, checkmark, background, size }) => {
return (
{title}
{children}
)
}
const HackathonGrant = () => {
return (
<>
A{' '}
$500
{' '}
grant for your in-person hackathon.
This program ended December 31st, 2024.
Hack Club provided $500 grants (and waived{' '}
HCB
{' '}
fees) to in-person high school hackathons.
Want to attend a hackathon?
Check if your hackathon qualifies
Your hackathon should be free for all attendees
and meet the following requirements:
We want to bring back high schooler-led events around the world,
so we're only offering this grant for high school hackathons that
take place throughout 2024 (until December 31st).
This is not an annual program and has only been renewed until
the end of 2024.
By high schoolers, for high schoolers>}
checkmark="profile-fill"
background="https://icons.hackclub.com/api/icons/0x212025/glyph:profile.svg"
size={36}
>
To create a uniquely tailored high school hackathon, your
hackathon should be organized by high school students*. All
attendees should be 18 & under AND not full-time
college students.
Maximum of 1 college student is allowed on your organizing team.
Hacking is a social activity, and we're supporting hackathons that
bring hackers together IRL. We believe that fully IRL (not hybrid)
events allow organisers to maximize the unique hackathon
experience for attendees.
Your event must be at least 8 consecutive hours long to qualify
for the grant.
You will need to provide a scan of an email, contract, or an{' '}
MOU
{' '}
with your venue. Your scan should have the date of your hackathon
and address, contact details, and the specific commitment of your
venue.
If your venue is a school, attendance must not be limited to a
specific school or club.
We believe the best hackathons embody the hacker spirit by
building their own website. Complex or simple, beautiful or janky–
build your own instead of using nontechnical tools like Wix or
Devpost.
You will need to share a link to your website. Don't have a
domain? HCB provides a free domain. Check out this{' '}
guide on building hackathon websites
{' '}
or ask in{' '}
Slack
{' '}
if you need help.
You'll receive your grant through HCB, our financial platform for
hackathons, and spend it in the open with{' '}
Transparency Mode
. Sign up for{' '}
HCB
{' '}
before applying.
If you're unable to use HCB, we're unfortunately unable to
support you through this grant program.
If you'd like to list us on your site (optional), you can use the
logos found on the respective brand guides for{' '}
Hack Club
{'.'}
This program ended on December 31st, 2024.
Questions?
Reach out to hcb@hackclub.com
>
)
}
export default HackathonGrant
================================================
FILE: pages/hackathons/index.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Container } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import Recap from '../../components/hackathons/recap'
import Slack from '../../components/hackathons/features/slack'
import Landing from '../../components/hackathons/landing'
import Marketing from '../../components/hackathons/features/marketing'
import Overview from '../../components/hackathons/overview'
import ScrollingHackathons from '../../components/hackathons/scrolling-hackathons'
import KeepExploring from '../../components/hackathons/keep-exploring'
export default function Hackathons({ data }) {
return (
<>
>
)
}
export async function getStaticProps() {
let data: any
try {
const res = await fetch(
'https://hackathons.hackclub.com/api/events/upcoming'
)
if (res.ok) {
data = await res.json()
} else {
data = []
}
} catch (error) {
data = []
}
return {
props: {
data
},
revalidate: 10
}
}
================================================
FILE: pages/imprint.tsx
================================================
import { BaseStyles, Box, Container, Heading, Link, Text } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import Nav from '../components/nav'
import ForceTheme from '../components/force-theme'
import Footer from '../components/footer'
const Imprint = () => (
<>
Imprint (Legal Notice)
Legal notice and contact details for Hack Club (The Hack Foundation).
Operator p': { maxWidth: 'copy' },
h2: { variant: 'text.headline', mt: 4 }
}}
>
The Hack Foundation ("Hack Club")
Non-profit corporation (501(c)(3)), incorporated in the United States
Registered Address
8605 Santa Monica Blvd #86294
West Hollywood, CA 90069
United States
Authorized Representative
Zach Latta, Founder
Contact
Email: team@hackclub.com
Phone: +1-855-625-HACK
VAT ID
Not applicable (U.S. nonprofit organization)
Legal Status
Hack Club is a California nonprofit public benefit corporation,
recognized as a 501(c)(3) organization under U.S. law.
EIN: 81-2908499
Responsible for Content (§ 18 MStV)
Zach Latta, Founder
8605 Santa Monica Blvd #86294
West Hollywood, CA 90069
United States
Dispute Resolution
Information on consumer dispute resolution entities in EU Member States
is available at{' '}
consumer-redress.ec.europa.eu
. As a U.S. nonprofit organization, Hack Club is not obligated or
willing to participate in dispute resolution proceedings before a
consumer arbitration board.
Notes
This imprint is provided to satisfy common EU/German transparency
expectations (e.g., §5 DDG) and US best practices.
>
)
export default Imprint
================================================
FILE: pages/index.tsx
================================================
/** @jsxImportSource theme-ui */
import {
Badge,
Box,
Button,
Card,
Flex,
Grid,
Heading,
Link,
Text
} from 'theme-ui'
import { useEffect, useRef, useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import Meta from '@hackclub/meta'
import Nav from '../components/nav'
import BGImg from '../components/background-image'
import ForceTheme from '../components/force-theme'
import Footer from '../components/footer'
import Stage from '../components/stage'
import Carousel from '../components/index/carousel'
import Sprig from '../components/index/cards/sprig'
import Sinerider from '../components/index/cards/sinerider'
import SprigConsole from '../components/index/cards/sprig-console'
import Clubs from '../components/index/cards/clubs'
import Workshops from '../components/index/cards/workshops'
import HCB from '../components/index/cards/hcb'
import Hackathons from '../components/index/cards/hackathons'
import OuternetImgFile from '../public/home/outernet-110.jpg'
import JSConfetti from 'js-confetti'
import Secret from '../components/secret'
import MailingList from '../components/index/cards/mailing-list'
import Slack from '../components/index/cards/slack'
import Icon from '../components/icon'
import GitHub from '../components/index/github'
import Photo from '../components/photo'
import Comma from '../components/comma'
import Haxidraw from '../components/index/cards/haxidraw'
import Flavortown from '../components/index/cards/flavortown'
import HackClubTheGame from '../components/index/cards/hctg'
import Sleepover from '../components/index/cards/sleepover'
import Stasis from '../components/index/cards/stasis'
import Jackpot from '../components/index/cards/jackpot'
import Fallout from '../components/index/cards/fallout'
import Macondo from '../components/index/cards/macondo'
import Beest from '../components/index/cards/beest'
import CTAS from '../components/index/ctas'
import { slackData as SlackDataLib } from '../lib/slackData'
import Horizons from '../components/index/cards/horizons'
declare global {
interface Window {
kc: string
paper: string
}
}
const redBadgeSx = {
px: 2,
backgroundColor: 'red',
borderRadius: 10,
color: 'white',
whiteSpace: 'nowrap'
}
function Page({
hackathonsData,
bankData,
slackData,
gitHubData,
consoleCount,
stars,
game,
events,
carouselCards,
ctaCards
}) {
const [reveal, setReveal] = useState(false)
const { asPath } = useRouter()
const jsConfetti = useRef(null)
useEffect(() => {
jsConfetti.current = new JSConfetti()
window.kc = `In the days of old, when gaming was young \nA mysterious code was found among \nA sequence of buttons, pressed in a row \nIt unlocked something special, we all know \n\nUp, up, down, down, left, right, left, right \nB, A, Start, we all have heard it's plight \nIn the 8-bit days, it was all the rage \nAnd it still lives on, with time, it will never age \n\nKonami Code, it's a legend of days gone by \nIt's a reminder of the classics we still try \nNo matter the game, no matter the system \nThe code will live on, and still be with them \n\nSo the next time you play, take a moment to pause \nAnd remember the code, and the Konami cause \nIt's a part of gaming's history, and a part of our lives \nLet's keep it alive, and let the Konami Code thrive!\n`
window.paper = `Welcome, intrepid hacker! We'd love to have you in our community. Get your invite at hack.af/slack. Under "Why do you want to join the Hack Club Slack?" add a 🦄 and we'll ship you some exclusive stickers! `
}, [])
// easter egg detector, one-shot
const [konamiActivated, setKonamiActivated] = useState(false)
useEffect(() => {
const konamiSequence = [
'ArrowUp',
'ArrowUp',
'ArrowDown',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'ArrowLeft',
'ArrowRight',
'b',
'a'
]
let konamiPosition = 0
const handleKeyDown = (e: KeyboardEvent) => {
if (!konamiActivated && e.key === konamiSequence[konamiPosition]) {
konamiPosition++
if (konamiPosition === konamiSequence.length) {
easterEgg()
setKonamiActivated(true)
konamiPosition = 0
}
} else if (!konamiActivated) {
konamiPosition = 0
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [konamiActivated])
const easterEgg = () => {
alert('Hey, you typed the Konami Code!')
jsConfetti.current.addConfetti({
confettiColors: [
// Hack Club colours!
'#ec3750',
'#ff8c37',
'#f1c40f',
'#33d6a6',
'#5bc0de',
'#338eda',
'#a633d6'
]
})
}
const [count, setCount] = useState(0)
const images = [
{ alt: 'Map of Hack Clubs around the world', src: '/home/map.png' },
{
alt: 'Hack Clubbers at SpaceX HQ in LA',
src: '/home/zephyr-spacex.jpeg'
},
{
alt: 'MA Hacks, Hack Clubber organized hackathon',
src: '/hackathons/mahacks.jpeg'
},
{ alt: 'AMA with Sal Khan', src: '/home/ama.png' },
{ alt: 'Hack Clubbers at Flagship, 2019', src: '/home/flagship_4.jpg' }
]
// Spotlight effect
const spotlightRef = useRef(null)
useEffect(() => {
const handler = event => {
const rect = document.getElementById('spotlight').getBoundingClientRect()
const x = event.clientX - rect.left //x position within the element.
const y = event.clientY - rect.top //y position within the element.
spotlightRef.current.style.background = `radial-gradient(
circle at ${x}px ${y}px,
rgba(132, 146, 166, 0) 10px,
rgba(249, 250, 252, 0.9) 80px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return (
<>
{
setReveal(true)
}}
onMouseOut={() => {
setReveal(false)
}}
/>
{konamiActivated && (
Hey, I'm an Easter Egg! Look at me!
)}
Welcome to Hack Club
We are{' '}
{
!reveal ? setReveal(true) : setReveal(false)
}}
sx={{
...redBadgeSx,
position: 'absolute',
transform: 'rotate(-2deg) translateY(-5px)',
textDecoration: 'none',
'&:hover': {
cursor: 'pointer'
}
}}
aria-hidden="true"
>
{slackData.total_members_count} teen
hackers
{slackData.total_members_count} teen
hackers
teen hackers
{' '}
from around the world who code together
Or, check out our programs:
Hackers at Outernet in Vermont
Discover the{' '}
t.util.gx('red', 'orange'),
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
}}
>
joy of code
, together.
Every day, thousands of Hack Clubbers gather online and
in-person to make things with code. Whether you're a beginner
programmer or have years of experience, there's a place for you at
Hack Club. Read about our{' '}
hacker ethic
.
{
setCount(prevCount => (prevCount + 1) % images.length)
}}
>
1
Connect with other teenage coders
Have a coding question? Looking for project feedback? You'll
find hundreds of fabulous people to talk to in our global{' '}
Slack{' '}
(like Discord), active at all hours.
2
Build open source learning tools
We build large open source projects together (
16k+ PRs a year
) like this website, a game engine, daily streak system, and
more!
3
Gather IRL with other makers
Meet other Hack Clubbers in your community to build
together at one of the 1000+{' '}
Hack Clubs
{' '}
and{' '}
high school hackathons
.
Connect with{' '}
builders
{' '}
from around the world
We gather both online and in-person to share our love of code
and make things together!
We build{' '}
open source
{' '}
games and tools together
In collaboration with engineers on the Hack Club team,
Hack Clubbers build learning tools for each other. Get
involved with these projects by building something with our
tools or contribute to the tools themselves.
{gitHubData && (
a:nth-child(n+4)': {
display: ['none', null, null, 'flex']
}
}}
>
Live from GitHub
{gitHubData
.filter(data => !data.user.endsWith('[bot]'))
.slice(0, 4)
.map((data, key) => {
return (
)
})}
)}
Find your{' '}
IRL community.
Thousands of Hack Clubbers organize and participate in
hackathons and after school coding clubs.
{/* */}
We've got a lot going on - Let's recap
Find your second home at{' '}
t.util.gx('red', 'orange'),
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent'
}}
>
Hack Club
a, > div': {
borderRadius: 'extra',
boxShadow: 'elevated',
p: [3, null, 4]
},
span: {
boxShadow:
'-2px -2px 6px rgba(255,255,255,0.125), inset 2px 2px 6px rgba(0,0,0,0.1), 2px 2px 8px rgba(0,0,0,0.0625)'
},
svg: { fill: 'currentColor' }
}}
>
{new URL(asPath, 'http://example.com').searchParams.get('gen') ===
'z' && (
<>
>
)}
>
)
}
export async function getStaticProps() {
const carouselCards = require('../public/carousel.json')
const allCtaCards = require('../lib/cta.json')
const ctaCards =
allCtaCards.length > 3
? allCtaCards.sort(() => Math.random() - 0.5).slice(0, 3)
: allCtaCards
// HCB: get total raised
const bankData = []
const initialBankData = await fetch('https://hcb.hackclub.com/stats')
try {
const bd = await initialBankData.json()
const raised = bd.raised / 100
bankData.push(
`💰 ${raised.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
})} raised`
)
} catch {
bankData.push('error')
}
const slackData = await SlackDataLib()
// GitHub: get latest github activity (currently this is erroring and
// preventing the site from deploying)
const gitHubData = null
// GitHub: get latest GitHub stars
const { fetchStars } = require('./api/stars')
const stars = await fetchStars()
// Sprig: get newest games
const { getGames } = require('./api/games')
const game = await getGames()
// Sprig: get console count
const { getConsoles } = require('./api/sprig-console')
const consoleCount = await getConsoles()
// Hackathons: get latest hackathons
let hackathonsData
try {
const response = await fetch(
'https://hackathons.hackclub.com/api/events/upcoming'
)
if (response.ok) {
hackathonsData = await response.json()
} else {
hackathonsData = [] // or some default value if the fetch fails
}
} catch (error) {
hackathonsData = [] // or some default value if an error occurs
}
hackathonsData.sort(
(a: { start: string }, b: { start: string }) =>
new Date(a.start).getTime() - new Date(b.start).getTime()
)
const events = []
return {
props: {
game,
gitHubData,
consoleCount,
hackathonsData,
bankData,
slackData,
stars,
events,
carouselCards,
ctaCards
},
revalidate: 60
}
}
export default Page
================================================
FILE: pages/jobs/index.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Container, Heading, Card, Text, Grid } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import Icon from '../../components/icon'
import Image from 'next/image'
import zephyrPic from '../../public/jobs/zephyr-group-pic.jpg'
import { compact } from 'lodash'
import { decodeHtmlEntities } from '../../lib/helpers'
const JobListing = ({
positionName,
positionDesc,
positionLink,
positionLocation,
positionType
}) => (
svg': {
opacity: '0',
transition: '0.3s ease-in-out'
},
'&:hover span > svg': {
opacity: '1'
}
}}
>
{positionName}
{compact([positionDesc, positionLocation, positionType]).join(' • ')}
)
const Page = ({ jobs }) => (
<>
Join the Hack Club Team
$ ssh jobs.hackclub.com -p 1337
{' '}
or scroll down to learn more...
{jobs.items.length > 0 ? (
jobs.items.map(job => (
))
) : (
No open roles at this time. Check back later!
)}
>
)
export default Page
export async function getStaticProps() {
const data = await fetch(
'https://api.polymer.co/v1/hire/organizations/hackclub/jobs'
)
const jobs = await data.json()
return {
props: {
jobs
},
revalidate: 60
}
}
================================================
FILE: pages/minecraft.tsx
================================================
import {
Box,
Button,
Card,
Container,
Grid,
Heading,
Image,
Link,
Text
} from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import NextLink from 'next/link'
import Nav from '../components/nav'
import SlideDown from '../components/slide-down'
import FadeIn from '../components/fade-in'
import Icon from '../components/icon'
import Footer from '../components/footer'
const Page = () => (
<>
Vanilla Server
Hang out with the tree-punchers of Hack Club playing on the
official server, mc.hackclub.com.{' '}
Check out the map »
Modded Server
Want a unique Minecraft experience? Come explore the limits of
Minecraft with us on the official modded server!
Compete weekly
Join weekly Minecraft Monday calls & compete in the monthly
Minecraft Showdown to win prizes.
Build plugins
Many Hack Clubbers first found coding via Minecraft plugins, and
we have an active community scripting plugins on our server.
Chat in #minecraft on Slack
Hundreds of players around the world.
>
)
export default Page
================================================
FILE: pages/night.tsx
================================================
import { Box, Container, Heading, Image, Link, Text } from 'theme-ui'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import NextLink from 'next/link'
import Nav from '../components/nav'
import SlideDown from '../components/slide-down'
import Footer from '../components/footer'
import { keyframes } from '@emotion/react'
const floating = keyframes`
from {
transform: translateY(20px);
}
to {
transform: translateY(-20px);
}
`
// (msw) Credit for this totally goes to https://codepen.io/WebSonick/pen/vjmgu
const twinkling = keyframes`
from { background-position: 0 0; }
to { background-position: -10000px 5000px; }
`
const color = '#50E3C2'
const Page = () => (
<>
Hack Night
The Hack Club community regularly gathers on Zoom or Huddles. It’s a
chance to meet new friends, livestream what you’re hacking on, or just
hang out on a chill call.
Hack nights are hosted regularly by Hack Clubbers. Come join or start
an impromptu Hack session on{' '}
#hack-night
!
>
)
export default Page
================================================
FILE: pages/onboard/board/[slug].tsx
================================================
import { Box, Button, Flex, Heading, Image, Link, Text } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from '../../../components/nav'
import { useEffect, useRef } from 'react'
import { remark } from 'remark'
import html from 'remark-html'
import { getOnboardProject } from '../../api/onboard/p/[project]'
import { getAllOnboardProjects } from '../../api/onboard/p'
import Icon from '@hackclub/icons'
import Tilt from '../../../components/tilt'
type ProjectType = {
name: string
imageTop: string
readmeData: {
frontmatter: {
github_handle: string
}
description: string
}
}
type BoardPageProps = {
project: ProjectType
}
const BoardPage = ({ project }: BoardPageProps) => {
const spotlightRef = useRef(null)
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 (
<>
{project.name}
by {project?.readmeData?.frontmatter?.github_handle}
{
// two-column layout - image on left, title + desc on right
}
{project.name}
{project?.readmeData?.frontmatter?.github_handle
? `by ${project?.readmeData?.frontmatter?.github_handle}`
: ''}
View on GitHub
{process.env.NODE_ENV !== 'production' && (
<>
Plus! FIRST team members get a limited edition PCB badge
designed with Dean Kamen.
Join 1,000 others to create your own circuit board.
Community & Friends
Share your progress and ask for help with Hack Club teens who
are designing their own circuit boards.
Free Manufacturing
We’ll pay $100 to cover manufacturing costs, enough for 2-3
iterations of your design.
Never made a circuit board before? No problem.
Learn how to design your own circuit boards from scratch with our{' '}
official tutorials and jams, like Maggie’s{' '}
intro to PCB design jam
. Ask in the{' '}
Hack Club Slack
{' '}
if you have any questions!
FRC team #4272 "Maverick Robotics" made this{' '}
swerve-drive encoder for their robot drive train
that's cheaper than stock sensors and saves ports on their CAN.
{/* What have people made already?
A fidget spinner without any moving parts.
See Micha's work
→
A movement sensor add-on to an open source{' '}
game console.
Read the source →
Hugo's USB-C hub for the best{' '}
hackathon swag ever.
Build one for your event
→
Karmanyaah's digital level, SparkleTilt.
Learn how to make your own → */}
How to Get a Grant
{[
<>
Design a PCB using any tool that can export
Gerber files.
>,
<>
Upload your design to JLCPCB and take a
screenshot.
>,
<>
Open source your design on GitHub and{' '}
apply for the grant
! You must be a teenager in high school to apply.
>
].map((text, i) => (
-60,
position: 'absolute'
}}
>
{text}
))}
Join the Hack Club Slack
Meet others learning how to make their own circuit boards.
Collaborate, get help, and support others as you take the leap.
{/* For accessibility and screen readers: */}
Let's Recap
{recapPixels.map((_, i) => (
))}
$100 Grants
We’ll pay $100 to manufacture your boards, all components
included.
Learn to PCB
Read our tutorials to learn how to make a simple circuit boards
from start to end.
Community
Share progress with fellow participants and ask for help in the
Hack Club Slack.
{stickerButtonText.split(' ').map((line, i) => (
{line}
))}
>
)
}
export default ShipPage
================================================
FILE: pages/onboard/gallery/index.tsx
================================================
/** @jsxImportSource theme-ui */
import { GalleryPage } from '../../../components/onboard/gallery-paginated'
import { getAllOnboardProjects } from '../../api/onboard/p'
import { getOnboardProject } from '../../api/onboard/p/[project]'
export default function Index({ projects, itemCount }) {
return (
)
}
export async function getStaticProps() {
const allProjects = await getAllOnboardProjects()
const data = allProjects.slice(0, 10)
const projects = []
for (const project of data) {
projects.push(await getOnboardProject(project.name))
}
return {
props: {
projects,
itemCount: allProjects.length
},
revalidate: 120 // 2 minutes
}
}
================================================
FILE: pages/onboard/index.tsx
================================================
/** @jsxImportSource theme-ui */
import { Box, Button, Flex, Grid, Heading, Image, Link, Text } from 'theme-ui'
import Balancer from 'react-wrap-balancer'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from '../../components/nav'
import Footer from '../../components/footer'
import FadeIn from '../../components/fade-in'
import Sparkles from '../../components/sparkles'
import Tilt from '../../components/tilt'
import Recap from '../../components/onboard/recap'
import usePrefersReducedMotion from '../../lib/use-prefers-reduced-motion'
import { useEffect, useRef, useState } from 'react'
import sleep from '../../lib/sleep'
import Announcement from '../../components/announcement'
import YoutubeVideo from '../../components/onboard/youtube-video'
import Icon from '@hackclub/icons'
/**
* @type {import('theme-ui').ThemeUIStyleObject}
*/
const traceSx = {
width: 6,
bg: '#e2b747',
alignSelf: 'stretch',
mr: 100,
position: 'relative'
}
const dimBg = '#151515'
const slackLink = 'https://slack.hackclub.com'
const stickerButtonText = 'Click 4 Stickers'
const stickerButtonFont = 'Oleo Script'
const stickerButtonFontStylesheet = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(
stickerButtonFont
)}&display=swap&text=${encodeURIComponent(stickerButtonText)}`
const wandImgTraced =
'https://cloud-8lszi55ph-hack-club-bot.vercel.app/10frame_2.png'
const wandImgRendered =
'https://cloud-8lszi55ph-hack-club-bot.vercel.app/00frame_1.png'
const ShipPage = () => {
const prefersReducedMotion = usePrefersReducedMotion()
// Wand flicker animation
const [wandImg, setWandImg] = useState(wandImgTraced)
const wandAnimated = useRef(false)
useEffect(() => {
let canceled = false
const flicker = async () => {
if (canceled) return
setWandImg(wandImgTraced)
await sleep(Math.random() * 80 + 10)
if (canceled) return
setWandImg(wandImgRendered)
setTimeout(flicker, Math.random() * 4000 + 500)
}
const animate = async () => {
if (wandAnimated.current) return
wandAnimated.current = true
await sleep(1500)
if (canceled) return
setWandImg(wandImgRendered)
await sleep(60)
if (canceled) return
setWandImg(wandImgTraced)
await sleep(340)
if (canceled) return
setWandImg(wandImgRendered)
await sleep(14)
if (canceled) return
setWandImg(wandImgTraced)
await sleep(55)
if (canceled) return
setWandImg(wandImgRendered)
await sleep(10)
if (canceled) return
setWandImg(wandImgTraced)
await sleep(150)
if (canceled) return
setWandImg(wandImgRendered)
setTimeout(flicker, 1200)
}
if (prefersReducedMotion) {
setWandImg(wandImgRendered)
} else {
animate()
}
return () => {
canceled = true
}
}, [prefersReducedMotion])
// Spotlight effect
const spotlightRef = useRef(null)
useEffect(() => {
const handler = event => {
const rect = spotlightRef.current.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
spotlightRef.current.style.background = `radial-gradient(
circle at ${x}px ${y}px,
rgba(0, 0, 0, 0) 10px,
rgba(0, 0, 0, 0.8) 80px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
// Calculating the bus height to match the bottom left of the first connector.
const [busHeight, setBusHeight] = useState(null)
const containerRef = useRef(null) // For ResizeObserver
const connectorRef = useRef(null) // To get bottom left position
const busRef = useRef(null) // To calculate height differential
useEffect(() => {
const observer = new ResizeObserver(() => {
const connectorRect = connectorRef.current.getBoundingClientRect()
const busRect = busRef.current.getBoundingClientRect()
setBusHeight(busRect.bottom - connectorRect.bottom + 4.5)
})
observer.observe(containerRef.current)
return () => observer.disconnect()
}, [])
return (
<>
OnBoard has ended!{' '}
Click here
{' '}
to sign up for Fallout - the next hardware program
OnBoard
Circuit boards are{' '}
magical{'.'}{' '}
You design one, we'll print it!
Join 1,000 others to create your own circuit board.
Community & Friends
Share your progress and ask for help with Hack Club teens who
are designing their own circuit boards.
Free Manufacturing
We’ll pay $100 to cover manufacturing costs, enough for 2-3
iterations of your design.
Never made a circuit board before? No problem.
See the{' '}
full playlist
Learn how to design your own circuit boards from scratch with our{' '}
official tutorials and jams, like Maggie’s{' '}
intro to PCB design jam
. Ask in the{' '}
Hack Club Slack
{' '}
if you have any questions!
What have people made already?
A fidget spinner without any moving parts.
See Micha's work
→
{/*
The cutest, tiniest raspberry pi-base developer board.
Read Paolo's work → */}
Hugo's USB-C hub for the best{' '}
hackathon swag ever.
Build one for your event
→
Karmanyaah's digital level, SparkleTilt.
Learn how to make your own
→
Build your own hardware key.
Learn how to make your own
→
How to Get a Free Circuit Board
{[
<>
Design a PCB using any tool that can export
Gerber files.
>,
<>
Upload your design to JLCPCB and take a
screenshot.
>,
<>
Open source your design on GitHub and{' '}
apply for the grant
! You must be a teenager in high school or younger to apply.
>
].map((text, i) => (
-60,
position: 'absolute'
}}
>
{text}
))}
Join the Hack Club Slack
Meet others learning how to make their own circuit boards.
Collaborate, get help, and support others as you take the leap.
{/* For accessibility and screen readers: */}
Let's Recap
$100 Grants
We’ll pay $100 to manufacture your boards, all components
included.
Learn to PCB
Read our tutorials to learn how to make a simple circuit boards
from start to end.
Community
Share progress with fellow participants and ask for help in the
Hack Club Slack.
{stickerButtonText.split(' ').map((line, i) => (
{line}
))}
>
)
}
export default ShipPage
================================================
FILE: pages/opensource.tsx
================================================
import {
Box,
Button,
Card,
Container,
Flex,
Grid,
Heading,
Text,
Link
} from 'theme-ui'
import Meta from '@hackclub/meta'
import Icon from '@hackclub/icons'
import Head from 'next/head'
import Nav from '../components/nav'
import Footer from '../components/footer'
import { Octokit } from '@octokit/rest'
import ForceTheme from '../components/force-theme'
export const BankProject = ({ name, url }) => (
svg': {
opacity: '0',
transition: '0.3s ease-in-out'
},
'&:hover > svg': {
opacity: '1'
}
}}
>
{name}
)
const Page = ({ repos, transparentAccounts }) => (
<>
Open Source at Hack Club
Explore our finances, code, planning documents and more.
Finances
All open sourced through HCB Transparency Mode.
{transparentAccounts.map(account => (
))}
Events
Includes planning documents, partnership emails, meeting notes etc.
Content
GitHub Repositories
Want to contribute?
{repos
.sort(function (a, b) {
const keyA = a.stargazers_count,
keyB = b.stargazers_count
if (keyA < keyB) return 1
if (keyA > keyB) return -1
return 0
})
.map(repo => (
{repo.name}
{repo.description?.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)}
{repo.stargazers_count} ★
))}
>
)
export default Page
export async function getStaticProps() {
const octokit = new Octokit({
auth: process.env.GITHUB || process.env.GITHUB_TOKEN
})
let repos
try {
const x = await octokit.paginate('GET /orgs/{org}/repos', {
org: 'hackclub'
})
repos = x.map(repo => ({
id: repo.id,
name: repo.name,
description: repo.description,
stargazers_count: repo.stargazers_count
}))
} catch (e) {
console.error(e)
return { props: { repos: [], transparentAccounts: [] }, revalidate: 30 }
}
const transparentAccounts = (
await fetch('https://hcb.hackclub.com/api/v3/organizations').then(res =>
res.json()
)
).filter(account => account.category?.replaceAll(' ', '_') === 'hack_club_hq')
return { props: { repos, transparentAccounts }, revalidate: 30 }
}
================================================
FILE: pages/philanthropy/index.tsx
================================================
import {
Avatar,
Box,
Button,
Card,
Container,
Flex,
Grid,
Heading,
Link,
Text
} from 'theme-ui'
import styled from '@emotion/styled'
import Image from 'next/image'
import Meta from '@hackclub/meta'
import Head from 'next/head'
import Nav from '../../components/nav'
import ForceTheme from '../../components/force-theme'
import Footer from '../../components/footer'
import ReactBeforeSliderComponent from 'react-before-after-slider-component'
import 'react-before-after-slider-component/dist/build.css'
import { Fade, Slide } from '../../components/react-reveal-compat'
import Marquee from '../../components/marquee'
import ExecuteBig from '../../public/donate/codedaydc_hack.jpg'
import HackCamp from '../../public/donate/sf.jpg'
import HackerGames from '../../public/donate/0img_20210830_161125.jpg'
import LaptopDonations from '../../public/donate/0screenshot_2021-10-03_at_4.20.30_pm.png'
import Kerala from '../../public/donate/0img-20210918-wa0091.jpg'
import HackPenn from '../../public/donate/0color_pop.jpg'
import ElonAMA from '../../public/donate/elon.jpg'
import SpaceX from '../../public/donate/0spacex_and_hack_club.jpg'
import Flagship from '../../public/donate/flagship.png'
import MAHacks from '../../public/donate/0screenshot_2021-10-03_at_4.07.51_pm.png'
import HackCamp2020 from '../../public/donate/0img_6447.jpg'
import InnovationCircuit from '../../public/donate/0screenshot_2021-10-03_at_3.45.54_pm.png'
import WindyCity from '../../public/donate/6screenshot_2021-10-03_at_3.29.29_pm.png'
import ZephyrFun from '../../public/donate/0screenshot_2021-10-03_at_3.59.34_pm.png'
import GoldenTrain from '../../public/home/golden-train.png'
import usePrefersMotion from '../../lib/use-prefers-motion'
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from 'recharts'
const Header = styled(Box)`
background: url('/pattern.svg');
`
const AMarquee = Marquee as any
const PhotoRow = ({ photos }) => (
{}}
onFinish={() => {}}
>
{photos.map((photo, index) => (
))}
{}}
onFinish={() => {}}
>
{photos.map((photo, index) => (
))}
)
const data = [
{ name: '3452 Teenagers', Teenagers: 3452, year: '2018' },
{ name: '6932 Teenagers', Teenagers: 6932, year: '2019' },
{ name: '13530 Teenagers', Teenagers: 13530, year: '2020' },
{ name: '18347 Teenagers', Teenagers: 18347, year: '2021' },
{ name: '21790 Teenagers', Teenagers: 21790, year: '2022' },
{ name: '28720 Teenagers', Teenagers: 28720, year: '2023' }
]
const Sheet = styled(Card)`
position: relative;
overflow: hidden;
border-radius: 8px;
width: 100%;
color: white;
height: 100%;
`
const Q = styled(Sheet)`
position: relative;
&:after {
content: '';
background-color: #ec3750;
height: 100%;
width: 10px;
position: absolute;
bottom: 0;
left: 0;
}
color: #3c4858;
`
const Stat = ({ num, words, background }) => {
return (
{num}
{words}
)
}
const Graph = () => {
return (
<>
Teenagers in Slack per year
>
)
}
const Highlight = ({ children }) => {
return {children}
}
const Line = () => {
return (
)
}
const Quote = ({ children }) => {
return (
{children}
)
}
type PropPilled = {
logo?: string
name: string
}
const Pill = ({ logo, name }: PropPilled) => {
return (
{logo ? (
) : (
<>>
)}
{name}
)
}
const HackClubber = ({ photo, quote, info }) => {
return (
"{quote}"{info}
)
}
const FIRST_IMAGE = {
imageUrl: '/philanthropy/after1.png'
}
const SECOND_IMAGE = {
imageUrl: '/philanthropy/before.png'
}
const delimiterIconStyles = {
width: '50px',
height: '50px',
backgroundSize: '110%',
backgroundPosition: 'center',
borderRadius: 'none',
backgroundImage:
'url(https://cloud-1rqn9rwxm-hack-club-bot.vercel.app/0frame_43.svg)'
}
const Map = () => {
return (
What Hack Club could look like with your support
)
}
const Philanthropy = ({ posts = [] }) => {
return (
<>
{/*
Investing in the future
Your contribution is tax-deductible.
Hack Club is a 501(c)(3) nonprofit with the EIN 81-2908499.
*/}
Invest in the future.
Contribute today to empower the next generation.
Your contribution is tax-deductible.
Hack Club is a 501(c)(3) nonprofit with the EIN 81-2908499.
“With major support, I am confident Hack Club
will change the world.”—Tom Preston-Werner, GitHub Co-founder
In the next ten years, Hack Club will discover, foster and inspire
thousands more teenagers to use technical skills to solve
problems.
Led by young engineers, with early backing from the 21st century’s
most iconic creators, Hack Club already reaches tens of thousands
of teenagers, and represents the largest network of technical
teens in the world. Each day, new projects are shipped, new lines
of code are written, and new friendships are forged through
collaborative, problem-solving technical projects happening at
Hack Club.
Hack Club is always free for teenagers and with your support, Hack
Club can grow to hundreds of thousands of teen hackers, bringing
free computer science education, a hacker mindset, and an equal
shot at success to every teenager, regardless of where they’re
from, how they identify, or what their parents do.
Over time, Hack Clubbers will reshape societies as entrepreneurs,
environmentalists, political leaders, activists and policy makers.
We help shape the values of these future leaders, modeling and
incentivizing them to be curious, humble, kind, optimistic problem
solvers.{' '}
We need your support to make this vision a reality.
To discuss a gift:
Reach out toChristina AsquithCo-founder, COO, and Board Member
christina@hackclub.com
Send physical checksThe Hack Foundation
8605 Santa Monica Blvd #86294, West Hollywood, CA, 90069
EIN: 81-2908499
Donate online to Hack Club ▶
We also accept crypto, stocks, and other forms of support.
View Hack Club's IRS Form 990s2025 Form will be shared when ready.
Starting in 2021, Hack Club has engaged with an external auditing
firm and has audited financials through the current fiscal year.
View Hack Club's Annual ReportsReports from 2022-2024
Explore Hack Club's annual reports from 2022 onward, showcasing
each year's impact and key milestones.
{/*
Donate ▶
*/}
Hack Club is already making a difference!
“Hack Club helped me fall in love with creating and made me feel
like I belong.”
Belle, 17, Malaysia
As the largest network of technical teenagers, we are featured in
the news:
Board of Directors
Tom Preston-Werner
Co-founder, GitHub
Quinn Slack
Co-founder and CEO, Sourcegraph
Zach Latta
Founder and Executive Director, Hack Club
Christina Asquith
Co-founder and COO, Hack Club
Board advisor: John Abele (Founder, Boston Scientific)
“Hack Club is the organization I wish I had
when I was a teenager.
In 2017, I joined as a founding board member, and I’ve seen
firsthand the leadership team act with integrity and
transparency since Day 1. Founder Zach Latta and COO Christina
Asquith are efficient, responsible and disciplined stewards of
every dollar, and I've proudly grown my donations over the
years.
I believe in Hack Club, and I'm looking forward to staying
involved for the long term. I also personally intend to
continue and grow my financial support of their mission.
”
— Tom Preston-Werner, Co-founder, Preston-Werner Ventures /
Co-founder and former CEO, GitHub
Join our community of generous donors
$5M - $10M{' '}
Tom Preston-Werner (9x)
Musk Foundation (6x)
{' '}
$1M - $5M{' '}
Dr. Lisa Su
Michael Dell (3x)
Craig Newmark (4x)
Tobi Lutke
Advanced Micro Devices
The Libermans
Lizzy Danhakl & Andrew Reed (4x)
Patrick J. McGovern (3x)
$500k - $1M
GitHub Education (6x)
Argosy Foundation (5x)
Endless Network (4x)
FUTO (3x)
Joe Liemandt
$200k - $500k
Ron Conway (6x)
Adam Ross (3x)
Gwynne Shotwell
Ron Baron
Jack Dorsey
Vitalik Buterin
$100k- $200k
Quinn Slack (3x)
Peter Levine
Mitchell Hashimoto
Proton Foundation
Chuck & Marna Davis
Kellogg Foundation
Pinkerton Foundation
* The numbers in bracket indicates # of gifts since 2018
A few others who support Hack Club
and many more...
Only through their support are we able to empower students like
Obrey and Maggie
Obrey Muchena started a Hack Club in his senior year of high
school at Kabulonga Boys' Secondary School, and now more than
a dozen students are coding.
Thanks to our donor-funded laptop program, Hack Club sent him
a MacBook Air. In his Hack Club, Obrey and his best friend
Edward built robots that won Canada’s Humanitarian Activist
Award.
Obrey Muchena19, Zambia
In 2021, Maggie joined the Hack Club community; she has since
shipped 10+ coding projects from widgets to raycast
extensions.
The Hack Club community "inspired me to step outside my
comfort zone and take on challenges I never previously would
have — starting a CS Club at my school, (co-)hosting AMAs, and
even organizing Leland Hacks, the first in-person hackathon in
my city after the pandemic.”
Maggie Liu17, California
Hack Club invites the 21st century’s leading thinkers, builders
and disrupters to join our small, core network of donors with a
gift.
We envision thousands of diverse Hack Club leaders in towns
and cities across America and the world, connected online, and
self-organizing events and hackathons–driven by a can-do
culture and a rigorous dedication to building real things in
the real world.
Founded in 2014, Hack Club grew 700 percent during the
COVID-19 pandemic, and Hack Club’s team of engineers can’t
keep up with demand.
Your gift will:
Increase support to serve thousands more teenagers, with a
strong focus on serving those who face additional barriers to
contributing their talents to the world
Create hundreds more Hack Clubs in high schools and communities
across the country and world
Inspire a problem-solving mindset and a hacker identity, where
teenagers are empowered to build what they want to see in the
world
Make Hack Club the best place to be a teenager on the internet,
incentivizing a shift among teenagers from consumers to creators
of technology
Launch special projects, in which Hack Clubbers collaborate with
SpaceX, Vercel, Cloudflare, Replit, Dogecoin and others
Popularize transparent accounting, open source building, and
high-integrity leadership
Grow the team, mostly engineers
Host dozens of in-person events, including our summer adventure
Extend mini-grants of hardware and internet access to hundreds
of teenagers
Bring computer science and engineering skills to thousands more
teenagers
Thank you for your consideration!
Sincerely,
Christina Asquith, Co-founder and COOZach Latta, Founder and Executive DirectorThe Hack Foundation
Address: The Hack Foundation at 8605 Santa Monica Blvd #86294,
West Hollywood, CA, 90069
EIN: 81-2908499Reach out toChristina AsquithCo-founder, COO, and Board Member
christina@hackclub.com
Site by Belle, 17, Hack Clubber
>
)
}
export default Philanthropy
================================================
FILE: pages/philanthropy/supporters.tsx
================================================
/** @jsxImportSource theme-ui */
import styled from '@emotion/styled'
import {
Box,
Button,
Container,
Flex,
Heading,
Card,
Link as A,
Text
} from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import ForceTheme from '../../components/force-theme'
import Nav from '../../components/nav'
import Sponsors from '../../components/donate/sponsors'
import donors from '../../components/donate/donors.json'
import Footer from '../../components/footer'
const Header = styled(Box)`
background: url('/pattern.svg');
`
const Sheet = styled(Card)`
position: relative;
overflow: hidden;
border-radius: 8px;
width: 100%;
color: white;
`
const Row = styled(Box)`
text-align: left;
@media screen and (min-width: 48em) {
display: grid;
grid-gap: 18px;
grid-template-columns: 2fr 3fr;
}
`
const subhline = { fontSize: [3, 4], style: { lineHeight: '1.375' } }
const contentContainer = {
maxWidth: 72,
width: 1,
p: 3,
color: 'black',
style: { position: 'relative' }
}
const content = { maxWidth: 48, mx: 0, color: 'black' }
const title = 'Donate'
const desc =
'Contribute today to empower the next generation and help start a coding club at every high school.'
const DonorGrid = styled(Box)`
display: grid;
grid-gap: 6px;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
align-items: center;
p,
a {
width: 100%;
}
@media screen and (min-width: 48em) {
grid-gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
}
`
const DonorCardBase = styled(Sheet)`
display: flex;
justify-content: center;
align-items: center;
@media screen and (max-width: 32em) {
border-radius: 0;
box-shadow: none;
}
`
const DonorCard = ({ name, link = false }) => (
{name}
)
const DonorListing = ({ name, url }) => {
if (url) {
return (
)
} else {
return
}
}
export default function Donate() {
return (
We rely on people like you to bring coding to the world.
Contribute today to empower the next generation. Help start a
Hack Club at every high school.
Your contribution is tax-deductible.
Hack Club is a 501(c)(3) nonprofit with the EIN 81-2908499.
A few of our amazing donors.
{Object.keys(donors).map(name => (
))}
and many more…
These fabulous companies donate their products to us.
)
}
================================================
FILE: pages/philosophy.tsx
================================================
import Meta from '@hackclub/meta'
import Head from 'next/head'
import { Box, Heading, Container, Text, Button, Link } from 'theme-ui'
import Nav from '../components/nav'
import styled from '@emotion/styled'
import Footer from '../components/footer'
import NextLink from 'next/link'
const Header = styled(Box)`
color: white;
background-image: linear-gradient(
32deg,
rgb(207, 45, 228) 0%,
rgb(228, 45, 66) 64%,
rgb(206, 41, 60) 100%
);
clip-path: polygon(0% 0%, 100% 0, 100% 100%, 0% 90%);
> div {
position: relative;
}
`
const Seal = styled(Box)`
border-radius: 9999px;
background-color: white;
color: black;
mix-blend-mode: screen;
text-align: center;
width: 12rem;
height: 12rem;
position: absolute;
margin-top: -1rem;
transform: rotate(4deg);
@media screen and (min-width: 32em) {
transform: rotate(3deg);
margin-top: -3rem;
}
`
const Ultraline = styled(Heading)`
line-height: 1.125 !important;
text-transform: uppercase;
color: 'white';
caps: true;
&:nth-of-type(2) {
padding-left: 1.5rem;
@media screen and (min-width: 48em) {
padding-left: 6rem;
}
}
&:nth-of-type(3) {
text-align: center;
}
&:nth-of-type(4) {
text-align: right;
position: relative;
&:before {
content: '';
position: absolute;
clip-path: polygon(8% 0%, 100% 0%, 92% 100%, 0% 100%);
background-color: rgba(252, 252, 252, 0.625);
mix-blend-mode: overlay;
right: -0.5rem;
width: 9.5rem;
height: 2.5rem;
@media screen and (min-width: 32em) {
width: 20rem;
height: 5.5rem;
}
}
}
`
const Row = styled(Container)`
px: 3;
py: [4, 5];
color: 'black';
display: grid;
text-align: left;
h2 {
line-height: 1;
margin-bottom: 18px;
}
@media screen and (min-width: 48em) {
grid-gap: 24px;
grid-template-columns: 2fr 3fr;
}
`
const Super = styled(Text)`
background-color: rgb(228, 115, 45);
clip-path: polygon(4% 0%, 100% 0%, 96% 100%, 0% 100%);
color: rgb(255, 255, 255);
display: inline-block;
padding-bottom: 12px;
padding-left: 18px;
padding-right: 18px;
`
export default function Philosophy() {
return (
We're
at our best
when we're
making.
The Hack Club
Philosophy
Coding is a superpower.
Learning to code is uniquely like gaining a superpower: it converts
you from a consumer to a creator. Suddenly, computers become a tool
for creating.
Make, from anywhere.
There’s never been a better time for making: anywhere in the world,
anyone with a laptop and an internet connection can learn to make an
app. Building things has never been so globally democratized.
Hack, hack, hack.
The goal of Hack Club is to help you become a hacker.
{' '}
We want a space at every school where people are making interesting
things with code, every week. Most schools don’t provide that, so
we’re creating it in every school to make building things accessible
to everyone.
Start building.
Most coding classes teach you programming concepts instead of how to
write real code—it’s like trying to learn carpentry without any
wood. So at Hack Club, you learn to code entirely through building
things. You start with no experience and build and ship a project
every meeting.
Learn as you build.
Just as the best carpenters didn’t learn in the classroom, neither
did the best programmers. Through our{' '}
workshops, you’ll be walked through
building projects. Starting out, you won’t understand how the code
works, but you’ll build understanding as you go. You’ll get stuck
along the way, but we’re here to help.
Be part of a community.
Hack Club gives you a worldwide community of thousands of other
young makers to talk to. We’re artists, writers, engineers,
tinkerers, campers, filmmakers, volunteers. We make things. We help
one another. We have fun. Join us.
(t as any).util.gx('orange', 'red'),
margin: 'auto',
width: '600px',
maxWidth: '90%',
mb: 4,
borderRadius: 8,
color: 'white',
textAlign: 'center',
p: 4
}}
>
Join the movement!
)
}
================================================
FILE: pages/pizza.tsx
================================================
import {
Box,
Link,
Grid,
Image as ThemeImage,
Container,
Button,
Heading,
Text
} from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import ForceTheme from '../components/force-theme'
import Footer from '../components/footer'
import Nav from '../components/nav'
import Tilt from '../components/tilt'
import Ticker from 'react-ticker'
import { useState, useEffect } from 'react'
import Image from 'next/image'
const PizzaPage = () => {
const [isModalOpen, setIsModalOpen] = useState(false)
useEffect(() => {
setIsModalOpen(true)
}, [])
useEffect(() => {
if (isModalOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'auto'
}
}, [isModalOpen])
const getColor = idx => {
const colors = ['#EEA820', '#FF8C37', '#EC3750']
return colors[idx % colors.length]
}
const pizzasByClubs = [
{
sprite:
'https://cloud-l0q2898m7-hack-club-bot.vercel.app/0sprite__1_.png',
author: 'Thomas',
age: 18,
from: 'South Carolina',
response:
"I love pineapple pizza & hosting club meets! It's awesome how every week I get to get together with friends and build awesome open source projects. SO GLAD I STARTED MY CLUB!!!"
},
{
sprite: 'https://cloud-mpql3aoi9-hack-club-bot.vercel.app/0sprite.png',
author: 'Odysseus',
age: 14,
from: 'Epanomi',
response:
"I am addicted to margherita pizza and I am super excited to host club meetings! We meet every Saturday on Discord and we build projects together! On our next meeting, we will be creating a web-based operating system! I'm so happy to be a part of my club and Hack Club!"
},
{
sprite: 'https://cloud-7sioop5e1-hack-club-bot.vercel.app/0sprite.png',
author: 'Sarvesh',
age: 16,
from: 'Ottawa',
response:
'I love meat lovers pizza and sharing my passion for technology. I love to get together with friends with the same mindset as me and work on amazing open source projects! MAKING A HACK CLUB WAS A GREAT DECISION!!!'
},
{
sprite: 'https://cloud-8rvh6jo64-hack-club-bot.vercel.app/0sprite.png',
author: 'Dieter',
age: 18,
from: 'South Carolina',
response:
"I'm a big fan of Cheese and Spinach pizza—the texture is amazing! I started my club to fill a gap in computer science projects at my school. Leading this club allows me to challenge members; for instance, we use Sprig to create our own 2-bit games. It's Open Source, which significantly enhances our projects!"
},
{
sprite: 'https://cloud-2ca30e1bb-hack-club-bot.vercel.app/0sprite.png',
author: 'JC',
age: 17,
from: 'Massachusetts',
response:
"Leading a club is a lot of fun! You get to build cool stuff with other club members, but being a leader means you also get to teach people how to code. Plus who isn't excited about a Christmas pizza party (with pineapple of course)?"
},
{
sprite: 'https://cloud-d16y68pgi-hack-club-bot.vercel.app/0sprite.png',
author: 'Miguel',
age: 17,
from: 'Illinois',
response:
"I love Costco pizza, and we had some at my Hack Club's hackathon! I decided to lead the Hersey Hack Club to bring the magic of code to my classmates and build a coding community at my high school. Open source projects like Code Jams have given the Hersey Hack Club a great stream of new, constantly improving workshops to host for members!"
},
{
sprite:
'https://cloud-i23dx2r15-hack-club-bot.vercel.app/0sprite__8_.png',
author: 'Jaime',
age: 18,
from: 'South Carolina',
response:
'I love cheese pizzaaa (Ik I am kinda basic but it is good 😭). I lead the club because it is a great opportunity to meet people in my school who are so talented and skilled in areas where I may not be. And it is a great experience to interact and make friends with them!!!'
},
{
sprite:
'https://cloud-ed5wo5bt9-hack-club-bot.vercel.app/0sprite__1_.png',
author: 'Sarah',
age: 12,
from: 'Massachusetts',
response:
'I love pepperoni pizza and my Hack Club! I love leading a Hack Club and sharing cool open source projects. yay!!'
},
{
sprite: 'https://cloud-5kl9y9pup-hack-club-bot.vercel.app/0sprite.png',
author: 'Shubham',
age: 15,
from: 'Bay Area',
response:
"I love eating veggie pizza and hosting club meets at Mission San Jose High School's Hack Club! Hosting club meets is more than superficial for me—seeing everyone in the room, all exhibiting the same amount of excitement for code is something unique, and I'm glad to be hosting clubs meets for this passion to run wild."
}
]
return (
<>
{isModalOpen && (
setIsModalOpen(false)}
sx={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
>
e.stopPropagation()}
sx={{
bg: 'white',
borderRadius: '16px',
p: 4,
maxWidth: '600px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
position: 'relative'
}}
>
The pizza grant is ending! (for now)
Hey there! The pizza grant has ended, but we're working on a
system to bring it back soon. For now, we've temporarily disabled
submissions. Keep an eye on your Leaders newsletter for updates!
)}
Throw A Pizza Party
After Every Project
GitHub is providing pizza grants to thousands of
Hack Clubs. {/* You're not too late! */}
{/* */}
Create A Space for Makers
Hack Club is a place for technical teens to get together and build
projects together. Create a club at your high school and help
others discover the joy of coding through building projects.
Join A Community of Teen Hackers
In our Slack community of over 55,000 hackers, you'll be invited
to a space for Hack Club leaders to ask questions & chat, share
projects, & attend events.
Tools & Perks To Lead Your Club
As a club leader, you'll get to use community projects like Sprig
& Jams in your Hack Club! Your Club will also get free access to
Zoom Pro & Figma Pro.
Lead Weekly Club Meetings
Every week you can craft club meetings to help makers at your
school discover the joy of coding. Get inspired by some Jams we
built to help you lead your club.{' '}
Pizzas & Clubs by Leaders
{' '}
{() => (
{pizzasByClubs.map((pizzaByClub, idx) => (
{pizzaByClub.author} ({pizzaByClub.age}) from{' '}
{pizzaByClub.from}
{pizzaByClub.response}
))}
)}
Above
Get Your Pizza
Start Your Club
Every Hack Club starts with a teenager like you who wants to
bring an amazing community to their high school.
Run Your Meeting
Host an engaging workshop where your club members create and
complete their own coding projects. Want ideas? check out{' '}
ysws.hackclub.com{' '}
or{' '}
jams.hackclub.com!
{/* */}
Order Pizza
Submit your members' projects on{' '}
the dashboard{' '}
and recieve $5 every hour up to $20 per project for pizza!
{/*
p.s. if you already lead a club, you can still get pizza! draw a
pizza in{' '}
#pizza-party
*/}
Need help getting your Pizza Grant? Give us a hollar at{' '}
clubs@hackclub.com.