diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json index ce441d99..3b55de43 100644 --- a/apps/admin-web/package.json +++ b/apps/admin-web/package.json @@ -6,6 +6,8 @@ "@apollo/react-hooks": "^3.0.0", "@auth0/auth0-spa-js": "^1.2.2", "antd": "^4.1.3", + "export-to-csv": "^0.2.1", + "graphql": "^14", "graphql-tag": "^2.10.1", "qrcode.react": "^1.0.0", "react": "^16.7.0", @@ -56,4 +58,4 @@ "not ie <= 11", "not op_mini all" ] -} +} \ No newline at end of file diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 0d8e691f..cdb9f104 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -4,6 +4,7 @@ import { Layout, Menu, PageHeader, Spin } from "antd"; import { ToolList } from "./Tools"; import { Events } from "./screens/Events"; +import { EventData } from "./screens/EventData"; import { Membership } from "./screens/Membership"; import { RedemptionCodes } from "./screens/RedemptionCodes"; import { config } from "./config"; @@ -21,7 +22,8 @@ const MainContent: React.SFC<{}> = (): JSX.Element => { return ( - + + diff --git a/apps/admin-web/src/generated/graphql.tsx b/apps/admin-web/src/generated/graphql.tsx index 5d19d855..64f36c89 100644 --- a/apps/admin-web/src/generated/graphql.tsx +++ b/apps/admin-web/src/generated/graphql.tsx @@ -40,6 +40,8 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -119,6 +121,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + recordInterest: Event; }; @@ -247,6 +250,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationRecordInterestArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -299,6 +307,9 @@ export type Query = { currentEvents: Array; event: Event; eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -328,6 +339,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -408,6 +429,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { @@ -428,6 +450,37 @@ export type UserUpdateInput = { email?: Maybe; }; +export type EventsWithKeyQueryVariables = Exact<{ + urlKey: Scalars['String']; +}>; + + +export type EventsWithKeyQuery = ( + { __typename?: 'Query' } + & { eventsWithKey: Array<( + { __typename?: 'Event' } + & Pick + & { attendees?: Maybe + )>>, usersInterested?: Maybe + )>> } + )> } +); + +export type YearEventsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type YearEventsQuery = ( + { __typename?: 'Query' } + & { yearEvents: Array<( + { __typename?: 'Event' } + & Pick + )> } +); + export type SigsQueryVariables = Exact<{ [key: string]: never; }>; @@ -621,6 +674,82 @@ export type GetProductsQuery = ( ); +export const EventsWithKeyDocument = gql` + query eventsWithKey($urlKey: String!) { + eventsWithKey(urlKey: $urlKey) { + eventTitle + attendees { + firstName + lastName + email + } + usersInterested { + firstName + lastName + email + } + } +} + `; + +/** + * __useEventsWithKeyQuery__ + * + * To run a query within a React component, call `useEventsWithKeyQuery` and pass it any options that fit your needs. + * When your component renders, `useEventsWithKeyQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEventsWithKeyQuery({ + * variables: { + * urlKey: // value for 'urlKey' + * }, + * }); + */ +export function useEventsWithKeyQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(EventsWithKeyDocument, baseOptions); + } +export function useEventsWithKeyLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(EventsWithKeyDocument, baseOptions); + } +export type EventsWithKeyQueryHookResult = ReturnType; +export type EventsWithKeyLazyQueryHookResult = ReturnType; +export type EventsWithKeyQueryResult = ApolloReactCommon.QueryResult; +export const YearEventsDocument = gql` + query yearEvents { + yearEvents { + urlKey + numAttendees + } +} + `; + +/** + * __useYearEventsQuery__ + * + * To run a query within a React component, call `useYearEventsQuery` and pass it any options that fit your needs. + * When your component renders, `useYearEventsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useYearEventsQuery({ + * variables: { + * }, + * }); + */ +export function useYearEventsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(YearEventsDocument, baseOptions); + } +export function useYearEventsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(YearEventsDocument, baseOptions); + } +export type YearEventsQueryHookResult = ReturnType; +export type YearEventsLazyQueryHookResult = ReturnType; +export type YearEventsQueryResult = ApolloReactCommon.QueryResult; export const SigsDocument = gql` query sigs { sigs { diff --git a/apps/admin-web/src/screens/EventData/QRModal.tsx b/apps/admin-web/src/screens/EventData/QRModal.tsx new file mode 100644 index 00000000..09b40b43 --- /dev/null +++ b/apps/admin-web/src/screens/EventData/QRModal.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import Modal from "antd/lib/modal/Modal"; + +var QRCode = require('qrcode.react'); + +interface IQRModalProps { + visible: boolean; + setVisible: (arg: boolean) => void; + eventUrlKey?: string; +} + +const QRModal: React.FC = ({ + visible, + setVisible, + eventUrlKey +}: IQRModalProps): JSX.Element => { + const handleCancel: () => void = (): void => { + setVisible(false); + }; + + const registrationLink: string = "https://mstacm.org/" + eventUrlKey; + + if(eventUrlKey == null) { + return ( + +

This event does not have a registration URL key.

+
+ ); + } + + return ( + +

{registrationLink}

+ +
+ ); +}; + +export { QRModal }; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/helpers.ts b/apps/admin-web/src/screens/EventData/helpers.ts new file mode 100644 index 00000000..6fd562d6 --- /dev/null +++ b/apps/admin-web/src/screens/EventData/helpers.ts @@ -0,0 +1,28 @@ +import gql from "graphql-tag"; + +export const EVENTS_WITH_KEY: any = gql` + query eventsWithKey($urlKey: String!) { + eventsWithKey(urlKey: $urlKey) { + eventTitle + attendees { + firstName + lastName + email + } + usersInterested { + firstName + lastName + email + } + } + } +`; + +export const YEAR_EVENTS: any = gql` + query yearEvents { + yearEvents { + urlKey + numAttendees + } + } +`; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/index.tsx b/apps/admin-web/src/screens/EventData/index.tsx new file mode 100644 index 00000000..633d116e --- /dev/null +++ b/apps/admin-web/src/screens/EventData/index.tsx @@ -0,0 +1,212 @@ +import React, { useEffect, useState } from "react"; +import { Statistic, Row, Col, Table, message } from 'antd'; +import { ExportToCsv } from "export-to-csv"; +import { IUser, IEvent, IYearEvent } from "./interfaces"; +import { QRModal } from "./QRModal" +import { EventsWithKeyQueryHookResult, + useEventsWithKeyQuery, + useYearEventsQuery, + YearEventsQueryHookResult} from "../../generated/graphql"; +import { RouteComponentProps } from "react-router-dom"; + +interface IMatchParams { + eventId: string; +} + +const EventData: React.FC> = (props) => { + const eventUrlKey: string = props.match.params.eventId; + + const [event, setEvent] = useState(); + const [attendees, setAttendees] = useState(); + const [usersInterested, setUsersInterested] = useState(); + + const [yearEvents, setYearEvents] = useState(); + const [attendancePlace, setAttendancePlace] = useState(); + + const [QRVisible, setQRVisible] = useState(false); + + const [qrKey, setQrKey] = useState(); + + const csvOptions = { + filename: 'email addresses', + fieldSeparator: ',', + quoteStrings: '', + decimalSeparator: '.', + showLabels: true, + showTitle: true, + title: 'Emails', + useTextFile: false, + useBom: true, + useKeysAsHeaders: true, + headers: ['Emails'] + }; + + const { + loading: eventLoading, + error: eventError, + data: eventData, + }: EventsWithKeyQueryHookResult = useEventsWithKeyQuery({pollInterval: 500, variables: { urlKey: eventUrlKey }}); + + const { + loading: yearEventsLoading, + error: yearEventsError, + data: yearEventsData, + }: YearEventsQueryHookResult = useYearEventsQuery({pollInterval: 500}); + + useEffect(() => { + if (eventLoading) + message.info("Event data loading..."); + else if (eventError) + message.info("An error occured loading event data."); + else if (eventData) { + setEvent((eventData.eventsWithKey)[0] as IEvent); + } + }, [eventData, eventError, eventLoading]); + + useEffect(() => { + if (yearEventsError) { + message.info("An error occured loading year event data."); + } + else if (yearEventsData) { + setYearEvents(yearEventsData.yearEvents as IYearEvent[]); + } + }, [yearEventsData, yearEventsError, yearEventsLoading]); + + useEffect(() => { + if(event !== undefined) { + setAttendees(event.attendees); + setUsersInterested(event.usersInterested); + } + }, [event]); + + useEffect(() => { + if(yearEvents !== undefined) { + setYearEvents(yearEvents.sort((a,b) => (a.numAttendees > b.numAttendees) ? -1 : ((b.numAttendees > a.numAttendees) ? 1 : 0))); + setAttendancePlace(yearEvents.map(function(e) { return e.urlKey }).indexOf(eventUrlKey) + 1); + } + }, [yearEvents]); + + if(eventUrlKey === null || !event) + return

This event does not exist.

+ + const get_ratio: Function = () => { + if(usersInterested !== undefined && attendees !== undefined && attendees.length !== 0) + return (usersInterested.length / attendees.length) * 100 + return "N/A" + }; + + const ordinal_suffix: Function = (num: number) => { + let j = num % 10, + k = num % 100; + if (j === 1 && k !== 11) + return "st"; + if (j === 2 && k !== 12) + return "nd"; + if (j === 3 && k !== 13) + return "rd"; + + return "th"; + }; + + const columns = [ + { + title: "Name", + key: "name", + render: (record: IUser) => ( + {`${record.firstName} ${record.lastName}`} + ), + }, + { + title: "Email", + dataIndex: "email", + key: "email", + } + ]; + + const handleQR: Function = (choice: number) => { + if (choice === 0) + setQrKey("e/" + eventUrlKey); + else if (choice === 1) + setQrKey("i/" + eventUrlKey); + + setQRVisible(true); + }; + + const downloadInterestCSV: () => void = (): void => { + csvOptions.filename = "interest_emails"; + csvOptions.title = "Interested Users"; + const csvExporter = new ExportToCsv(csvOptions); + const columns: any = usersInterested?.map(obj => ({Email: obj.email, Name: obj.firstName + " " + obj.lastName})) + csvExporter.generateCsv(columns); + }; + + const downloadAttendeeCSV: () => void = (): void => { + csvOptions.filename = "attendee_emails"; + csvOptions.title = "Attendees"; + const csvExporter = new ExportToCsv(csvOptions); + const columns: any = attendees?.map(obj => ({Email: obj.email, Name: obj.firstName + " " + obj.lastName})) + csvExporter.generateCsv(columns); + }; + + return ( +
+

{event?.eventTitle} Data

+ + + + + + + + + + + + + + + + +
+
+ + + +

Interested Users

+ + + handleQR(1)}>Interest Link / QR + + + Download Email CSV + +
+ + + +
+ + + +

Attendees

+ + + handleQR(0)}>Registration Link / QR + + + Download Email CSV + + + +
+ + + + ); +}; + +export { EventData }; \ No newline at end of file diff --git a/apps/admin-web/src/screens/EventData/interfaces.ts b/apps/admin-web/src/screens/EventData/interfaces.ts new file mode 100644 index 00000000..95f59db9 --- /dev/null +++ b/apps/admin-web/src/screens/EventData/interfaces.ts @@ -0,0 +1,18 @@ +interface IUser { + firstName: string; + lastName: string; + email: string; +} + +interface IEvent { + eventTitle: string; + attendees: IUser[]; + usersInterested: IUser[]; +} + +interface IYearEvent { + urlKey: string; + numAttendees: number; +} + +export type { IUser, IEvent, IYearEvent }; \ No newline at end of file diff --git a/apps/api/src/resources/Event/entity.ts b/apps/api/src/resources/Event/entity.ts index 3be925ad..37d53e96 100644 --- a/apps/api/src/resources/Event/entity.ts +++ b/apps/api/src/resources/Event/entity.ts @@ -93,4 +93,16 @@ export class Event extends BaseEntity { nullable: true }) public attendees: Lazy; + + @Field(() => [User], { nullable: true }) + @ManyToMany(() => User, (user: User) => user.interestedEvents, { + lazy: true, + nullable: true + }) + public usersInterested: Lazy; + + @Field({ defaultValue: 0 }) + @Column({ default: 0 }) + public numAttendees: number; + } diff --git a/apps/api/src/resources/Event/resolver.ts b/apps/api/src/resources/Event/resolver.ts index 1b2ad0bf..49409600 100644 --- a/apps/api/src/resources/Event/resolver.ts +++ b/apps/api/src/resources/Event/resolver.ts @@ -1,10 +1,10 @@ import { AuthenticationError, UserInputError } from "apollo-server"; -import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from "type-graphql"; +import { Arg, Authorized, Ctx, Mutation, Query, Resolver, FieldResolver, Root } from "type-graphql"; import { DeepPartial, getRepository, MoreThanOrEqual, - Repository + Repository, } from "typeorm"; import { GraphQLUpload } from "graphql-upload"; @@ -204,4 +204,40 @@ export class EventResolver { protected async eventsWithKey(@Arg("urlKey", () => String) urlKey: string): Promise { return this.repository.find({ urlKey }); } + + @Query(() => [User]) + protected async getAttendees(@Arg("urlKey", () => String) urlKey: string): Promise { + const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); + let users: User[] = await event.attendees; + + return users; + } + + @Query(() => [User]) + protected async getInterestedUsers(@Arg("urlKey", () => String) urlKey: string): Promise { + const event: Event = await Event.findOneOrFail({ urlKey: urlKey }); + let users: User[] = await event.usersInterested; + + return users; + } + + @Query(() => [Event]) + protected async yearEvents(): Promise { + const startDate: Date = new Date(Date.now()); + startDate.setMonth(7); + startDate.setDate(0); //August 1st, {year} + return this.repository.find({ + where: { + dateExpire: MoreThanOrEqual(startDate) + } + }); + } + + @FieldResolver((_: void) => Number, { nullable: false }) + public async numAttendees( + @Root() event: Event, + ): Promise { + const attendeeList: User[] = await event.attendees; + return attendeeList.length; + } } \ No newline at end of file diff --git a/apps/api/src/resources/User/entity.ts b/apps/api/src/resources/User/entity.ts index c2968f87..05c8102c 100644 --- a/apps/api/src/resources/User/entity.ts +++ b/apps/api/src/resources/User/entity.ts @@ -147,4 +147,12 @@ export class User extends BaseEntity { }) @JoinTable() public eventsAttended: Lazy; + + @Field(() => [Event], { nullable: true }) + @ManyToMany(() => Event, (event: Event) => event.usersInterested, { + lazy: true, + nullable: true + }) + @JoinTable() + public interestedEvents: Lazy; } diff --git a/apps/api/src/resources/User/resolver.ts b/apps/api/src/resources/User/resolver.ts index f0bd2f4e..7a4ac83a 100644 --- a/apps/api/src/resources/User/resolver.ts +++ b/apps/api/src/resources/User/resolver.ts @@ -167,6 +167,31 @@ export class UserResolver extends ResourceResolver( return event.save(); } + @Authorized() + @Mutation((_: void) => Event) + public async recordInterest( + @Ctx() context: IContext, + @Arg("eventId") eventId: number, + //@Arg("userId") userId: string, //for testing purposes + ): Promise { + const curUser: User | undefined = context.state.user; + //const curUser: User | undefined = await User.findOneOrFail({ id: userId }); //also for testing purposes + + const event: Event = await Event.findOneOrFail({ id: eventId }); + let users: User[] = await event.usersInterested; + + if(curUser == null) + return event; + + for(let i = 0; i < users.length; i++) + if(users[i].id == curUser.id) + return event; + + users.push(await User.findOneOrFail({ id: curUser.id })); + event.usersInterested = users; + return event.save(); + } + @Query((_: void) => resource, { nullable: true }) protected async me(@Ctx() context: IContext) { const user: User | undefined = context.state.user; diff --git a/apps/profile-web/src/App.tsx b/apps/profile-web/src/App.tsx index c35d53a2..032619fa 100644 --- a/apps/profile-web/src/App.tsx +++ b/apps/profile-web/src/App.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { BrowserRouter, Link, Route, Switch } from "react-router-dom"; import { SubmitResume } from "./components/SubmitResume"; -import { EventRegistration } from "./components/EventRegistration"; +import { EventRegistration, EventInterest } from "./components/EventRegistration"; import { config } from "./config"; import "./static/css/App.css"; @@ -17,6 +17,7 @@ const MainContent: React.SFC = (): JSX.Element => { + ); }; diff --git a/apps/profile-web/src/components/EventRegistration/helpers.ts b/apps/profile-web/src/components/EventRegistration/helpers.ts index 57e02296..049c3c56 100644 --- a/apps/profile-web/src/components/EventRegistration/helpers.ts +++ b/apps/profile-web/src/components/EventRegistration/helpers.ts @@ -28,4 +28,14 @@ export const ATTEND_EVENT: any = gql` } } } +`; + +export const RECORD_INTEREST: any = gql` + mutation recordInterest($eventId: Float!) { + recordInterest(eventId: $eventId) { + usersInterested { + id + } + } + } `; \ No newline at end of file diff --git a/apps/profile-web/src/components/EventRegistration/index.tsx b/apps/profile-web/src/components/EventRegistration/index.tsx index e142eb7b..316a44b4 100644 --- a/apps/profile-web/src/components/EventRegistration/index.tsx +++ b/apps/profile-web/src/components/EventRegistration/index.tsx @@ -1,11 +1,14 @@ import React, { useEffect, useState } from "react"; import { message } from "antd"; -import { useMeQuery, - useEventsQuery, - useAttendEventMutation, - Event as IEvent } from "../../generated/graphql"; +import { + useMeQuery, + useEventsQuery, + useAttendEventMutation, + useRecordInterestMutation, + Event as IEvent +} from "../../generated/graphql"; -const EventRegistration: React.FC<{match: any}> = ({match}: any) => { +const EventRegistration: React.FC<{ match: any }> = ({ match }: any) => { //style sheets for output const divStyle = { margin: "auto", @@ -36,12 +39,12 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { fontWeight: 100, fontStyle: "italic", } - + const { loading: userLoading, error: userError, }: any = useMeQuery(); - + const { loading: eventLoading, error: eventError, @@ -76,9 +79,9 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { //once all mutations/queries are loaded, the actual attendance is tracked useEffect(() => { - if(curEvents && attendEvent && result.data) { - if(result.data.me.id != null) - if(getEventId(eventUrlKey) != -1) { + if (curEvents && attendEvent && result.data) { + if (result.data.me.id !== null) + if (getEventId(eventUrlKey) !== -1) { attendEvent({ variables: { eventId: Number(getEventId(eventUrlKey)) @@ -102,10 +105,10 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { return -1; }; - if(eventLoading || eventError || userLoading || userError) + if (eventLoading || eventError || userLoading || userError) return

Loading...

- if(result.data == null || getEventId(eventUrlKey) == -1 || !attendEvent) + if (result.data == null || getEventId(eventUrlKey) == -1 || !attendEvent) return (

An error has occured.

@@ -123,4 +126,131 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { ); }; -export { EventRegistration } \ No newline at end of file +const EventInterest: React.FC<{ match: any }> = ({ match }: any) => { + //style sheets for output + const divStyle = { + margin: "auto", + justifyContent: "center", + textAlign: "center" as const, + marginTop: "10%", + } + + const errorStyle = { + color: "red", + fontSize: 32, + margin: "auto", + fontWeight: 100, + marginBottom: "10%", + } as React.CSSProperties; + + const successStyle = { + color: "#3c2eff", + fontSize: 32, + margin: "auto", + fontWeight: 100, + marginBottom: "10%", + } as React.CSSProperties; + + const linkStyle = { + fontSize: 30, + margin: "auto", + fontWeight: 100, + fontStyle: "italic", + } + + const { + loading: userLoading, + error: userError, + }: any = useMeQuery(); + + const { + loading: eventLoading, + error: eventError, + data: eventData, + }: any = useEventsQuery(); + + const [recordInterest]: any = useRecordInterestMutation(); + + const [curEvents, setCurEvents] = useState([]); + + const result: any = useMeQuery(); + + const onRecordInterestError: (e: Error) => void = (e: Error): void => { + message.error("Upload failed. Please try again."); + }; + + useEffect(() => { + if (eventLoading) { + //message.info("Event data loading..."); + } else if (eventError) { + message.info("An error occured loading event data."); + } else if (eventData) { + const events: IEvent[] = eventData.events; + setCurEvents(events); + //message.success("Event data loaded."); + } + }, [eventData, eventLoading, eventError]); + + useEffect(() => { + if (userLoading) { + //message.info("User data loading..."); + } else if (userError) { + message.info("An error occured loading user data."); + } + }, [userLoading, userError]); + + //once all mutations/queries are loaded, the actual attendance is tracked + useEffect(() => { + if (curEvents && recordInterest && result.data) { + if (result.data.me.id !== null) + if (getEventId(eventUrlKey) !== -1) { + try { + recordInterest({ + variables: { + eventId: Number(getEventId(eventUrlKey)) + } + }); + } + catch (e) { + onRecordInterestError(e); + } + } + else + message.error("Event does with URL key {" + eventUrlKey + "} does not exist."); + else + message.error("User ID is null."); + } + }, [curEvents, recordInterest]); + + const eventUrlKey: string | null = match.params.eventId; + + const getEventId: Function = (urlArg: string) => { + for (let i = 0; i < curEvents.length; i++) { + if (curEvents[i].urlKey === urlArg) + return curEvents[i].id; + } + return -1; + }; + + if (eventLoading || eventError || userLoading || userError) + return

Loading...

+ + if (result.data == null || getEventId(eventUrlKey) == -1 || !recordInterest) + return ( +
+

An error has occured.

+ Return to homepage +
+ ); + + //this default return statement can only be reached with + // the same conditions under which the attendance is tracked + return ( +
+

Interest has been successfully recorded!

+ Return to homepage +
+ ); +}; + +export { EventRegistration, EventInterest } \ No newline at end of file diff --git a/apps/profile-web/src/generated/graphql.tsx b/apps/profile-web/src/generated/graphql.tsx index 8e98e964..a7665778 100644 --- a/apps/profile-web/src/generated/graphql.tsx +++ b/apps/profile-web/src/generated/graphql.tsx @@ -40,6 +40,8 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -119,6 +121,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + recordInterest: Event; }; @@ -247,6 +250,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationRecordInterestArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -299,6 +307,9 @@ export type Query = { currentEvents: Array; event: Event; eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -328,6 +339,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -408,6 +429,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { @@ -459,6 +481,22 @@ export type AttendEventMutation = ( ) } ); +export type RecordInterestMutationVariables = Exact<{ + eventId: Scalars['Float']; +}>; + + +export type RecordInterestMutation = ( + { __typename?: 'Mutation' } + & { recordInterest: ( + { __typename?: 'Event' } + & { usersInterested?: Maybe + )>> } + ) } +); + export type UploadResumeMutationVariables = Exact<{ resume: Scalars['Upload']; grad: Scalars['DateTime']; @@ -565,6 +603,40 @@ export function useAttendEventMutation(baseOptions?: ApolloReactHooks.MutationHo export type AttendEventMutationHookResult = ReturnType; export type AttendEventMutationResult = ApolloReactCommon.MutationResult; export type AttendEventMutationOptions = ApolloReactCommon.BaseMutationOptions; +export const RecordInterestDocument = gql` + mutation recordInterest($eventId: Float!) { + recordInterest(eventId: $eventId) { + usersInterested { + id + } + } +} + `; +export type RecordInterestMutationFn = ApolloReactCommon.MutationFunction; + +/** + * __useRecordInterestMutation__ + * + * To run a mutation, you first call `useRecordInterestMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRecordInterestMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [recordInterestMutation, { data, loading, error }] = useRecordInterestMutation({ + * variables: { + * eventId: // value for 'eventId' + * }, + * }); + */ +export function useRecordInterestMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + return ApolloReactHooks.useMutation(RecordInterestDocument, baseOptions); + } +export type RecordInterestMutationHookResult = ReturnType; +export type RecordInterestMutationResult = ApolloReactCommon.MutationResult; +export type RecordInterestMutationOptions = ApolloReactCommon.BaseMutationOptions; export const UploadResumeDocument = gql` mutation uploadResume($resume: Upload!, $grad: DateTime!, $fname: String!, $lname: String!) { uploadResume(resume: $resume, graduationDate: $grad, firstName: $fname, lastName: $lname) { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index b9c68d87..bf899f93 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -4,7 +4,7 @@ import { BrowserRouter, Route, Switch } from "react-router-dom"; import ReactGA from 'react-ga'; import { config } from "./config"; -import { HomePage, NotFoundPage, EventRegistration } from "./screens"; +import { HomePage, NotFoundPage, EventRegistration, EventInterest } from "./screens"; import { useAuth0 } from "./utils/react-auth0-wrapper"; import "./static/css/App.css"; @@ -46,6 +46,7 @@ const App: React.FC<{}> = (): JSX.Element => { + diff --git a/apps/web/src/generated/graphql.tsx b/apps/web/src/generated/graphql.tsx index 2ec1e323..7a8d3431 100644 --- a/apps/web/src/generated/graphql.tsx +++ b/apps/web/src/generated/graphql.tsx @@ -40,6 +40,8 @@ export type Event = { eventLink?: Maybe; urlKey?: Maybe; attendees?: Maybe>; + usersInterested?: Maybe>; + numAttendees?: Maybe; }; export type EventCreateInput = { @@ -118,6 +120,7 @@ export type Mutation = { updateShirtReceived: User; resetShirtReceived: Array; attendEvent: Event; + recordInterest: Event; }; @@ -240,6 +243,11 @@ export type MutationAttendEventArgs = { eventId: Scalars['Float']; }; + +export type MutationRecordInterestArgs = { + eventId: Scalars['Float']; +}; + export type Permission = { __typename?: 'Permission'; name: Scalars['ID']; @@ -291,6 +299,10 @@ export type Query = { events: Array; currentEvents: Array; event: Event; + eventsWithKey: Array; + getAttendees: Array; + getInterestedUsers: Array; + yearEvents: Array; groups: Array; permissions: Array; products: Array; @@ -320,6 +332,16 @@ export type QueryEventsWithKeyArgs = { urlKey: Scalars['String']; }; + +export type QueryGetAttendeesArgs = { + urlKey: Scalars['String']; +}; + + +export type QueryGetInterestedUsersArgs = { + urlKey: Scalars['String']; +}; + export type RedemptionCode = { __typename?: 'RedemptionCode'; id: Scalars['ID']; @@ -400,6 +422,7 @@ export type User = { permissions?: Maybe>; groups: Array; eventsAttended?: Maybe>; + interestedEvents?: Maybe>; }; export type UserCreateInput = { diff --git a/apps/web/src/screens/EventRegistrationPage/index.tsx b/apps/web/src/screens/EventRegistrationPage/index.tsx index 949a84ea..98af37b1 100644 --- a/apps/web/src/screens/EventRegistrationPage/index.tsx +++ b/apps/web/src/screens/EventRegistrationPage/index.tsx @@ -17,4 +17,21 @@ const EventRegistration: React.FC<{match: any}> = ({match}: any) => { ); }; -export { EventRegistration } \ No newline at end of file +const EventInterest: React.FC<{match: any}> = ({match}: any) => { + useEffect(() => { + window.location.replace(redirectUrl); + }, []); + + const eventID: string | null = match.params.eventId; + + const redirectUrl: string = "https://profile.mstacm.org/interest/" + eventID; + + if(eventID === null) + return

Something went wrong while redirecting to your event.

; + + return ( + null + ); +}; + +export { EventRegistration, EventInterest } \ No newline at end of file diff --git a/apps/web/src/screens/index.ts b/apps/web/src/screens/index.ts index daeb6cc4..fe81217b 100644 --- a/apps/web/src/screens/index.ts +++ b/apps/web/src/screens/index.ts @@ -1,4 +1,4 @@ import { HomePage } from "./HomePage"; import { NotFoundPage } from "./NotFoundPage"; -import { EventRegistration } from "./EventRegistrationPage"; -export { HomePage, NotFoundPage, EventRegistration }; +import { EventRegistration, EventInterest } from "./EventRegistrationPage"; +export { HomePage, NotFoundPage, EventRegistration, EventInterest }; diff --git a/yarn.lock b/yarn.lock index e5acdd3e..6f42dd88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7196,6 +7196,11 @@ expect@^24.9.0: jest-message-util "^24.9.0" jest-regex-util "^24.9.0" +export-to-csv@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-0.2.1.tgz#8f997156feebc1cf995096da16341aa0100cce4e" + integrity sha512-KTbrd3CAZ0cFceJEZr1e5uiMasabeCpXq1/5uvVxDl53o4jXJHnltasQoj2NkzrxD8hU9kdwjnMhoir/7nNx/A== + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"