diff --git a/.eslintrc b/.eslintrc index cdaf6a6..ba627b3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,6 +13,7 @@ "react/require-default-props": "off", "linebreak-style": "off", "no-param-reassign": "off", + "react/prop-types": "off", "max-len": "off" } } \ No newline at end of file diff --git a/package.json b/package.json index 30bcc24..db7cacd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@typescript-eslint/parser": "^4.6.0", "eslint-config-airbnb-typescript": "^12.0.0", "node-sass": "^4.14.1", + "prop-types": "^15.7.2", "query-string": "^6.13.6", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/src/App.tsx b/src/App.tsx index 19cc654..c5d0ad9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,19 @@ import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; - +// import ReactGa from 'react-ga'; import Home from 'pages/Home'; import Auth from 'pages/Auth'; import StaticFileRedirect from 'components/StaticFileRedirect'; function App(): JSX.Element { + // const notifyGA = (path: string) => { + // switch (path) { + // case "/": + // ReactGa.pageview() + // break; + // }; + // }; + return ( diff --git a/src/GARouteUpdater.tsx b/src/GARouteUpdater.tsx new file mode 100644 index 0000000..da427da --- /dev/null +++ b/src/GARouteUpdater.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +const GARouteUpdater: React.FC = () => ( + <> + +); + +export default GARouteUpdater; diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..e960c7c --- /dev/null +++ b/src/api.ts @@ -0,0 +1,158 @@ +import { MethodType, FileType, RegistrationType, EventResType } from './types'; + +const API = 'https://api.hackillinois.org'; +export const isAuthenticated = () => sessionStorage.getItem('token'); + +function request(method: MethodType, endpoint: string, body?: any) { + const getSessionToken = (): string => { + const token = isAuthenticated(); + if (token === null) { + throw new Error('token is null'); + } + return token; + }; + return fetch(API + endpoint, { + method, + headers: { + Authorization: getSessionToken(), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + .then((res: Response) => { + if (res.status === 200) { + return res.json(); + } + throw new Error('response status code not 200'); + }) + .catch((err: Error) => { + console.error(err.message); + }); +} + +export function authenticate(to: string) { + if (process.env.REACT_APP_TOKEN) { + sessionStorage.setItem('token', process.env.REACT_APP_TOKEN); + } else { + to = `${API}/auth/github/?redirect_uri=${to}`; + } + window.location.replace(to); +} + +export function getToken(code: string | string[] | null) { + return request('POST', '/auth/code/github/', { code }).then((res) => res.token); +} + +type GetRolesResType = { + id: string; + roles: string[]; +}; + +export function getRoles() { + return request('GET', '/auth/roles/').then( + (res) => (res as GetRolesResType).roles, + ); +} + +export function getRolesSync(): string[] { + const token = sessionStorage.getItem('token'); + if (token) { + try { + const tokenData = JSON.parse(atob(token.split('.')[1])); + return tokenData.roles; + } catch (e) { + // if the token is incorrectly formatted, we just ignore it and return the default [] + } + } + return []; +} + +export function getRegistration(role: string): 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: string, + registration: RegistrationType, +) { + const method = isEditing ? 'PUT' : 'POST'; + return request(method, `/registration/${role}/`, registration); +} + +type GetRsvpResType = { + id: string; + isAttending: boolean, +}; + +export function getRSVP(): Promise { + return request('GET', '/rsvp/'); +} + +export function rsvp(isEditing: boolean, registration: RegistrationType): Promise { + const method = isEditing ? 'PUT' : 'POST'; + return request(method, '/rsvp/', registration); +} + +export function uploadFile(file: File, type: FileType) { + return request('GET', `/upload/${type}/upload/`) + .then((res) => fetch(res[type], { + method: 'PUT', + headers: { 'Content-Type': file.type }, + body: file, + })) + .then((res) => { + if (res.ok) { + return res; + } + throw new Error('response did not have status 200'); + }) + .catch((err: Error) => { + console.error(err); + }); +} + +type GetQrResType = { + id: string; + qrInfo:string; +}; + +export function getQR():Promise { + return request('GET', '/user/qr/'); +} + +export function getPrizes() { + return request('GET', '/upload/blobstore/prizes/').then((res) => res.data); +} + +type RefreshTokenResType = { + token: string; +}; +export function refreshToken(): Promise { + return request('GET', '/auth/token/refresh/').then((res:RefreshTokenResType) => sessionStorage.setItem('token', res.token)); +} + +export function getMentorTimeslots() { + return request('GET', '/upload/blobstore/mentor-timeslots/').then( + (res) => res.data, + ); +} + +export function setMentorTimeslots(data: Record) { + return request('PUT', '/upload/blobstore/', { + id: 'mentor-timeslots', + data, + }) + .then((res: Response) => res.json()) + .then((res: any) => res.data) + .catch((err: Error) => console.log(err)); +} + +type GetEventsResType = { + events: EventResType[] +}; + +export function getEvents() { + return request('GET', '/event/').then((res: GetEventsResType) => res.events); +} diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..ea7049b --- /dev/null +++ b/src/events.ts @@ -0,0 +1,106 @@ +/* Example return object: +[ + { + date: Date(), + dayOfWeek: "Friday", + month: "February", + dayOfMonth: 28, + events: [ ... ] // all the event objects occuring on this day + }, + ... +] +*/ + +import { DayType, EventType, WeekType } from './types'; + +export const sortEventsIntoDays = (events: EventType[]) => { + // separate events by day into a map like so {"2/28/2019": [], "3/1/2019": [], ...} + const eventsByDay: Map = new Map(); + events.forEach((event) => { + const dateString = new Date(event.startTime * 1000).toLocaleDateString( + 'en-US', + ); + const eventsOnDay = eventsByDay.get(dateString); + if (eventsOnDay) { + eventsOnDay.push(event); + } else { + eventsByDay.set(dateString, [event]); + } + }); + + // convert the map into an array of day objects + const days: DayType[] = []; + + Array.from(eventsByDay.entries()).forEach(([dateString, eventsPerDay]) => { + const date = new Date(dateString); + days.push({ + date, + dayOfWeek: date.toLocaleDateString('en-US', { weekday: 'long' }), + month: date.toLocaleString('en-US', { month: 'long' }), + dayOfMonth: date.getDate(), + events: eventsPerDay.sort((a: EventType, b: EventType) => { + if (a.startTime === b.startTime) { + return a.endTime - b.endTime; + } + return a.startTime - b.startTime; + }), + }); + }); + + // sort the days in order (just using the startTime of the first event + // on that day to prevent additional calculations) + days.sort((a, b) => a.events[0].startTime - b.events[0].startTime); + + return days; +}; + +const addDays = (date: Date, days: number): Date => { + const newDate = new Date(date); + newDate.setDate(date.getDate() + days); + return newDate; +}; + +// Returns a 2d array in following format +// [[{ date: Date, index?: Number }, ...], ...] +// if index is specified, then that date was part of the dates provided as a parameter + +export const getSurroundingWeeks = (startDate: Date, numDays: number) => { + if (startDate && numDays >= 1) { + const weeks: [WeekType[]] = [[]]; + let currentWeek = weeks[0]; + + // Add all the days preceding startDate in the week of startDate + for (let i = -startDate.getDay(); i < 0; i += 1) { + // -startDate.getDay() effectively gets the offset to the sunday before startDate + currentWeek.push({ + date: addDays(startDate, i), + }); + } + + // Add all the days that were specified in the parameters + for (let i = 0; i < numDays; i += 1) { + if (currentWeek.length >= 7) { + // when we get to the end of this week, add another week + currentWeek = []; + weeks.push(currentWeek); + } + + currentWeek.push({ + date: addDays(startDate, i), + index: i, // note that these dates get indices + }); + } + + // Add remaining days in the week of the last date + const endDate = currentWeek[currentWeek.length - 1].date; + for (let i = 1; i < 7 - endDate.getDay(); i += 1) { + currentWeek.push({ + date: addDays(endDate, i), + }); + } + + return weeks; + } + + return []; +}; diff --git a/src/index.tsx b/src/index.tsx index 2ebd1e1..6851659 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,7 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; ReactGA.initialize('G-2D2S4GLZPR', { - testMode: process.env.NODE_ENV !== 'production', + // testMode: process.env.NODE_ENV !== 'production', }); ReactDOM.render( diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx index 22c3066..e3eb56f 100644 --- a/src/pages/Auth/index.tsx +++ b/src/pages/Auth/index.tsx @@ -1,40 +1,47 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import queryString from 'query-string'; import { useLocation } from 'react-router-dom'; - +import ReactGa from 'react-ga'; import { getToken } from 'util/api'; // import Loading from 'components/Loading'; function mobileRedirect(os: 'android' | 'ios', code: string) { + ReactGa.event({ + category: 'mobile', + action: os, + }); const to = `hackillinois://org.hackillinois.${os}/auth?code=${code}`; window.location.replace(to); } +type QueryTypes = { + code?: string; + isAndroid?: string; + isiOS?: string; + to?: string; +}; + const Auth: React.FC = () => { const location = useLocation(); - type QueryTypes = { - code?: string; - isAndroid?: string; - isiOS?: string; - to?: string; - }; - const { code, isAndroid, isiOS, to }: QueryTypes = queryString.parse(location.search); + useEffect(() => { + ReactGa.pageview('/auth'); + const { code, isAndroid, isiOS, to }: QueryTypes = queryString.parse(location.search); - if (code) { - if (isAndroid || isiOS) { - const os = isAndroid ? 'android' : 'ios'; - mobileRedirect(os, code); + if (code) { + if (isAndroid || isiOS) { + const os = isAndroid ? 'android' : 'ios'; + mobileRedirect(os, code); + } else { + getToken(code).then((token) => { + sessionStorage.setItem('token', token); + window.location.replace(to as string); + }); + } } else { - getToken(code).then((token) => { - sessionStorage.setItem('token', token); - window.location.replace(to as string); - }); + window.location.replace('/'); } - } else { - window.location.replace('/'); - } - + }, []); return
Loading...
; // ; }; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index caf11c1..282dc65 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,13 +1,21 @@ -import React from 'react'; - +import React, { useEffect } from 'react'; +import ReactGa from 'react-ga'; import Hero from './Hero'; // import EventInfo from './EventInfo'; -const Home: React.FC = () => ( - <> - - {/* */} - -); +const Home: React.FC = () => { + useEffect(() => { + ReactGa.pageview('/'); + }, []); + + return ( + <> + + { + /* */ + } + + ); +}; export default Home; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c7ab31e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,59 @@ +export type MethodType = 'GET' | 'POST' | 'PUT' | 'DELETE'; +export type FileType = 'resume' | 'photo' | 'blobstore'; +export type RegistrationType = { + firstName: string; + lastName: string; + timezone: string; + email: string; + location: string; + gender: string | null; + race: Array; // NOTE: ignore the nullable part; race will always be a (non-null) array + degreePursued: + | 'ASSOCIATES' + | 'BACHELORS' + | 'MASTERS' + | 'PHD' + | 'GRADUATED' + | 'OTHER'; + graduationYear: number; + school: string; + major: string; + programmingYears: number; + programmingAbility: number; + hasInternship: boolean; + resumeFilename: string | null; +}; + +export interface EventType { + name: string; + description: string; + startTime: number; + endTime: number; + locations: [ + { + description: string; + tags: string[]; + latitude: number; + longitude: number; + }, + ]; + sponsor: string; + eventType: string; +} + +export interface EventResType extends EventType { + id: string; +} + +export type DayType = { + date: Date; + dayOfWeek: string; + month: string; + dayOfMonth: number; + events: EventType[]; +}; + +export type WeekType = { + index?: number; + date: Date; +};