├── .gitignore ├── LICENSE ├── README.md ├── components ├── ama-rsvp.js ├── api.mdx ├── event.js ├── month.js ├── nav.js ├── rsvp.js └── sparkles.js ├── hooks └── use-form.js ├── lib ├── data.js ├── use-prefers-reduced-motion.js └── use-random-interval.js ├── mdx-components.js ├── next.config.js ├── package.json ├── pages ├── [slug].js ├── _app.js ├── _document.js ├── ama-success.js ├── api │ ├── amas.js │ ├── events │ │ ├── all-monthly.js │ │ ├── all.js │ │ ├── upcoming-monthly.js │ │ └── upcoming.js │ └── rsvp.js ├── data.js ├── index.js └── past.js ├── prettier.config.js ├── public ├── card.png └── robots.txt ├── vercel.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | .next 3 | node_modules 4 | .DS_Store 5 | .env 6 | .vercel 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Hack Club 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Hack Club logo

2 |

Hack Club Events

3 |

The source code for events.hackclub.com

4 | 5 | ## Development 6 | 7 | To get started, run the following in your terminal: 8 | 9 | Download the code to your computer: 10 | 11 | $ git clone https://github.com/hackclub/events && cd events 12 | 13 | Install dependencies: 14 | 15 | $ yarn 16 | 17 | Start running the website on your computer: 18 | 19 | $ yarn run dev 20 | 21 | And then open up your web browser and go to [localhost:3000](http://localhost:3000). 22 | 23 | Powered by [Next.js] with [MDX], [Theme UI], & [Hack Club Theme]. 24 | 25 | --- 26 | 27 | Hack Club, 2023. MIT License. 28 | 29 | [next.js]: https://nextjs.org 30 | [mdx]: https://mdxjs.com 31 | [theme ui]: https://theme-ui.com 32 | [hack club theme]: https://theme.hackclub.com 33 | -------------------------------------------------------------------------------- /components/ama-rsvp.js: -------------------------------------------------------------------------------- 1 | import { Button, Card, Heading, Text, Link } from 'theme-ui' 2 | 3 | const AMARsvp = ({ id, amaId }) => { 4 | return ( 5 | 6 | 7 | RSVP for this AMA 8 | 9 | 10 | Click the button below. 11 | 12 | 13 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default AMARsvp 30 | -------------------------------------------------------------------------------- /components/api.mdx: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | A simple JSON API to events on this site. 4 | 5 | ## Endpoints 6 | 7 | - [`/api/events/upcoming`](/api/events/upcoming) – all upcoming events 8 | - [`/api/events/all-upcoming`](/api/events/upcoming) – all upcoming, grouped by month 9 | - [`/api/events/all`](/api/events/all) – all events ever 10 | - [`/api/events/all-monthly`](/api/events/all) – all events, grouped by month 11 | - [`/api/amas`](/api/amas) – all AMAs ever 12 | 13 | ## Event Schema 14 | 15 | - IDs are internal to our Airtable. 16 | - Descriptions are in plain text but sometimes use Markdown formatting. 17 | 18 | ### Regular event 19 | 20 | ```json 21 | { 22 | "id": "recTSGw3ZQh5gbOB2", 23 | "slug": "code-in-the-dark", 24 | "title": "Code in the Dark", 25 | "desc": "On a live Zoom call, we’re going to get as many people as we can to compete in 5 minute rounds to reproduce popular websites without previewing the result while making it. Once each round is over, everyone will demo their creations and we’ll start the next round.", 26 | "leader": "@amogh", 27 | "cal": "https://www.google.com/calendar/render?action=TEMPLATE&text=Code%20in%20the%20Dark&details=A%20Hack%20Club%20Event%20by%20@amogh&dates=20200402T000000Z%2F20200402T010000Z", 28 | "start": "2020-04-02T00:00:00.000Z", 29 | "end": "2020-04-02T01:00:00.000Z", 30 | "youtube": null, 31 | "ama": false, 32 | "amaId": "", 33 | "amaAvatar": "", 34 | "avatar": "https://dl.airtable.com/.attachmentThumbnails/74bb685e087f1c0851a3dc73be0ce133/413d2f59" 35 | } 36 | ``` 37 | 38 | ### AMA 39 | 40 | ```json 41 | { 42 | "id": "reczFFj0eHvpj3BUg", 43 | "slug": "ama-with-guillermo-rauch", 44 | "title": "AMA with Guillermo Rauch", 45 | "desc": "Guillermo is the founder of [Vercel](https://vercel.com) (formerly ZEIT, makers of Next.js & Now) and is the creator of the wildly popular open source projects socket.io, mongoose, and slackin. At age 16, he was named a core developer of MooTools, a JavaScript framework that predated jQuery.", 46 | "leader": "@lachlanjc", 47 | "cal": "https://www.google.com/calendar/render?action=TEMPLATE&text=AMA%20with%20Guillermo%20Rauch&details=A%20Hack%20Club%20Event%20by%20@lachlanjc&dates=20200514T200000Z%2F20200514T210000Z", 48 | "start": "2020-05-14T20:00:00.000Z", 49 | "end": "2020-05-14T21:00:00.000Z", 50 | "youtube": "https://youtu.be/PXlDzMMZydk", 51 | "ama": true, 52 | "amaId": "recx1wwapHrBPZ3Mq", 53 | "amaAvatar": "https://dl.airtable.com/.attachmentThumbnails/399a28f9be682aeac7742a3f0dbfbab0/d12faf1d", 54 | "avatar": "https://dl.airtable.com/.attachmentThumbnails/3f622515afe02ee23f1283c02832ff80/499f3a5c" 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /components/event.js: -------------------------------------------------------------------------------- 1 | import { Box, Text, Flex, Avatar, Heading } from 'theme-ui' 2 | import tt from 'tinytime' 3 | import Link from 'next/link' 4 | import Sparkles from './sparkles' 5 | 6 | const past = dt => new Date(dt) < new Date() 7 | const now = (start, end) => 8 | new Date() > new Date(start) && new Date() < new Date(end) 9 | 10 | const Event = ({ id, slug, title, desc, leader, avatar, start, end, cal }) => ( 11 | 12 | 22 | 34 | 35 | {tt('{MM} {Do}').render(new Date(start))}{' '} 36 | {tt('{h}:{mm}').render(new Date(start))}– 37 | {tt('{h}:{mm} {a}').render(new Date(end))} 38 | 39 | 40 | 41 | {title} 42 | 43 | 49 | {now(start, end)} 50 | {!avatar.includes('emoji') && ( 51 | 57 | )} 58 | {leader} 59 | 60 | {now(start, end) && ( 61 | 72 | )} 73 | 74 | 75 | ) 76 | 77 | export default Event 78 | -------------------------------------------------------------------------------- /components/month.js: -------------------------------------------------------------------------------- 1 | import { Heading, Grid } from 'theme-ui' 2 | import { format } from 'date-fns' 3 | import Event from './event' 4 | 5 | export default ({ month, events }) => ( 6 | <> 7 | 8 | {format(new Date(`${month}-02`), 'MMMM yyyy')} 9 | 10 | 21 | {events.map(event => ( 22 | 23 | ))} 24 | 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /components/nav.js: -------------------------------------------------------------------------------- 1 | import { ArrowLeft, Moon, GitHub } from 'react-feather' 2 | import { Box, Container, IconButton, Image, Link as A } from 'theme-ui' 3 | import { useColorMode } from 'theme-ui' 4 | import { useRouter } from 'next/router' 5 | import Link from 'next/link' 6 | 7 | const NavButton = ({ sx, ...props }) => ( 8 | 21 | ) 22 | 23 | const BackButton = ({ to = '/', text = 'All Events' }) => ( 24 | 25 | 30 | 31 | {text} 32 | 33 | 34 | ) 35 | 36 | const Flag = () => ( 37 | 44 | Hack Club flag 49 | 50 | ) 51 | 52 | const ColorSwitcher = props => { 53 | const [mode, setMode] = useColorMode() 54 | return ( 55 | setMode(mode === 'dark' ? 'light' : 'dark')} 58 | title="Reverse color scheme" 59 | > 60 | 61 | 62 | ) 63 | } 64 | 65 | export default () => { 66 | const [mode] = useColorMode() 67 | const router = useRouter() 68 | const home = router.pathname === '/' 69 | return ( 70 | 78 | 90 | {!home ? : } 91 | 97 | 98 | 99 | 100 | 101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /components/rsvp.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { Button, Card, Grid, Heading, Input, Label, Text } from 'theme-ui' 3 | 4 | const RSVP = ({ id }) => { 5 | const [phone, setPhone] = useState('') 6 | const [status, setStatus] = useState('') 7 | useEffect(() => { 8 | setTimeout(() => { 9 | setPhone('') 10 | setStatus('') 11 | }, 1500) 12 | }, [status]) 13 | return ( 14 | 15 | 16 | RSVP 17 | 18 | (This doesn’t work yet!) 19 |
{ 22 | e.preventDefault() 23 | fetch(`/api/rsvp?id=${id}`, { 24 | method: 'POST', 25 | data: JSON.stringify({ phone }) 26 | }) 27 | .then(r => r.json()) 28 | .then(r => setStatus(r.status)) 29 | }} 30 | > 31 | 32 |
33 | 34 | setPhone(e.target.value)} 41 | sx={{ bg: 'sunken' }} 42 | /> 43 |
44 | 139 | )} 140 | {/* !event.ama && */} 141 | 142 | 143 | {event.ama && ( 144 | 158 | {past(event.start) || event.youtube ? ( 159 | <> 160 | {event.youtube && ( 161 | 162 | 163 | 164 | )} 165 | 166 | 167 | 168 | 169 | ) : null} 170 | {!past(event.start) && ( 171 | 183 | {event.amaForm ? : ''} 184 | 185 | 186 | Not part of the{' '} 187 | Hack Club Slack? 188 | 189 | 190 | We’ll post the event recording to YouTube. 191 | 192 | 193 | 194 | 195 | )} 196 | 197 | )} 198 | 199 | ) 200 | 201 | let emojisRecachedThisPageload = false 202 | /** 203 | * Gets a full list of emojis from the Badger API. 204 | * Caches the result, and uses results from previous page loads but re-fetches in the background for future page loads. 205 | * This is necessary because we currently need to download _every_ emoji on each page load, which can take multiple seconds. 206 | * It would be nice if Badger could cache and only send emojis we need. 207 | */ 208 | async function getEmojis(bypassCache = false) { 209 | if (!bypassCache) { 210 | const cached = localStorage.getItem('emojis') 211 | if (cached) { 212 | if (!emojisRecachedThisPageload) { 213 | emojisRecachedThisPageload = true 214 | setTimeout( 215 | async () => 216 | localStorage.setItem( 217 | 'emojis', 218 | JSON.stringify(await getEmojis(true)) 219 | ), 220 | 500 221 | ) 222 | } 223 | 224 | return JSON.parse(cached) 225 | } 226 | } 227 | 228 | try { 229 | const emojiData = await ( 230 | await fetch('https://badger.hackclub.dev/api/emoji/') 231 | ).json() 232 | localStorage.setItem('emojis', JSON.stringify(emojiData)) 233 | return emojiData 234 | } catch (e) { 235 | console.error('Failed to fetch emojis:', e) 236 | return null 237 | } 238 | } 239 | 240 | /** 241 | * Renders the description of the event, replacing emoji shortcodes with actual images. 242 | * The event description is currently stored as HTML, so we manipulate it as a string directly. 243 | * This isn't an ideal solution, though; it may be better to store the description as Markdown, 244 | * especially considering we don't use any HTML-specific features at the moment. 245 | */ 246 | const EventDescription = ({ html: initialHTML }) => { 247 | const [html, setHtml] = useState(initialHTML) 248 | 249 | const emojiRegex = /(:[^ .,;`\u2013~!@#$%^&*(){}=\\:"<>?|A-Z]+:)/g 250 | 251 | useEffect(() => { 252 | async function replaceEmoji() { 253 | const emojis = await getEmojis() 254 | if (!emojis) return 255 | 256 | setHtml( 257 | html.replace(emojiRegex, match => { 258 | const emojiName = match.slice(1, -1) 259 | const emojiURL = emojis[emojiName] 260 | 261 | if (!emojiURL || !emojiURL.startsWith('http')) return match 262 | return `${emojiName}` 263 | }) 264 | ) 265 | } 266 | replaceEmoji() 267 | }, []) 268 | 269 | return ( 270 | 275 | ) 276 | } 277 | 278 | const Embed = props => ( 279 | 302 | ) 303 | 304 | const Subscribe = () => ( 305 | 314 | ) 315 | 316 | export default props => { 317 | const router = useRouter() 318 | 319 | if (router.isFallback) { 320 | return ( 321 | 322 | 323 | 324 | ) 325 | } else { 326 | return 327 | } 328 | } 329 | 330 | export const getStaticPaths = async () => { 331 | const events = await getEvents() 332 | const slugs = map(events, 'slug') 333 | const paths = slugs.map(slug => ({ params: { slug } })) 334 | return { paths, fallback: true } 335 | } 336 | 337 | export const getStaticProps = async ({ params }) => { 338 | const { slug } = params 339 | const events = await getEvents() 340 | const event = find(events, { slug }) 341 | event.html = await parse(event.desc) 342 | event.desc ??= null 343 | return { props: { event }, revalidate: 2 } 344 | } 345 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | import Meta from '@hackclub/meta' 5 | import '@hackclub/theme/fonts/reg-bold.css' 6 | import theme from '@hackclub/theme' 7 | import { ThemeUIProvider } from 'theme-ui' 8 | import Nav from '../components/nav' 9 | 10 | const App = ({ Component, pageProps }) => ( 11 | 12 | 19 |