├── .env.template ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report_issue_template.yml │ ├── feature_request_issue_template.md │ ├── internal_issue_template.md │ └── refactor_issue_template.md └── config.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── .idea ├── .gitignore ├── hackportal.iml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── modules.xml └── vcs.xml ├── .prettierignore ├── .prettierrc.js ├── .vscode └── launch.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── commitlint.config.js ├── components ├── AboutHeader.tsx ├── AppHeader.tsx ├── AppHeader2 │ ├── AdminNavbarColumn.tsx │ ├── AdminNavbarGrid.tsx │ ├── QRCodeReaderV2.tsx │ ├── QRScanDialog.tsx │ ├── core.tsx │ ├── visual1-no_offset.jpeg │ ├── visual2-with_offset.jpeg │ └── wrapper.tsx ├── AppNavbarBottom │ └── AppNavbarBottom.tsx ├── BackgroundCircles.tsx ├── ErrorCard.tsx ├── ErrorList.tsx ├── LoadIcon.tsx ├── NavLink.tsx ├── Particles │ ├── IParticlesProps.ts │ ├── Particles.tsx │ ├── ParticlesProvider.tsx │ └── index.ts ├── ProfileDialog.tsx ├── ScanType.tsx ├── adminComponents │ ├── AdminHeader.tsx │ ├── AdminStatsCard.tsx │ ├── AllUsersAdminView.tsx │ ├── FilterComponent.tsx │ ├── NivoBarChart.tsx │ ├── NivoPieChart.tsx │ ├── SuccessCard.tsx │ ├── UserAdminPagination.tsx │ ├── UserAdminView.tsx │ ├── UserAdminViews.tsx │ ├── UserList.tsx │ ├── challengeComponents │ │ ├── ChallengeForm.tsx │ │ └── ChallengeList.tsx │ └── eventComponents │ │ ├── EventDetailLink.tsx │ │ ├── EventForm.tsx │ │ └── EventList.tsx ├── authComponents │ ├── EmailInput.tsx │ └── PasswordInput.tsx ├── dashboardComponents │ ├── AnnouncementCards.tsx │ ├── AnsweredQuestion.tsx │ ├── ChallengeCard.tsx │ ├── DashboardHeader.tsx │ ├── PendingQuestion.tsx │ ├── QRCode.tsx │ ├── QRCodeReader.tsx │ ├── Sidebar.tsx │ └── SpotlightCard.tsx ├── hackerpackComponents │ ├── DocLinks.tsx │ ├── HackerpackDisplay.tsx │ ├── HackerpackSidebar.tsx │ ├── MobileDropdownMenu.tsx │ ├── hackerpack-settings.json │ ├── markdown │ │ └── index.md │ └── sidebar-content.json ├── homeComponents │ ├── Faq.tsx │ ├── FaqDisclosure.tsx │ ├── FaqExpandCloseButton.tsx │ ├── HackCountdown.module.css │ ├── HackCountdown.tsx │ ├── HomeAbout.tsx │ ├── HomeChallengeCard.tsx │ ├── HomeChallenges.tsx │ ├── HomeFaq.tsx │ ├── HomeFooter.tsx │ ├── HomeHero.tsx │ ├── HomeHero2.tsx │ ├── HomeNotif.tsx │ ├── HomePrizes.tsx │ ├── HomePrizesCard.tsx │ ├── HomeSchedule.tsx │ ├── HomeSpeakers.tsx │ ├── HomeSponsors.tsx │ ├── HomeTeam.tsx │ ├── HomeVideoStats.tsx │ ├── KeynoteSpeaker.tsx │ ├── MemberCards.tsx │ └── SponsorCard.tsx └── registerComponents │ ├── DisplayQuestion.module.css │ ├── DisplayQuestion.tsx │ └── RegistrationQuestion.tsx ├── docs ├── contributors.md ├── firebase-setup.md ├── hackerpack-setup.md ├── images │ ├── set-up-1.png │ ├── set-up-2.png │ ├── set-up-3.png │ ├── set-up-5.png │ ├── set-up-6.png │ ├── set-up-7.png │ ├── set-up-8.png │ └── set-up-9.png ├── index.md ├── registration-setup.md └── set-up.md ├── hackportal.config.ts ├── lib ├── admin │ └── init.ts ├── authorization │ └── check-authorization.ts ├── data.ts ├── firebase-client.ts ├── hooks.ts ├── profile │ └── user-data.ts ├── request-helper.ts ├── service-worker │ └── FCMContext.tsx ├── stats │ └── field.ts ├── types.d.ts ├── user │ └── AuthContext.tsx └── util.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── admin │ ├── challenges │ │ ├── add.tsx │ │ └── index.tsx │ ├── events │ │ ├── add.tsx │ │ └── index.tsx │ ├── index.tsx │ ├── resolve │ │ └── [questionId].tsx │ ├── scan │ │ └── index.tsx │ ├── stats.tsx │ └── users.tsx ├── api │ ├── acceptreject │ │ └── index.ts │ ├── announcements │ │ └── index.ts │ ├── applications │ │ ├── [applicationId].tsx │ │ └── index.tsx │ ├── challenges │ │ ├── index.tsx │ │ └── reorder.ts │ ├── creid.ts │ ├── dates │ │ └── index.tsx │ ├── keynotespeakers │ │ └── index.tsx │ ├── members │ │ └── index.ts │ ├── prizes.ts │ ├── questions │ │ ├── [userId] │ │ │ ├── answered.ts │ │ │ └── pending.ts │ │ ├── faq.ts │ │ ├── index.ts │ │ └── pending │ │ │ ├── [questionId].ts │ │ │ └── index.ts │ ├── registrations │ │ ├── status.ts │ │ └── toggle.ts │ ├── resume │ │ └── upload │ │ │ └── index.ts │ ├── scan.tsx │ ├── scan │ │ ├── create.ts │ │ ├── delete.ts │ │ └── update.ts │ ├── scantypes.tsx │ ├── schedule │ │ └── index.tsx │ ├── sponsor │ │ └── index.tsx │ ├── spotlightevents │ │ └── index.tsx │ ├── stats.ts │ ├── tokens │ │ └── index.ts │ ├── userinfo.tsx │ └── users │ │ ├── index.ts │ │ └── roles.ts ├── auth │ ├── index.tsx │ └── signOut.tsx ├── dashboard │ ├── index.tsx │ ├── questions.tsx │ └── submit.tsx ├── hackerpacks │ ├── [id].tsx │ └── index.tsx ├── index.tsx ├── profile.tsx ├── register.tsx └── schedule │ └── index.tsx ├── postcss.config.js ├── public ├── assets │ ├── bg2.jpeg │ ├── circles.svg │ ├── defaultPFP.jpg │ ├── fillerAsset.png │ ├── hp-logo.png │ └── mlh-sticker.png ├── favicon.ico ├── firebase-messaging-sw.js ├── fonts │ ├── DMSans-Bold.ttf │ ├── DMSans-Regular.ttf │ ├── DS-DIGI.TTF │ ├── FredokaOne-Regular.ttf │ ├── Nunito-Bold.ttf │ ├── Roboto-Black.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-Regular.ttf │ ├── Roboto-Thin.ttf │ ├── Wavehaus-128Bold.otf │ ├── Wavehaus-128Bold.ttf │ ├── Wavehaus-128Bold.woff │ ├── Wavehaus-128Bold.woff2 │ ├── Wavehaus-158ExtraBold.otf │ ├── Wavehaus-158ExtraBold.ttf │ ├── Wavehaus-158ExtraBold.woff │ ├── Wavehaus-158ExtraBold.woff2 │ ├── Wavehaus-28Thin.otf │ ├── Wavehaus-28Thin.ttf │ ├── Wavehaus-28Thin.woff │ ├── Wavehaus-28Thin.woff2 │ ├── Wavehaus-42Light.otf │ ├── Wavehaus-42Light.ttf │ ├── Wavehaus-42Light.woff │ ├── Wavehaus-42Light.woff2 │ ├── Wavehaus-66Book.otf │ ├── Wavehaus-66Book.ttf │ ├── Wavehaus-66Book.woff │ ├── Wavehaus-66Book.woff2 │ ├── Wavehaus-95SemiBold.otf │ ├── Wavehaus-95SemiBold.ttf │ ├── Wavehaus-95SemiBold.woff │ ├── Wavehaus-95SemiBold.woff2 │ └── stylesheet.css ├── icons │ ├── admin.svg │ ├── apple-touch-icon.png │ ├── calendar.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── file-plus.svg │ ├── googleicon.png │ ├── icon-128x128.png │ ├── icon-144x144.png │ ├── icon-152x152.png │ ├── icon-192x192.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── icon-72x72.png │ ├── icon-96x96.png │ ├── question.svg │ └── trophy.svg ├── majors.json ├── manifest.json ├── schools.json └── vercel.svg ├── scripts └── generate-types.js ├── server.js ├── styles ├── Home.module.css ├── globals.css └── tailwind.css ├── tailwind.config.js └── tsconfig.json /.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FIREBASE_API_KEY= 2 | NEXT_PUBLIC_FIREBASE_APP_ID= 3 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 4 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= 5 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 6 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= 7 | NEXT_PUBLIC_RESUME_UPLOAD_PASSWORD= 8 | NEXT_PUBLIC_RESUME_UPLOAD_SERVICE_ACCOUNT= 9 | NEXT_PUBLIC_VAPID_KEY= 10 | NEXT_PUBLIC_MEASUREMENT_ID= 11 | SERVICE_ACCOUNT_CLIENT_EMAIL= 12 | SERVICE_ACCOUNT_PRIVATE_KEY= 13 | SERVICE_ACCOUNT_PROJECT_ID= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Don't ever lint node_modules 2 | node_modules 3 | # Don't lint build output (make sure it's set to your correct build folder name) 4 | build 5 | # Don't lint nyc coverage output 6 | coverage 7 | 8 | # A temporary fix until rewrite is complete 9 | old 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_issue_template.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: '[Bug] ' 4 | labels: ['Type: Bug Report', 'Status: Awaiting Triage'] 5 | assignees: 6 | - abdullah50053 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! By giving as much detail as possible, you'll help us identify what went wrong and fixing the behavior. 12 | - type: textarea 13 | id: what-was-expected 14 | attributes: 15 | label: What did you expect to happen? 16 | description: Describe the expected functionality of the feature/component. 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: what-happened 21 | attributes: 22 | label: What happened? 23 | description: | 24 | Describe the behavior you encountered. Please be as detailed as possible. 25 | placeholder: Tell us what you see! 26 | validations: 27 | required: true 28 | - type: dropdown 29 | id: scope 30 | attributes: 31 | label: Scope 32 | description: What part of the project is this for? 33 | options: 34 | - Core 35 | - Auth 36 | - Admin/API 37 | - Profile 38 | - Registration 39 | - Events 40 | - Check-in 41 | - HackerPack 42 | validations: 43 | required: true 44 | - type: dropdown 45 | id: browsers 46 | attributes: 47 | label: What browsers are you seeing the problem on? 48 | multiple: true 49 | options: 50 | - Firefox 51 | - Chrome 52 | - Safari 53 | - Microsoft Edge 54 | - type: textarea 55 | id: logs 56 | attributes: 57 | label: Relevant log output 58 | description: | 59 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 60 | render: shell 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Track or request a new feature. 4 | title: '[Feature] ' 5 | labels: ['Type: Feature Request', 'Status: Awaiting Triage'] 6 | assignees: 7 | --- 8 | 9 | # Overview 10 | 11 | [In a few sentences, this feature and the motivation behind it. What problems/ 12 | issues will this feature solve/address?] 13 | 14 | # Proposal 15 | 16 | [Outline all tasks that need to be completed for this issue to be resolved. Feel 17 | free to create additional issues and link them here if the scope if this issue 18 | is large.] 19 | 20 | # Supplemental Information 21 | 22 | [Provide images or architecture diagrams of this new feature. Features with a 23 | larger scope must include diagrams or detailed descriptions to describe how they 24 | function. User-facing UI/UX must include mock-ups and descriptions to explain 25 | how they function.] 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/internal_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Internal/Chore Task 3 | about: Track a chore task that isn't user-facing. 4 | title: '[Internal] ' 5 | labels: ['Type: Internal/Chore', 'Status: Awaiting Triage'] 6 | assignees: 7 | --- 8 | 9 | # Overview 10 | 11 | [In a few sentences, describe what the scope of the task. What problems/issues 12 | will these changes solve/address?] 13 | 14 | # Proposal 15 | 16 | [Outline all tasks that need to be completed for this issue to be resolved. Feel 17 | free to create additional issues and link them here if the scope if this issue 18 | is large.] 19 | 20 | # Supplemental Information 21 | 22 | [Provide architecture diagrams of this new feature. Tasks with larger scope must 23 | include diagrams or detailed descriptions to describe how they function.] 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor_issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: Outline and track code that needs to be refactored. 4 | title: '[Refactor] ' 5 | labels: ['Type: Refactor', 'Status: Awaiting Triage'] 6 | assignees: 7 | --- 8 | 9 | # Overview 10 | 11 | [Why is this refactor happening? Describe the motivations.] 12 | 13 | # What Needs to Change 14 | 15 | [Specify all files that will be changed.] 16 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Project Discussion 4 | url: https://github.com/acmutd/hackportal/discussions 5 | about: Talk about general project features and ideas here. 6 | - name: ACM Development 7 | url: https://acmutd.co/team.html 8 | about: More information about the core HackPortal maintainers. 9 | - name: Inquiries for Maintainers 10 | url: development@acmutd.co 11 | about: If you have a question specific to the maintainers, contact them here. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | /.idea 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | /private_keys 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | .eslintcache 39 | 40 | # Ignores automatically-generated workbox files for next-pwa 41 | public/sw.* 42 | public/workbox-*.* 43 | 44 | hackportal.config.js -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . "$(dirname "$0")/_/husky.sh" 4 | exec < /dev/tty && node_modules/.bin/cz --hook || true 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/hackportal.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .husky 3 | .next 4 | .vscode 5 | package.json 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 100, 6 | tabWidth: 2, 7 | overrides: [ 8 | { 9 | files: '*.svg', 10 | options: { 11 | parser: 'html', 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach Next.js Debugger", 11 | "skipFiles": ["/**"], 12 | "port": 9229 13 | }, 14 | { 15 | "type": "pwa-chrome", 16 | "request": "launch", 17 | "name": "Launch Chrome", 18 | "url": "http://localhost:3000", 19 | "webRoot": "${workspaceFolder}" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Request project lead by default 2 | * @DedsecKnight 3 | * @acmutd/hackutd-tech -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ACM UTD 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HackPortal 2 | ### _A platform for user-friendly hackathon event management._ 3 | 4 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 5 | 6 | [Join the HackPortal HQ Discord!](https://discord.gg/GueKFPdN64) 7 | ## Features Summary 8 | - Fully customizable front end 9 | - Sign in with email/ Google 10 | - Hacker registration 11 | - Images, challenges, sponsors, FAQ and more fetched from backend 12 | - Push notifications 13 | - Spotlight carousel highlighting ongoing events 14 | - QR code check in and swag claims 15 | - Report submission/ Ask a question 16 | - Built-in and easy to set up schedule 17 | - Hacker, Admin, and Super Admin roles 18 | - Admin console to send announcement, update user roles, show number of check-ins, swag claims 19 | - And more! 20 | 21 | 22 | ## Setting up HackPortal for a Hackathon 23 | 24 | _If you are using HackPortal for your event, please add yourself to this [list](https://github.com/acmutd/hackportal/wiki/HackPortal-Users). It only takes a moment and shows us our software is helping real organizations._ 25 | 26 | Follow the instructions in the [set-up docs](./docs/set-up.md) to set up HackPortal for your hackathon. 27 | 28 | ## Making Changes 29 | Want to contribute? Follow the instructions in the [contributor docs](./docs/contributors.md) to see contributing guidelines. 30 | 31 | 32 | ## HackPortal Contributors 33 | HackPortal would not be possible without these people. 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [2, 'always', ['sentence-case', 'start-case']], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /components/AboutHeader.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | 4 | /** 5 | * 6 | * Props used by About Header component 7 | * 8 | * @param active - contains information used to determine current page user is at in About Section 9 | * 10 | */ 11 | interface AboutHeaderProps { 12 | active: '/about' | '/about/faq'; 13 | } 14 | 15 | /** 16 | * An about header. 17 | */ 18 | export default function AboutHeader({ active }: AboutHeaderProps) { 19 | return ( 20 |
21 |
22 |
23 | 27 | 28 | About 29 | 30 | 34 | 35 | FAQ 36 | 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /components/AppHeader2/AdminNavbarColumn.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from '@headlessui/react'; 2 | 3 | interface AdminNavbarColumnProps { 4 | sectionTitle: string; 5 | options: Array<{ 6 | optionName: string; 7 | onClick: () => void; 8 | }>; 9 | } 10 | 11 | export default function AdminNavbarColumn({ sectionTitle, options }: AdminNavbarColumnProps) { 12 | return ( 13 |
14 |

{sectionTitle}

15 | {options.map((option) => ( 16 | 17 | {({ active }) => ( 18 | 26 | )} 27 | 28 | ))} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/AppHeader2/AdminNavbarGrid.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from '@headlessui/react'; 2 | 3 | interface AdminNavbarGridProps { 4 | numCols?: number; 5 | sectionTitle: string; 6 | options: Array<{ 7 | optionName: string; 8 | onClick: () => void; 9 | }>; 10 | } 11 | 12 | export default function AdminNavbarGrid({ 13 | sectionTitle, 14 | options, 15 | numCols = 3, 16 | }: AdminNavbarGridProps) { 17 | return ( 18 |
19 |

{sectionTitle}

20 |
21 | {options.map((option) => ( 22 | 23 | {({ active }) => ( 24 | 32 | )} 33 | 34 | ))} 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/AppHeader2/QRCodeReaderV2.tsx: -------------------------------------------------------------------------------- 1 | import QrScanner from 'qr-scanner'; 2 | import { useEffect, useRef, useState } from 'react'; 3 | 4 | interface QRCodeReaderV2Props { 5 | onScanSuccess: (result: QrScanner.ScanResult) => void; 6 | onScanFail: (err: string | Error) => void; 7 | } 8 | 9 | export default function QRCodeReaderV2({ onScanSuccess, onScanFail }) { 10 | const scanner = useRef(); 11 | const videoElement = useRef(null); 12 | const [qrOn, setQrOn] = useState(true); 13 | 14 | useEffect(() => { 15 | if (videoElement?.current && !scanner.current) { 16 | scanner.current = new QrScanner(videoElement?.current, onScanSuccess, { 17 | onDecodeError: onScanFail, 18 | preferredCamera: 'environment', 19 | }); 20 | 21 | scanner?.current 22 | ?.start() 23 | .then(() => setQrOn(true)) 24 | .catch((err) => { 25 | if (err) setQrOn(false); 26 | }); 27 | } 28 | 29 | return () => { 30 | if (!videoElement?.current) { 31 | scanner?.current?.stop(); 32 | } 33 | }; 34 | }, []); 35 | 36 | useEffect(() => { 37 | if (!qrOn) 38 | alert( 39 | 'Camera is blocked or not accessible. Please allow camera in your browser permissions and Reload.', 40 | ); 41 | }, [qrOn]); 42 | 43 | return ( 44 |
45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/AppHeader2/visual1-no_offset.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/components/AppHeader2/visual1-no_offset.jpeg -------------------------------------------------------------------------------- /components/AppHeader2/visual2-with_offset.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/components/AppHeader2/visual2-with_offset.jpeg -------------------------------------------------------------------------------- /components/AppHeader2/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | import clsx from 'clsx'; 4 | import AppHeader2_Core from './core'; 5 | 6 | export const APP_HEADER_HEIGHT = 86; 7 | const INITIAL_HEADER_HEIGHT = APP_HEADER_HEIGHT; 8 | const TOP_OFFSET = 0; // Can be set to APP_HEADER_HEIGHT if want to app bar to be separated from the Hero part 9 | 10 | export default function AppHeader2_Wrapper() { 11 | // Handle scrolling state 12 | 13 | const [height, setHeight] = useState(INITIAL_HEADER_HEIGHT); 14 | 15 | const prevScrollY = useRef(0); 16 | const appHeaderRef = useRef(null); 17 | 18 | // Handle scrolling effect 19 | 20 | useEffect(() => { 21 | const handleUp = () => { 22 | // Reset 23 | if (window.scrollY <= 0) { 24 | setHeight(INITIAL_HEADER_HEIGHT); 25 | return; 26 | } 27 | 28 | const appHeaderTop = appHeaderRef.current?.getBoundingClientRect().top ?? 0; 29 | 30 | if (appHeaderTop < -APP_HEADER_HEIGHT) { 31 | // App header is not near viewport? 32 | // -> Set new height to make menu near viewport 33 | if (window.scrollY > INITIAL_HEADER_HEIGHT) { 34 | setHeight(window.scrollY); 35 | } 36 | } 37 | }; 38 | 39 | const handleDown = () => { 40 | // Reset 41 | if (window.scrollY <= 0) { 42 | setHeight(INITIAL_HEADER_HEIGHT); 43 | return; 44 | } 45 | 46 | const appHeaderTop = appHeaderRef.current?.getBoundingClientRect().top ?? 0; 47 | 48 | if (appHeaderTop >= 0) { 49 | // App header is at top of viewport? 50 | // -> Set new height to make menu disappear gradually 51 | setHeight(window.scrollY + APP_HEADER_HEIGHT); 52 | } 53 | }; 54 | 55 | // Main Part 56 | 57 | const handleScroll = (_event: Event) => { 58 | if (prevScrollY.current > window.scrollY) { 59 | // Scroll up 60 | handleUp(); 61 | } else { 62 | // Scroll down 63 | handleDown(); 64 | } 65 | 66 | prevScrollY.current = window.scrollY; 67 | }; 68 | 69 | window.addEventListener('scroll', handleScroll); 70 | 71 | return () => { 72 | window.removeEventListener('scroll', handleScroll); 73 | }; 74 | }, []); 75 | 76 | return ( 77 |
89 | {/* App header core */} 90 |
100 | 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /components/AppNavbarBottom/AppNavbarBottom.tsx: -------------------------------------------------------------------------------- 1 | import FilePlusIcon from '@/public/icons/file-plus.svg'; 2 | import CalendarIcon from '@/public/icons/calendar.svg'; 3 | import TrophyIcon from '@/public/icons/trophy.svg'; 4 | import QuestionIcon from '@/public/icons/question.svg'; 5 | import AdminIcon from '@/public/icons/admin.svg'; 6 | import clsx from 'clsx'; 7 | import Link from 'next/link'; 8 | 9 | export default function AppNavbarBottom() { 10 | return ( 11 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/BackgroundCircles.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from 'react'; 2 | import { Container, ISourceOptions, RotateDirection } from '@tsparticles/engine'; 3 | import Particles from './Particles'; 4 | import { useParticles } from './Particles/ParticlesProvider'; 5 | 6 | export default function BackgroundCircles() { 7 | const { state: particlesState } = useParticles(); 8 | 9 | const [windowWidth, setWindowWidth] = useState(0); 10 | const [windowHeight, setWindowHeight] = useState(0); 11 | const timeoutSetWindowSize = useRef(-1); 12 | 13 | // tsparticles does not have documentation on size.value unit 14 | // after experimenting with it, the scaling factor to convert from pixels to size.value is to multiply pixels with 0.5 15 | const scaleFactor = 0.5; 16 | 17 | const particlesLoaded = (_container?: Container) => { 18 | return Promise.resolve(); 19 | }; 20 | 21 | useEffect(() => { 22 | const resizeHandler = () => { 23 | if (timeoutSetWindowSize.current) { 24 | window.clearTimeout(timeoutSetWindowSize.current); 25 | } 26 | 27 | timeoutSetWindowSize.current = window.setTimeout(() => { 28 | setWindowWidth(window.innerWidth); 29 | setWindowHeight(window.innerHeight); 30 | }, 200); 31 | }; 32 | 33 | resizeHandler(); // Init 34 | window.addEventListener('resize', resizeHandler); 35 | 36 | return () => { 37 | window.removeEventListener('resize', resizeHandler); 38 | }; 39 | }, []); 40 | 41 | const options = useMemo( 42 | () => ({ 43 | fpsLimit: 120, 44 | fullScreen: { 45 | enable: false, 46 | }, 47 | detectRetina: true, 48 | 49 | particles: { 50 | // https://github.com/tsparticles/tsparticles/blob/main/markdown/Options/Particles/Rotate.md 51 | rotate: { 52 | direction: RotateDirection.random, 53 | value: { min: 90, max: 270 }, 54 | animation: { 55 | enable: true, 56 | speed: 2, 57 | }, 58 | }, 59 | move: { 60 | direction: 'none', 61 | enable: true, 62 | speed: 6, 63 | outModes: { 64 | default: 'bounce', 65 | }, 66 | random: false, 67 | straight: false, 68 | }, 69 | number: { 70 | value: 1, 71 | }, 72 | opacity: { 73 | value: 1, 74 | }, 75 | shape: { 76 | type: 'image', 77 | options: { 78 | image: { 79 | src: '/assets/circles.svg', 80 | replaceColor: false, 81 | }, 82 | }, 83 | }, 84 | size: { 85 | value: Math.min( 86 | Math.min(windowWidth, windowHeight) * 0.75 * scaleFactor, 87 | 600 * scaleFactor, 88 | ), 89 | }, 90 | }, 91 | }), 92 | [windowWidth, windowHeight], 93 | ); 94 | 95 | if (particlesState.init) { 96 | return ( 97 | 103 | ); 104 | } 105 | 106 | return <>; 107 | } 108 | -------------------------------------------------------------------------------- /components/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import { XIcon } from '@heroicons/react/solid'; 2 | 3 | interface ErrorCardProps { 4 | errorMsg: string; 5 | onClose: () => void; 6 | } 7 | 8 | export default function ErrorCard({ errorMsg, onClose }: ErrorCardProps) { 9 | return ( 10 |
11 |

{errorMsg}

12 | { 15 | onClose(); 16 | }} 17 | /> 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/ErrorList.tsx: -------------------------------------------------------------------------------- 1 | import ErrorCard from './ErrorCard'; 2 | 3 | interface ErrorListProps { 4 | errors: string[]; 5 | onClose: (idx: number) => void; 6 | } 7 | 8 | export default function ErrorList({ errors, onClose }: ErrorListProps) { 9 | return ( 10 |
11 | {errors.map((error, idx) => ( 12 | { 16 | onClose(idx); 17 | }} 18 | /> 19 | ))} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/LoadIcon.tsx: -------------------------------------------------------------------------------- 1 | export interface LoadIconProps { 2 | width: number; 3 | height: number; 4 | } 5 | export default function LoadIcon({ width, height }: LoadIconProps) { 6 | return ( 7 | 14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/NavLink.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Link from 'next/link'; 3 | import PropTypes from 'prop-types'; 4 | 5 | NavLink.propTypes = { 6 | href: PropTypes.string.isRequired, 7 | exact: PropTypes.bool, 8 | activeOptions: PropTypes.string, //css options when link is active 9 | }; 10 | 11 | NavLink.defaultProps = { 12 | exact: false, 13 | }; 14 | 15 | function NavLink({ href, exact, activeOptions, children, ...props }) { 16 | const { pathname } = useRouter(); 17 | const isActive = exact ? pathname === href : pathname.startsWith(href); 18 | 19 | if (isActive) { 20 | props.className += ' active ' + activeOptions; 21 | } 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | export default NavLink; 31 | -------------------------------------------------------------------------------- /components/Particles/IParticlesProps.ts: -------------------------------------------------------------------------------- 1 | import type { Container, ISourceOptions } from '@tsparticles/engine'; 2 | import type { CSSProperties } from 'react'; 3 | 4 | export interface IParticlesProps { 5 | id?: string; 6 | options?: ISourceOptions; 7 | url?: string; 8 | style?: CSSProperties; 9 | className?: string; 10 | particlesLoaded?: (container?: Container) => Promise; 11 | } 12 | -------------------------------------------------------------------------------- /components/Particles/Particles.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react'; 2 | import type { IParticlesProps } from './IParticlesProps'; 3 | import { tsParticles, type Container } from '@tsparticles/engine'; 4 | import { loadRotateUpdater } from '@tsparticles/updater-rotate'; 5 | 6 | const Particles: FC = (props) => { 7 | const id = props.id ?? 'tsparticles'; 8 | 9 | useEffect(() => { 10 | let container: Container | undefined; 11 | 12 | loadRotateUpdater(tsParticles) 13 | .then(async () => { 14 | await tsParticles.load({ id, url: props.url, options: props.options }).then((c) => { 15 | container = c; 16 | props.particlesLoaded?.(c); 17 | }); 18 | }) 19 | .catch((err) => { 20 | console.error(err); 21 | }); 22 | 23 | return () => { 24 | container?.destroy(); 25 | }; 26 | }, [id, props, props.url, props.options]); 27 | 28 | return
; 29 | }; 30 | 31 | export default Particles; 32 | -------------------------------------------------------------------------------- /components/Particles/ParticlesProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, createContext, useContext, useState } from 'react'; 2 | 3 | export const ParticlesContext = createContext<{ 4 | state: { init: boolean }; 5 | actions: { setInit: Dispatch> }; 6 | }>({ state: { init: false }, actions: { setInit: () => {} } }); 7 | 8 | export function useParticles() { 9 | return useContext(ParticlesContext); 10 | } 11 | -------------------------------------------------------------------------------- /components/Particles/index.ts: -------------------------------------------------------------------------------- 1 | import { Engine, tsParticles } from '@tsparticles/engine'; 2 | import Particles from './Particles'; 3 | 4 | /* 5 | NOTE: 6 | - These files within Particles folder is copied manually from @tsparticles/react package 7 | - Because the package itself depends on react v18, while when I worked on this, our HackPortal has react v17 and many conflicts that prevent updating to react v18 right away 8 | */ 9 | 10 | export type { IParticlesProps } from './IParticlesProps'; 11 | 12 | export async function initParticlesEngine(cb: (engine: Engine) => Promise): Promise { 13 | await cb(tsParticles); 14 | } 15 | 16 | export default Particles; 17 | export { Particles }; 18 | -------------------------------------------------------------------------------- /components/ScanType.tsx: -------------------------------------------------------------------------------- 1 | export interface ScanTypeProps { 2 | /** 3 | * Raw JSON of the scan. 4 | */ 5 | data: object; 6 | /** 7 | * Name of the scan. 8 | */ 9 | name: string; 10 | /** 11 | * Click callback. 12 | */ 13 | onClick: () => void; 14 | } 15 | export default function ScanType({ name, onClick }: ScanTypeProps) { 16 | return ( 17 |
21 |
{name}
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/adminComponents/AdminStatsCard.tsx: -------------------------------------------------------------------------------- 1 | import { SvgIconTypeMap } from '@mui/material'; 2 | import { OverridableComponent } from '@mui/material/OverridableComponent'; 3 | 4 | interface AdminStatsCardProps { 5 | title: string; 6 | value: number; 7 | icon: any; 8 | } 9 | 10 | export default function AdminStatsCard({ title, value, icon }: AdminStatsCardProps) { 11 | return ( 12 |
13 |
14 | {icon} 15 |
16 |

{title}

17 |

{value}

18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/adminComponents/FilterComponent.tsx: -------------------------------------------------------------------------------- 1 | import Checkbox from '@mui/material/Checkbox'; 2 | import { blue } from '@mui/material/colors'; 3 | 4 | interface FilterComponentProps { 5 | title: string; 6 | checked: boolean; 7 | onCheck: () => void; 8 | } 9 | 10 | export default function FilterComponent({ title, checked, onCheck }: FilterComponentProps) { 11 | const CustomCheckbox = () => ( 12 | { 15 | onCheck(); 16 | }} 17 | color="default" 18 | sx={{ 19 | color: blue[400], 20 | ':checked': { 21 | color: blue[600], 22 | }, 23 | }} 24 | /> 25 | ); 26 | 27 | return ( 28 |
29 | 30 |

{title}

31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/adminComponents/NivoBarChart.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | // https://github.com/plouc/nivo/issues/2310 4 | const ResponsiveBar = dynamic(() => import('@nivo/bar').then((m) => m.ResponsiveBar), { 5 | ssr: false, 6 | }); 7 | 8 | interface NivoBarChartProps { 9 | name: string; 10 | items: Array<{ 11 | itemName: string; 12 | itemValue: string; 13 | }>; 14 | } 15 | 16 | export default function NivoBarChart({ name, items }: NivoBarChartProps) { 17 | const longestKey = items.reduce((prev, curr) => Math.max(prev, curr.itemName.length), 0); 18 | const items_ = items.map(({ itemName, itemValue }) => ({ 19 | itemName, 20 | [name]: itemValue, 21 | })); 22 | return ( 23 |
24 |

{name}

25 | = 20 ? 30 : 0, 49 | legendPosition: 'middle', 50 | }} 51 | /> 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/adminComponents/NivoPieChart.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | // https://github.com/plouc/nivo/issues/2310 4 | const ResponsivePie = dynamic(() => import('@nivo/pie').then((m) => m.ResponsivePie), { 5 | ssr: false, 6 | }); 7 | 8 | interface NivoPieChartProps { 9 | name: string; 10 | items: Array<{ 11 | id: string; 12 | value: number; 13 | }>; 14 | } 15 | 16 | export default function NivoPieChart({ name, items }: NivoPieChartProps) { 17 | return ( 18 |
19 |

{name}

20 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/adminComponents/SuccessCard.tsx: -------------------------------------------------------------------------------- 1 | interface SuccessCardProps { 2 | msg: string; 3 | } 4 | 5 | export default function SuccessCard({ msg }: SuccessCardProps) { 6 | return ( 7 |
8 |

{msg}

9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /components/adminComponents/UserList.tsx: -------------------------------------------------------------------------------- 1 | interface UserListProps { 2 | users: UserIdentifier[]; 3 | selectedUsers: string[]; 4 | 5 | onUserClick: (id: string) => void; 6 | onUserSelect: (id: string) => void; 7 | } 8 | 9 | export default function UserList({ 10 | users, 11 | selectedUsers, 12 | onUserClick, 13 | onUserSelect, 14 | }: UserListProps) { 15 | const userList = []; 16 | 17 | users.forEach((user, idx) => { 18 | const bgColor = idx % 2 ? 'bg-white' : 'bg-gray-100'; 19 | 20 | userList.push( 21 |
onUserClick(user.id)} 31 | > 32 | {/* 33 | Name 34 | Status 35 | University 36 | Major 37 | Year 38 | */} 39 |
{ 42 | e.stopPropagation(); 43 | // onUserSelect(user.id); 44 | }} 45 | > 46 |
47 | { 49 | e.stopPropagation(); 50 | onUserSelect(user.id); 51 | }} 52 | checked={user.selected} 53 | type="checkbox" 54 | className="w-4 h-4 mr-2 rounded-sm border-[1px] border-complementary bg-transparent text-primaryDark" 55 | /> 56 |
57 |
{ 60 | e.stopPropagation(); 61 | onUserClick(user.id); 62 | }} 63 | > 64 | {`${user.user.firstName} ${user.user.lastName}`} 65 |
66 |
67 |
68 | 74 | {user.status} 75 | 76 |
77 |
78 | {user.university} 79 |
80 |
81 | {user.major} 82 |
83 |
84 | {user.studyLevel} 85 |
86 |
, 87 | ); 88 | }); 89 | 90 | return
{userList}
; 91 | } 92 | -------------------------------------------------------------------------------- /components/adminComponents/challengeComponents/ChallengeList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChallengeCard from '../../dashboardComponents/ChallengeCard'; 3 | import { 4 | DndContext, 5 | useSensors, 6 | useSensor, 7 | KeyboardSensor, 8 | PointerSensor, 9 | closestCenter, 10 | DragEndEvent, 11 | } from '@dnd-kit/core'; 12 | 13 | import { 14 | SortableContext, 15 | sortableKeyboardCoordinates, 16 | verticalListSortingStrategy, 17 | } from '@dnd-kit/sortable'; 18 | 19 | interface ChallengeListProps { 20 | challenges: SortableObject[]; 21 | onUpdateOrder: (oldIndex: number, newIndex: number) => void; 22 | onChallengeEditClick: (challengeIndex: number) => void; 23 | onChallengeDeleteClick: (challengeIndex: number) => void; 24 | } 25 | 26 | export default function ChallengeList({ 27 | challenges, 28 | onUpdateOrder, 29 | onChallengeEditClick, 30 | onChallengeDeleteClick, 31 | }: ChallengeListProps) { 32 | const sensors = useSensors( 33 | useSensor(PointerSensor, { 34 | activationConstraint: { 35 | distance: 5, 36 | }, 37 | }), 38 | useSensor(KeyboardSensor, { 39 | coordinateGetter: sortableKeyboardCoordinates, 40 | }), 41 | ); 42 | 43 | function handleDragEnd(event: DragEndEvent) { 44 | const { active, over } = event; 45 | 46 | if (active.id !== over.id) { 47 | const oldIndex = challenges.findIndex((obj) => obj.id === active.id); 48 | const newIndex = challenges.findIndex((obj) => obj.id === over.id); 49 | onUpdateOrder(oldIndex, newIndex); 50 | } 51 | } 52 | 53 | return ( 54 |
55 | 56 | 57 | {challenges.map(({ title, description, prizes }, idx) => ( 58 | 59 | ))} 60 | 61 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/adminComponents/eventComponents/EventDetailLink.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowNarrowRightIcon } from '@heroicons/react/solid'; 2 | import Link from 'next/link'; 3 | 4 | interface EventDetailLinkProps { 5 | title: string; 6 | href: string; 7 | } 8 | 9 | export default function EventDetailLink({ title, href }: EventDetailLinkProps) { 10 | return ( 11 | 12 |
13 |

14 | {title} 15 |

16 | 17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/adminComponents/eventComponents/EventList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface EventListProps { 4 | events: ScheduleEvent[]; 5 | onEventEditClick: (eventIndex: number) => void; 6 | onEventDeleteClick: (eventIndex: number) => void; 7 | } 8 | 9 | export default function EventList({ 10 | events, 11 | onEventEditClick, 12 | onEventDeleteClick, 13 | }: EventListProps) { 14 | return ( 15 |
16 | {events.map((event, idx) => ( 17 |
18 |

{event.title}

19 |
20 | 26 | 32 |
33 |
34 | ))} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/authComponents/EmailInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; 3 | 4 | // Define the type for the props by extending React.HTMLProps 5 | interface InputProps extends React.InputHTMLAttributes {} 6 | 7 | const EmailInput: React.FC = (props) => { 8 | const [isFocused, setIsFocused] = React.useState(false); 9 | 10 | return ( 11 |
18 | 21 |
22 | 23 | { 32 | if (props.onFocus) props.onFocus(e); 33 | setIsFocused(true); 34 | }} 35 | onBlur={(e) => { 36 | if (props.onBlur) props.onBlur(e); 37 | setIsFocused(false); 38 | }} 39 | /> 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default EmailInput; 46 | -------------------------------------------------------------------------------- /components/authComponents/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; 3 | import VisibilityIcon from '@mui/icons-material/Visibility'; 4 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; 5 | 6 | // Define the type for the props by extending React.HTMLProps 7 | interface InputProps extends React.InputHTMLAttributes {} 8 | 9 | const PasswordInput: React.FC = (props) => { 10 | const [isFocused, setIsFocused] = React.useState(false); 11 | 12 | const [showPassword, setShowPassword] = React.useState(false); 13 | const toggleShowPassword = () => setShowPassword(!showPassword); 14 | 15 | return ( 16 |
23 | 26 |
27 | 28 | { 37 | if (props.onFocus) props.onFocus(e); 38 | setIsFocused(true); 39 | }} 40 | onBlur={(e) => { 41 | if (props.onBlur) props.onBlur(e); 42 | setIsFocused(false); 43 | }} 44 | /> 45 |
46 | {showPassword ? : } 47 |
48 |
49 |
50 | ); 51 | }; 52 | 53 | export default PasswordInput; 54 | -------------------------------------------------------------------------------- /components/dashboardComponents/AnnouncementCards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Announcement Cards Component 5 | * 6 | * Cards for announcements section in hack center 7 | */ 8 | 9 | function AnouncementCard(props) { 10 | return ( 11 | <> 12 |
13 | {props.text} 14 |
15 |

{props.time}

16 | 17 | ); 18 | } 19 | 20 | export default AnouncementCard; 21 | -------------------------------------------------------------------------------- /components/dashboardComponents/AnsweredQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure } from '@headlessui/react'; 2 | import { CheckCircleFilled } from '@ant-design/icons'; 3 | import { ChevronUpIcon } from '@heroicons/react/solid'; 4 | 5 | /** 6 | * 7 | * Props used by AnsweredQuestion component used in /about/questions 8 | * 9 | * @param question question asked by current user 10 | * @param answer answer from organizer for corresponding question 11 | * @param colorCode color code used for background of component 12 | * @param iconColorCode color code used for checkmark icon that goes with the component 13 | * 14 | */ 15 | interface AnsweredQuestionProps { 16 | question: string; 17 | answer: string; 18 | colorCode: string; 19 | iconColorCode: string; 20 | isOpen: boolean; 21 | toggleDisclosure: () => void; 22 | } 23 | 24 | /** 25 | * 26 | * Component representing an answered question in /about/questions 27 | * 28 | */ 29 | export default function AnsweredQuestion({ 30 | question, 31 | answer, 32 | colorCode, 33 | iconColorCode, 34 | isOpen, 35 | toggleDisclosure, 36 | }: AnsweredQuestionProps) { 37 | return ( 38 |
39 | 40 | {({ open }) => ( 41 |
42 |
43 | 44 | 45 | 59 | 60 |
61 | {isOpen && ( 62 | 63 | {answer} 64 | 65 | )} 66 |
67 | )} 68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /components/dashboardComponents/ChallengeCard.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Challenge Cards Component 3 | * 4 | * Cards for challenge section in hack center 5 | * To add a linebreak for the description, simply add \n into the string value where needed in firebase 6 | */ 7 | 8 | function ChallengeCard(props) { 9 | var description; 10 | if (props.description !== undefined && props.description !== null) { 11 | description = props.description.replaceAll('\\n', '\n'); 12 | } 13 | return ( 14 |
15 |
{props.title}
16 |
17 |
{description}
18 |
19 | {props.prizes !== null && props.prizes !== undefined && ( 20 |
21 |
Prizes
22 |
    23 | {props.prizes.map((prize, idx) => ( 24 |
  • {prize}
  • 25 | ))} 26 |
27 |
28 | )} 29 |
30 | ); 31 | } 32 | 33 | export default ChallengeCard; 34 | -------------------------------------------------------------------------------- /components/dashboardComponents/DashboardHeader.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import NavLink from '../NavLink'; 3 | import { useRef, useState } from 'react'; 4 | import { ChevronRightIcon } from '@heroicons/react/solid'; 5 | 6 | /** 7 | * A dashboard header. 8 | */ 9 | export default function DashboardHeader() { 10 | const accordian = useRef(null); 11 | const [isOpen, setIsOpen] = useState(false); 12 | 13 | const triggerAccordion = () => { 14 | let acc = accordian.current; 15 | setIsOpen(!isOpen); 16 | acc.classList.toggle('menuactive'); 17 | var panel = acc.nextElementSibling; 18 | if (panel.style.maxHeight) { 19 | panel.style.maxHeight = null; 20 | } else { 21 | panel.style.maxHeight = panel.scrollHeight + 'px'; 22 | } 23 | }; 24 | 25 | return ( 26 |
27 |
28 |
29 | 35 | HackCenter 36 | 37 | 43 | Ask a Question 44 | 45 |
46 |
47 |
48 | 56 |
57 |
    58 |
  • 59 | 60 |
    HackCenter
    61 | 62 |
  • 63 |
  • 64 | 65 |
    Ask a Question
    66 | 67 |
  • 68 |
69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/dashboardComponents/PendingQuestion.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Representing props used by PendingQuestion component 3 | * 4 | * @param question question from user waiting to be answered 5 | */ 6 | interface PendingQuestionProps { 7 | question: string; 8 | } 9 | 10 | /** 11 | * 12 | * Component representing an unanswered question in /about/questions 13 | * 14 | */ 15 | export default function PendingQuestion({ question }: PendingQuestionProps) { 16 | return ( 17 |
18 | 19 | 20 | 21 |
22 |

{question}

23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/dashboardComponents/QRCode.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import LoadIcon from '../LoadIcon'; 3 | import { toCanvas } from 'qrcode'; 4 | 5 | export interface QRCodeProps { 6 | /** 7 | * The data to display in the QRCode. 8 | */ 9 | data: string; 10 | /** 11 | * QR load state. 12 | */ 13 | loading: boolean; 14 | /** 15 | * QR width/height in pixels. 16 | */ 17 | width: number; 18 | height: number; 19 | /** 20 | * Dark represents dots, Light represents the background 21 | */ 22 | darkColor: string; 23 | lightColor: string; 24 | } 25 | 26 | export default function QRCode({ 27 | data, 28 | loading, 29 | width, 30 | height, 31 | darkColor, 32 | lightColor, 33 | }: QRCodeProps) { 34 | const canvas = useRef(null); 35 | useEffect(() => { 36 | if (!canvas.current) return console.error('Invalid QRCode canvas referenece...'); 37 | if (!data || data === '') return console.warn('No QRCode data found, aborting display...'); 38 | toCanvas(canvas.current, data, { width, color: { dark: darkColor, light: lightColor } }); 39 | }); 40 | return !loading ? : ; 41 | } 42 | -------------------------------------------------------------------------------- /components/dashboardComponents/QRCodeReader.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import jsQR from 'jsqr'; 3 | import { Point } from 'jsqr/dist/locator'; 4 | import LoadIcon from '../LoadIcon'; 5 | 6 | export interface QRCodeReaderProps { 7 | callback?: ( 8 | data: string, 9 | video: HTMLVideoElement, 10 | setVideoReady: (state: boolean) => void, 11 | setPaused: (state: boolean) => void, 12 | tick: () => void, 13 | ) => void; 14 | /** 15 | * Width & height in pixels. 16 | */ 17 | width: number; 18 | height: number; 19 | } 20 | 21 | export const drawLine = (begin: Point, end: Point, context: CanvasRenderingContext2D) => { 22 | context.beginPath(); 23 | context.moveTo(begin.x, begin.y); 24 | context.lineTo(end.x, end.y); 25 | context.lineWidth = 4; 26 | context.stroke(); 27 | }; 28 | 29 | export default function QRCodeReader({ callback, width, height }: QRCodeReaderProps) { 30 | const canvas = useRef(null); 31 | const [videoReady, setVideoReady] = useState(false); 32 | const [paused, setPaused] = useState(false); 33 | const video = document.createElement('video'); 34 | video.playsInline = true; 35 | 36 | const tick = () => { 37 | if (video.readyState === video.HAVE_ENOUGH_DATA) { 38 | setVideoReady(true); 39 | const canvasElement: HTMLCanvasElement = canvas.current; 40 | if (!canvasElement) return requestAnimationFrame(tick); 41 | canvasElement.width = video.videoWidth; 42 | canvasElement.height = video.videoHeight; 43 | const context = canvasElement.getContext('2d'); 44 | context.drawImage(video, 0, 0, canvasElement.width, canvasElement.height); 45 | const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); 46 | var qrCode = jsQR(imageData.data, imageData.width, imageData.height, { 47 | inversionAttempts: 'dontInvert', 48 | }); 49 | if (qrCode) { 50 | drawLine(qrCode.location.topLeftCorner, qrCode.location.topRightCorner, context); 51 | drawLine(qrCode.location.topRightCorner, qrCode.location.bottomRightCorner, context); 52 | drawLine(qrCode.location.bottomRightCorner, qrCode.location.bottomLeftCorner, context); 53 | drawLine(qrCode.location.bottomLeftCorner, qrCode.location.topLeftCorner, context); 54 | video.pause(); 55 | setPaused(true); 56 | setVideoReady(false); 57 | callback(qrCode.data, video, setVideoReady, setPaused, tick); 58 | return; 59 | } 60 | } 61 | requestAnimationFrame(tick); 62 | }; 63 | 64 | !paused && 65 | navigator.mediaDevices 66 | .getUserMedia({ video: { facingMode: 'environment', frameRate: 30, width, height } }) 67 | .then((stream) => { 68 | video.srcObject = stream; 69 | video.play(); 70 | requestAnimationFrame(tick); 71 | }); 72 | return ( 73 |
74 | {videoReady && !paused ? : } 75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /components/dashboardComponents/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useUser } from '../../lib/profile/user-data'; 3 | import { useAuthContext } from '../../lib/user/AuthContext'; 4 | 5 | function Sidebar() { 6 | const { isSignedIn } = useAuthContext(); 7 | const user = useUser(); 8 | const role = user.permissions?.length > 0 ? user.permissions[0] : ''; 9 | return ( 10 | <> 11 | {/* ghost section to fill in for fixed sidebar */} 12 |
16 | 17 | 27 | 28 | ); 29 | } 30 | 31 | export default Sidebar; 32 | -------------------------------------------------------------------------------- /components/hackerpackComponents/DocLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DescriptionIcon from '@mui/icons-material/Description'; 3 | import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; 4 | import { blue as Blue, red as Red } from '@mui/material/colors'; 5 | 6 | /** 7 | * Doc Link Component 8 | * 9 | * Links for documents section in hackerpack 10 | * props.type determines the icon that corresponds to specified type 11 | */ 12 | 13 | function DocLink(props: { type: 'doc' | 'pdf'; link: string; title: string }) { 14 | let icon = <>; 15 | if (props.type == 'doc') { 16 | icon = ; 17 | } else if (props.type == 'pdf') { 18 | icon = ; 19 | } 20 | 21 | return ( 22 |
23 | {icon}{' '} 24 | 25 | {props.title} 26 | 27 |
28 | ); 29 | } 30 | 31 | export default DocLink; 32 | -------------------------------------------------------------------------------- /components/hackerpackComponents/HackerpackSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useUser } from '../../lib/profile/user-data'; 3 | import { useAuthContext } from '../../lib/user/AuthContext'; 4 | 5 | /** 6 | * Sidebar for the hackerpack; this is hidden on mobile 7 | */ 8 | export default function HackerpackSidebar({ content }: { content: SidebarSection[] }) { 9 | const { isSignedIn } = useAuthContext(); 10 | const user = useUser(); 11 | const role = user.permissions?.length > 0 ? user.permissions[0] : ''; 12 | return ( 13 | <> 14 | {/* ghost section to fill in for fixed sidebar */} 15 |
19 | 20 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/hackerpackComponents/hackerpack-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": true, 3 | "mainContent": "notion", 4 | "darkMode": false, 5 | "notionPageId": "798d9911fa64472fb6a2983708d7bc26" 6 | } 7 | -------------------------------------------------------------------------------- /components/hackerpackComponents/sidebar-content.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "General", 4 | "href": "#Section1", 5 | "sections": [ 6 | { 7 | "title": "Food", 8 | "href": "#Subsection1" 9 | }, 10 | { 11 | "title": "Mentors", 12 | "href": "#Subsection2" 13 | } 14 | ] 15 | }, 16 | { 17 | "title": "Tech Workshop Packs", 18 | "href": "#Section2", 19 | "sections": [ 20 | { 21 | "title": "Name of Workshop 1", 22 | "href": "#Subsection3" 23 | }, 24 | { 25 | "title": "Name of Workshop 2", 26 | "href": "#Subsection4" 27 | }, 28 | { 29 | "title": "Name of Workshop 3", 30 | "href": "#Subsection5" 31 | } 32 | ] 33 | }, 34 | { 35 | "title": "Sponsor Workshop Packs", 36 | "href": "#Section3", 37 | "sections": [ 38 | { 39 | "title": "Name of Workshop 4", 40 | "href": "#Subsection6" 41 | } 42 | ] 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /components/homeComponents/FaqDisclosure.tsx: -------------------------------------------------------------------------------- 1 | import { Disclosure, Transition } from '@headlessui/react'; 2 | import { PlusIcon, MinusIcon } from '@heroicons/react/solid'; 3 | 4 | /** 5 | * 6 | * Represents props used by FaqDisclosure component 7 | * 8 | * @param question a frequently asked question 9 | * @param answer answer to corresponding question 10 | * @param isOpen boolean variable used to determine whether the disclosure should be open or not 11 | * @param toggleDisclosure function to call when user wants to open/close disclosure 12 | * 13 | */ 14 | interface FaqDisclosureProps { 15 | question: string; 16 | answer: string; 17 | isOpen: boolean; 18 | toggleDisclosure: () => void; 19 | } 20 | 21 | /** 22 | * 23 | * Component representing a FAQ question in /about/faq 24 | * 25 | */ 26 | export default function FaqDisclosure({ 27 | question, 28 | answer, 29 | isOpen, 30 | toggleDisclosure, 31 | }: FaqDisclosureProps) { 32 | return ( 33 | 34 |
40 | 44 | 64 | 65 | 66 | {/* {isOpen && ( */} 67 | 76 | 81 | {answer} 82 | 83 | 84 | {/* )} */} 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /components/homeComponents/FaqExpandCloseButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronDownIcon } from '@heroicons/react/solid'; 3 | 4 | interface FaqExpandCloseButtonProps { 5 | disclosuresStatus: boolean[]; 6 | setDisclosureStatus: React.Dispatch>; 7 | } 8 | 9 | function FaqExpandCloseButton({ 10 | disclosuresStatus, 11 | setDisclosureStatus, 12 | }: FaqExpandCloseButtonProps) { 13 | function handleExpandCloseAll(disclosuresStatus) { 14 | return () => { 15 | if (disclosuresStatus.every((status) => status)) { 16 | setDisclosureStatus(disclosuresStatus.map(() => false)); 17 | } else { 18 | setDisclosureStatus(disclosuresStatus.map(() => true)); 19 | } 20 | }; 21 | } 22 | return ( 23 | <> 24 | 27 | status) 30 | ? 'transform rotate-180 transition duration-500 ease-in-out' 31 | : 'transition duration-500 ease-in-out' 32 | } w-5 h-5`} 33 | /> 34 | 35 | ); 36 | } 37 | 38 | export default FaqExpandCloseButton; 39 | -------------------------------------------------------------------------------- /components/homeComponents/HackCountdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, CSSProperties } from 'react'; 2 | import styles from './HackCountdown.module.css'; 3 | import { config } from '../../hackportal.config'; 4 | 5 | interface CountdownProps { 6 | targetDate: string; 7 | } 8 | 9 | const HackCountdown: React.FC = ({ targetDate }) => { 10 | const calculateTimeLeft = () => { 11 | const difference = +new Date(targetDate) - +new Date(); 12 | let timeLeft = { 13 | days: 0, 14 | hours: 0, 15 | minutes: 0, 16 | }; 17 | 18 | if (difference > 0) { 19 | timeLeft = { 20 | days: Math.floor(difference / (1000 * 60 * 60 * 24)), 21 | hours: Math.floor((difference / (1000 * 60 * 60)) % 24), 22 | minutes: Math.floor((difference / 1000 / 60) % 60), 23 | }; 24 | } 25 | 26 | return timeLeft; 27 | }; 28 | 29 | const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()); 30 | 31 | useEffect(() => { 32 | const timer = setTimeout(() => { 33 | setTimeLeft(calculateTimeLeft()); 34 | }, 1000); 35 | 36 | return () => clearTimeout(timer); 37 | }); 38 | 39 | return ( 40 |
41 |
42 | {Object.entries(timeLeft).map(([unit, value]) => { 43 | const digits = value.toString().padStart(2, '0').split(''); 44 | return ( 45 |
46 |
47 | {digits.map((digit, index) => ( 48 |
49 | {digit} 50 |
51 | ))} 52 |
53 |
{unit.toUpperCase()}
54 |
55 | ); 56 | })} 57 |
58 |
59 |

We will let you know when we are launching

60 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | const HackUTDCountdown: React.FC = () => { 67 | return ; 68 | }; 69 | 70 | export default HackUTDCountdown; 71 | -------------------------------------------------------------------------------- /components/homeComponents/HomeChallengeCard.tsx: -------------------------------------------------------------------------------- 1 | export default function HomeChallengesCard(props: { challenge: Challenge; blockType: number }) { 2 | const borderConfiguration = ['rounded-tr-[100px]', 'rounded-br-[100px]', 'rounded-tl-[100px]']; 3 | return ( 4 |
5 |
6 | {/* Block */} 7 |
10 |   11 |
12 |
13 | {/* Challenge Name */} 14 |

15 | {props.challenge.title.toUpperCase()} 16 |

17 | {/* Company Name */} 18 |

19 | {props.challenge.organization} 20 |

21 | {/* Description */} 22 |
23 |

{props.challenge.description}

24 |
25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/homeComponents/HomeChallenges.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import HomeChallengesCard from './HomeChallengeCard'; 3 | 4 | export default function HomeChallengesComponent(props: { challenges: Challenge[] }) { 5 | return ( 6 | props.challenges.length !== 0 && ( 7 |
8 |
9 |
10 | 11 | Challenge Tracks 12 | 13 |
14 |
15 |
16 |
17 |

18 | Hackathons are 24-hour gatherings where students collaborate to create innovative 19 | projects, forge new connections, and compete for prizes. 20 |

21 |
22 |
23 |
24 | {props.challenges.map((challenge, idx) => ( 25 | 26 | ))} 27 |
28 |
29 | ) 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/homeComponents/HomeFaq.tsx: -------------------------------------------------------------------------------- 1 | import Faq from './Faq'; 2 | 3 | export default function HomeFaq(props: { answeredQuestion: AnsweredQuestion[] }) { 4 | return ( 5 | props.answeredQuestion.length != 0 && ( 6 |
7 | 8 |
9 | ) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /components/homeComponents/HomeHero.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { buttonDatas } from '../../lib/data'; 3 | 4 | export default function HomeHero() { 5 | const router = useRouter(); 6 | 7 | return ( 8 |
9 |
13 |

HackPortal

{' '} 14 | {/* !change */} 15 |

16 | {' '} 17 | {/* !change */}Powered by HackUTD and ACM Dev 18 |

19 |
20 | {/* TODO: Programmatically show these based on configured times/organizer preference */} 21 | 22 |
23 | {buttonDatas.map((button) => ( 24 | 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/homeComponents/HomeHero2.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import MLH_Sticker from '../../public/assets/mlh-sticker.png'; 3 | import BackgroundCircles from '../BackgroundCircles'; 4 | import { useAuthContext } from '../../lib/user/AuthContext'; 5 | import AppHeader2_Wrapper from '../AppHeader2/wrapper'; 6 | import { useRouter } from 'next/router'; 7 | 8 | export default function HomeHero() { 9 | return ( 10 |
11 | {/* App header */} 12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | {/* MLH sticker */} 21 |
22 | MLH sticker 29 |
30 | 31 | {/* Big welcome */} 32 |
33 |

Welcome To

34 |

35 | HACKPORTAL 36 |

37 |
38 |
39 |
40 | 41 | {/* Bottom banner */} 42 |
43 |

44 | SAMPLE TEXT • SAMPLE TEXT • SAMPLE TEXT • SAMPLE TEXT • SAMPLE TEXT • SAMPLE TEXT • SAMPLE 45 | TEXT • SAMPLE TEXT • SAMPLE TEXT 46 |

47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /components/homeComponents/HomeNotif.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react'; 2 | import firebase from 'firebase/compat/app'; 3 | import 'firebase/compat/messaging'; 4 | 5 | export default function HomeNotif() { 6 | const [notif, setNotif] = useState(true); 7 | const popup = useRef(null); 8 | 9 | useEffect(() => { 10 | setNotif(checkNotif()); 11 | triggerPopup(); 12 | }, []); 13 | 14 | const checkNotif = () => { 15 | //pop up visible if user did not enable push notif and browser supports push notif 16 | const isSupported = 17 | 'Notification' in window && 18 | 'serviceWorker' in navigator && 19 | 'PushManager' in window && 20 | firebase.messaging.isSupported(); 21 | if (isSupported && Notification.permission !== 'granted') { 22 | Notification.requestPermission(); 23 | return true; 24 | } 25 | return false; 26 | }; 27 | 28 | const triggerPopup = () => { 29 | popup.current.classList.add('show'); 30 | setTimeout(() => setNotif(false), 4000); 31 | }; 32 | 33 | return ( 34 | notif && ( 35 | 49 | ) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /components/homeComponents/HomePrizes.tsx: -------------------------------------------------------------------------------- 1 | import HomePrizesCard from './HomePrizesCard'; 2 | 3 | interface HomePrizesProps { 4 | prizes: Array<{ 5 | rank: number; 6 | prizeName: string; 7 | }>; 8 | } 9 | 10 | export default function HomePrizes({ prizes }: HomePrizesProps) { 11 | return ( 12 |
13 | {/* Banner */} 14 |
15 |

16 | {Array.apply(null, Array(200)) 17 | .map(() => 'PRIZES') 18 | .join(' ')} 19 |

20 |
21 | {/* Component */} 22 |
23 |

24 | Prizes 25 |

26 |

Potential prize that participants can win!

27 |
28 | {prizes.map((prize, idx) => ( 29 | 30 | ))} 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/homeComponents/HomePrizesCard.tsx: -------------------------------------------------------------------------------- 1 | interface Prize { 2 | rank: number; 3 | prizeName: string; 4 | } 5 | 6 | export default function HomePrizesCard(props: { prize: Prize; blockType: number }) { 7 | const borderConfiguration = ['rounded-tr-[100px]', 'rounded-br-[100px]', 'rounded-tl-[100px]']; 8 | const rankName = ['first', 'second', 'third']; 9 | return ( 10 |
11 |
12 | {/* Block */} 13 |
14 |   15 |
16 | {/* Prize rank */} 17 | {props.prize.rank < rankName.length && ( 18 |

19 | {rankName[props.prize.rank].toUpperCase()} PLACE 20 |

21 | )} 22 | {/* Prize name */} 23 |

24 | {props.prize.prizeName.toUpperCase()} 25 |

26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/homeComponents/HomeSpeakers.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Swiper, SwiperSlide } from 'swiper/react'; 3 | import { Navigation } from 'swiper/modules'; 4 | import 'swiper/css'; 5 | import 'swiper/css/navigation'; 6 | import KeynoteSpeaker from './KeynoteSpeaker'; 7 | 8 | export default function HomeSpeakers(props: { keynoteSpeakers: KeynoteSpeaker[] }) { 9 | const [speakers, setSpeakers] = useState([]); 10 | 11 | useEffect(() => { 12 | setSpeakers(props.keynoteSpeakers); 13 | }, []); 14 | 15 | return ( 16 | speakers.length != 0 && ( 17 |
18 |
19 | Speakers 20 |
21 |
22 | The members that make HackPortal possible 23 |
24 | 25 |
26 | = 320px 35 | 320: { 36 | slidesPerView: 1, 37 | spaceBetween: 5, 38 | }, 39 | // when window width is >= 480px 40 | 480: { 41 | slidesPerView: 1, 42 | spaceBetween: 10, 43 | }, 44 | 620: { 45 | slidesPerView: 2, 46 | spaceBetween: 15, 47 | }, 48 | // when window width is >= 640px 49 | 840: { 50 | slidesPerView: 2, 51 | spaceBetween: 20, 52 | }, 53 | 1024: { 54 | slidesPerView: 3, 55 | spaceBetween: 10, 56 | }, 57 | }} 58 | > 59 | {speakers.map( 60 | ( 61 | { name, subtitle, description, fileName, githubLink, linkedinLink, websiteLink }, 62 | idx, 63 | ) => ( 64 | 65 | 74 | 75 | ), 76 | )} 77 | 78 |
79 |
80 | ) 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /components/homeComponents/HomeSponsors.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import SponsorCard from './SponsorCard'; 3 | 4 | export default function HomeSponsors(props: { sponsorCard: Sponsor[] }) { 5 | const [sponsor, setSponsor] = useState([]); 6 | 7 | useEffect(() => { 8 | setSponsor(props.sponsorCard); 9 | }); 10 | 11 | return ( 12 | sponsor.length != 0 && ( 13 |
14 |
15 |

23 | Our Sponsors 24 |

25 |

If you would like to sponsor HackPortal,

26 |

27 | please reach out to us at  28 | 34 | email@organization.com 35 | 36 |

37 | {/* Sponsor Card */} 38 |
39 |
40 | {sponsor.map(({ link, reference }, idx) => ( 41 | 42 | ))} 43 |
44 |
45 |
46 |
47 | ) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/homeComponents/HomeTeam.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import MemberCards from './MemberCards'; 3 | 4 | export default function HomeTeam(props: { members: TeamMember[] }) { 5 | const [members, setMembers] = useState([]); 6 | 7 | useEffect(() => { 8 | //Organize members in order by rank given in firebase 9 | setMembers(props.members.sort((a, b) => (a.rank > b.rank ? 1 : -1))); 10 | }, []); 11 | 12 | return ( 13 | members.length != 0 && ( 14 |
15 | {/* Team Members */} 16 |
17 |
18 |

19 | Meet the Team 20 |

{' '} 21 | {/* !change */} 22 |
23 |
24 | {/* Member Cards */} 25 | {members.map( 26 | ({ name, description, linkedin, github, personalSite, fileName }, idx) => ( 27 | 36 | ), 37 | )} 38 |
39 |
40 |
41 |
42 |
43 | ) 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/homeComponents/HomeVideoStats.tsx: -------------------------------------------------------------------------------- 1 | import { stats } from '../../lib/data'; 2 | 3 | export default function HomeVideoStats() { 4 | return ( 5 |
6 |
7 | {/* Video */} 8 | {/* !change */} 9 | 18 | 19 | {/* Stats */} 20 |
21 | {stats.map((stat, index) => ( 22 |
28 |

{stat.data}

29 |

{stat.object}

30 |
31 | ))} 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/homeComponents/KeynoteSpeaker.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { useEffect, useState } from 'react'; 3 | import 'firebase/storage'; 4 | import firebase from 'firebase/compat/app'; 5 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 6 | import GitHubIcon from '@mui/icons-material/GitHub'; 7 | import LanguageIcon from '@mui/icons-material/Language'; 8 | 9 | /** 10 | * Keynote Speaker card for landing page. 11 | */ 12 | export default function KeynoteSpeaker(props) { 13 | const [imageLink, setImageLink] = useState(); 14 | 15 | useEffect(() => { 16 | if (props.imageLink !== undefined) { 17 | const storageRef = firebase.storage().ref(); 18 | storageRef 19 | .child(`speaker_images/${props.imageLink}`) 20 | .getDownloadURL() 21 | .then((url) => { 22 | setImageLink(url); 23 | }) 24 | .catch((error) => { 25 | console.error('Could not find matching image file'); 26 | }); 27 | } 28 | }, []); 29 | 30 | return ( 31 |
32 |
33 | {props.imageLink !== undefined && imageLink !== undefined && ( 34 | 35 | )} 36 |
37 |
38 |
39 |
{props.name}
40 |
{props.subtitle}
41 |
{props.description}
42 |
43 |
44 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /components/homeComponents/MemberCards.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import firebase from 'firebase/compat/app'; 3 | import 'firebase/compat/storage'; 4 | import Image from 'next/image'; 5 | import defaultPFP from '../../public/assets/defaultPFP.jpg'; 6 | import GitHubIcon from '@mui/icons-material/GitHub'; 7 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 8 | import PersonIcon from '@mui/icons-material/Person'; 9 | 10 | /** 11 | * card for each member of the team 12 | */ 13 | 14 | export default function MemberCards(props) { 15 | const [imageLink, setImageLink] = useState(); 16 | 17 | useEffect(() => { 18 | if (props.fileName !== undefined) { 19 | const storageRef = firebase.storage().ref(); 20 | storageRef 21 | .child(`member_images/${props.fileName}`) 22 | .getDownloadURL() 23 | .then((url) => { 24 | setImageLink(url); 25 | }) 26 | .catch((error) => { 27 | console.error('Could not find matching image file'); 28 | }); 29 | } 30 | }, []); 31 | 32 | return ( 33 |
34 | {/* Profile Image */} 35 |
36 | Your profile 44 |
45 | {/* Main Body */} 46 | 47 |

{props.name}

48 |

{props.description}

49 |
50 | {props.github !== undefined && ( 51 | 52 | 53 | 54 | )} 55 | {props.linkedin !== undefined && ( 56 | 57 | 58 | 59 | )} 60 | {props.personalSite !== undefined && ( 61 | 62 | 63 | 64 | )} 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/homeComponents/SponsorCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import firebase from 'firebase/compat/app'; 3 | import 'firebase/compat/storage'; 4 | import LoadIcon from '../LoadIcon'; 5 | import Image from 'next/image'; 6 | 7 | interface SponsorCardProps { 8 | link: string; 9 | reference: string; 10 | } 11 | 12 | /** 13 | * Keynote Speaker card for landing page. 14 | */ 15 | export default function SponsorCard(props: SponsorCardProps) { 16 | const [imgSrc, setImgSrc] = useState(); 17 | const [loading, setLoading] = useState(true); 18 | 19 | useEffect(() => { 20 | if (props.reference !== undefined) { 21 | const storageRef = firebase.storage().ref(); 22 | storageRef 23 | .child(`sponsor_images/${props.reference}`) 24 | .getDownloadURL() 25 | .then((url) => { 26 | setImgSrc(url); 27 | setLoading(false); 28 | }) 29 | .catch((error) => { 30 | setLoading(false); 31 | console.error('Could not find matching image file'); 32 | }); 33 | } 34 | }, []); 35 | 36 | if (loading) return ; 37 | 38 | return ( 39 | <> 40 | {imgSrc !== undefined && ( 41 |
42 | 43 | {`Sponsor 51 | 52 |

53 |
54 | )} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/registerComponents/DisplayQuestion.module.css: -------------------------------------------------------------------------------- 1 | .textInputQuestionsContainer { 2 | display: grid; 3 | grid-template-columns: repeat(2, 1fr); 4 | grid-template-rows: repeat(2, 1fr); 5 | grid-column-gap: 8px; 6 | grid-row-gap: 0px; 7 | } 8 | 9 | .textInputQuestionsContainer > :first-child { 10 | grid-area: 1 / 1 / 2 / 2; 11 | } 12 | 13 | .textInputQuestionsContainer > :nth-child(2) { 14 | grid-area: 1 / 2 / 2 / 3; 15 | } 16 | 17 | .textInputQuestionsContainer > :nth-child(3) { 18 | grid-area: 2 / 1 / 3 / 3; 19 | } 20 | 21 | .dropdownQuestionsContainer { 22 | display: grid; 23 | grid-template-columns: repeat(2, 1fr); 24 | grid-template-rows: repeat(2, 1fr); 25 | grid-column-gap: 8px; 26 | grid-row-gap: 0px; 27 | } 28 | 29 | .dropdownQuestionsContainer > :first-child { 30 | grid-area: 1 / 1 / 2 / 3; 31 | } 32 | 33 | .dropdownQuestionsContainer > :nth-child(2) { 34 | grid-area: 2 / 1 / 3 / 2; 35 | } 36 | 37 | .dropdownQuestionsContainer > :nth-child(3) { 38 | grid-area: 2 / 2 / 3 / 3; 39 | } 40 | -------------------------------------------------------------------------------- /components/registerComponents/DisplayQuestion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useLayoutEffect, Fragment } from 'react'; 2 | import { Field, ErrorMessage } from 'formik'; 3 | import Question from './RegistrationQuestion'; 4 | import styles from './DisplayQuestion.module.css'; 5 | /** 6 | *Display registration questions Component 7 | * 8 | * 9 | */ 10 | 11 | function DisplayQuestion(props) { 12 | return ( 13 | 14 | {/* Display text input questions */} 15 |
16 | {props.obj.textInputQuestions?.map((inputObj) => ( 17 | 18 | ))} 19 |
20 | {/* Display number input questions */} 21 | {props.obj.numberInputQuestions?.map((inputObj) => ( 22 | 29 | ))} 30 | {/* Display dropdown input questions */} 31 |
32 | {props.obj.dropdownQuestions?.map((inputObj) => ( 33 | 34 | ))} 35 |
36 | 37 | {/* Display datalist input questions */} 38 | {props.obj.datalistQuestions?.map((inputObj) => ( 39 | 40 | ))} 41 | {/* Display checkbox input questions */} 42 | {props.obj.checkboxQuestions?.map((inputObj) => ( 43 | 44 | ))} 45 | {/* Display text area input questions */} 46 | {props.obj.textAreaQuestions?.map((inputObj) => ( 47 | 48 | ))} 49 |
50 | ); 51 | } 52 | 53 | export default DisplayQuestion; 54 | -------------------------------------------------------------------------------- /docs/contributors.md: -------------------------------------------------------------------------------- 1 | 2 | # General Contributor Guidlines 3 | 4 | The custom Git hooks for the repository will reject your commits if they aren't 5 | formatted properly. This project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 6 | spec. Please read it before making your commits. Commits for this project take 7 | the form of: 8 | 9 | ``` 10 | [optional scope]: 11 | 12 | [optional body] 13 | 14 | [optional footer(s)] 15 | ``` 16 | 17 | The easiest way to make commits is simply to use `git commit` from the command 18 | line. Commitizen will give you a prompt asking you for information to make your 19 | commit. An example of a properly formatted commit message: 20 | 21 | ``` 22 | feat(events): Allow user to favorite an event 23 | 24 | A user can now favorite events to add them to their personal schedule. Favorited 25 | events appear in a user's profile. 26 | ``` 27 | 28 | Non-trivial feature additions require explanation. Think of writing commit 29 | messages like writing little stories to other contributors about what you've 30 | done. The effort you put into writing a good commit message now will help later 31 | when you need to track down a bug or when someone else is understanding why a 32 | change was made. 33 | 34 | Please read [this](https://chris.beams.io/posts/git-commit/) to understand 35 | how to write a good commit message. 36 | 37 | You can use your IDE to make commits, but commitlint will reject 38 | improperly-formatted commit messages. As long as they're formatted properly, 39 | you can use your IDE (but `git commit` on the command line is probably easier). -------------------------------------------------------------------------------- /docs/hackerpack-setup.md: -------------------------------------------------------------------------------- 1 | # Hackerpack Instructions 2 | 3 | The hackerpack is where we place all the resources that could aid a hacker during and before the hackathon. Some commonly placed categories on this page include: 4 | 5 | - General Information 6 | - Food (both provided and nearby restaurants) 7 | - Workshop Info 8 | - FAQ 9 | - ...and anything else that could help! 10 | 11 | ## Customization Options 12 | 13 | We designed the hackerpack to be as easy as possible to customize! In the hackerpack components directory (`components/hackerpackComponents`), you should see a `hackerpack-settings.json` file. This file contains the main toggleable options for editing the hackerpack. The format is as shown (with the example data): 14 | 15 | ```json 16 | { 17 | "sidebar": true, 18 | "mainContent": "markdown", 19 | "notionPageId": "b76d2dee46474cd0a9bb7f62b384ad25" 20 | } 21 | ``` 22 | 23 | - The `sidebar` attribute (boolean) will show the sidebar + mobile menu navigator when set to `true`. 24 | - The `mainContent` attribute (string) determines how the page will be displayed, with 3 possible options: 25 | - `markdown` - generate content from a Markdown file (replace the `pages/hackerpacks/Components/markdown/index.md` file with your custom markdown file) 26 | - `notion` - generate content from a Notion page 27 | - `html` - manually add React/JSX in the `pages/hackerpacks/index.tsx` file) 28 | - If you pick the Notion page option, make sure to include the `notionPageId` attribute, which is an ID for a **public** Notion page (see [the official Notion docs](https://developers.notion.com/docs/working-with-page-content#creating-a-page-with-content)). 29 | 30 | ## Sidebar Generation 31 | 32 | The sidebar will display `h1` and `h2` headings, with the `h2` tags nested under their respective `h1` tag. This will differ in generation methods based on how you generate your main content. The sidebar is automatically generated if content is either rendered from Markdown or from Notion. 33 | 34 | * If the `mainContent` attribute is set to `markdown`, the sidebar will be generated based on `# [main headings]` and `## [subheadings]` as the `h1` and `h2` tags. 35 | * If the `mainContent` attribute is set to `notion`, the sidebar will be generated based on Notion's `Heading 1` and `Heading 2` blocks. 36 | 37 | **NOTE**: Pages should contain a main heading (`# [heading name]` or Notion's `Heading 1`) first to generate the sidebar correctly, as subheadings are nested under the main headings. 38 | 39 | Unfortunately, there is no good way to read hardcoded HTML and generate a sidebar, so if you have a custom HTML page, you will need to modify the `sidebar-content.json` file to generate a working sidebar. 40 | -------------------------------------------------------------------------------- /docs/images/set-up-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-1.png -------------------------------------------------------------------------------- /docs/images/set-up-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-2.png -------------------------------------------------------------------------------- /docs/images/set-up-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-3.png -------------------------------------------------------------------------------- /docs/images/set-up-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-5.png -------------------------------------------------------------------------------- /docs/images/set-up-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-6.png -------------------------------------------------------------------------------- /docs/images/set-up-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-7.png -------------------------------------------------------------------------------- /docs/images/set-up-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-8.png -------------------------------------------------------------------------------- /docs/images/set-up-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/docs/images/set-up-9.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Before extending and configuring this project, please set up the project using 4 | the instructions in the [Getting Set Up](./set-up.md) docs. 5 | 6 | # Configuration 7 | 8 | HackPortal currently uses: 9 | 10 | - Cloud Firestore as a NoSQL database 11 | - Firebase Auth (server-side) for authentication -------------------------------------------------------------------------------- /lib/admin/init.ts: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin'; 2 | 3 | let apiInitialized = false; 4 | //This path is a JSON object for the Firebase service account's private key 5 | // let servAcc = require('../../private_keys/acmutd-hackportal-firebase-adminsdk-ev404-afcb7fdeb3.json'); 6 | 7 | /** 8 | * Initializes all services used to power API routes. 9 | * 10 | * Each API's route should must call this function before the handler takes 11 | * over. To add more services to the back-end API like database services or 12 | * other middleware, those services should be called in this function. 13 | */ 14 | export default function initializeApi() { 15 | if (apiInitialized) { 16 | return; 17 | } 18 | // Put API initializations here. 19 | initializeFirebase(); 20 | 21 | apiInitialized = true; 22 | } 23 | 24 | /** 25 | * Initializes Firebase admin APIs using environment variables. 26 | */ 27 | function initializeFirebase() { 28 | if (admin.apps.length < 1) { 29 | admin.initializeApp({ 30 | credential: admin.credential.cert({ 31 | projectId: process.env.SERVICE_ACCOUNT_PROJECT_ID, 32 | clientEmail: process.env.SERVICE_ACCOUNT_CLIENT_EMAIL, 33 | privateKey: process.env.SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\n/g, '\n'), 34 | }), 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/authorization/check-authorization.ts: -------------------------------------------------------------------------------- 1 | import { auth, firestore } from 'firebase-admin'; 2 | 3 | const REGISTRATION_COLLECTION = '/registrations'; 4 | 5 | export async function userIsAuthorized(token: string, roles?: string[]) { 6 | // TODO: Check if token is from actual user using Admin API 7 | // TODO: Check if token was revoked, and send an appropriate error to client 8 | if (!token) return false; 9 | const roleList = roles || ['admin', 'super_admin', 'hacker']; 10 | const payload = await auth().verifyIdToken(token); 11 | const snapshot = await firestore() 12 | .collection(REGISTRATION_COLLECTION) 13 | .where('id', '==', payload.uid) 14 | .get(); 15 | if (snapshot.empty) return false; 16 | // console.log(snapshot.docs[0].data().user.permissions as string[]); 17 | for (let userRole of snapshot.docs[0].data().user.permissions as string[]) { 18 | if (roleList.includes(userRole)) return true; 19 | } 20 | return false; 21 | } 22 | -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | export const buttonDatas = [ 2 | { text: 'Hacker App', path: '/' }, 3 | { text: 'Mentor App', path: '/' }, 4 | { text: 'Sponsor App', path: '/' }, 5 | ]; 6 | 7 | export const navItems = [ 8 | { text: 'Home', path: '/' }, 9 | { text: 'Dashboard', path: '/dashboard' }, 10 | { text: 'Schedule', path: '/schedule' }, 11 | { text: 'HackerPack', path: '/hackerpacks' }, 12 | ]; 13 | 14 | export const stats = [ 15 | { 16 | data: 'Big', 17 | object: 'statistic 1', 18 | }, 19 | { 20 | data: 'Shocking', 21 | object: 'statistic 2', 22 | }, 23 | { 24 | data: 'Incredible', 25 | object: 'statistic 3', 26 | }, 27 | ]; 28 | 29 | export const DEFAULT_EVENT_FORM_DATA: ScheduleEvent = { 30 | description: '', 31 | title: '', 32 | page: '', 33 | type: '', 34 | track: '', 35 | location: '', 36 | speakers: [], 37 | startDate: new Date(), 38 | endDate: new Date(), 39 | Event: -1, 40 | }; 41 | -------------------------------------------------------------------------------- /lib/firebase-client.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/compat/app'; 2 | 3 | export const firebaseConfig = { 4 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 5 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 6 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 7 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 8 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 9 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 10 | measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID, 11 | }; 12 | 13 | /** 14 | * Initializes Firebase APIs using environment variables. 15 | * 16 | * See the contributor docs for more info. 17 | */ 18 | export function initFirebase() { 19 | if (firebase.apps.length > 0) { 20 | return; 21 | } 22 | 23 | firebase.initializeApp(firebaseConfig); 24 | } 25 | -------------------------------------------------------------------------------- /lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export const useUrlHash = (initialValue: string) => { 5 | const router = useRouter(); 6 | const [hash, setHash] = useState(initialValue); 7 | 8 | const updateHash = (str: string) => { 9 | if (!str) return; 10 | setHash(str.split('#')[1]); 11 | }; 12 | 13 | useEffect(() => { 14 | // Init 15 | updateHash(router.asPath); 16 | 17 | const onNextJSHashChange = (url: string) => updateHash(url); 18 | router.events.on('hashChangeStart', onNextJSHashChange); 19 | 20 | return () => { 21 | router.events.off('hashChangeStart', onNextJSHashChange); 22 | }; 23 | }, [router.asPath, router.events]); 24 | 25 | return hash; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/profile/user-data.ts: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from '../user/AuthContext'; 2 | 3 | /** 4 | * A hook to get user profile data. 5 | */ 6 | export function useUser() { 7 | const { user: authUser } = useAuthContext(); 8 | 9 | return { 10 | ...authUser, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /lib/request-helper.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleSqlOutlined } from '@ant-design/icons'; 2 | 3 | /** 4 | * 5 | * A utility class created to make the process of making request to backend easier and also to enforce type-checking 6 | * Function designs are inspired by axios 7 | * 8 | */ 9 | export class RequestHelper { 10 | /** 11 | * 12 | * Will make a POST request to provided url with provided config and provided body 13 | * 14 | * @param url url to which the request will be made to 15 | * @param config config that can be added into request. Usually used to add information to the headers 16 | * @param body request body 17 | * @returns response data 18 | * 19 | */ 20 | static async post( 21 | url: string, 22 | config: RequestInit, 23 | body?: ReqBody, 24 | ): Promise> { 25 | const temp = await fetch(url, { 26 | ...config, 27 | method: 'POST', 28 | mode: 'cors', 29 | body: JSON.stringify(body), 30 | }); 31 | const data = await temp.json(); 32 | return { 33 | status: temp.status, 34 | data, 35 | }; 36 | } 37 | 38 | /** 39 | * 40 | * Will make a GET request to provided url with provided config and provided body 41 | * 42 | * @param url url to which the request will be made to 43 | * @param config config that can be added into request. Usually used to add information to the headers 44 | * @returns response data 45 | * 46 | */ 47 | static async get(url: string, config: RequestInit): Promise> { 48 | const temp = await fetch(url, { 49 | ...config, 50 | method: 'GET', 51 | mode: 'cors', 52 | }); 53 | const data = await temp.json(); 54 | return { 55 | status: temp.status, 56 | data, 57 | }; 58 | } 59 | 60 | /** 61 | * 62 | * Will make a DELETE request to provided url with provided config and provided body 63 | * 64 | * @param url url to which the request will be made to 65 | * @param config config that can be added into request. Usually used to add information to the headers 66 | * @param body request body 67 | * @returns response data 68 | * 69 | */ 70 | static async delete( 71 | url: string, 72 | config: RequestInit, 73 | body?: ReqBody, 74 | ): Promise> { 75 | const temp = await fetch(url, { 76 | ...config, 77 | method: 'DELETE', 78 | mode: 'cors', 79 | body: JSON.stringify(body), 80 | }); 81 | const data = await temp.json(); 82 | return { 83 | status: temp.status, 84 | data, 85 | }; 86 | } 87 | } 88 | 89 | /** 90 | * 91 | * Represent response data object 92 | * 93 | */ 94 | interface ResponseData { 95 | /** 96 | * status code of request 97 | */ 98 | status: number; 99 | 100 | /** 101 | * 102 | * Data returned by response 103 | */ 104 | data: T; 105 | } 106 | -------------------------------------------------------------------------------- /lib/service-worker/FCMContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from 'react'; 2 | import firebase from 'firebase/compat/app'; 3 | import 'firebase/compat/messaging'; 4 | import { RequestHelper } from '../request-helper'; 5 | import { firebaseConfig } from '../firebase-client'; 6 | 7 | interface FCMContextState { 8 | fcmSw: ServiceWorkerRegistration; 9 | messageToken: string; 10 | } 11 | 12 | const FCMContext = createContext(undefined); 13 | 14 | function useFCMContext(): FCMContextState { 15 | const context = useContext(FCMContext); 16 | if (!context) throw new Error('useSWContext must be used in a provider'); 17 | return context; 18 | } 19 | 20 | function FCMProvider({ children }: React.PropsWithChildren>): JSX.Element { 21 | const [swRegistration, setSwRegistration] = useState(); 22 | const [messageToken, setMessageToken] = useState(); 23 | 24 | useEffect(() => { 25 | if ('serviceWorker' in window.navigator) { 26 | window.navigator.serviceWorker 27 | .register(`/firebase-messaging-sw.js`) 28 | .then(listenForNotifications, (error) => { 29 | console.log('Service worker registration failed:', error); 30 | }); 31 | } 32 | }, []); 33 | 34 | /** 35 | * Initializes the firebase messaging API 36 | * to listen for and recieve announcements 37 | */ 38 | const listenForNotifications = async (registration: ServiceWorkerRegistration) => { 39 | // Set service worker registration object to state variable 40 | setSwRegistration(registration); 41 | console.log('Service Worker registered successfully'); 42 | 43 | // Initialize firebase app and get messaging 44 | if (firebase.apps.length <= 0) firebase.initializeApp(firebaseConfig); 45 | const messaging = firebase.messaging(); 46 | 47 | // Ask user to enable notifications 48 | // If not granted, exit 49 | if (Notification.permission === 'default') await Notification.requestPermission(); 50 | if (Notification.permission !== 'granted') return; 51 | 52 | // Get token and save in database 53 | let token = await messaging.getToken({ 54 | vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY, 55 | }); 56 | await RequestHelper.post<{ token: string }, void>( 57 | '/api/tokens', 58 | { headers: { 'Content-Type': 'application/json' } }, 59 | { token }, 60 | ); 61 | setMessageToken(token); 62 | 63 | // Listen for messages 64 | messaging.onMessage((payload) => { 65 | const { announcement, iconUrl } = payload.data; 66 | const options = { 67 | body: announcement, 68 | icon: iconUrl, 69 | tag: new Date().toUTCString(), 70 | }; 71 | registration.showNotification('HackPortal Announcement', options); 72 | }); 73 | }; 74 | 75 | const swContextValue: FCMContextState = { 76 | fcmSw: swRegistration, 77 | messageToken, 78 | }; 79 | 80 | return {children}; 81 | } 82 | 83 | export { FCMContext, FCMProvider, useFCMContext }; 84 | -------------------------------------------------------------------------------- /lib/stats/field.ts: -------------------------------------------------------------------------------- 1 | import { fieldNames, arrayField, singleField } from '../../hackportal.config'; 2 | export const singleFields = singleField; 3 | 4 | export const arrayFields = arrayField; 5 | 6 | export const fieldToName = fieldNames; 7 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | export const getFileExtension = (filename: string) => { 2 | const seg = filename.split('.'); 3 | return `.${seg[seg.length - 1]}`; 4 | }; 5 | 6 | export enum RegistrationState { 7 | UNINITIALIZED = -1, 8 | OPEN = 1, 9 | CLOSED = 0, 10 | } 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const runtimeCaching = require('next-pwa/cache'); 2 | const withFonts = require('next-fonts'); 3 | 4 | const withPWA = require('next-pwa')({ 5 | dest: 'public', 6 | runtimeCaching, 7 | disable: !process.env.ENABLE_PWA && process.env.NODE_ENV === 'development', 8 | }); 9 | 10 | (module.exports = withPWA({ 11 | reactStrictMode: true, 12 | 13 | images: { 14 | remotePatterns: [ 15 | { 16 | protocol: 'https', 17 | hostname: 'lh3.googleusercontent.com', 18 | }, 19 | { 20 | protocol: 'https', 21 | hostname: 'firebasestorage.googleapis.com', 22 | }, 23 | ], 24 | }, 25 | 26 | // https://sebhastian.com/javascript-unexpected-token-export/ 27 | // https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages 28 | transpilePackages: ['@ant-design/icons-svg', 'rc-util'], 29 | 30 | webpack(config, options) { 31 | config.module.rules.push({ 32 | test: /\.md$/, 33 | use: 'raw-loader', 34 | }); 35 | 36 | // For loading svg as React component 37 | config.module.rules.push({ 38 | test: /\.svg$/, 39 | use: ['@svgr/webpack'], 40 | }); 41 | 42 | return config; 43 | }, 44 | })), 45 | withFonts({ 46 | enableSvg: true, 47 | webpack(config, options) { 48 | return config; 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackportal", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "dev:debug": "NODE_OPTIONS='--inspect' next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "prepare": "husky install" 12 | }, 13 | "dependencies": { 14 | "@ant-design/icons": "^4.7.0", 15 | "@devexpress/dx-react-core": "^3.0.4", 16 | "@devexpress/dx-react-scheduler": "^3.0.4", 17 | "@devexpress/dx-react-scheduler-material-ui": "^3.0.4", 18 | "@dnd-kit/core": "^6.0.5", 19 | "@dnd-kit/sortable": "^7.0.1", 20 | "@emotion/react": "^11.8.1", 21 | "@emotion/styled": "^11.8.1", 22 | "@headlessui/react": "^1.6.4", 23 | "@heroicons/react": "^1.0.5", 24 | "@mui/icons-material": "^5.15.15", 25 | "@mui/material": "^5.15.15", 26 | "@mui/system": "^5.15.15", 27 | "@nivo/bar": "^0.87.0", 28 | "@nivo/core": "^0.87.0", 29 | "@nivo/pie": "^0.87.0", 30 | "@svgr/webpack": "^8.1.0", 31 | "@syncfusion/ej2-react-schedule": "^19.4.48", 32 | "@tsparticles/engine": "^3.4.0", 33 | "@tsparticles/slim": "^3.4.0", 34 | "@types/multer": "^1.4.7", 35 | "acorn": "^8.4.1", 36 | "commitizen": "^4.2.4", 37 | "date-fns": "^2.28.0", 38 | "doppler": "^1.0.0", 39 | "firebase": "^10.7.1", 40 | "firebase-admin": "^12.0.0", 41 | "formik": "^2.2.9", 42 | "github-slugger": "^1.4.0", 43 | "jsqr": "^1.4.0", 44 | "localforage": "^1.10.0", 45 | "multer": "^1.4.3", 46 | "next": "^14.1.4", 47 | "next-connect": "^0.11.0", 48 | "next-fonts": "^1.5.1", 49 | "next-pwa": "^5.6.0", 50 | "notion-client": "^6.12.9", 51 | "qr-scanner": "^1.4.2", 52 | "qrcode": "^1.4.4", 53 | "raw-loader": "^4.0.2", 54 | "react": "^18.2.0", 55 | "react-dom": "^18.2.0", 56 | "react-markdown": "^8.0.3", 57 | "react-material-ui-carousel": "^3.4.2", 58 | "react-notion-x": "^6.12.10", 59 | "react-select": "^5.8.0", 60 | "rehype-slug": "^5.0.1", 61 | "remark-gfm": "^3.0.1", 62 | "swiper": "^11.1.1", 63 | "webpack": "^5.89.0" 64 | }, 65 | "devDependencies": { 66 | "@commitlint/cli": "^19.3.0", 67 | "@commitlint/config-conventional": "12.1.4", 68 | "@tailwindcss/forms": "^0.5.3", 69 | "@types/github-slugger": "^1.3.0", 70 | "@types/jquery": "^3.5.8", 71 | "@types/qrcode": "^1.4.1", 72 | "@types/react": "17.0.38", 73 | "autoprefixer": "^10.4.13", 74 | "cz-conventional-changelog": "^3.3.0", 75 | "eslint": "8.56.0", 76 | "eslint-config-next": "^14.1.4", 77 | "eslint-config-prettier": "^9.1.0", 78 | "husky": "^6.0.0", 79 | "lint-staged": "^11.0.0", 80 | "postcss": "^8.4.21", 81 | "prettier": "^2.3.2", 82 | "tailwindcss": "^3.4.3", 83 | "typescript": "^5.3.3" 84 | }, 85 | "lint-staged": { 86 | "**/*.{js,ts,tsx}": [ 87 | "eslint --cache --fix", 88 | "prettier --write '**/*.{js,ts,tsx}'" 89 | ] 90 | }, 91 | "config": { 92 | "commitizen": { 93 | "path": "./node_modules/cz-conventional-changelog", 94 | "disableSubjectLowerCase": true, 95 | "maxHeaderWidth": 72, 96 | "maxLineWidth": 80, 97 | "defaultType": "chore" 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pages/admin/challenges/add.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import ChallengeForm from '../../../components/adminComponents/challengeComponents/ChallengeForm'; 3 | import { RequestHelper } from '../../../lib/request-helper'; 4 | import { useAuthContext } from '../../../lib/user/AuthContext'; 5 | import Link from 'next/link'; 6 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 7 | 8 | function isAuthorized(user): boolean { 9 | if (!user || !user.permissions) return false; 10 | return (user.permissions as string[]).includes('super_admin'); 11 | } 12 | 13 | export default function AddChallengePage() { 14 | const { user, isSignedIn } = useAuthContext(); 15 | const router = useRouter(); 16 | 17 | const submitAddChallengeRequest = async (challengeData: Challenge) => { 18 | try { 19 | await RequestHelper.post( 20 | '/api/challenges', 21 | { 22 | headers: { 23 | Authorization: user.token, 24 | }, 25 | }, 26 | { 27 | ...challengeData, 28 | rank: parseInt(router.query.id as string), 29 | }, 30 | ); 31 | alert('Challenge created'); 32 | router.push('/admin/challenges'); 33 | } catch (error) { 34 | alert('Unexpected error! Please try again'); 35 | console.error(error); 36 | } 37 | }; 38 | 39 | if (!isSignedIn || !isAuthorized(user)) 40 | return
Unauthorized
; 41 | 42 | return ( 43 |
44 |
45 | 46 |
47 | 48 | Return to challenges 49 |
50 | 51 |
52 |
53 | { 55 | await submitAddChallengeRequest(challenge); 56 | }} 57 | formAction="Add" 58 | /> 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /pages/admin/events/add.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import EventForm from '../../../components/adminComponents/eventComponents/EventForm'; 3 | import { RequestHelper } from '../../../lib/request-helper'; 4 | import { useAuthContext } from '../../../lib/user/AuthContext'; 5 | import Link from 'next/link'; 6 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 7 | 8 | function isAuthorized(user): boolean { 9 | if (!user || !user.permissions) return false; 10 | return (user.permissions as string[]).includes('super_admin'); 11 | } 12 | 13 | export default function AddEventPage() { 14 | const { user, isSignedIn } = useAuthContext(); 15 | const router = useRouter(); 16 | 17 | const submitAddEventRequest = async (eventData: ScheduleEvent) => { 18 | try { 19 | await RequestHelper.post( 20 | '/api/schedule', 21 | { 22 | headers: { 23 | Authorization: user.token, 24 | }, 25 | }, 26 | { 27 | ...eventData, 28 | Event: parseInt(router.query.id as string), 29 | }, 30 | ); 31 | alert('Event created'); 32 | router.push('/admin/events'); 33 | } catch (error) { 34 | alert('Unexpected error! Please try again'); 35 | console.error(error); 36 | } 37 | }; 38 | 39 | if (!isSignedIn || !isAuthorized(user)) 40 | return
Unauthorized
; 41 | 42 | return ( 43 |
44 |
45 | 46 |
47 | 48 | Return to events 49 |
50 | 51 |
52 |
53 | { 55 | await submitAddEventRequest(event); 56 | }} 57 | formAction="Add" 58 | /> 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /pages/api/applications/[applicationId].tsx: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { auth, firestore } from 'firebase-admin'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | 8 | const db = firestore(); 9 | 10 | const APPLICATIONS_COLLECTION = '/registrations'; 11 | const USERS_COLLECTION = '/users'; 12 | 13 | function extractHeaderToken(input: string) { 14 | const result = input; 15 | return result; 16 | } 17 | 18 | /** 19 | * Handles GET requests to /api/application/. 20 | * 21 | * This returns the application the authorized user wants to see. 22 | * 23 | * @param req The HTTP request 24 | * @param res The HTTP response 25 | */ 26 | async function handleGetApplication(req: NextApiRequest, res: NextApiResponse) { 27 | // TODO: Handle user authorization 28 | const { 29 | query: { token, id }, 30 | headers, 31 | } = req; 32 | 33 | // 34 | // Check if request header contains token 35 | // TODO: Figure out how to handle the string | string[] mess. 36 | const userToken = (token as string) || (headers['authorization'] as string); 37 | 38 | const isAuthorized = await userIsAuthorized(userToken); 39 | 40 | // TODO: Extract from bearer token 41 | // Probably not safe 42 | if (!isAuthorized) { 43 | return res.status(401).send({ 44 | type: 'request-unauthorized', 45 | message: 'Request is not authorized to perform admin functionality.', 46 | }); 47 | } 48 | const userID = id as string; 49 | 50 | try { 51 | const application = await db.collection(APPLICATIONS_COLLECTION).doc(userID); 52 | const data = await application.get(); 53 | if (!data.exists) { 54 | res.status(404).json({ 55 | code: 'not-found', 56 | message: 'Application ID invalid, or the user is not registered.', 57 | }); 58 | } else { 59 | res.status(200).json(data.data()); 60 | } 61 | } catch (error) { 62 | console.error('Error when fetching applications', error); 63 | res.status(500).json({ 64 | code: 'internal-error', 65 | message: 'Something went wrong when processing this request. Try again later.', 66 | }); 67 | } 68 | return; 69 | } 70 | 71 | /** 72 | * Get application data. 73 | * 74 | * Corresponds to /api/applications/[applicationId] route; 75 | */ 76 | export default function handleApplications(req: NextApiRequest, res: NextApiResponse) { 77 | // Get /applications collection in Cloud Firestore 78 | // GET: Return this application 79 | // PATCH: Modify an application 80 | // DELETE: Delete this applications 81 | const { method } = req; 82 | if (method === 'GET') { 83 | return handleGetApplication(req, res); 84 | } else if (method === 'PATCH') { 85 | } else if (method === 'DELETE') { 86 | // Maybe check for additional authorization so only organizers can delete individual applications? 87 | } else { 88 | res.setHeader('Allow', ['GET', 'PATCH', 'DELETE']); 89 | res.status(405).end(`Method ${method} Not Allowed`); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pages/api/challenges/reorder.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | 9 | const CHALLENGES = '/challenges'; 10 | 11 | async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 12 | const reorderedChallenges: Challenge[] = req.body; 13 | const userToken = req.headers['authorization'] as string; 14 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin']); 15 | if (!isAuthorized) { 16 | return res.status(403).json({ 17 | statusCode: 403, 18 | msg: 'Request is not authorized to perform admin functionality', 19 | }); 20 | } 21 | const challenges = await db.collection(CHALLENGES).get(); 22 | challenges.forEach(async (doc) => { 23 | const newRank = reorderedChallenges.findIndex((obj) => obj.rank === doc.data().rank); 24 | await doc.ref.update({ 25 | ...doc.data(), 26 | rank: newRank, 27 | }); 28 | }); 29 | return res.json({ 30 | msg: 'ok', 31 | }); 32 | } 33 | 34 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 35 | const { method } = req; 36 | switch (method) { 37 | case 'POST': { 38 | return handlePostRequest(req, res); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/api/creid.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import initializeApi from '../../lib/admin/init'; 3 | import { firestore } from 'firebase-admin'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | async function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 9 | if (process.env.NODE_ENV !== 'development') { 10 | return res.status(404).json({ 11 | msg: 'Route not found', 12 | }); 13 | } 14 | const snapshot = await db.collection('/registrations').get(); 15 | const users = []; 16 | snapshot.forEach((doc) => { 17 | users.push({ 18 | id: doc.data().id, 19 | user: { 20 | firstName: doc.data().user.firstName, 21 | lastName: doc.data().user.lastName, 22 | permissions: doc.data().user.permissions, 23 | }, 24 | }); 25 | }); 26 | await db.collection('/miscellaneous').doc('allusers').set({ users }); 27 | return res.json({ 28 | msg: 'Operation completed', 29 | }); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.end(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/api/dates/index.tsx: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const DATES = '/dates'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of keynote speakers from backend for the keynote speakers section in home page 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getDates(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(DATES).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getDates(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/keynotespeakers/index.tsx: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const KEYNOTE_SPEAKERS = '/keynotespeakers'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of keynote speakers from backend for the keynote speakers section in home page 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getKeynoteSpeakers(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(KEYNOTE_SPEAKERS).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getKeynoteSpeakers(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/members/index.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const MEMBERS_COLLECTION = '/members'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of members from backend for the "Meet the team" section 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getMembersData(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(MEMBERS_COLLECTION).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getMembersData(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/prizes.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../lib/admin/init'; 4 | ``; 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const PRIZES = '/prizes'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of prizes from backend for the prizes section in home page 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getChallenges(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(PRIZES).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | data.sort((a, b) => a.rank - b.rank); 26 | res.json(data); 27 | } 28 | 29 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 30 | return getChallenges(req, res); 31 | } 32 | 33 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 34 | const { method } = req; 35 | switch (method) { 36 | case 'GET': { 37 | return handleGetRequest(req, res); 38 | } 39 | default: { 40 | return res.status(404).json({ 41 | msg: 'Route not found', 42 | }); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pages/api/questions/[userId]/answered.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const QUESTIONS_COLLECTION = '/questions'; 9 | 10 | /** 11 | * 12 | * Fetch all answered question asked by a certain from the database 13 | * 14 | * @param req request object 15 | * @param res response object 16 | * 17 | * 18 | */ 19 | async function getAnsweredQuestionByUserId(req: NextApiRequest, res: NextApiResponse) { 20 | const { userId } = req.query; 21 | 22 | const snapshot = await db 23 | .collection(QUESTIONS_COLLECTION) 24 | .where('userId', '==', userId) 25 | .where('status', '==', 'answered') 26 | .get(); 27 | let data = []; 28 | 29 | snapshot.forEach((doc) => { 30 | data.push(doc.data()); 31 | }); 32 | 33 | res.json(data); 34 | } 35 | 36 | async function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 37 | return getAnsweredQuestionByUserId(req, res); 38 | } 39 | 40 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 41 | const { method } = req; 42 | if (method === 'GET') { 43 | return handleGetRequest(req, res); 44 | } else { 45 | return res.status(404).json({ 46 | msg: 'Route not found', 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/api/questions/[userId]/pending.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const QUESTIONS_COLLECTION = '/questions'; 9 | 10 | /** 11 | * 12 | * Fetch all pending question asked by a certain from the database 13 | * 14 | * @param req request object 15 | * @param res response object 16 | * 17 | * 18 | */ 19 | async function getPendingQuestionsByUserId(req: NextApiRequest, res: NextApiResponse) { 20 | const { userId } = req.query; 21 | 22 | const snapshot = await db 23 | .collection(QUESTIONS_COLLECTION) 24 | .where('userId', '==', userId) 25 | .where('status', '==', 'pending') 26 | .get(); 27 | let data = []; 28 | 29 | snapshot.forEach((doc) => { 30 | data.push(doc.data()); 31 | }); 32 | 33 | res.json(data); 34 | } 35 | 36 | async function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 37 | return getPendingQuestionsByUserId(req, res); 38 | } 39 | 40 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 41 | const { method } = req; 42 | if (method === 'GET') { 43 | return handleGetRequest(req, res); 44 | } else { 45 | return res.status(404).json({ 46 | msg: 'Route not found', 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/api/questions/faq.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const FAQS_COLLECTION = '/faqs'; 9 | 10 | /** 11 | * 12 | * Fetch all FAQs from the database 13 | * 14 | * @param req request object 15 | * @param res response object 16 | * 17 | * 18 | */ 19 | async function getFaqs(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(FAQS_COLLECTION).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getFaqs(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/questions/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { firestore } from 'firebase-admin'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const QUESTIONS_COLLECTION = '/questions'; 9 | 10 | export interface QAReqBody { 11 | userId: string; 12 | question: string; 13 | } 14 | 15 | export interface QADocument extends QAReqBody { 16 | status: 'pending' | 'answered'; 17 | answer: string; 18 | id: string; 19 | } 20 | 21 | /** 22 | * 23 | * Post a question asked by user to the backend 24 | * 25 | * @param req request object 26 | * @param res response object 27 | * 28 | * 29 | */ 30 | async function postQuestionToDB(req: NextApiRequest, res: NextApiResponse) { 31 | const questionDoc: Partial = { 32 | userId: req.body.userId, 33 | question: req.body.question, 34 | answer: '', 35 | status: 'pending', 36 | }; 37 | 38 | await db.collection(QUESTIONS_COLLECTION).add(questionDoc); 39 | 40 | res.status(200).json({ 41 | status: 'completed', 42 | }); 43 | } 44 | 45 | async function getAllQuestions(req: NextApiRequest, res: NextApiResponse) { 46 | const snapshot = await db.collection(QUESTIONS_COLLECTION).get(); 47 | let questions = []; 48 | snapshot.forEach((doc) => { 49 | questions.push(doc.data()); 50 | }); 51 | res.json(questions); 52 | } 53 | 54 | async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 55 | return postQuestionToDB(req, res); 56 | } 57 | 58 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 59 | return getAllQuestions(req, res); 60 | } 61 | 62 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 63 | const { method } = req; 64 | switch (method) { 65 | case 'GET': { 66 | return handleGetRequest(req, res); 67 | } 68 | case 'POST': { 69 | return handlePostRequest(req, res); 70 | } 71 | default: { 72 | return res.status(404).json({ 73 | msg: 'Route not found', 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pages/api/questions/pending/[questionId].ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | 9 | const QUESTION_COLLECTION = '/questions'; 10 | 11 | /** 12 | * 13 | * API endpoint to fetch a pending question with given ID 14 | * 15 | * @param req HTTP request object 16 | * @param res HTTP response object 17 | * 18 | * 19 | */ 20 | async function getPendingQuestionById(req: NextApiRequest, res: NextApiResponse) { 21 | const snapshot = await db 22 | .collection(QUESTION_COLLECTION) 23 | .doc(req.query.questionId as string) 24 | .get(); 25 | res.json(snapshot.data()); 26 | } 27 | 28 | /** 29 | * 30 | * API endpoint to post an answer to a pending question 31 | * 32 | * @param req HTTP request object 33 | * @param res HTTP response object 34 | * 35 | * 36 | */ 37 | async function resolvePendingQuestionById(req: NextApiRequest, res: NextApiResponse) { 38 | const { headers } = req; 39 | const userToken = headers['authorization']; 40 | 41 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin', 'admin']); 42 | if (!isAuthorized) { 43 | return res.status(403).json({ 44 | msg: 'Request is not authorized to perform admin functionality.', 45 | }); 46 | } 47 | 48 | const newData = { 49 | ...JSON.parse(req.body), 50 | status: 'answered', 51 | }; 52 | const doc = await db 53 | .collection(QUESTION_COLLECTION) 54 | .doc(req.query.questionId as string) 55 | .set(newData, { 56 | merge: true, 57 | }); 58 | res.json(doc); 59 | } 60 | 61 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 62 | return getPendingQuestionById(req, res); 63 | } 64 | 65 | function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 66 | return resolvePendingQuestionById(req, res); 67 | } 68 | 69 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 70 | const { method } = req; 71 | switch (method) { 72 | case 'GET': { 73 | return handleGetRequest(req, res); 74 | } 75 | case 'POST': { 76 | return handlePostRequest(req, res); 77 | } 78 | default: { 79 | return res.status(404).json({ 80 | msg: 'Route not found', 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pages/api/questions/pending/index.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const QUESTIONS_COLLECTION = '/questions'; 9 | 10 | /** 11 | * 12 | * API endpoint to fetch all pending questions 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getPendingQuestions(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(QUESTIONS_COLLECTION).where('status', '==', 'pending').get(); 21 | let questions = []; 22 | snapshot.forEach((doc) => { 23 | questions.push({ 24 | ...doc.data(), 25 | id: doc.id, 26 | }); 27 | }); 28 | res.json(questions); 29 | } 30 | 31 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 32 | return getPendingQuestions(req, res); 33 | } 34 | 35 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 36 | const { method } = req; 37 | switch (method) { 38 | case 'GET': { 39 | return handleGetRequest(req, res); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pages/api/registrations/status.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import initializeApi from '../../../lib/admin/init'; 3 | import { firestore } from 'firebase-admin'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | 9 | async function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 10 | const preferenceDoc = await db.collection('miscellaneous').doc('preferences').get(); 11 | return res.status(200).json({ 12 | allowRegistrations: preferenceDoc.data().allowRegistrations ?? false, 13 | }); 14 | } 15 | 16 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 17 | const { method } = req; 18 | switch (method) { 19 | case 'GET': { 20 | return handleGetRequest(req, res); 21 | } 22 | default: { 23 | res.setHeader('Allow', ['GET', 'POST']); 24 | return res.status(405).end(`Method ${method} Not Allowed`); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/api/registrations/toggle.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { firestore } from 'firebase-admin'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | 9 | async function handleToggleRequest(req: NextApiRequest, res: NextApiResponse) { 10 | const { headers } = req; 11 | const userToken = headers['authorization']; 12 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin']); 13 | 14 | if (!isAuthorized) { 15 | return res.status(403).json({ 16 | msg: 'Request is not authorized to perform admin functionality.', 17 | }); 18 | } 19 | 20 | const preferenceDoc = await db.collection('miscellaneous').doc('preferences').get(); 21 | const preferenceData = { 22 | ...preferenceDoc.data(), 23 | allowRegistrations: req.body.allowRegistrations, 24 | }; 25 | await preferenceDoc.ref.update(preferenceData); 26 | res.status(200).json({ 27 | msg: `${ 28 | req.body.allowRegistrations ? 'Registration set to open' : 'Registration set to closed' 29 | }`, 30 | }); 31 | } 32 | 33 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 34 | const { method } = req; 35 | switch (method) { 36 | case 'POST': { 37 | return handleToggleRequest(req, res); 38 | } 39 | default: { 40 | res.setHeader('Allow', ['GET', 'POST']); 41 | return res.status(405).end(`Method ${method} Not Allowed`); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/resume/upload/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import firebase from 'firebase/compat/app'; 3 | import 'firebase/compat/storage'; 4 | import nc from 'next-connect'; 5 | import multer from 'multer'; 6 | 7 | interface NCNextApiRequest extends NextApiRequest { 8 | file: Express.Multer.File; 9 | } 10 | 11 | const handler = nc({ 12 | onError: (err, req, res, next) => { 13 | console.log(err); 14 | res.status(500).json({ 15 | msg: 'Server error', 16 | }); 17 | }, 18 | onNoMatch: (req, res, next) => { 19 | res.status(404).json({ 20 | msg: 'Route not found', 21 | }); 22 | }, 23 | }); 24 | 25 | handler.use(multer().single('resume')); 26 | handler.post(async (req, res) => { 27 | if (!req.file) res.end(); 28 | if (firebase.apps.length <= 0) 29 | firebase.initializeApp({ 30 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 31 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 32 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 33 | }); 34 | 35 | await firebase 36 | .auth() 37 | .signInWithEmailAndPassword( 38 | process.env.NEXT_PUBLIC_RESUME_UPLOAD_SERVICE_ACCOUNT, 39 | process.env.NEXT_PUBLIC_RESUME_UPLOAD_PASSWORD, 40 | ); 41 | 42 | const storageRef = firebase.storage().ref(); 43 | const studyLevelRef = storageRef.child('resumes/' + req.body.studyLevel); 44 | const majorRef = studyLevelRef.child(req.body.major); 45 | const fileRef = majorRef.child(req.body.fileName); 46 | 47 | await fileRef.put(req.file.buffer); 48 | const fileUrl = await fileRef.getDownloadURL(); 49 | res.status(200).json({ 50 | url: fileUrl, 51 | }); 52 | }); 53 | 54 | export const config = { 55 | api: { 56 | bodyParser: false, // Disallow body parsing, consume as stream 57 | }, 58 | }; 59 | 60 | export default handler; 61 | -------------------------------------------------------------------------------- /pages/api/scan/create.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | const SCANTYPES_COLLECTION = '/scan-types'; 9 | 10 | async function checkIfNameAlreadyExists(name: string) { 11 | const snapshot = await db.collection(SCANTYPES_COLLECTION).where('name', '==', name).get(); 12 | return !snapshot.empty; 13 | } 14 | 15 | async function checkIfCheckInAlreadyExists() { 16 | const snapshot = await db.collection(SCANTYPES_COLLECTION).where('isCheckIn', '==', true).get(); 17 | return !snapshot.empty; 18 | } 19 | 20 | async function createScan(req: NextApiRequest, res: NextApiResponse) { 21 | try { 22 | const scanData = JSON.parse(req.body); 23 | scanData.name = scanData.name.trim(); 24 | if (await checkIfNameAlreadyExists(scanData.name)) { 25 | return res.status(400).json({ 26 | msg: 'Scantype already exists', 27 | }); 28 | } 29 | 30 | if (scanData.isCheckIn) { 31 | const hasCheckIn = await checkIfCheckInAlreadyExists(); 32 | if (hasCheckIn) { 33 | return res.status(400).json({ 34 | msg: 'Check-in scantype already exists', 35 | }); 36 | } 37 | } 38 | 39 | await db.collection(SCANTYPES_COLLECTION).add({ 40 | ...scanData, 41 | startTime: new Date(scanData.startTime), 42 | endTime: new Date(scanData.endTime), 43 | }); 44 | return res.status(201).json({ 45 | msg: 'ScanType created', 46 | }); 47 | } catch (error) { 48 | return res.status(500).json({ 49 | msg: 'Unexpected error. Please try again later', 50 | }); 51 | } 52 | } 53 | 54 | async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 55 | const userToken = req.headers['authorization'] as string; 56 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin']); 57 | 58 | if (!isAuthorized) { 59 | return res.status(403).json({ 60 | statusCode: 403, 61 | msg: 'Request is not authorized to perform admin functionality', 62 | }); 63 | } 64 | 65 | return createScan(req, res); 66 | } 67 | 68 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 69 | const { method } = req; 70 | switch (method) { 71 | case 'POST': { 72 | return handlePostRequest(req, res); 73 | } 74 | default: { 75 | return res.status(404).json({ 76 | msg: 'Route not found', 77 | }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pages/api/scan/delete.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | const SCANTYPES_COLLECTION = '/scan-types'; 9 | const REGISTRATION_COLLECTION = '/registrations'; 10 | 11 | async function updateUserDoc(targetScanName: string) { 12 | try { 13 | const snapshot = await db.collection(REGISTRATION_COLLECTION).get(); 14 | snapshot.forEach(async (doc) => { 15 | if (doc.data().scans) { 16 | const newScans = doc.data().scans.filter((scan) => scan !== targetScanName); 17 | await db 18 | .collection(REGISTRATION_COLLECTION) 19 | .doc(doc.id) 20 | .update({ 21 | ...doc.data(), 22 | scans: newScans, 23 | }); 24 | } 25 | }); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | } 30 | 31 | async function deleteScanType(req: NextApiRequest, res: NextApiResponse) { 32 | try { 33 | const { scanData } = req.body; 34 | if (scanData.isCheckIn) { 35 | return res.status(400).json({ 36 | msg: 'Check-in scan can not be deleted', 37 | }); 38 | } 39 | const snapshot = await db 40 | .collection(SCANTYPES_COLLECTION) 41 | .where('name', '==', scanData.name) 42 | .get(); 43 | if (snapshot.empty) { 44 | return res.status(404).json({ 45 | msg: 'ScanType not found', 46 | }); 47 | } 48 | snapshot.forEach(async (doc) => { 49 | await db.collection(SCANTYPES_COLLECTION).doc(doc.id).delete(); 50 | }); 51 | return res.status(200).json({ 52 | msg: 'ScanType deleted', 53 | }); 54 | } catch (error) { 55 | return res.status(500).json({ 56 | msg: 'Unexpected error. Please try again later', 57 | }); 58 | } 59 | } 60 | 61 | async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 62 | const userToken = req.headers['authorization'] as string; 63 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin']); 64 | 65 | if (!isAuthorized) { 66 | return res.status(403).json({ 67 | msg: 'Request is not allowed to perform super admin functionality', 68 | }); 69 | } 70 | 71 | return deleteScanType(req, res); 72 | } 73 | 74 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 75 | const { method } = req; 76 | switch (method) { 77 | case 'POST': { 78 | return handlePostRequest(req, res); 79 | } 80 | default: { 81 | return res.status(404).json({ 82 | msg: 'Route not found', 83 | }); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pages/api/scantypes.tsx: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { firestore } from 'firebase-admin'; 3 | import initializeApi from '../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | 8 | const db = firestore(); 9 | 10 | const SCAN_TYPE_COLLECTION = '/scan-types'; 11 | 12 | /** 13 | * Handles GET requests to /api/scantypes. 14 | * 15 | * This returns all scantypes the user is authorized to see. 16 | * 17 | * @param req The HTTP request 18 | * @param res The HTTP response 19 | */ 20 | async function handleGetScanTypes(req: NextApiRequest, res: NextApiResponse) { 21 | // TODO: Handle user authorization 22 | const { 23 | query: { token }, 24 | headers, 25 | } = req; 26 | 27 | // 28 | // Check if request header contains token 29 | // TODO: Figure out how to handle the string | string[] mess. 30 | const userToken = (token as string) || (headers['authorization'] as string); 31 | // TODO: Extract from bearer token 32 | // Probably not safe 33 | const isAuthorized = await userIsAuthorized(userToken, ['admin', 'super_admin']); 34 | if (!isAuthorized) { 35 | // Return empty array for navbar 36 | return res.json([]); 37 | } 38 | 39 | try { 40 | const snapshot = await db.collection(SCAN_TYPE_COLLECTION).get(); 41 | const scantypes = snapshot.docs.map((snap) => { 42 | // TODO: Verify the application is accurate and report if something is off 43 | return snap.data(); 44 | }); 45 | scantypes.sort((a, b) => a.precedence - b.precedence); 46 | res.status(200).json(scantypes); 47 | } catch (error) { 48 | console.error('Error when fetching applications', error); 49 | res.status(500).json({ 50 | code: 'internal-error', 51 | message: 'Something went wrong when processing this request. Try again later.', 52 | }); 53 | } 54 | } 55 | 56 | type ApplicationsResponse = {}; 57 | 58 | /** 59 | * Fetches scantype data. 60 | * 61 | * Corresponds to /api/scantypes route. 62 | */ 63 | export default async function handleScanTypes( 64 | req: NextApiRequest, 65 | res: NextApiResponse, 66 | ) { 67 | const { method } = req; 68 | 69 | if (method === 'GET') { 70 | return handleGetScanTypes(req, res); 71 | } else { 72 | res.setHeader('Allow', ['GET', 'POST']); 73 | res.status(405).end(`Method ${method} Not Allowed`); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pages/api/sponsor/index.tsx: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const SPONSORS = '/sponsors'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of keynote speakers from backend for the keynote speakers section in home page 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getSponsors(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(SPONSORS).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getSponsors(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/spotlightevents/index.tsx: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const Spotlight_Events = '/spotlightevents'; 9 | 10 | /** 11 | * 12 | * API endpoint to get data of events from backend for the spotlight events section 13 | * 14 | * @param req HTTP request object 15 | * @param res HTTP response object 16 | * 17 | * 18 | */ 19 | async function getSpotlightEvents(req: NextApiRequest, res: NextApiResponse) { 20 | const snapshot = await db.collection(Spotlight_Events).get(); 21 | let data = []; 22 | snapshot.forEach((doc) => { 23 | data.push(doc.data()); 24 | }); 25 | res.json(data); 26 | } 27 | 28 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 29 | return getSpotlightEvents(req, res); 30 | } 31 | 32 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 33 | const { method } = req; 34 | switch (method) { 35 | case 'GET': { 36 | return handleGetRequest(req, res); 37 | } 38 | default: { 39 | return res.status(404).json({ 40 | msg: 'Route not found', 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/tokens/index.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | const db = firestore(); 7 | 8 | const TOKENS_COLLECTION = '/tokens'; 9 | 10 | async function postTokenToDB(req: NextApiRequest, res: NextApiResponse) { 11 | try { 12 | const snapshot = await db 13 | .collection(TOKENS_COLLECTION) 14 | .where('token', '==', req.body.token) 15 | .get(); 16 | if (!snapshot.empty) { 17 | return res.json({ 18 | msg: 'Token already exists', 19 | }); 20 | } 21 | await db.collection(TOKENS_COLLECTION).add(req.body); 22 | return res.status(200).send({ 23 | msg: 'Token saved', 24 | }); 25 | } catch (error) { 26 | console.error(error); 27 | return res.status(500).send({ 28 | msg: 'Server Error', 29 | }); 30 | } 31 | } 32 | 33 | function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 34 | return postTokenToDB(req, res); 35 | } 36 | 37 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 38 | const { method } = req; 39 | switch (method) { 40 | case 'POST': { 41 | return handlePostRequest(req, res); 42 | } 43 | default: { 44 | return res.status(404).json({ 45 | msg: 'Route not found', 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/api/userinfo.tsx: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { auth, firestore } from 'firebase-admin'; 3 | import initializeApi from '../../lib/admin/init'; 4 | 5 | initializeApi(); 6 | 7 | const db = firestore(); 8 | 9 | const REGISTRATION_COLLECTION = '/registrations'; 10 | 11 | async function userIsAuthorized(token: string, queryId: string) { 12 | if (!token) return false; 13 | try { 14 | const payload = await auth().verifyIdToken(token); 15 | if (payload.uid === queryId) return true; 16 | const snapshot = await firestore() 17 | .collection(REGISTRATION_COLLECTION) 18 | .where('id', '==', payload.uid) 19 | .get(); 20 | if (snapshot.empty) return false; 21 | for (let userRole of snapshot.docs[0].data().user.permissions as string[]) { 22 | if (userRole === 'super_admin' || userRole === 'admin') return true; 23 | } 24 | return false; 25 | } catch (error) { 26 | console.error(error); 27 | return false; 28 | } 29 | } 30 | 31 | /** 32 | * Handles GET requests to /api/scantypes. 33 | * 34 | * This returns all scantypes the user is authorized to see. 35 | * 36 | * @param req The HTTP request 37 | * @param res The HTTP response 38 | */ 39 | async function handleUserInfo(req: NextApiRequest, res: NextApiResponse) { 40 | // TODO: Handle user authorization 41 | const { 42 | query: { token, id }, 43 | headers, 44 | } = req; 45 | 46 | // 47 | // Check if request header contains token 48 | // TODO: Figure out how to handle the string | string[] mess. 49 | const userToken = (token as string) || (headers['authorization'] as string); 50 | 51 | // TODO: Extract from bearer token 52 | // Probably not safe 53 | const isAuthorized = await userIsAuthorized(userToken, id as string); 54 | if (!isAuthorized) { 55 | return res.status(401).send({ 56 | type: 'request-unauthorized', 57 | message: 'Request is not authorized to perform admin functionality.', 58 | }); 59 | } 60 | 61 | const userID = id as string; 62 | 63 | try { 64 | const snapshot = await db.collection(REGISTRATION_COLLECTION).doc(userID).get(); 65 | if (!snapshot.exists) 66 | return res.status(404).json({ code: 'not found', message: "User doesn't exist..." }); 67 | res.status(200).json(snapshot.data()); 68 | } catch (error) { 69 | console.error('Error when fetching applications', error); 70 | res.status(500).json({ 71 | code: 'internal-error', 72 | message: 'Something went wrong when processing this request. Try again later.', 73 | }); 74 | } 75 | } 76 | 77 | type ApplicationsResponse = {}; 78 | 79 | /** 80 | * Fetches scantype data. 81 | * 82 | * Corresponds to /api/scantypes route. 83 | */ 84 | export default async function handleScanTypes( 85 | req: NextApiRequest, 86 | res: NextApiResponse, 87 | ) { 88 | const { method } = req; 89 | 90 | if (method === 'GET') { 91 | handleUserInfo(req, res); 92 | } else { 93 | res.setHeader('Allow', ['GET', 'POST']); 94 | res.status(405).end(`Method ${method} Not Allowed`); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pages/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { auth } from 'firebase-admin'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | import initializeApi from '../../../lib/admin/init'; 5 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 6 | 7 | initializeApi(); 8 | const db = firestore(); 9 | 10 | const USERS_COLLECTION = '/registrations'; 11 | const MISC_COLLECTION = '/miscellaneous'; 12 | 13 | /** 14 | * 15 | * Represent how data of a User is stored in the backend 16 | * 17 | */ 18 | export interface UserData { 19 | id: string; 20 | scans?: string[]; 21 | user: { 22 | firstName: string; 23 | lastName: string; 24 | permissions: string[]; 25 | }; 26 | } 27 | 28 | /** 29 | * 30 | * API endpoint to fetch all users from the database 31 | * 32 | * @param req HTTP request object 33 | * @param res HTTP response object 34 | * 35 | * 36 | */ 37 | async function getAllUsers(req: NextApiRequest, res: NextApiResponse) { 38 | const { headers } = req; 39 | 40 | const userToken = headers['authorization']; 41 | const isAuthorized = await userIsAuthorized(userToken); 42 | 43 | if (!isAuthorized) { 44 | return res.status(403).json({ 45 | msg: 'Request is not authorized to perform admin functionality.', 46 | }); 47 | } 48 | 49 | const doc = await db.collection(MISC_COLLECTION).doc('allusers').get(); 50 | 51 | return res.json(doc.data().users); 52 | } 53 | 54 | /** 55 | * 56 | * API endpoint to fetch all users from the database 57 | * 58 | * @param req HTTP request object 59 | * @param res HTTP response object 60 | * 61 | * 62 | */ 63 | async function getAllRegistrations(req: NextApiRequest, res: NextApiResponse) { 64 | const { headers } = req; 65 | 66 | const userToken = headers['authorization']; 67 | const isAuthorized = await userIsAuthorized(userToken); 68 | 69 | if (!isAuthorized) { 70 | return res.status(403).json({ 71 | msg: 'Request is not authorized to perform admin functionality.', 72 | }); 73 | } 74 | 75 | const collectionRef = await db.collection(USERS_COLLECTION).get(); 76 | const data = collectionRef.docs.map((doc) => doc.data()); 77 | 78 | return res.json(data); 79 | } 80 | 81 | function handleGetRequest(req: NextApiRequest, res: NextApiResponse) { 82 | return getAllRegistrations(req, res); 83 | } 84 | 85 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 86 | const { method } = req; 87 | switch (method) { 88 | case 'GET': { 89 | return handleGetRequest(req, res); 90 | } 91 | default: { 92 | return res.status(404).json({ 93 | msg: 'Route not found', 94 | }); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pages/api/users/roles.ts: -------------------------------------------------------------------------------- 1 | import { firestore } from 'firebase-admin'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import initializeApi from '../../../lib/admin/init'; 4 | import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; 5 | 6 | initializeApi(); 7 | const db = firestore(); 8 | const USERS_COLLECTION = '/registrations'; 9 | const MISC_COLLECTION = '/miscellaneous'; 10 | 11 | async function updateAllUserDoc(userId: string, newRole: string) { 12 | const docRef = db.collection(MISC_COLLECTION).doc('allusers'); 13 | const data = await docRef.get(); 14 | 15 | const userData = data.data().users.map((obj) => { 16 | if (obj.id === userId) return { ...obj, user: { ...obj.user, permissions: [newRole] } }; 17 | return obj; 18 | }); 19 | await docRef.set({ 20 | users: userData, 21 | }); 22 | } 23 | 24 | async function updateUserRole( 25 | userId: string, 26 | newRole: string, 27 | ): Promise<{ statusCode: number; msg: string }> { 28 | const docRef = db.collection(USERS_COLLECTION).doc(userId); 29 | const data = await docRef.get(); 30 | if (!data.exists) { 31 | return { 32 | statusCode: 404, 33 | msg: 'User not found', 34 | }; 35 | } 36 | const userData = data.data(); 37 | await docRef.set({ 38 | ...userData, 39 | user: { 40 | ...userData.user, 41 | permissions: [newRole], 42 | }, 43 | }); 44 | await updateAllUserDoc(userId, newRole); 45 | return { 46 | statusCode: 200, 47 | msg: 'Update completed', 48 | }; 49 | } 50 | 51 | async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { 52 | const { headers } = req; 53 | const userToken = headers['authorization']; 54 | const isAuthorized = await userIsAuthorized(userToken, ['super_admin']); 55 | 56 | if (!isAuthorized) { 57 | return res.status(403).json({ 58 | statusCode: 403, 59 | msg: 'Request is not authorized to perform admin functionality', 60 | }); 61 | } 62 | 63 | const { userId, newRole } = JSON.parse(req.body); 64 | 65 | const updateResult = await updateUserRole(userId, newRole); 66 | res.json(updateResult); 67 | } 68 | 69 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 70 | const { method } = req; 71 | if (method === 'POST') { 72 | return handlePostRequest(req, res); 73 | } else { 74 | return res.status(404).json({ 75 | statusCode: 404, 76 | msg: 'Route not found', 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pages/auth/signOut.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import React from 'react'; 3 | import { useAuthContext } from '../../lib/user/AuthContext'; 4 | 5 | /** 6 | * A page that signs the user out and redirects to the index (/) page. 7 | */ 8 | export default function SignOutPage() { 9 | const { signOut } = useAuthContext(); 10 | 11 | const router = useRouter(); 12 | 13 | React.useEffect(() => { 14 | signOut() 15 | .then(() => { 16 | router.push('/'); 17 | }) 18 | .catch((error) => { 19 | console.error('Could not sign out', error); 20 | }); 21 | }, [router]); 22 | 23 | return ( 24 |
25 |
Signing out of HackPortal
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /pages/dashboard/submit.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import React from 'react'; 3 | import DashboardHeader from '../../components/dashboardComponents/DashboardHeader'; 4 | import Sidebar from '../../components/dashboardComponents/Sidebar'; 5 | 6 | /** 7 | * NOTE: THIS IS NOT BEING USED. 8 | * FEEL FREE TO DELETE THIS PAGE OR LINK IT TO THE HACKATHON DEVPOST. 9 | * 10 | * 11 | * The dashboard / submit. 12 | * 13 | * Landing: /submit 14 | */ 15 | export default function Submit() { 16 | return ( 17 |
18 | 19 | Project Submissions 20 | {/* !change */} 21 | 22 | 23 | 24 | 25 |
26 | 27 |
Big Heading
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /pages/hackerpacks/[id].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next'; 2 | import { NotionAPI } from 'notion-client'; 3 | import { useRouter } from 'next/router'; 4 | 5 | import hackerpackSettings from '../../components/hackerpackComponents/hackerpack-settings.json'; 6 | import HackerpackDisplay from '../../components/hackerpackComponents/HackerpackDisplay'; 7 | 8 | export default function NotionSubpage(props: { content: any; error: boolean }) { 9 | const router = useRouter(); 10 | // If the hackerpack is not a Notion page, ignore it 11 | if (props.error) { 12 | router.push('/hackerpacks'); 13 | return null; 14 | } 15 | 16 | // Generate the sidebar content from Notion 17 | // Find the root block because Notion IDs have hyphens 18 | const rootId = Object.keys(props.content.block).find( 19 | (k) => k === router.asPath.replace('/hackerpacks/', ''), 20 | ); 21 | 22 | // TODO: Support for Markdown subpages 23 | return ; 24 | } 25 | 26 | export const getServerSideProps: GetServerSideProps = async (context) => { 27 | // Load Notion page data from Notion API 28 | // Catch invalid IDs and make sure content is generated from Notion pages 29 | try { 30 | if (hackerpackSettings.mainContent === 'notion') { 31 | const notion = new NotionAPI(); 32 | const page = await notion.getPage(context.params['id'] as string); 33 | return { props: { content: page, error: false } }; 34 | } 35 | } catch (err) {} 36 | return { props: { content: null, error: true } }; 37 | }; 38 | -------------------------------------------------------------------------------- /pages/hackerpacks/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GetStaticProps } from 'next'; 3 | import { NotionAPI } from 'notion-client'; 4 | import hackerpackSettings from '../../components/hackerpackComponents/hackerpack-settings.json'; 5 | import HackerpackDisplay from '../../components/hackerpackComponents/HackerpackDisplay'; 6 | 7 | /** 8 | * The hackerpack page. 9 | * 10 | * HackerPack: / 11 | */ 12 | export default function HackerPack(props: { content: any }) { 13 | // Find the root block because Notion IDs have hyphens 14 | const rootId = 15 | hackerpackSettings.mainContent === 'notion' 16 | ? Object.keys(props.content.block).find( 17 | (k) => k.replaceAll(/-/g, '') === hackerpackSettings.notionPageId, 18 | ) 19 | : null; 20 | 21 | return ; 22 | } 23 | 24 | export const getStaticProps: GetStaticProps = async () => { 25 | // Load Notion page data from Notion API if using notion 26 | if (hackerpackSettings.mainContent === 'notion') { 27 | const notion = new NotionAPI(); 28 | const page = await notion.getPage(hackerpackSettings.notionPageId); 29 | return { props: { content: page } }; 30 | } 31 | return { props: { content: null } }; 32 | }; 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/assets/bg2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/assets/bg2.jpeg -------------------------------------------------------------------------------- /public/assets/circles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/defaultPFP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/assets/defaultPFP.jpg -------------------------------------------------------------------------------- /public/assets/fillerAsset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/assets/fillerAsset.png -------------------------------------------------------------------------------- /public/assets/hp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/assets/hp-logo.png -------------------------------------------------------------------------------- /public/assets/mlh-sticker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/assets/mlh-sticker.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/favicon.ico -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/8.9.0/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/8.9.0/firebase-messaging.js'); 3 | 4 | // set up service worker 5 | self.addEventListener('push', async function (event) { 6 | const { announcement, iconUrl } = event.data.json().data; 7 | var options = { 8 | body: announcement, 9 | icon: iconUrl, 10 | }; 11 | event.waitUntil(self.registration.showNotification('HackPortal Announcement', options)); 12 | }); 13 | 14 | // add notification click handler 15 | self.addEventListener('notificationclick', function (event) { 16 | event.notification.close(); 17 | event.waitUntil( 18 | clients 19 | .matchAll({ 20 | type: 'window', 21 | }) 22 | .then(function (clientList) { 23 | for (const client of clientList) { 24 | if (client.url == '/dashboard' && 'focus' in client) return client.focus(); 25 | } 26 | if (clients.openWindow) return clients.openWindow('/dashboard'); 27 | }), 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /public/fonts/DMSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/DMSans-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/DMSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/DMSans-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/DS-DIGI.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/DS-DIGI.TTF -------------------------------------------------------------------------------- /public/fonts/FredokaOne-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/FredokaOne-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Nunito-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Nunito-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Black.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-128Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-128Bold.otf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-128Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-128Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-128Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-128Bold.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-128Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-128Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Wavehaus-158ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-158ExtraBold.otf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-158ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-158ExtraBold.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-158ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-158ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-158ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-158ExtraBold.woff2 -------------------------------------------------------------------------------- /public/fonts/Wavehaus-28Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-28Thin.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-28Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-28Thin.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-28Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-28Thin.woff2 -------------------------------------------------------------------------------- /public/fonts/Wavehaus-42Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-42Light.otf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-42Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-42Light.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-42Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-42Light.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-42Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-42Light.woff2 -------------------------------------------------------------------------------- /public/fonts/Wavehaus-66Book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-66Book.otf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-66Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-66Book.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-66Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-66Book.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-66Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-66Book.woff2 -------------------------------------------------------------------------------- /public/fonts/Wavehaus-95SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-95SemiBold.otf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-95SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-95SemiBold.ttf -------------------------------------------------------------------------------- /public/fonts/Wavehaus-95SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-95SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/Wavehaus-95SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/fonts/Wavehaus-95SemiBold.woff2 -------------------------------------------------------------------------------- /public/icons/admin.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/file-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /public/icons/googleicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/googleicon.png -------------------------------------------------------------------------------- /public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-384x384.png -------------------------------------------------------------------------------- /public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /public/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acmutd/hackportal/ffaaafd00c6ff345284c015c5dd4136b48cbacb1/public/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/icons/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/icons/trophy.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HackPortal Dev", 3 | "short_name": "HackPortal Dev", 4 | "icons": [ 5 | { 6 | "src": "/icons/icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "any maskable" 10 | }, 11 | { 12 | "src": "/icons/icon-384x384.png", 13 | "sizes": "384x384", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icons/icon-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ], 22 | "theme_color": "#5D5FEF", 23 | "background_color": "#5D5FEF", 24 | "start_url": "/", 25 | "display": "standalone" 26 | } 27 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const next = require('next'); 3 | const port = parseInt(process.env.PORT, 10) || 3000; 4 | const dev = process.env.NODE_ENV !== 'production'; 5 | const app = next({ dev }); 6 | const handle = app.getRequestHandler(); 7 | app.prepare().then(() => { 8 | const server = express(); 9 | server.get('/service-worker.js', (req, res) => { 10 | app.serveStatic(req, res, './.next/service-worker.js'); 11 | }); 12 | //scoping the service workers 13 | const serviceWorkers = [ 14 | { 15 | filename: 'service-worker.js', 16 | path: './.next/service-worker.js', 17 | }, 18 | { 19 | filename: 'firebase-messaging-sw.js', 20 | path: './public/firebase-messaging-sw.js', 21 | }, 22 | ]; 23 | serviceWorkers.forEach(({ filename, path }) => { 24 | server.get(`/${filename}`, (req, res) => { 25 | app.serveStatic(req, res, path); 26 | }); 27 | }); 28 | server.get('*', (req, res) => { 29 | return handle(req, res); 30 | }); 31 | server.listen(port, (err) => { 32 | if (err) throw err; 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .element { 82 | font-family: 'wavehaus'; 83 | } 84 | 85 | .card { 86 | margin: 1rem; 87 | padding: 1.5rem; 88 | text-align: left; 89 | color: inherit; 90 | text-decoration: none; 91 | border: 1px solid #eaeaea; 92 | border-radius: 10px; 93 | transition: color 0.15s ease, border-color 0.15s ease; 94 | width: 45%; 95 | } 96 | 97 | .card:hover, 98 | .card:focus, 99 | .card:active { 100 | color: #0070f3; 101 | border-color: #0070f3; 102 | } 103 | 104 | .card h2 { 105 | margin: 0 0 1rem 0; 106 | font-size: 1.5rem; 107 | } 108 | 109 | .card p { 110 | margin: 0; 111 | font-size: 1.25rem; 112 | line-height: 1.5; 113 | } 114 | 115 | .logo { 116 | height: 1em; 117 | margin-left: 0.5rem; 118 | } 119 | 120 | @font-face { 121 | font-family: 'wavehaus'; 122 | src: url('/fonts/Wavehaus-66Book.ttf') format("truetype"); 123 | font-display: auto; 124 | } 125 | 126 | @media (max-width: 600px) { 127 | .grid { 128 | width: 100%; 129 | flex-direction: column; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 4 | // darkMode: 'class', 5 | theme: { 6 | extend: { 7 | colors: { 8 | primary: '#7B81FF', //purple 9 | primaryDark: '#05149C', 10 | secondary: '#F2F3FF', //light purple 11 | secondaryDark: '#BABFFF', 12 | complementary: '#05149C', //dark purple 13 | complementaryDark: '#252427', 14 | complementaryLight: '#999999', 15 | }, 16 | fontFamily: { 17 | dmSans: ['DM Sans', 'sans-serif'], 18 | fredoka: ['Fredoka', 'sans-serif'], 19 | fredokaOne: ['Fredoka One', 'sans-serif'], 20 | nunito: ['Nunito', 'sans-serif'], 21 | poppins: ['Poppins', 'sans-serif'], 22 | inter: ['Inter', 'sans-serif'], 23 | sans: ['Fredoka', 'Roboto', 'sans-serif'], 24 | }, 25 | width: { 26 | '1/8': '12.5%', 27 | '3/8': '37.5%', 28 | '5/8': '62.5%', 29 | '7/8': '87.5%', 30 | '1/7': '14.29%', 31 | '6/7': '85.71%', 32 | }, 33 | height: { 34 | '9/10': '90%', 35 | }, 36 | minWidth: { 37 | 64: '16rem', 38 | 56: '14rem', 39 | '160px': '160px', 40 | '3/4': '75%', 41 | '9/10': '90%', 42 | }, 43 | minHeight: { 44 | '1/3': '33.33%', 45 | '9/10': '90%', 46 | '1/2': '50%', 47 | '1/4': '25%', 48 | '5/8': '62.5%', 49 | 16: '4rem', 50 | }, 51 | backgroundColor: (theme) => ({ 52 | lightBackground: '#F4F4F4', 53 | aqua: '#D8F8FF', 54 | darkAqua: '#B0F1FF', 55 | }), 56 | backgroundImage: { 57 | 'hero-pattern': `url(${'/assets/bg2.jpeg'})`, // !change 58 | }, 59 | }, 60 | }, 61 | variants: { 62 | extend: { 63 | display: ['group-hover'], 64 | }, 65 | }, 66 | plugins: [require('@tailwindcss/forms')], 67 | }; 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./*"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "pages/_app.tsx" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | --------------------------------------------------------------------------------