diff --git a/.eslintrc b/.eslintrc index 4957a09..64997fb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,6 +19,7 @@ "assert": "either" }], "react/jsx-one-expression-per-line": "off", - "react/button-has-type": "off" + "react/button-has-type": "off", + "no-bitwise": "off" } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 936e26b..2bff066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3244,6 +3244,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/luxon": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-1.26.2.tgz", + "integrity": "sha512-2pvzy4LuxBMBBLAbml6PDcJPiIeZQ0Hqj3PE31IxkNI250qeoRMDovTrHXeDkIL4auvtarSdpTkLHs+st43EYQ==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3354,6 +3359,11 @@ "@types/node": "*" } }, + "@types/seedrandom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.0.tgz", + "integrity": "sha512-Jr03BtXs7v6M/wtTum2VOMLBq7R8zpjdZLynhA/IOJq83czXrnVo8iSUI05gcq8Ecq0Swt+nuoK3y2ji/TWyMA==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -11786,6 +11796,11 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.26.0.tgz", + "integrity": "sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A==" + }, "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", @@ -14718,11 +14733,6 @@ "prop-types": "^15.5.8" } }, - "react-ga": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.0.tgz", - "integrity": "sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ==" - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -15691,6 +15701,11 @@ "ajv-keywords": "^3.5.2" } }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", diff --git a/package.json b/package.json index b358ae9..12a4ff2 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", + "@types/luxon": "^1.26.2", "@types/node": "^14.14.5", "@types/react": "^16.9.53", "@types/react-custom-scrollbars": "^4.0.7", @@ -15,11 +16,13 @@ "@types/react-gtm-module": "^2.0.0", "@types/react-router-dom": "^5.1.6", "@types/react-select": "^3.1.2", + "@types/seedrandom": "^3.0.0", "@typescript-eslint/eslint-plugin": "^4.6.0", "@typescript-eslint/parser": "^4.6.0", "clsx": "^1.1.1", "eslint-config-airbnb-typescript": "^12.0.0", "lottie-web": "^5.7.6", + "luxon": "^1.26.0", "query-string": "^6.13.6", "react": "^17.0.1", "react-custom-scrollbars": "^4.2.1", @@ -30,6 +33,7 @@ "react-scripts": "^4.0.3", "react-select": "^3.1.1", "sass": "^1.32.8", + "seedrandom": "^3.0.5", "typescript": "^4.0.5", "web-vitals": "^0.2.4", "zod": "^1.11.11" diff --git a/src/App.tsx b/src/App.tsx index 0117286..27d9b2d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-d import Home from 'pages/Home'; import Auth from 'pages/Auth'; import Registration from 'pages/Registration'; +import RSVP from 'pages/RSVP'; import StaticFileRedirect from 'components/StaticFileRedirect'; import AuthenticatedRoute from 'components/AuthenticatedRoute'; @@ -23,6 +24,10 @@ function App(): JSX.Element { + + + + diff --git a/src/assets/discord_username_how_to.png b/src/assets/discord_username_how_to.png new file mode 100644 index 0000000..9792728 Binary files /dev/null and b/src/assets/discord_username_how_to.png differ diff --git a/src/components/form/Input/HelpIcon.tsx b/src/components/form/Input/HelpIcon.tsx new file mode 100644 index 0000000..d5be90e --- /dev/null +++ b/src/components/form/Input/HelpIcon.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +type PropTypes = { + color?: string; + [key: string]: unknown; +}; + +const HelpIcon = ({ color, ...props }: PropTypes): JSX.Element => ( + + + + +); + +export default HelpIcon; diff --git a/src/components/form/Input/StyledInput/index.tsx b/src/components/form/Input/StyledInput/index.tsx index a028eaa..5e42daf 100644 --- a/src/components/form/Input/StyledInput/index.tsx +++ b/src/components/form/Input/StyledInput/index.tsx @@ -1,15 +1,42 @@ -import clsx from 'clsx'; import React, { forwardRef } from 'react'; +import clsx from 'clsx'; import styles from './styles.module.scss'; type PropTypes = { className?: string; + multiline?: boolean; [key: string]: unknown; }; -const StyledInput = forwardRef(({ className, ...props }: PropTypes, ref): JSX.Element => ( - -)); +// Adapted from https://stackoverflow.com/a/46777664 +const adjustHeight = (textarea: HTMLTextAreaElement|null) => { + if (textarea) { + textarea.style.height = ''; + textarea.style.height = `${textarea.scrollHeight}px`; + } +}; + +const StyledInput = forwardRef(({ className, multiline = false, ...props }: PropTypes, ref): JSX.Element => { + if (multiline) { + return ( + adjustHeight(target)} + ref={(r) => { + if (typeof ref === 'function') { + ref(r); + } else if (ref) { + ref.current = r; + } + adjustHeight(r); // set initial height when the texarea is loaded + }} + /> + ); + } + + return } />; +}); export default StyledInput; diff --git a/src/components/form/Input/StyledInput/styles.module.scss b/src/components/form/Input/StyledInput/styles.module.scss index c644463..d717c68 100644 --- a/src/components/form/Input/StyledInput/styles.module.scss +++ b/src/components/form/Input/StyledInput/styles.module.scss @@ -15,4 +15,8 @@ color: #525051; font-size: 0.9em; } + + &.multiline { + font-size: 1.1em; + } } \ No newline at end of file diff --git a/src/components/form/Input/index.tsx b/src/components/form/Input/index.tsx index 7db9dc8..b86b2e2 100644 --- a/src/components/form/Input/index.tsx +++ b/src/components/form/Input/index.tsx @@ -3,18 +3,29 @@ import { useFormContext } from 'react-hook-form'; import ErrorMessage from '../ErrorMessage'; import StyledInput from './StyledInput'; +import HelpIcon from './HelpIcon'; +import styles from './styles.module.scss'; type PropTypes = { name: string, + multiline?: boolean; + helpLink?: string; [key: string]: unknown; }; -const Input = ({ name, ...props }: PropTypes): JSX.Element => { +const Input = ({ name, multiline, helpLink, ...props }: PropTypes): JSX.Element => { const { register } = useFormContext(); return ( <> - + + + {helpLink && ( + + + + )} + > ); diff --git a/src/components/form/Input/styles.module.scss b/src/components/form/Input/styles.module.scss new file mode 100644 index 0000000..70e63e7 --- /dev/null +++ b/src/components/form/Input/styles.module.scss @@ -0,0 +1,9 @@ +.inputContainer { + display: flex; + + .helpLink { + align-self: flex-end; + margin-left: 5px; + color: #3C519C; + } +} diff --git a/src/components/form/Random/index.tsx b/src/components/form/Random/index.tsx new file mode 100644 index 0000000..d66afb2 --- /dev/null +++ b/src/components/form/Random/index.tsx @@ -0,0 +1,45 @@ +import { useEffect } from 'react'; +import { useController, useFormContext } from 'react-hook-form'; +import seedrandom from 'seedrandom'; + +type WithOptionList = { options: unknown[] }; +type WithFunctionGenerator = { + min: number, // inclusive + max: number, // exclusive + generateValue: (num: number) => unknown +}; +type ValueChoices = WithOptionList | WithFunctionGenerator; +type PropTypes = { name: string, seed?: string } & ValueChoices; + +function areOptionsSpecified(props: ValueChoices): props is WithOptionList { + return (props as WithOptionList).options !== undefined; +} + +const Random = ({ name, seed, ...props }: PropTypes): JSX.Element|null => { + const { control } = useFormContext(); + const { field } = useController({ name, control, defaultValue: null }); + + useEffect(() => { + const random = () => { + if (seed) { + const rng = seedrandom(seed); + return rng(); + } + return Math.random(); + }; + + if (areOptionsSpecified(props)) { + const { options } = props; + const index = Math.floor(random() * options.length); + field.onChange(options[index]); + } else { + const { min, max, generateValue } = props; + const num = Math.floor(random() * (max - min)) + min; + field.onChange(generateValue(num)); + } + }, [field, props, seed]); + + return null; +}; + +export default Random; diff --git a/src/components/form/Select/index.tsx b/src/components/form/Select/index.tsx index a7ab776..782accb 100644 --- a/src/components/form/Select/index.tsx +++ b/src/components/form/Select/index.tsx @@ -82,7 +82,8 @@ const Select = ({ name, options = [], creatable, isMulti = false, className, ... { setIsFocused(false); onBlur(); }} onFocus={() => setIsFocused(true)} diff --git a/src/pages/RSVP/Background/index.tsx b/src/pages/RSVP/Background/index.tsx new file mode 100644 index 0000000..2471679 --- /dev/null +++ b/src/pages/RSVP/Background/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import MOUNTAINS from 'assets/registration/mountains.svg'; +import STARS from 'assets/registration/stars.svg'; +import styles from './styles.module.scss'; + +const Background: React.FC = () => ( + + + + + +); + +export default Background; diff --git a/src/pages/RSVP/Background/styles.module.scss b/src/pages/RSVP/Background/styles.module.scss new file mode 100644 index 0000000..0f41378 --- /dev/null +++ b/src/pages/RSVP/Background/styles.module.scss @@ -0,0 +1,40 @@ +.background { + background: linear-gradient(180deg, #2A3170 19.44%, #2B6776 59.7%); + width: 100vw; + height: 100%; + position: absolute; + overflow: hidden; + + .stars { + width: 100%; + height: calc(100% - 28vw); // ensure that the stars don't overlap with the translucent mountains + max-height: 425px; + background-position: center top; + background-size: 1500px; + } + + .mountains { + position: absolute; + bottom: 0; + width: 115%; + left: 53%; + transform: translateX(-50%); + + @media (min-width: 2000px) { + width: 100%; + left: 50%; + } + } + + .road { + position: absolute; + bottom: 0; + width: 100%; + height: 6vw; + background-color: #282939; + + @media (min-width: 2000px) { + height: 5.5vw; + } + } +} diff --git a/src/pages/RSVP/Form/index.tsx b/src/pages/RSVP/Form/index.tsx new file mode 100644 index 0000000..74e0d8e --- /dev/null +++ b/src/pages/RSVP/Form/index.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useState } from 'react'; +import Scrollbars from 'react-custom-scrollbars'; +import { useForm, SubmitHandler, SubmitErrorHandler, FormProvider } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DateTime } from 'luxon'; + +import PROJECTOR from 'assets/registration/projector.svg'; +import LOGO_LARGE from 'assets/logo_large.svg'; +import Input from 'components/form/Input'; +import Select from 'components/form/Select'; +import Button from 'components/form/Button'; +import Constant from 'components/form/Constant'; +import Random from 'components/form/Random'; +import { createProfile, getRegistration, refreshToken, rsvp, getRoles, getProfile } from 'util/api'; +import { RegistrationType, WithId } from 'util/types'; +import DISCORD_HELP from 'assets/discord_username_how_to.png'; + +import styles from './styles.module.scss'; +import { rsvpSchema, RSVPSchema, errorMap, defaultValues } from '../validation'; +import interests from './interests.json'; + +const NUM_PROFILE_PICTURES = 11; +const getProfilePicture = (index: number) => ((index >= 0 && index < NUM_PROFILE_PICTURES) + ? `https://hackillinois-upload.s3.amazonaws.com/photos/profiles-2021/profile-${index}.png` + : '' +); + +const capitalSnakeCase = (str: string) => str.toUpperCase().replace(/ /g, '_'); +const teamStatusOptions = ['Looking for Team', 'Looking for Members', 'Not Looking'].map((label) => ({ label, value: capitalSnakeCase(label) })); +const interestOptions = Object.values(interests) + .flat() + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) + .map((value) => ({ label: value, value })); + +const Form = (): JSX.Element => { + const [isLoading, setIsLoading] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [registration, setRegistration] = useState | null>(null); + const [finished, setFinished] = useState(false); + + const methods = useForm({ + resolver: zodResolver(rsvpSchema, { errorMap }), + defaultValues, + }); + const { handleSubmit } = methods; + + useEffect(() => { + const initialize = async () => { + const roles = await getRoles(); + if (roles.includes('Applicant')) { + const registrationData = await getRegistration('attendee'); + setRegistration(registrationData); + + if (roles.includes('Attendee')) { + setIsEditing(true); + const { id, points, ...profileData } = await getProfile(); + methods.reset(profileData); + } + } else { // user hasn't registered yet + // TODO: maybe display message or redirect to registration page? + } + }; + + initialize().finally(() => setIsLoading(false)); + }, []); // deliberately not including `methods` + + const onSubmit: SubmitHandler = async (data) => { + setIsLoading(true); + try { + await Promise.all([ + rsvp(isEditing, { isAttending: true }).then(() => refreshToken()), + createProfile(isEditing, data), + ]); + setFinished(true); + } catch (e) { + console.log(e); + alert('There was an error while submitting. If this error persists, please email contact@hackillinois.org'); + } finally { + setIsLoading(false); + } + }; + + const onError: SubmitErrorHandler = (errors) => { + console.log('error', errors); + }; + + return ( + + + + {(!finished) ? ( + <> + + + RSVP + + + + + + + + + + + + + {isLoading && Loading...} + {!isLoading && Submit} + + > + ) : ( + + + + + Thank you for RSVPing for HackIllinois 2021! Be sure to follow our instagram (@hackillinois) and our twitter (@hackillinois). We will be posting live updates during the event that you won’t want to miss! + + )} + + + + ); +}; + +export default Form; diff --git a/src/pages/RSVP/Form/interests.json b/src/pages/RSVP/Form/interests.json new file mode 100644 index 0000000..c8987a9 --- /dev/null +++ b/src/pages/RSVP/Form/interests.json @@ -0,0 +1,11 @@ +{ + "programmingLanguages": ["C","C#","C++","CSS","Go","HTML","Haskell","Java","JavaScript","Kotlin","NativeScript","PHP","Python","Ruby","Rust","Swift","TypeScript"], + "cloudComputing": ["AWS", "Azure", "Digital Ocean", "Firebase", "Google Cloud"], + "mobileDevelopment": ["Android", "Flutter", "iOS", "React Native"], + "hardware": ["Arduino", "Raspberry Pi"], + "gameDevelopment": ["Godot", "Unity", "Unreal"], + "design": ["Adobe Illustrator", "Adobe InDesign", "Adobe Photoshop", "Adobe XD", "Blender", "Canva", "Figma"], + "versionControl": ["git", "GitHub"], + "databases": ["MongoDB", "MySQL", "Neo4J", "PostgreSQL"] +} + diff --git a/src/pages/RSVP/Form/styles.module.scss b/src/pages/RSVP/Form/styles.module.scss new file mode 100644 index 0000000..d3f184a --- /dev/null +++ b/src/pages/RSVP/Form/styles.module.scss @@ -0,0 +1,99 @@ +@import 'common.scss'; + +.container { + flex: 1; + position: relative; + background-size: auto 108%; + background-position: center top; + background-repeat: no-repeat; + + + @media (min-height: 900px) { + flex: none; + height: 750px; + } + + @media (max-width: 1150px) { + background-position: 58.3% top; + } + + @include registration-floating-screen { + min-height: max(min(calc(100vh - 100px), 750px), 620px); + max-height: min(70%, 750px); + margin-top: 25px; + } + + .form { + position: absolute; + z-index: 2; + height: 70%; + width: calc(100vh - 120px); + min-width: calc(650px - 120px); // 650px is min-height of registration page + max-width: 780px; + top: min(max(75px, 50vh - 350px), 100px); + left: 50%; + transform: translateX(-40%); + display: flex; + flex-direction: column; + + @media (max-width: 1150px) { + transform: translateX(-50%); + left: 53%; + } + @include registration-floating-screen { + min-width: 0; + width: 90%; + left: 50%; + } + + .screenContainer { + margin-right: 20px; + padding-bottom: 1px; + + .title { + font-size: 2em; + font-weight: 500; + margin-top: 15px; + margin-bottom: -15px; + + @media screen and (max-height: 700px) { + margin-bottom: -20px; + } + + @include registration-floating-screen { + margin-bottom: -5px; + } + } + } + + .buttons { + display: flex; + justify-content: flex-end; + z-index: 5; + margin-top: 10px; + } + } + + .finish { + display: flex; + flex-direction: column; + justify-content: space-evenly; + box-sizing: border-box; + overflow: hidden; + + .logo { + display: block; + height: 50%; + + img { + height: 100%; + margin: 0 auto; + display: block; + } + } + + .text { + text-align: center; + } + } +} diff --git a/src/pages/RSVP/index.tsx b/src/pages/RSVP/index.tsx new file mode 100644 index 0000000..2cc572a --- /dev/null +++ b/src/pages/RSVP/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import styles from './styles.module.scss'; +import Background from './Background'; +import Form from './Form'; + +const RSVP = (): JSX.Element => ( + + + + + +); + +export default RSVP; diff --git a/src/pages/RSVP/styles.module.scss b/src/pages/RSVP/styles.module.scss new file mode 100644 index 0000000..87fcee3 --- /dev/null +++ b/src/pages/RSVP/styles.module.scss @@ -0,0 +1,24 @@ +@import 'common.scss'; + +.rsvp { + display: flex; + flex-direction: column; + justify-content: space-between; + + overflow: hidden; + + height: 100%; + min-height: 650px; + position: relative; + + @include registration-floating-screen { + justify-content: center; + padding: 30px 0; + box-sizing: border-box; + min-height: 430px; + } + + .spacer { + height: 75px; + } +} diff --git a/src/pages/RSVP/validation.ts b/src/pages/RSVP/validation.ts new file mode 100644 index 0000000..7caeff4 --- /dev/null +++ b/src/pages/RSVP/validation.ts @@ -0,0 +1,30 @@ +import * as z from 'zod'; + +export const rsvpSchema = z.object({ + firstName: z.string().optional(), + lastName: z.string().optional(), + timezone: z.string(), + discord: z.string().regex(/^.{1,32}#\d{4}$/, 'Please enter a valid Discord username (e.g. "name#1234")').nonempty(), + avatarUrl: z.string(), + teamStatus: z.enum(['LOOKING_FOR_TEAM', 'LOOKING_FOR_MEMBERS', 'NOT_LOOKING']), + interests: z.array(z.string()).optional(), + description: z.string().max(400).optional(), +}); + +export type RSVPSchema = z.infer; + +export const errorMap: z.ZodErrorMap = (error, ctx) => { + if (error.message) return { message: error.message }; + + if (error.code === z.ZodErrorCode.too_small && error.type === 'string') { + return { message: 'Required' }; + } + + if (error.code === z.ZodErrorCode.invalid_enum_value) { + return { message: 'Required' }; + } + + return { message: ctx.defaultError }; +}; + +export const defaultValues = {}; diff --git a/src/util/api.ts b/src/util/api.ts index 3b47549..619f3d2 100644 --- a/src/util/api.ts +++ b/src/util/api.ts @@ -1,4 +1,4 @@ -import { MethodType, FileType, RegistrationType, RegistrationTypeWithId, PrizeType, MentorTimeslotType, EventType, RegistrationRole } from 'util/types'; +import { WithId, MethodType, FileType, RegistrationType, PrizeType, MentorTimeslotType, EventType, RegistrationRole, ProfileType, RSVPType, ProfileResponseType } from 'util/types'; const API = 'https://api.hackillinois.org'; @@ -52,28 +52,23 @@ export function getRolesSync(): string[] { return []; } -export function getRegistration(role: RegistrationRole): Promise { +export function getRegistration(role: RegistrationRole): Promise> { return request('GET', `/registration/${role}/`); } // this function does not have a return type because different roles have different response types -export function register(isEditing: boolean, role: RegistrationRole, registration: RegistrationType): Promise { +export function register(isEditing: boolean, role: RegistrationRole, registration: RegistrationType): Promise> { const method = isEditing ? 'PUT' : 'POST'; return request(method, `/registration/${role}/`, registration); } -type GetRsvpResType = { - id: string; - isAttending: boolean, -}; - -export function getRSVP(): Promise { +export function getRSVP(): Promise> { return request('GET', '/rsvp/'); } -export function rsvp(isEditing: boolean, registration: RegistrationType): Promise { +export function rsvp(isEditing: boolean, rsvpData: RSVPType): Promise> { const method = isEditing ? 'PUT' : 'POST'; - return request(method, '/rsvp/', registration); + return request(method, '/rsvp/', rsvpData); } export function uploadFile(file: File, type: FileType): Promise { @@ -124,3 +119,12 @@ export function setMentorTimeslots(data: MentorTimeslotType[]): Promise { return request('GET', '/event/').then((res) => res.events); } + +export function getProfile(): Promise { + return request('GET', '/profile/'); +} + +export function createProfile(isEditing: boolean, profile: ProfileType): Promise { + const method = isEditing ? 'PUT' : 'POST'; + return request(method, '/profile/', profile); +} diff --git a/src/util/types.ts b/src/util/types.ts index cb54b75..3969d71 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -1,3 +1,5 @@ +export type WithId = Type & { id: string; }; + export type MethodType = 'GET' | 'POST' | 'PUT' | 'DELETE'; export type FileType = 'resume' | 'photo' | 'blobstore'; @@ -25,10 +27,6 @@ export type RegistrationType = { resumeFilename?: string; }; -export interface RegistrationTypeWithId extends RegistrationType { - id: string; -} - export type RegistrationRole = 'attendee' | 'mentor'; export interface EventType { @@ -72,3 +70,20 @@ export type MentorTimeslotType = { start_date: string; end_date: string; }; + +export type RSVPType = { + isAttending: boolean; +}; + +export type ProfileType = Partial<{ + firstName: string; + lastName: string; + timezone: string; + discord: string; + avatarUrl: string; + teamStatus: 'LOOKING_FOR_TEAM' | 'LOOKING_FOR_MEMBERS' | 'NOT_LOOKING'; + interests: string[]; + description: string; +}>; + +export type ProfileResponseType = WithId> & { points: number };
Thank you for RSVPing for HackIllinois 2021! Be sure to follow our instagram (@hackillinois) and our twitter (@hackillinois). We will be posting live updates during the event that you won’t want to miss!