From 04291906624fb9171078e5abb74124fd1723c86e Mon Sep 17 00:00:00 2001 From: Benjamin Shen Date: Thu, 4 Mar 2021 16:21:08 -0500 Subject: [PATCH] Refactor firestore modifiers and gtag events (#342) * Refactor semester firestore modifier functions * Refactor gtag * Format * Fix typo --- src/components/Semester/Semester.vue | 63 ++++++----------- src/components/Semester/SemesterView.vue | 59 ++-------------- src/containers/Login.vue | 15 +++-- src/global-firestore-data.ts | 86 ++++++++++++++++++++---- src/gtag.ts | 64 ++++++++++++++++++ 5 files changed, 172 insertions(+), 115 deletions(-) create mode 100644 src/gtag.ts diff --git a/src/components/Semester/Semester.vue b/src/components/Semester/Semester.vue index 4470684a3..0c01b860a 100644 --- a/src/components/Semester/Semester.vue +++ b/src/components/Semester/Semester.vue @@ -127,7 +127,12 @@ import fall from '@/assets/images/fallEmoji.svg'; import spring from '@/assets/images/springEmoji.svg'; import winter from '@/assets/images/winterEmoji.svg'; import summer from '@/assets/images/summerEmoji.svg'; -import { chooseSelectableRequirementOption } from '@/global-firestore-data'; +import { + editSemester, + addCourseToSemester, + deleteCourseFromSemester, + chooseSelectableRequirementOption, +} from '@/global-firestore-data'; import { cornellCourseRosterCourseToFirebaseSemesterCourse } from '@/user-data-converter'; import store from '@/store'; @@ -185,7 +190,10 @@ export default Vue.extend({ required: true, }, compact: { type: Boolean, required: true }, - activatedCourse: { type: Object as PropType, required: true }, + activatedCourse: { + type: Object as PropType, + required: true, + }, isFirstSem: { type: Boolean, required: true }, }, mounted() { @@ -209,8 +217,7 @@ export default Vue.extend({ return this.courses; }, set(newCourses: readonly FirestoreSemesterCourse[]) { - this.$emit( - 'edit-semester', + editSemester( this.year, this.type, (semester: FirestoreSemester): FirestoreSemester => ({ @@ -312,52 +319,22 @@ export default Vue.extend({ }, addCourse(data: CornellCourseRosterCourse, requirementID: string) { const newCourse = cornellCourseRosterCourseToFirebaseSemesterCourse(data); + addCourseToSemester(this.type, this.year, newCourse); chooseSelectableRequirementOption({ ...store.state.selectableRequirementChoices, [newCourse.uniqueID]: requirementID, }); - const courseCode = `${data.subject} ${data.catalogNbr}`; - - this.$emit( - 'edit-semester', - this.year, - this.type, - (semester: FirestoreSemester): FirestoreSemester => ({ - ...semester, - courses: [...this.courses, newCourse], - }) - ); - - const confirmationMsg = `Added ${courseCode} to ${this.type} ${this.year}`; - this.openConfirmationModal(confirmationMsg); - this.$gtag.event('add-course', { - event_category: 'course', - event_label: 'add', - value: 1, - }); + const courseCode = `${data.subject} ${data.catalogNbr}`; + this.openConfirmationModal(`Added ${courseCode} to ${this.type} ${this.year}`); }, deleteCourse(courseCode: string, uniqueID: number) { - this.openConfirmationModal(`Removed ${courseCode} from ${this.type} ${this.year}`); - this.$gtag.event('delete-course', { - event_category: 'course', - event_label: 'delete', - value: 1, - }); + deleteCourseFromSemester(this.type, this.year, uniqueID); // Update requirements menu - this.$emit( - 'edit-semester', - this.year, - this.type, - (semester: FirestoreSemester): FirestoreSemester => ({ - ...semester, - courses: this.courses.filter(course => course.uniqueID !== uniqueID), - }) - ); + this.openConfirmationModal(`Removed ${courseCode} from ${this.type} ${this.year}`); }, colorCourse(color: string, uniqueID: number) { - this.$emit( - 'edit-semester', + editSemester( this.year, this.type, (semester: FirestoreSemester): FirestoreSemester => ({ @@ -372,8 +349,7 @@ export default Vue.extend({ this.$emit('course-onclick', course); }, editCourseCredit(credit: number, uniqueID: number) { - this.$emit( - 'edit-semester', + editSemester( this.year, this.type, (semester: FirestoreSemester): FirestoreSemester => ({ @@ -418,8 +394,7 @@ export default Vue.extend({ this.isEditSemesterOpen = true; }, editSemester(seasonInput: 'Fall' | 'Spring' | 'Winter' | 'Summer', yearInput: number) { - this.$emit( - 'edit-semester', + editSemester( this.year, this.type, (oldSemester: FirestoreSemester): FirestoreSemester => ({ diff --git a/src/components/Semester/SemesterView.vue b/src/components/Semester/SemesterView.vue index f3895ee04..7c356ac44 100644 --- a/src/components/Semester/SemesterView.vue +++ b/src/components/Semester/SemesterView.vue @@ -64,7 +64,6 @@ @course-onclick="courseOnClick" @new-semester="openSemesterModal" @delete-semester="deleteSemester" - @edit-semester="editSemester" @open-caution-modal="openCautionModal" @add-course-to-semester="addNewCourse" /> @@ -98,12 +97,8 @@ import SemesterCaution from '@/components/Semester/SemesterCaution.vue'; import NewSemesterModal from '@/components/Modals/NewSemesterModal.vue'; import store from '@/store'; -import { - addCourseToSemester, - compareFirestoreSemesters, - createSemester, - editSemesters, -} from '@/global-firestore-data'; +import { GTagEvent } from '@/gtag'; +import { addSemester, deleteSemester, addCourseToSemester } from '@/global-firestore-data'; import { closeBottomBar } from '@/components/BottomBar/BottomBarState'; export default Vue.extend({ @@ -142,21 +137,13 @@ export default Vue.extend({ setCompact() { if (!this.compact) { this.$emit('compact-updated', !this.compact); - this.$gtag.event('to-compact', { - event_category: 'views', - event_label: 'compact', - value: 1, - }); + GTagEvent(this.$gtag, 'to-compact'); } }, setNotCompact() { if (this.compact) { this.$emit('compact-updated', !this.compact); - this.$gtag.event('to-not-compact', { - event_category: 'views', - event_label: 'not-compact', - value: 1, - }); + GTagEvent(this.$gtag, 'to-not-compact'); } }, openSemesterConfirmationModal(type: FirestoreSemesterType, year: number, isAdd: boolean) { @@ -190,46 +177,12 @@ export default Vue.extend({ addCourseToSemester(season, year, course); }, addSemester(type: FirestoreSemesterType, year: number) { - this.$gtag.event('add-semester', { - event_category: 'semester', - event_label: 'add', - value: 1, - }); - - editSemesters(oldSemesters => - [...oldSemesters, createSemester([], type, year)].sort(compareFirestoreSemesters) - ); + addSemester(type, year); this.openSemesterConfirmationModal(type, year, true); }, deleteSemester(type: FirestoreSemesterType, year: number) { - this.$gtag.event('delete-semester', { - event_category: 'semester', - event_label: 'delete', - value: 1, - }); - - // Confirm success with alert + deleteSemester(type, year); this.openSemesterConfirmationModal(type, year, false); - - // Update requirements menu from dashboard - editSemesters(oldSemesters => - oldSemesters.filter(semester => semester.type !== type || semester.year !== year) - ); - }, - editSemester( - year: number, - type: FirestoreSemesterType, - updater: (oldSemester: FirestoreSemester) => FirestoreSemester - ) { - editSemesters(oldSemesters => - oldSemesters - .map(currentSemester => - currentSemester.year === year && currentSemester.type === type - ? updater(currentSemester) - : currentSemester - ) - .sort(compareFirestoreSemesters) - ); }, courseOnClick(course: FirestoreSemesterCourse) { this.activatedCourse = course; diff --git a/src/containers/Login.vue b/src/containers/Login.vue index b47eb9591..7a4708ce2 100644 --- a/src/containers/Login.vue +++ b/src/containers/Login.vue @@ -51,19 +51,25 @@
-
checklist
+
+ checklist +

Fully personalized to track your requirements

-
browser
+
+ browser +

Customizable interface to view your courses

-
Network
+
+ Network +

Built-in system to check your progress

@@ -175,6 +181,7 @@ import firebase from 'firebase/app'; import CustomFooter from '@/components/Footer.vue'; import TopBar from '@/components/TopBar.vue'; +import { GTagLoginEvent } from '@/gtag'; import * as fb from '@/firebaseConfig'; const { whitelistCollection, landingEmailsCollection } = fb; @@ -232,7 +239,7 @@ export default Vue.extend({ if (doc.exists) { this.performingRequest = false; this.$router.push('/'); - this.$gtag.event('login', { method: 'Google' }); + GTagLoginEvent(this.$gtag, 'Google'); } else { this.handleUserWithoutAccess(); } diff --git a/src/global-firestore-data.ts b/src/global-firestore-data.ts index 30bcd69f1..cdf49164e 100644 --- a/src/global-firestore-data.ts +++ b/src/global-firestore-data.ts @@ -10,6 +10,7 @@ import { uniqueIncrementerCollection, } from './firebaseConfig'; import store from './store'; +import { GTag, GTagEvent } from './gtag'; // enum to define seasons as integers in season order export const SeasonsEnum = Object.freeze({ @@ -19,14 +20,6 @@ export const SeasonsEnum = Object.freeze({ Fall: 3, }); -export const editSemesters = ( - updater: (oldSemesters: readonly FirestoreSemester[]) => readonly FirestoreSemester[] -): void => { - const newSemesters = updater(store.state.semesters); - store.commit('setSemesters', newSemesters); - semestersCollection.doc(store.state.currentFirebaseUser.email).set({ semesters: newSemesters }); -}; - // compare function for FirestoreSemester to determine which comes first by year and type/season export const compareFirestoreSemesters = (a: FirestoreSemester, b: FirestoreSemester): number => { if (a.type === b.type && a.year === b.year) { @@ -44,21 +37,62 @@ export const compareFirestoreSemesters = (a: FirestoreSemester, b: FirestoreSeme return -1; }; -export const createSemester = ( - courses: readonly FirestoreSemesterCourse[], +export const editSemesters = ( + updater: (oldSemesters: readonly FirestoreSemester[]) => readonly FirestoreSemester[] +): void => { + const newSemesters = updater(store.state.semesters); + store.commit('setSemesters', newSemesters); + semestersCollection.doc(store.state.currentFirebaseUser.email).set({ semesters: newSemesters }); +}; + +export const editSemester = ( + year: number, + type: FirestoreSemesterType, + updater: (oldSemester: FirestoreSemester) => FirestoreSemester +): void => { + editSemesters(oldSemesters => + oldSemesters + .map(sem => (sem.year === year && sem.type === type ? updater(sem) : sem)) + .sort(compareFirestoreSemesters) + ); +}; + +const createSemester = ( type: FirestoreSemesterType, - year: number -): { courses: readonly FirestoreSemesterCourse[]; type: FirestoreSemesterType; year: number } => ({ + year: number, + courses: readonly FirestoreSemesterCourse[] +): { type: FirestoreSemesterType; year: number; courses: readonly FirestoreSemesterCourse[] } => ({ courses, type, year, }); +export const addSemester = ( + type: FirestoreSemesterType, + year: number, + gtag?: GTag, + courses: readonly FirestoreSemesterCourse[] = [] +): void => { + GTagEvent(gtag, 'add-semester'); + editSemesters(oldSemesters => + [...oldSemesters, createSemester(type, year, courses)].sort(compareFirestoreSemesters) + ); +}; + +export const deleteSemester = (type: FirestoreSemesterType, year: number, gtag?: GTag): void => { + GTagEvent(gtag, 'delete-semester'); + editSemesters(oldSemesters => + oldSemesters.filter(semester => semester.type !== type || semester.year !== year) + ); +}; + export const addCourseToSemester = ( season: FirestoreSemesterType, year: number, - newCourse: FirestoreSemesterCourse + newCourse: FirestoreSemesterCourse, + gtag?: GTag ): void => { + GTagEvent(gtag, 'add-course'); editSemesters(oldSemesters => { let semesterFound = false; const newSemestersWithCourse = oldSemesters.map(sem => { @@ -69,12 +103,36 @@ export const addCourseToSemester = ( return sem; }); if (semesterFound) return newSemestersWithCourse; - return [...oldSemesters, createSemester([newCourse], season, year)].sort( + return [...oldSemesters, createSemester(season, year, [newCourse])].sort( compareFirestoreSemesters ); }); }; +export const deleteCourseFromSemester = ( + season: FirestoreSemesterType, + year: number, + courseUniqueID: number, + gtag?: GTag +): void => { + GTagEvent(gtag, 'delete-course'); + editSemesters(oldSemesters => { + let semesterFound = false; + const newSemestersWithoutCourse = oldSemesters.map(sem => { + if (sem.type === season && sem.year === year) { + semesterFound = true; + return { + ...sem, + courses: sem.courses.filter(course => course.uniqueID !== courseUniqueID), + }; + } + return sem; + }); + if (semesterFound) return newSemestersWithoutCourse; + return oldSemesters; + }); +}; + export const chooseToggleableRequirementOption = ( toggleableRequirementChoices: AppToggleableRequirementChoices ): void => { diff --git a/src/gtag.ts b/src/gtag.ts new file mode 100644 index 000000000..d6d72f199 --- /dev/null +++ b/src/gtag.ts @@ -0,0 +1,64 @@ +/* eslint-disable camelcase */ +type EventPayload = { event_category: string; event_label: string; value: number }; +type LoginEventPayload = { method: string }; + +export type GTag = { + event(eventType: string, eventPayload: LoginEventPayload | EventPayload): void; +}; + +export const GTagLoginEvent = (gtag: GTag | undefined, method: string): void => { + if (!gtag) return; + gtag.event('login', { method }); +}; + +export const GTagEvent = (gtag: GTag | undefined, eventType: string): void => { + if (!gtag) return; + let eventPayload: EventPayload | undefined; + switch (eventType) { + case 'to-compact': + eventPayload = { + event_category: 'views', + event_label: 'compact', + value: 1, + }; + break; + case 'to-not-compact': + eventPayload = { + event_category: 'views', + event_label: 'not-compact', + value: 1, + }; + break; + case 'add-semester': + eventPayload = { + event_category: 'semester', + event_label: 'add', + value: 1, + }; + break; + case 'delete-semester': + eventPayload = { + event_category: 'semester', + event_label: 'delete', + value: 1, + }; + break; + case 'add-course': + eventPayload = { + event_category: 'course', + event_label: 'add', + value: 1, + }; + break; + case 'delete-course': + eventPayload = { + event_category: 'course', + event_label: 'delete', + value: 1, + }; + break; + default: + return; + } + gtag.event(eventType, eventPayload); +};