├── .devcontainer └── devcontainer.json ├── .github └── workflows │ └── caniuse-update.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── components ├── Card.js ├── FancyCard.js ├── Map.js ├── Plausible.js └── mdx │ ├── Alert.js │ ├── Code.js │ └── MDXContent.js ├── content ├── brilliant.mdx ├── codeday.mdx ├── figma.mdx ├── orpheus.mdx ├── stickers.mdx └── zoom.mdx ├── lib ├── content.js ├── github.js └── utils.js ├── manifest.js ├── next.config.js ├── package.json ├── pages ├── 404.js ├── [slug].js ├── _app.js └── api │ ├── [key].js │ └── auth │ └── [...nextauth].js ├── prettier.config.js ├── public ├── adafruit-banner.jpg ├── banner.png ├── cards │ ├── bakebuild.png │ ├── boba.png │ ├── cider.png │ ├── hackaccino.png │ ├── hackcraft.png │ ├── hacklet.png │ ├── jams.png │ ├── nest.png │ ├── onboard.png │ ├── puttingtheyouincpu.png │ ├── raspapi.webp │ ├── shipwrecked.png │ ├── sidequests.png │ ├── sinerider.png │ ├── solder.png │ ├── someassemblyrequired.png │ ├── spaces.png │ ├── sprig.png │ ├── swirl.svg │ ├── tonic.png │ ├── toppings.png │ └── visioneer.png ├── club-stats.png ├── codeday.png ├── dino_handing_docs-2.png ├── figma.png ├── leaflet.css ├── robots.txt ├── scrollbar.css ├── sparkle.svg ├── stickers.png ├── style.css └── zoom.png ├── styles └── globals.css └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "test": "echo \"No test script defined in package.json\"", 4 | "build": "yarn install && yarn build", 5 | "launch": "yarn install && yarn dev" 6 | } 7 | } -------------------------------------------------------------------------------- /.github/workflows/caniuse-update.yml: -------------------------------------------------------------------------------- 1 | name: Update Browserslist database 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 2 1,15 * *' 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update-browserslist-database: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Configure git 21 | run: | 22 | git config --global user.email "action@github.com" 23 | git config --global user.name "GitHub Action" 24 | - name: Update Browserslist database and create PR if applies 25 | uses: c2corg/browserslist-update-action@v2 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | branch: browserslist-update 29 | base_branch: main 30 | commit_message: 'build: update Browserslist db' 31 | title: 'build: update Browserslist db' 32 | body: Auto-generated by [browserslist-update-action](https://github.com/c2corg/browserslist-update-action/). Caniuse database has been updated. Review changes, merge this PR, and be merry. 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.yarn 6 | /.pnp 7 | .pnp.js 8 | .pnp.cjs 9 | .pnp.loader.mjs 10 | package-lock.json 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.yarn 6 | /.pnp 7 | .pnp.js 8 | .pnp.cjs 9 | .pnp.loader.mjs 10 | package-lock.json 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Hack Foundation 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 |

2 | Hack Club logo 3 |

4 |

5 | The new Hack Club Toolbox! 6 |

7 | 8 | Deals for Hack Clubbers—powered by [Next.js], [Theme UI] & [MDX]. 9 | 10 | [next.js]: https://nextjs.org 11 | [mdx]: https://mdxjs.com 12 | [theme ui]: http://theme-ui.com 13 | 14 | ## How do I add a card? 15 | 16 | All the cards except for the current ones are stored in `manifest.js`. If you take a look at it, you'll notice it's in JSON: 17 | 18 | ```js 19 | export default [ 20 | { 21 | category: 'Perks', 22 | color: 'primary', 23 | icon: 'bag', 24 | items: [ 25 | { 26 | name: 'Zoom Pro Meetings', 27 | description: 'Run Zoom Pro meetings for free in your Hack Club', 28 | icon: 'welcome', 29 | external: false, 30 | url: '/zoom.mdx', 31 | forUseBy: 'clubbers' 32 | }, 33 | ... 34 | ] 35 | }, 36 | ... 37 | ] 38 | ``` 39 | 40 | To add a card to an existing category, you simply add to the cards in the category, like so: 41 | 42 | ```diff 43 | export default [ 44 | { 45 | category: 'Perks', 46 | color: 'primary', 47 | icon: 'bag', 48 | items: [ 49 | { 50 | name: 'Zoom Pro Meetings', 51 | description: 'Run Zoom Pro meetings for free in your Hack Club', 52 | icon: 'welcome', 53 | external: false, 54 | url: '/zoom.mdx', 55 | forUseBy: 'clubbers' 56 | }, 57 | + { 58 | + name: 'Theme', 59 | + description: "Hack Club's theme, using Theme UI", 60 | + icon: 'grid', 61 | + external: true, 62 | + url: 'https://theme.hackclub.com', 63 | + forUseBy: 'everyone' 64 | + } 65 | ] 66 | }, 67 | ``` 68 | 69 | To add a new category, you'll follow a similar pattern at the bottom of the file: 70 | 71 | ```diff 72 | export default [ 73 | { 74 | category: 'Perks', 75 | color: 'primary', 76 | icon: 'bag', 77 | items: [ 78 | { 79 | name: 'Zoom Pro Meetings', 80 | description: 'Run Zoom Pro meetings for free in your Hack Club', 81 | icon: 'welcome', 82 | external: false, 83 | url: '/zoom.mdx', 84 | forUseBy: 'clubbers' 85 | }, 86 | ] 87 | }, 88 | + { 89 | + category: '', 90 | + color: '', 91 | + icon: '' 92 | + items: [ 93 | + ... 94 | + ] 95 | + } 96 | ] 97 | ``` 98 | 99 | _For `color` and `icon`, you can use [Hack Club's theme](https://theme.hackclub.com)._ 100 | 101 | When you edit the file to make these changes, you'll be asked to make a pull request. 102 | 103 | Let us know if you have any questions in [Slack](https://hackclub.slack.com)! 104 | -------------------------------------------------------------------------------- /components/Card.js: -------------------------------------------------------------------------------- 1 | import { Box, Card, Flex, Heading, Text, Link, Badge } from 'theme-ui' 2 | import Icon from '@hackclub/icons' 3 | import { useRouter } from 'next/router' 4 | import { buildRoute } from '../lib/utils' 5 | 6 | export function InternalCard({ 7 | item, 8 | onMobile, 9 | onHover, 10 | onHoverLeave, 11 | categories, 12 | query, 13 | sx = {} 14 | }) { 15 | const router = useRouter() 16 | return ( 17 | 20 | router.push( 21 | buildRoute({ 22 | route: item.url, 23 | categories, 24 | query 25 | }), 26 | undefined, 27 | { 28 | scroll: false 29 | } 30 | ) 31 | } 32 | target={!item.external ? '' : '_blank'} 33 | className={`plausible-event-name=${item.name.replace(' ', '-')}`} 34 | sx={{ 35 | textDecoration: 'none', 36 | alignSelf: 'flex-start', 37 | zIndex: 999, 38 | cursor: 'pointer', 39 | transition: 'transform .125s ease-in-out, box-shadow .125s ease-in-out', 40 | ':hover,:focus': { 41 | transform: 'scale(1.0225)' 42 | }, 43 | p: '0 !important', 44 | ...(!onMobile && { 45 | '@media screen and (min-width: 992px)': { 46 | maxHeight: '175px', 47 | position: 'relative', 48 | '::after': { 49 | content: '""', 50 | position: 'absolute', 51 | zIndex: 1, 52 | bottom: 0, 53 | left: 0, 54 | pointerEvents: 'none', 55 | backgroundImage: 56 | 'linear-gradient(to bottom, rgba(255,255,255, 0), rgba(255,255,255, 1) 90%)', 57 | width: '100%', 58 | height: '75px' 59 | }, 60 | ':hover': { 61 | overflow: 'visible', 62 | zIndex: 1000, 63 | '::after': { 64 | display: 'none' 65 | }, 66 | '.more': { 67 | display: 'none' 68 | } 69 | } 70 | } 71 | }), 72 | ...sx 73 | }} 74 | onMouseEnter={onHover} 75 | onMouseLeave={onHoverLeave} 76 | > 77 | 93 | 94 | 95 | 104 | 114 | {item.icon && } 115 | 116 | 117 | {item.name} 118 | 119 | 120 | {item.description} 121 | 122 | 123 | {item.current && ( 124 | 133 | #current 134 | 135 | )} 136 | {item.category && ( 137 | 146 | #{item.category.toLowerCase()} 147 | 148 | )} 149 | 150 | 151 | 152 | ) 153 | } 154 | 155 | export function ExternalCard({ 156 | item, 157 | onMobile, 158 | onHover, 159 | onHoverLeave, 160 | sx = {} 161 | }) { 162 | return ( 163 | 211 | 227 | 228 | 229 | 239 | 249 | {item.icon && } 250 | 251 | 252 | {item.name} 253 | 254 | 255 | {item.description} 256 | 257 | 258 | {item.current && ( 259 | 268 | #current 269 | 270 | )} 271 | {item.category && ( 272 | 281 | #{item.category.toLowerCase()} 282 | 283 | )} 284 | 285 | 286 | 287 | ) 288 | } 289 | 290 | export default function InfoCard(props) { 291 | if (props.item.external) return 292 | else return 293 | } 294 | -------------------------------------------------------------------------------- /components/FancyCard.js: -------------------------------------------------------------------------------- 1 | import { Box, Card, Image, Link, Text } from 'theme-ui' 2 | import Icon from '@hackclub/icons' 3 | import { useState } from 'react' 4 | 5 | export default function FancyCard({ 6 | item, 7 | sx = {}, 8 | onMobile, 9 | onHover, 10 | onHoverLeave 11 | }) { 12 | const { 13 | background, 14 | titleColor, 15 | descriptionColor, 16 | arrowColor, 17 | name, 18 | description, 19 | img, 20 | url 21 | } = item 22 | return ( 23 | 41 | 55 | onHover()} 58 | onMouseLeave={() => onHoverLeave()} 59 | sx={{ 60 | mr: 3, 61 | background: background.includes('linear-gradient') 62 | ? '' 63 | : background, 64 | position: 'relative', 65 | p: '0 !important', 66 | width: '100%', 67 | '@media screen and (max-width: 992px)': { 68 | height: '100%' 69 | }, 70 | height: onMobile ? '100%' : 'initial', 71 | ...(!onMobile && { 72 | '@media screen and (min-width: 992px)': { 73 | maxHeight: '175px', 74 | position: 'relative', 75 | '::after': { 76 | content: '""', 77 | position: 'absolute', 78 | zIndex: 1, 79 | bottom: 0, 80 | left: 0, 81 | pointerEvents: 'none', 82 | backgroundColor: background.includes('linear-gradient') 83 | ? 'black' 84 | : background, 85 | width: '100%', 86 | height: '25px' 87 | }, 88 | ':hover': { 89 | overflow: 'visible', 90 | zIndex: 1000, 91 | '::after': { 92 | display: 'none' 93 | }, 94 | '.more': { 95 | display: 'none' 96 | } 97 | } 98 | } 99 | }), 100 | overflow: "hidden", 101 | }} 102 | > 103 | 112 | 118 | 119 | 135 | 136 | 137 | 150 | 158 | 159 | {name} 160 | 161 | 162 | {description} 163 | 164 | 165 | 166 | 167 | 168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /components/Map.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled' 2 | import { useState, useEffect } from 'react' 3 | import { Box, Card, Embed } from 'theme-ui' 4 | import { 5 | MapContainer, 6 | TileLayer, 7 | Marker, 8 | Popup, 9 | Tooltip, 10 | useMapEvents 11 | } from 'react-leaflet' 12 | import 'leaflet/dist/leaflet.css' 13 | import L from 'leaflet' 14 | 15 | const mapIcon = new L.Icon({ 16 | iconUrl: 'https://assets.hackclub.com/icon-rounded.png', 17 | iconRetinaUrl: 'https://assets.hackclub.com/icon-rounded.png', 18 | iconAnchor: null, 19 | popupAnchor: null, 20 | shadowUrl: null, 21 | shadowSize: null, 22 | shadowAnchor: null, 23 | iconSize: new L.Point(20, 20), 24 | popupAnchor: [0, 0] 25 | }) 26 | 27 | const StyledMapContainer = styled(MapContainer)` 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | ` 32 | 33 | function MapEvents({ showFrame }) { 34 | const map = useMapEvents({ 35 | dblclick() { 36 | window.open('https://hackclub.github.io/map/', '_blank') 37 | } 38 | }) 39 | } 40 | 41 | export default function Map() { 42 | const [clubs, setClubs] = useState([]) 43 | const [center, setCenter] = useState( 44 | window.innerWidth > 767.98 ? [35.683, -25.099] : [55, -100] 45 | ) 46 | const [frame, setFrame] = useState(false) // Originally used to overlay a map over entire page. Not in use right now 47 | 48 | useEffect(() => { 49 | fetch( 50 | 'https://api2.hackclub.com/v0.1/Club%20Applications/Clubs%20Dashboard' 51 | ) 52 | .then(res => res.json()) 53 | .then(json => { 54 | setClubs( 55 | json 56 | .filter( 57 | club => 58 | club.fields.Status && 59 | club.fields.Status != 'inactive' && 60 | club.fields.Status != 'unknown' && 61 | club.fields.Latitude && 62 | club.fields.Longitude 63 | ) 64 | .map(club => ({ 65 | latitude: club.fields.Latitude, 66 | longitude: club.fields.Longitude, 67 | venue: club.fields.Venue 68 | })) 69 | ) 70 | }) 71 | .catch(err => console.log(err)) 72 | }, []) 73 | 74 | return ( 75 | <> 76 | {/* 77 | 85 | 89 | {clubs.map((club, idx) => ( 90 | 95 | {club.venue} 96 | 97 | ))} 98 | setFrame(true)} /> 99 | 100 | */} 101 |