├── .gitignore ├── README.md ├── components ├── ConfettiOnSuccess.js ├── Popup.js ├── Timeline.js ├── color-switcher.js ├── flag.js ├── force-theme.js ├── fullstory.js ├── locale-switcher.js ├── nprogress.js └── plausible.js ├── lib ├── airtable.js ├── countrycodes.js └── helpers.js ├── manifest.js ├── next.config.js ├── package.json ├── pages ├── [application] │ └── [leader] │ │ ├── [type].js │ │ ├── index.js │ │ ├── review.js │ │ └── status.js ├── _app.js ├── api │ ├── [type] │ │ └── save.js │ ├── auth │ │ └── [token].js │ ├── invite.js │ ├── login.js │ ├── manifest.js │ ├── remove.js │ └── submit.js └── index.js ├── prettier.config.js ├── public ├── card_1.png ├── favicon.png ├── fullstory.js ├── orpheus_flag.svg ├── underline-green.svg └── underline.svg ├── styles └── app.css ├── translations ├── en-US.js └── pt-BR.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | .next 3 | node_modules 4 | .DS_Store 5 | .env 6 | .env.* 7 | package-lock.json 8 | .vercel 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hack Club Applications 2 | 3 | Apply to [Hack Club](https://hackclub.com/), built with [React](https://reactjs.org/) and [Next.js](https://nextjs.org). 4 | 5 | ## Setup 6 | 7 | 1. Clone the repository and enter it 8 | 9 | ``` 10 | git clone https://github.com/hackclub/apply.git 11 | cd apply 12 | ``` 13 | 14 | 2. Install packages & run 15 | 16 | ``` 17 | yarn && yarn run dev 18 | ``` 19 | 20 | 4. It should now be running, open [localhost:3000](http://localhost:3000) to view it 21 | -------------------------------------------------------------------------------- /components/ConfettiOnSuccess.js: -------------------------------------------------------------------------------- 1 | import ReactCanvasConfetti from 'react-canvas-confetti' 2 | import { useCallback, useEffect, useRef, useState } from 'react' 3 | import { useRouter } from 'next/router' 4 | 5 | function randomInRange(min, max) { 6 | return Math.random() * (max - min) + min 7 | } 8 | 9 | const canvasStyles = { 10 | position: 'fixed', 11 | pointerEvents: 'none', 12 | width: '100%', 13 | height: '100%', 14 | top: 0, 15 | left: 0 16 | } 17 | 18 | export default function ConfettiOnSuccess({ applicationStatus }) { 19 | const refAnimationInstance = useRef(null) 20 | const router = useRouter() 21 | 22 | function getFireworkAnimationSettings(originXA, originXB) { 23 | return { 24 | startVelocity: 35, 25 | spread: 360, 26 | ticks: 100, 27 | zIndex: 0, 28 | particleCount: 100, 29 | origin: { 30 | x: randomInRange(originXA, originXB), 31 | y: Math.random() 32 | } 33 | } 34 | } 35 | 36 | const getInstance = useCallback(instance => { 37 | refAnimationInstance.current = instance 38 | }, []) 39 | 40 | const nextFireworkTickAnimation = useCallback(() => { 41 | if (refAnimationInstance.current) { 42 | refAnimationInstance.current(getFireworkAnimationSettings(0.2, 0.3)) 43 | refAnimationInstance.current(getFireworkAnimationSettings(0.7, 0.9)) 44 | } 45 | }, []) 46 | 47 | useEffect(() => { 48 | if (applicationStatus !== 'rejected') { 49 | setTimeout(nextFireworkTickAnimation, 1000) 50 | setTimeout(nextFireworkTickAnimation, 1800) 51 | setTimeout(nextFireworkTickAnimation, 2600) 52 | setTimeout(nextFireworkTickAnimation, 3400) 53 | setTimeout(nextFireworkTickAnimation, 4200) 54 | } 55 | }, []) 56 | 57 | return ( 58 | <> 59 | 60 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /components/Popup.js: -------------------------------------------------------------------------------- 1 | import { Button, Box, Card, Text } from 'theme-ui' 2 | 3 | export default function Popup({ content, onClose }) { 4 | return ( 5 | 18 | 26 | {content} 27 | 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/Timeline.js: -------------------------------------------------------------------------------- 1 | import { Box, Card, Text, Flex } from 'theme-ui' 2 | import Icon from '@hackclub/icons' 3 | import { useState, useEffect } from 'react' 4 | import { useRouter } from 'next/router' 5 | import { returnLocalizedMessage } from '../lib/helpers' 6 | 7 | export default function TimelineCard({ 8 | params, 9 | applicationsRecord, 10 | leaderRecord, 11 | trackerRecord, 12 | saved 13 | }) { 14 | const [clubProgress, setClubProgress] = useState( 15 | (Object.keys(applicationsRecord.fields).length - 6) / 10 16 | ) 17 | const [leaderProgress, setLeaderProgress] = useState( 18 | (Object.keys(leaderRecord.fields).length - 7) / 9 19 | ) 20 | const [status, setStatus] = useState(0) 21 | const [submission, setSubmission] = useState(0) 22 | const applicationStatus = trackerRecord[0]?.fields.Status 23 | const router = useRouter() 24 | 25 | useEffect(() => { 26 | if (leaderProgress > 1) { 27 | setLeaderProgress(1) 28 | } else if (leaderProgress < 0) { 29 | setLeaderProgress(0) 30 | } 31 | if (clubProgress > 1) { 32 | setClubProgress(1) 33 | } else if (clubProgress < 0) { 34 | setClubProgress(0) 35 | } 36 | 37 | applicationsRecord.fields['Submitted'] 38 | ? setSubmission(100) 39 | : setSubmission(0) 40 | }, [saved]) 41 | 42 | useEffect(() => { 43 | if (applicationStatus === 'rejected') { 44 | setStatus(100) 45 | } else if (applicationsRecord.fields['Submitted'] != 1) { 46 | setStatus(0) 47 | } else if (applicationStatus === 'awaiting onboarding') { 48 | setStatus(75) 49 | } else if (applicationStatus === 'applied') { 50 | setStatus(50) 51 | } else if (applicationStatus === 'inactive') { 52 | setStatus(100) 53 | } else if (applicationStatus === 'onboarded') { 54 | setStatus(100) 55 | } else { 56 | setStatus(0) 57 | } 58 | }, []) 59 | 60 | const timelineRouting = [ 61 | { 62 | href: `/${params.application}/${params.leader}`, 63 | color: '#33d6a6', 64 | icon: 'home', 65 | progress: null, 66 | value: returnLocalizedMessage(router.locale, 'HOME'), 67 | slug: '' 68 | }, 69 | { 70 | href: `/${params.application}/${params.leader}/leader`, 71 | color: '#33d6a6', 72 | icon: 'profile', 73 | progress: `${leaderProgress * 100}%`, 74 | value: returnLocalizedMessage(router.locale, 'LEADER_PROFILE'), 75 | slug: 'leader' 76 | }, 77 | { 78 | href: `/${params.application}/${params.leader}/club`, 79 | color: '#33d6a6', 80 | icon: 'flag', 81 | progress: `${clubProgress * 100}%`, 82 | value: returnLocalizedMessage(router.locale, 'YOUR_CLUB'), 83 | slug: 'club' 84 | }, 85 | { 86 | href: `/${params.application}/${params.leader}/review`, 87 | color: '#33d6a6', 88 | icon: 'checkmark', 89 | progress: `${submission}%`, 90 | value: returnLocalizedMessage(router.locale, 'REVIEW'), 91 | slug: 'review' 92 | }, 93 | { 94 | href: `/${params.application}/${params.leader}/status`, 95 | color: `${ 96 | applicationStatus === 'inactive' 97 | ? '#ff8c37' 98 | : applicationStatus != 'rejected' 99 | ? '#33d6a6' 100 | : '#ec3750' 101 | }`, 102 | icon: 'emoji', 103 | progress: `${status}%`, 104 | value: returnLocalizedMessage(router.locale, 'STATUS'), 105 | slug: 'status' 106 | } 107 | ] 108 | 109 | function ProgressBar({ item }) { 110 | return ( 111 | 125 | 136 | 137 | ) 138 | } 139 | 140 | return ( 141 | svg': { display: ['none', 'sticky'] } 149 | }} 150 | > 151 | 160 | {timelineRouting.map((item, idx) => { 161 | return ( 162 | <> 163 | {item.value === 'Your Club' && 164 | leaderRecord.fields['Email'] != 165 | applicationsRecord.fields['Leaders Emails'][0] ? null : ( 166 | <> 167 | 174 | 189 | {item.progress == null ? null : ( 190 | 191 | )} 192 |
193 | { 195 | { 196 | item.slug === 'status' 197 | ? applicationsRecord.fields['Submitted'] === 198 | true 199 | ? router.push(item.href) 200 | : null 201 | : router.push(item.href) 202 | } 203 | }} 204 | > 205 | 229 | 230 | { 275 | { 276 | item.slug === 'status' 277 | ? applicationsRecord.fields['Submitted'] === 278 | true 279 | ? router.push(item.href) 280 | : null 281 | : router.push(item.href) 282 | } 283 | }} 284 | > 285 | {item.value} 286 | 287 |
288 |
289 |
290 | 291 | )} 292 | 293 | ) 294 | })} 295 |
296 | 297 | {/* mobile timeline */} 298 | 299 | 307 | {timelineRouting.map((item, idx) => { 308 | return ( 309 | 316 | 331 |
332 | { 334 | { 335 | item.slug === 'status' 336 | ? applicationsRecord.fields['Submitted'] === true 337 | ? router.push(item.href) 338 | : null 339 | : router.push(item.href) 340 | } 341 | }} 342 | > 343 | 357 | 358 |
359 | 360 | 390 | {item.value} 391 | 392 |
393 |
394 | ) 395 | })} 396 |
397 |
398 | ) 399 | } 400 | -------------------------------------------------------------------------------- /components/color-switcher.js: -------------------------------------------------------------------------------- 1 | import { IconButton, useColorMode } from 'theme-ui' 2 | 3 | const ColorSwitcher = props => { 4 | const [mode, setMode] = useColorMode() 5 | return ( 6 | setMode(mode === 'dark' ? 'light' : 'dark')} 8 | title={`Switch to ${mode === 'dark' ? 'light' : 'dark'} mode`} 9 | sx={{ 10 | position: 'absolute', 11 | top: [2, 3], 12 | right: [2, 3], 13 | color: 'primary', 14 | cursor: 'pointer', 15 | borderRadius: 'circle', 16 | transition: 'box-shadow .125s ease-in-out', 17 | ':hover,:focus': { 18 | boxShadow: '0 0 0 3px', 19 | outline: 'none' 20 | } 21 | }} 22 | {...props} 23 | > 24 | 25 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export default ColorSwitcher 40 | -------------------------------------------------------------------------------- /components/flag.js: -------------------------------------------------------------------------------- 1 | const Flag = props => ( 2 | 8 | 49 | 50 | ) 51 | 52 | export default Flag 53 | -------------------------------------------------------------------------------- /components/force-theme.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useColorMode } from 'theme-ui' 3 | 4 | const ForceTheme = ({ theme }) => { 5 | const [colorMode, setColorMode] = useColorMode() 6 | // This looks redundant, but is done on purpose. 7 | // See for similar issue on seperate project: https://github.com/reduxjs/react-redux/issues/1640#issuecomment-705892206 8 | useEffect(() => { 9 | setColorMode(theme) 10 | }) 11 | return null 12 | } 13 | 14 | export default ForceTheme 15 | -------------------------------------------------------------------------------- /components/fullstory.js: -------------------------------------------------------------------------------- 1 | import Script from 'next/script' 2 | 3 | const Fullstory = () =>