From c475ccb38046434b6dedb689804bf5b82321b96e Mon Sep 17 00:00:00 2001 From: Hannah Zhou Date: Wed, 2 Oct 2024 18:10:19 -0400 Subject: [PATCH 1/6] added collections in store. add backend methods and gtags --- src/global-firestore-data/user-semesters.ts | 100 ++++++++++++++++++++ src/gtag.ts | 32 +++++++ src/store.ts | 11 ++- src/user-data.d.ts | 6 ++ 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/global-firestore-data/user-semesters.ts b/src/global-firestore-data/user-semesters.ts index f03e663ae..8051b7678 100644 --- a/src/global-firestore-data/user-semesters.ts +++ b/src/global-firestore-data/user-semesters.ts @@ -12,6 +12,16 @@ import { deleteCoursesFromRequirementChoices, } from './user-overridden-fulfillment-choices'; +export const editCollections = async ( + updater: (oldCollections: readonly Collection[]) => readonly Collection[] +): Promise => { + const collections = updater(store.state.collections); + store.commit('setCollections', collections); + await updateDoc(doc(semestersCollection, store.state.currentFirebaseUser.email), { + collections, + }); +}; + export const editSemesters = ( plan: Plan, updater: (oldSemesters: readonly FirestoreSemester[]) => readonly FirestoreSemester[] @@ -41,6 +51,15 @@ export const setOrderByNewest = (orderByNewest: boolean): void => { }); }; +export const editCollection = ( + name: string, + updater: (oldCollection: Collection) => Collection +): void => { + editCollections(oldCollection => + oldCollection.map(collection => (collection.name === name ? updater(collection) : collection)) + ); +}; + export const editSemester = ( plan: Plan, year: number, @@ -56,6 +75,17 @@ export const editPlan = (name: string, updater: (oldPlan: Plan) => Plan): void = editPlans(oldPlan => oldPlan.map(plan => (plan.name === name ? updater(plan) : plan))); }; +const createCollection = ( + name: string, + courses: readonly FirestoreSemesterCourse[] +): { + name: string; + courses: readonly FirestoreSemesterCourse[]; +} => ({ + name, + courses, +}); + const createSemester = ( year: number, season: FirestoreSemesterSeason, @@ -88,6 +118,19 @@ export const semesterEquals = ( season: FirestoreSemesterSeason ): boolean => semester.year === year && semester.season === season; +export const addCollection = async ( + name: string, + courses: readonly FirestoreSemesterCourse[], + gtag?: VueGtag +): Promise => { + GTagEvent(gtag, 'add-collection'); + await editCollections(oldCollections => [...oldCollections, createCollection(name, courses)]); + // store.commit( + // 'setCurrentPlan', + // store.state.plans.find(plan => plan.name === name) + // ); +}; + export const addSemester = ( plan: Plan, year: number, @@ -112,6 +155,16 @@ export const addPlan = async ( ); }; +/** [deleteCollection] delete an entire collection. Now all courses from + * the collection can be added to semesters. + */ +export const deleteCollection = async (name: string, gtag?: VueGtag): Promise => { + GTagEvent(gtag, 'delete-collection'); + if (store.state.collections.some(p => p.name === name)) { + await editCollections(oldCollections => oldCollections.filter(p => p.name !== name)); + } +}; + export const deleteSemester = ( plan: Plan, year: number, @@ -138,6 +191,53 @@ export const deletePlan = async (name: string, gtag?: VueGtag): Promise => store.commit('setCurrentPlan', store.state.plans[0]); }; +/** Add one course to multiple collections. + * This course can no longer be added to semesters and is removed + * from the requirement choices. + * */ +export const addCourseToCollections = ( + plan: Plan, + year: number, + season: FirestoreSemesterSeason, + courseUniqueID: number, + choiceUpdater: (choice: FirestoreCourseOptInOptOutChoices) => FirestoreCourseOptInOptOutChoices, + gtag?: VueGtag +): void => { + GTagEvent(gtag, 'add-course-collections'); + editCollections(oldCollections => + oldCollections.map(collection => { + if (collection.courses.some(course => course.uniqueID === courseUniqueID)) { + return collection; + } + return { + ...collection, + courses: [...collection.courses, store.getters.getCourse(courseUniqueID)], + }; + }) + ); + deleteCourseFromSemester(plan, year, season, courseUniqueID); + deleteCourseFromRequirementChoices(courseUniqueID); + // course cannot be added to semester anymore unless removed or drag & dropped back. +}; + +/** Delete a course from a certain collection. This course can now be +added into semeseters. */ +export const deleteCourseFromCollection = ( + plan: Plan, + year: number, + season: FirestoreSemesterSeason, + name: string, + courseUniqueID: number, + gtag?: VueGtag +): void => { + // delete course from collection + GTagEvent(gtag, 'delete-course-collection'); + editCollection(name, oldCollection => ({ + ...oldCollection, + courses: oldCollection.courses.filter(course => course.uniqueID !== courseUniqueID), + })); +}; + export const addCourseToSemester = ( plan: Plan, year: number, diff --git a/src/gtag.ts b/src/gtag.ts index f79c48d2f..d19c1eafe 100644 --- a/src/gtag.ts +++ b/src/gtag.ts @@ -23,7 +23,9 @@ export const GTagLoginEvent = (gtag: VueGtag | undefined, method: string): void }; type EventType = + | 'add-collection' // User adds a collection | 'add-course' // User adds a course + | 'add-course-collections' // User adds a course to a collection(s) | 'add-modal-edit-requirements' // User clicks Edit Requirements on Add Modal | 'add-semester' // User adds a semester | 'add-plan' @@ -35,7 +37,9 @@ type EventType = | 'bottom-bar-view-course-information-on-roster' // User clicks View Course Information on Roster link on Bottom Bar | 'course-edit-color' // User edits the course color | 'subject-edit-color' // User edits the subject color + | 'delete-collection' // User deletes a collection | 'delete-course' // User deletes a course + | 'delete-course-collection' // User deletes a course from a collection | 'delete-semester' // User deletes a semester | 'delete-semester-courses' // User deletes all courses in a semester | 'delete-plan' @@ -58,6 +62,13 @@ export const GTagEvent = (gtag: VueGtag | undefined, eventType: EventType): void if (!gtag) return; let eventPayload: EventPayload | undefined; switch (eventType) { + case 'add-collection': + eventPayload = { + event_category: 'collection', + event_label: 'add-collection', + value: 1, + }; + break; case 'add-course': eventPayload = { event_category: 'course', @@ -65,6 +76,13 @@ export const GTagEvent = (gtag: VueGtag | undefined, eventType: EventType): void value: 1, }; break; + case 'add-course-collections': + eventPayload = { + event_category: 'collection', + event_label: 'add-course-collections', + value: 1, + }; + break; case 'add-modal-edit-requirements': eventPayload = { event_category: 'add-modal', @@ -142,6 +160,13 @@ export const GTagEvent = (gtag: VueGtag | undefined, eventType: EventType): void value: 1, }; break; + case 'delete-collection': + eventPayload = { + event_category: 'collection', + event_label: 'delete-collection', + value: 1, + }; + break; case 'delete-course': eventPayload = { event_category: 'course', @@ -149,6 +174,13 @@ export const GTagEvent = (gtag: VueGtag | undefined, eventType: EventType): void value: 1, }; break; + case 'delete-course-collection': + eventPayload = { + event_category: 'collection', + event_label: 'delete-course-collection', + value: 1, + }; + break; case 'delete-semester': eventPayload = { event_category: 'semester', diff --git a/src/store.ts b/src/store.ts index 6a5dc8d26..53a2f52c0 100644 --- a/src/store.ts +++ b/src/store.ts @@ -51,6 +51,7 @@ export type VuexStoreState = { isTeleportModalOpen: boolean; plans: readonly Plan[]; currentPlan: Plan; + collections: readonly Collection[]; }; export class TypedVuexStore extends Store {} @@ -99,6 +100,7 @@ const store: TypedVuexStore = new TypedVuexStore({ isTeleportModalOpen: false, plans: [], currentPlan: { name: '', semesters: [] }, + collections: [], }, actions: {}, getters: { @@ -186,6 +188,9 @@ const store: TypedVuexStore = new TypedVuexStore({ setSawNewFeature(state: VuexStoreState, seen: boolean) { state.onboardingData.sawNewFeature = seen; }, + setCollections(state: VuexStoreState, collections: readonly Collection[]) { + state.collections = collections; + }, }, }); @@ -245,7 +250,8 @@ const autoRecomputeDerivedData = (): (() => void) => mutation.type === 'setToggleableRequirementChoices' || mutation.type === 'setOverriddenFulfillmentChoices' || mutation.type === 'setCurrentPlan' || - mutation.type === 'setPlans' + mutation.type === 'setPlans' || + mutation.type === 'setCollections' ) { if (state.onboardingData.college !== '') { store.commit( @@ -334,6 +340,8 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = // if user hasn't yet chosen an ordering, choose true by default store.commit('setOrderByNewest', orderByNewest === undefined ? true : orderByNewest); } else { + const collections = [{ name: 'All', courses: [] }]; + store.commit('setCollections', collections); const plans = [{ name: 'Plan 1', semesters: [] }]; store.commit('setPlans', plans); store.commit('setCurrentPlan', plans[0]); @@ -347,6 +355,7 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = orderByNewest: true, plans: [{ name: 'Plan 1', semesters: [newSemester] }], semesters: [newSemester], + collections: [{ name: 'All', courses: [] }], }); } semestersInitialLoadFinished = true; diff --git a/src/user-data.d.ts b/src/user-data.d.ts index ceb82ba4a..5e3d65cbe 100644 --- a/src/user-data.d.ts +++ b/src/user-data.d.ts @@ -36,6 +36,7 @@ type FirestoreSemester = { }; type FirestoreSemestersData = { + readonly collections: readonly Collection[]; readonly plans: readonly Plan[]; readonly semesters: readonly FirestoreSemester[]; readonly orderByNewest: boolean; @@ -225,3 +226,8 @@ type Plan = { readonly name: string; readonly semesters: readonly FirestoreSemester[]; }; + +type Collection = { + readonly name: string; + readonly courses: readonly FirestoreSemesterCourse[]; +}; From 3108134dfec48cf1d573e20e36aca7883f146f04 Mon Sep 17 00:00:00 2001 From: Hannah Zhou Date: Sat, 5 Oct 2024 11:59:05 -0400 Subject: [PATCH 2/6] added basic functionality for save course - can save a course when press 'done' - course is saved to an empty collection - add functions for add & edit collection --- src/components/Course/Course.vue | 24 +++++++---- src/components/Modals/SaveCourseModal.vue | 36 ++++++++++++----- src/components/Semester/Semester.vue | 44 +++++++++++++++++++++ src/global-firestore-data/index.ts | 6 +++ src/global-firestore-data/user-semesters.ts | 7 +--- 5 files changed, 94 insertions(+), 23 deletions(-) diff --git a/src/components/Course/Course.vue b/src/components/Course/Course.vue index c314f1636..e9549b37f 100644 --- a/src/components/Course/Course.vue +++ b/src/components/Course/Course.vue @@ -10,8 +10,9 @@ typeof course === 'object', 'edit-course-credit': (credit: number, uniqueID: number) => typeof credit === 'number' && typeof uniqueID === 'number', + 'save-course': (courseCode: string, collections: string[]) => + typeof courseCode === 'string' && typeof collections === 'object', + 'add-course-collection': (name: string) => typeof name === 'string', + 'edit-collection': (name: string, oldname: string) => + typeof name === 'string' && typeof oldname === 'string', }, data() { return { @@ -182,13 +188,15 @@ export default defineComponent({ closeEditColorModal() { this.isEditColorOpen = false; }, - addCollection() { - this.isSaveCourseOpen = false; - // TODO: implement add collection + addCollection(name: string) { + this.$emit('add-course-collection', name); }, - addCourseCollection() { - this.isSaveCourseOpen = false; - // TODO: implement save course + saveCourse(courseCode: string, collections: string[]) { + this.$emit('save-course', courseCode, collections); + }, + /* only to rename the collection */ + editCollection(name: string, oldname: string) { + this.$emit('edit-collection', name, oldname); }, colorCourse(color: string) { this.$emit('color-course', color, this.courseObj.uniqueID, this.courseObj.code); diff --git a/src/components/Modals/SaveCourseModal.vue b/src/components/Modals/SaveCourseModal.vue index 412ed81c6..8fcdfddb5 100644 --- a/src/components/Modals/SaveCourseModal.vue +++ b/src/components/Modals/SaveCourseModal.vue @@ -17,7 +17,7 @@
Collections -
@@ -25,7 +25,7 @@
-

{{ collection }}

+

{{ collection }}

+
+ + +
@@ -44,19 +49,26 @@ export default defineComponent({ components: { TeleportModal }, props: { courseCode: { type: String, required: true }, - isdefaultCollection: { type: Boolean, default: true }, }, data() { return { - collections: store.state.collections.map(collection => collection.name), + checkedCollections: [] as string[], // New data property to manage checked state }; }, computed: { - placeholderName() { + isDefaultCollection() { + const collections = store.state.collections.map(collection => collection.name); + return collections.length === 0; + }, + collections() { + const collections = store.state.collections.map(collection => collection.name); + return collections.length === 0 ? ['No collections added yet'] : collections; + }, + placeholder_name() { const oldcollections = store.state.collections.map(collection => collection.name); let newCollectionNum = 1; // eslint-disable-next-line no-loop-func - while (oldcollections.find(p => p === `Collection ${newCollectionNum}`)) { + while (oldcollections.find(p => p === `New Collection ${newCollectionNum}`)) { newCollectionNum += 1; } return `New Collection ${newCollectionNum}`; @@ -64,8 +76,7 @@ export default defineComponent({ }, emits: { 'close-save-course-modal': () => true, - 'save-course': (courseCode: string, collections: string[]) => - typeof courseCode === 'string' && typeof collections === 'object', + 'save-course': (collections: string[]) => typeof collections === 'object', 'add-collection': (name: string) => typeof name === 'string', }, methods: { @@ -73,11 +84,11 @@ export default defineComponent({ this.$emit('close-save-course-modal'); }, saveCourse() { - this.$emit('save-course', this.courseCode, this.collections); + this.$emit('save-course', this.checkedCollections); this.closeCurrentModal(); }, addCollection() { - this.$emit('add-collection', this.placeholderName); + this.$emit('add-collection', this.placeholder_name); }, }, }); diff --git a/src/components/Semester/Semester.vue b/src/components/Semester/Semester.vue index 22f53c267..daca988b0 100644 --- a/src/components/Semester/Semester.vue +++ b/src/components/Semester/Semester.vue @@ -110,7 +110,6 @@ @edit-course-credit="editCourseCredit" @save-course="saveCourse" @add-collection="addCollection" - @add-course-collection="addCourseToCollections" @edit-collection="editCollection" /> collection.name === oldname); @@ -441,11 +437,6 @@ export default defineComponent({ if (toEdit !== undefined) { editCollection(oldname, updater); } - store.commit( - 'setCurrentCollection', - store.state.collections.find(collection => collection.name === name) - ); - // store.commit('setOrderByNewest', store.state.orderByNewest); this.confirmationText = `${oldname} has been renamed to ${name}!`; this.isConfirmationOpen = true; setTimeout(() => { diff --git a/src/global-firestore-data/user-semesters.ts b/src/global-firestore-data/user-semesters.ts index 347e825e0..0ecb31e98 100644 --- a/src/global-firestore-data/user-semesters.ts +++ b/src/global-firestore-data/user-semesters.ts @@ -17,9 +17,6 @@ export const editCollections = async ( ): Promise => { const collections = updater(store.state.collections); store.commit('setCollections', collections); - await updateDoc(doc(semestersCollection, store.state.currentFirebaseUser.email), { - collections, - }); }; export const editSemesters = ( @@ -125,10 +122,6 @@ export const addCollection = async ( ): Promise => { GTagEvent(gtag, 'add-collection'); await editCollections(oldCollections => [...oldCollections, createCollection(name, courses)]); - // store.commit( - // 'setCurrentPlan', - // store.state.plans.find(plan => plan.name === name) - // ); }; export const addSemester = ( @@ -193,29 +186,27 @@ export const deletePlan = async (name: string, gtag?: VueGtag): Promise => /** Add one course to multiple collections. * This course is removed from the requirement choices. - * */ + */ export const addCourseToCollections = ( plan: Plan, year: number, season: FirestoreSemesterSeason, - courseUniqueID: number, - choiceUpdater: (choice: FirestoreCourseOptInOptOutChoices) => FirestoreCourseOptInOptOutChoices, + newCourse: FirestoreSemesterCourse, + collectionIDs: string[], // Array of collection IDs gtag?: VueGtag ): void => { GTagEvent(gtag, 'add-course-collections'); editCollections(oldCollections => oldCollections.map(collection => { - if (collection.courses.some(course => course.uniqueID === courseUniqueID)) { - return collection; + if (collectionIDs.includes(collection.name)) { + return { ...collection, courses: [...collection.courses, newCourse] }; } - return { - ...collection, - courses: [...collection.courses, store.getters.getCourse(courseUniqueID)], - }; + return collection; }) ); - deleteCourseFromSemester(plan, year, season, courseUniqueID); - deleteCourseFromRequirementChoices(courseUniqueID); + + deleteCourseFromSemester(plan, year, season, newCourse.uniqueID); + deleteCourseFromRequirementChoices(newCourse.uniqueID); }; /** Delete a course from a certain collection. */ diff --git a/src/store.ts b/src/store.ts index 53a2f52c0..90965f27b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -188,8 +188,8 @@ const store: TypedVuexStore = new TypedVuexStore({ setSawNewFeature(state: VuexStoreState, seen: boolean) { state.onboardingData.sawNewFeature = seen; }, - setCollections(state: VuexStoreState, collections: readonly Collection[]) { - state.collections = collections; + setCollections(state: VuexStoreState, newCollections: readonly Collection[]) { + state.collections = newCollections; }, }, }); @@ -250,8 +250,7 @@ const autoRecomputeDerivedData = (): (() => void) => mutation.type === 'setToggleableRequirementChoices' || mutation.type === 'setOverriddenFulfillmentChoices' || mutation.type === 'setCurrentPlan' || - mutation.type === 'setPlans' || - mutation.type === 'setCollections' + mutation.type === 'setPlans' ) { if (state.onboardingData.college !== '') { store.commit( @@ -332,6 +331,7 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = const plan = getFirstPlan(data); store.commit('setPlans', data.plans); store.commit('setCurrentPlan', plan); + // store.commit('setCollections', data.collections); Note: toggle this on and off to save collections progress after refresh const { orderByNewest } = data; store.commit('setSemesters', plan.semesters); updateDoc(doc(fb.semestersCollection, simplifiedUser.email), { @@ -340,8 +340,6 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = // if user hasn't yet chosen an ordering, choose true by default store.commit('setOrderByNewest', orderByNewest === undefined ? true : orderByNewest); } else { - const collections = [{ name: 'All', courses: [] }]; - store.commit('setCollections', collections); const plans = [{ name: 'Plan 1', semesters: [] }]; store.commit('setPlans', plans); store.commit('setCurrentPlan', plans[0]); @@ -355,7 +353,6 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = orderByNewest: true, plans: [{ name: 'Plan 1', semesters: [newSemester] }], semesters: [newSemester], - collections: [{ name: 'All', courses: [] }], }); } semestersInitialLoadFinished = true; diff --git a/src/user-data.d.ts b/src/user-data.d.ts index 5e3d65cbe..a396a8434 100644 --- a/src/user-data.d.ts +++ b/src/user-data.d.ts @@ -36,7 +36,7 @@ type FirestoreSemester = { }; type FirestoreSemestersData = { - readonly collections: readonly Collection[]; + // readonly collections: readonly Collection[]; Note: Not sure where to store collections readonly plans: readonly Plan[]; readonly semesters: readonly FirestoreSemester[]; readonly orderByNewest: boolean; From 36f9925678ea33c7b3e023b09a8e6cc5c5885987 Mon Sep 17 00:00:00 2001 From: Hannah Zhou Date: Thu, 17 Oct 2024 00:33:44 -0400 Subject: [PATCH 4/6] finished backend and fixed frontend - finished scrollable checkbox frontend - fixed the bug for selecting a singular checkbox - believed its stored in firestore when added to collection - renamed collection to savedCourses for firesetore clarity --- src/components/Modals/SaveCourseModal.vue | 140 +++++++++++++++++--- src/global-firestore-data/user-semesters.ts | 6 +- src/store.ts | 19 ++- src/user-data.d.ts | 2 +- 4 files changed, 134 insertions(+), 33 deletions(-) diff --git a/src/components/Modals/SaveCourseModal.vue b/src/components/Modals/SaveCourseModal.vue index e3c79e036..10762b41c 100644 --- a/src/components/Modals/SaveCourseModal.vue +++ b/src/components/Modals/SaveCourseModal.vue @@ -14,7 +14,6 @@
-
Collections
-
-
-
+
+
+
- +
+
@@ -57,21 +64,24 @@ export default defineComponent({ }, computed: { isDefaultCollection() { - const collections = store.state.collections.map(collection => collection.name); + const collections = store.state.savedCourses.map(collection => collection.name); return collections.length === 0; }, collections() { - const collections = store.state.collections.map(collection => collection.name); + const collections = store.state.savedCourses.map(collection => collection.name); return collections.length === 0 ? ['No collections added yet'] : collections; }, placeholder_name() { - const oldcollections = store.state.collections.map(collection => collection.name); + const oldcollections = store.state.savedCourses.map(collection => collection.name); let newCollectionNum = 1; // eslint-disable-next-line no-loop-func - while (oldcollections.find(p => p === `New Collection ${newCollectionNum}`)) { + while (oldcollections.find(p => p === `Collection ${newCollectionNum}`)) { newCollectionNum += 1; } - return `New Collection ${newCollectionNum}`; + return `Collection ${newCollectionNum}`; + }, + isNumCollectionGreaterThanFour() { + return store.state.savedCourses.length >= 4; }, }, emits: { @@ -96,6 +106,7 @@ export default defineComponent({ diff --git a/src/global-firestore-data/user-semesters.ts b/src/global-firestore-data/user-semesters.ts index 0ecb31e98..e486a69d6 100644 --- a/src/global-firestore-data/user-semesters.ts +++ b/src/global-firestore-data/user-semesters.ts @@ -15,7 +15,7 @@ import { export const editCollections = async ( updater: (oldCollections: readonly Collection[]) => readonly Collection[] ): Promise => { - const collections = updater(store.state.collections); + const collections = updater(store.state.savedCourses); store.commit('setCollections', collections); }; @@ -153,7 +153,7 @@ export const addPlan = async ( */ export const deleteCollection = async (name: string, gtag?: VueGtag): Promise => { GTagEvent(gtag, 'delete-collection'); - if (store.state.collections.some(p => p.name === name)) { + if (store.state.savedCourses.some(p => p.name === name)) { await editCollections(oldCollections => oldCollections.filter(p => p.name !== name)); } }; @@ -192,7 +192,7 @@ export const addCourseToCollections = ( year: number, season: FirestoreSemesterSeason, newCourse: FirestoreSemesterCourse, - collectionIDs: string[], // Array of collection IDs + collectionIDs: string[], gtag?: VueGtag ): void => { GTagEvent(gtag, 'add-course-collections'); diff --git a/src/store.ts b/src/store.ts index 90965f27b..cef3b537d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -51,7 +51,7 @@ export type VuexStoreState = { isTeleportModalOpen: boolean; plans: readonly Plan[]; currentPlan: Plan; - collections: readonly Collection[]; + savedCourses: readonly Collection[]; }; export class TypedVuexStore extends Store {} @@ -100,7 +100,7 @@ const store: TypedVuexStore = new TypedVuexStore({ isTeleportModalOpen: false, plans: [], currentPlan: { name: '', semesters: [] }, - collections: [], + savedCourses: [], }, actions: {}, getters: { @@ -188,8 +188,8 @@ const store: TypedVuexStore = new TypedVuexStore({ setSawNewFeature(state: VuexStoreState, seen: boolean) { state.onboardingData.sawNewFeature = seen; }, - setCollections(state: VuexStoreState, newCollections: readonly Collection[]) { - state.collections = newCollections; + setCollections(state: VuexStoreState, newSavedCourses: readonly Collection[]) { + state.savedCourses = newSavedCourses; }, }, }); @@ -208,7 +208,7 @@ const autoRecomputeDerivedData = (): (() => void) => ); break; } - case 'setSemesters' || 'setPlans': { + case 'setSemesters' || 'setPlans' || 'setSavedCourses': { const allCourseSet = new Set(); const duplicatedCourseCodeSet = new Set(); const courseMap: Record = {}; @@ -250,7 +250,8 @@ const autoRecomputeDerivedData = (): (() => void) => mutation.type === 'setToggleableRequirementChoices' || mutation.type === 'setOverriddenFulfillmentChoices' || mutation.type === 'setCurrentPlan' || - mutation.type === 'setPlans' + mutation.type === 'setPlans' || + mutation.type === 'setSavedCourses' ) { if (state.onboardingData.college !== '') { store.commit( @@ -331,18 +332,21 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = const plan = getFirstPlan(data); store.commit('setPlans', data.plans); store.commit('setCurrentPlan', plan); - // store.commit('setCollections', data.collections); Note: toggle this on and off to save collections progress after refresh + store.commit('setSavedCourses', data.savedCourses); // Note: toggle this on and off to save collections progress after refresh const { orderByNewest } = data; store.commit('setSemesters', plan.semesters); updateDoc(doc(fb.semestersCollection, simplifiedUser.email), { plans: data.plans, + // savedCourses: data.savedCourses, }); // if user hasn't yet chosen an ordering, choose true by default store.commit('setOrderByNewest', orderByNewest === undefined ? true : orderByNewest); } else { const plans = [{ name: 'Plan 1', semesters: [] }]; + const defaultCollection = [{ name: 'All', courses: [] }]; store.commit('setPlans', plans); store.commit('setCurrentPlan', plans[0]); + store.commit('setSavedCourses', defaultCollection); const newSemester: FirestoreSemester = { year: getCurrentYear(), season: getCurrentSeason(), @@ -353,6 +357,7 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) = orderByNewest: true, plans: [{ name: 'Plan 1', semesters: [newSemester] }], semesters: [newSemester], + savedCourses: [{ name: 'All', courses: [] }], }); } semestersInitialLoadFinished = true; diff --git a/src/user-data.d.ts b/src/user-data.d.ts index a396a8434..a35a804ee 100644 --- a/src/user-data.d.ts +++ b/src/user-data.d.ts @@ -36,7 +36,7 @@ type FirestoreSemester = { }; type FirestoreSemestersData = { - // readonly collections: readonly Collection[]; Note: Not sure where to store collections + readonly savedCourses: readonly Collection[]; // confirmed works: readonly plans: readonly Plan[]; readonly semesters: readonly FirestoreSemester[]; readonly orderByNewest: boolean; From 5d4df16ebe20b0bcd2999458cd8991769d4bfd1b Mon Sep 17 00:00:00 2001 From: Hannah Zhou Date: Thu, 17 Oct 2024 20:04:50 -0400 Subject: [PATCH 5/6] created editabilify for collection names - saveCourseModal allow user to name their new collections - moved scss into a separate file --- src/components/Modals/SaveCourseModal.scss | 194 ++++++++++++++++++ src/components/Modals/SaveCourseModal.vue | 220 +++++++-------------- 2 files changed, 262 insertions(+), 152 deletions(-) create mode 100644 src/components/Modals/SaveCourseModal.scss diff --git a/src/components/Modals/SaveCourseModal.scss b/src/components/Modals/SaveCourseModal.scss new file mode 100644 index 000000000..12ae20b16 --- /dev/null +++ b/src/components/Modals/SaveCourseModal.scss @@ -0,0 +1,194 @@ +@import '@/assets/scss/_variables.scss'; +.saveCourseModal { + &-title { + display: flex; + justify-content: space-between; + padding-top: 0.6rem; + gap: 0.5rem; + img { + margin-top: 2%; + align-self: flex-start; + } + } + + &-header { + display: flex; + align-self: center; + margin-bottom: 0.7rem; + width: 112%; + height: 2rem; + border: 0.3px solid $lightGray; + color: $primaryGray; + padding: 1rem; + + &-text { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 13px; + font-weight: 900; + width: 100%; + + &-addButton { + cursor: pointer; + &:hover { + opacity: 0.5; + } + } + } + } + + &-body { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + color: $primaryGray; + width: 100%; + position: relative; + max-height: 4.5rem; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; + + ::-webkit-scrollbar-button { + display: none; // Hide the up and down arrows + } + + &-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 0.5rem; + width: 100%; + + &.default-collection { + justify-content: center; + align-items: center; + } + + &-collection { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + color: $primaryGray; + gap: 0.5rem; + padding: 0rem; + + input[type='checkbox'] { + margin: 0; + padding: 0; + appearance: none; + width: 12px; + height: 12px; + border-radius: 10%; + border: 1px solid $lightGray; + background-color: white; + cursor: pointer; + position: relative; // For the ::before element positioning + user-select: none; + outline: none; + + &:hover { + border: 1px solid $emGreen; + } + + &:checked { + background-color: $emGreen; + border: 1px solid $emGreen; + } + + // Show checkbox vector when checked + &:checked::before { + content: ''; + background-image: url('@/assets/images/checkmark-color.svg'); + background-size: contain; + background-repeat: no-repeat; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-48%, -38%); + width: 9px; + height: 9px; + } + } + + label { + cursor: pointer; + user-select: none; + margin: 0; + padding: 0; + outline: none; + } + } + } + &-bottom { + display: flex; + align-items: center; + justify-items: center; + width: 100%; + + input[type='checkbox'] { + margin-right: 0.35rem; + appearance: none; + width: 12px; + height: 12px; + border-radius: 10%; + border: 1px solid $lightGray; + background-color: white; + cursor: pointer; + position: relative; // For the ::before element positioning + user-select: none; + outline: none; + + &:hover { + border: 1px solid $emGreen; + } + + &:checked { + background-color: $emGreen; + border: 1px solid $emGreen; + } + + // Show checkbox vector when checked + &:checked::before { + content: ''; + background-image: url('@/assets/images/checkmark-color.svg'); + background-size: contain; + background-repeat: no-repeat; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-48%, -38%); + width: 9px; + height: 9px; + } + } + + .new-collection-label { + flex-grow: 1; + display: flex; + align-items: center; + margin-top: 0.3rem; + + .editable-input { + border: 1px solid $lightGray; + border-radius: 4px; + box-sizing: border-box; + transition: border 0.2s ease; + width: 80%; + padding-left: 0.1rem; + } + } + } + } + &-divider-line { + display: flex; + align-self: center; + width: 112%; + height: 0.3px; + background-color: $lightGray; + } + } \ No newline at end of file diff --git a/src/components/Modals/SaveCourseModal.vue b/src/components/Modals/SaveCourseModal.vue index 10762b41c..7f48853b1 100644 --- a/src/components/Modals/SaveCourseModal.vue +++ b/src/components/Modals/SaveCourseModal.vue @@ -2,6 +2,7 @@ -
+
+ No collections added yet +
+
+
+ + +
+
+
+ +
+ +
+
@@ -59,23 +90,29 @@ export default defineComponent({ }, data() { return { - checkedCollections: [] as string[], // New data property to manage checked state + checkedCollections: [] as string[], + newCollectionName: '', // Holds the name of the new collection before it is added + isEditing: false, + currentEditingIndex: -1, }; }, computed: { + isUniqueName() { + return !this.collections.includes(this.newCollectionName); + }, isDefaultCollection() { const collections = store.state.savedCourses.map(collection => collection.name); return collections.length === 0; }, collections() { const collections = store.state.savedCourses.map(collection => collection.name); - return collections.length === 0 ? ['No collections added yet'] : collections; + return collections.length === 0 ? [] : collections; }, placeholder_name() { - const oldcollections = store.state.savedCourses.map(collection => collection.name); + const oldCollections = store.state.savedCourses.map(collection => collection.name); let newCollectionNum = 1; // eslint-disable-next-line no-loop-func - while (oldcollections.find(p => p === `Collection ${newCollectionNum}`)) { + while (oldCollections.find(p => p === `Collection ${newCollectionNum}`)) { newCollectionNum += 1; } return `Collection ${newCollectionNum}`; @@ -94,18 +131,31 @@ export default defineComponent({ this.$emit('close-save-course-modal'); }, saveCourse() { - this.$emit('save-course', this.checkedCollections); + if (this.checkedCollections.length !== 0) { + this.$emit('save-course', this.checkedCollections); + } this.closeCurrentModal(); }, addCollection() { - this.$emit('add-collection', this.placeholder_name); + this.newCollectionName = this.placeholder_name; + this.isEditing = true; + this.currentEditingIndex = this.collections.length; + }, + finishEditing() { + if (this.newCollectionName.trim() && this.isUniqueName) { + this.$emit('add-collection', this.newCollectionName.trim()); + this.newCollectionName = ''; + } + this.isEditing = false; + this.newCollectionName = ''; + this.currentEditingIndex = -1; }, }, }); From 726dbe3c8f487ee775e92a1588c1830a9c7d17bf Mon Sep 17 00:00:00 2001 From: Hannah Zhou Date: Sun, 27 Oct 2024 23:04:47 -0400 Subject: [PATCH 6/6] fixed backend error and logic - added updateDoc in backend functions - made edit Collections modal more functional with saveCourse --- scripts/migration/savedCourses-migration.ts | 31 ++++++++++++ src/components/Course/Course.vue | 21 ++++---- src/components/Modals/SaveCourseModal.scss | 10 +++- src/components/Modals/SaveCourseModal.vue | 54 ++++++++++++++++----- src/components/Semester/Semester.vue | 45 +++++++++++++++-- src/global-firestore-data/index.ts | 1 + src/global-firestore-data/user-semesters.ts | 42 +++++++++++----- src/store.ts | 15 ++++-- 8 files changed, 173 insertions(+), 46 deletions(-) create mode 100644 scripts/migration/savedCourses-migration.ts diff --git a/scripts/migration/savedCourses-migration.ts b/scripts/migration/savedCourses-migration.ts new file mode 100644 index 000000000..3ecb9ee59 --- /dev/null +++ b/scripts/migration/savedCourses-migration.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ + +import { usernameCollection, semestersCollection } from '../firebase-config'; + +/** + * Perform migration of a default The 'All' Collection for All Courses saved + */ +async function runOnUser(userEmail: string) { + const emptyCourses = []; + await semestersCollection.doc(userEmail).update({ + savedCourses: [{ name: 'All', emptyCourses }], + }); +} + +async function main() { + const userEmail = process.argv[2]; + if (userEmail != null) { + await runOnUser(userEmail); + return; + } + const collection = await usernameCollection.get(); + for (const { id } of collection.docs) { + console.group(`Running on ${id}...`); + // Intentionally await in a loop to have no interleaved console logs. + // eslint-disable-next-line no-await-in-loop + await runOnUser(id); + console.groupEnd(); + } +} + +main(); diff --git a/src/components/Course/Course.vue b/src/components/Course/Course.vue index 2a6b87cbc..81f7bd73d 100644 --- a/src/components/Course/Course.vue +++ b/src/components/Course/Course.vue @@ -12,7 +12,6 @@ @close-save-course-modal="closeSaveCourseModal" @save-course="saveCourse" @add-collection="addCollection" - @edit-collection="editCollection" v-if="isSaveCourseOpen" /> typeof course === 'object', 'edit-course-credit': (credit: number, uniqueID: number) => typeof credit === 'number' && typeof uniqueID === 'number', - 'save-course': (course: FirestoreSemesterCourse, collections: string[]) => - typeof course === 'object' && typeof collections === 'object', + 'save-course': ( + course: FirestoreSemesterCourse, + addedToCollections: string[], + deletedFromCollection: string[] + ) => + typeof course === 'object' && + typeof addedToCollections === 'object' && + typeof deletedFromCollection === 'object', 'add-collection': (name: string) => typeof name === 'string', - 'edit-collection': (name: string, oldname: string) => - typeof name === 'string' && typeof oldname === 'string', }, data() { return { @@ -191,13 +194,9 @@ export default defineComponent({ addCollection(name: string) { this.$emit('add-collection', name); }, - saveCourse(collections: string[]) { + saveCourse(addedToCollections: string[], deletedFromCollections: string[]) { const course = { ...this.courseObj }; - this.$emit('save-course', course, collections); - }, - /* only to rename the collection */ - editCollection(name: string, oldname: string) { - this.$emit('edit-collection', name, oldname); + this.$emit('save-course', course, addedToCollections, deletedFromCollections); }, colorCourse(color: string) { this.$emit('color-course', color, this.courseObj.uniqueID, this.courseObj.code); diff --git a/src/components/Modals/SaveCourseModal.scss b/src/components/Modals/SaveCourseModal.scss index 12ae20b16..79ed287c0 100644 --- a/src/components/Modals/SaveCourseModal.scss +++ b/src/components/Modals/SaveCourseModal.scss @@ -95,13 +95,13 @@ border: 1px solid $emGreen; } - &:checked { + &:checked{ background-color: $emGreen; border: 1px solid $emGreen; } // Show checkbox vector when checked - &:checked::before { + &:checked::before{ content: ''; background-image: url('@/assets/images/checkmark-color.svg'); background-size: contain; @@ -180,6 +180,12 @@ transition: border 0.2s ease; width: 80%; padding-left: 0.1rem; + + &:focus { + outline: none; + border-color: $lightGray; + box-shadow: none; + } } } } diff --git a/src/components/Modals/SaveCourseModal.vue b/src/components/Modals/SaveCourseModal.vue index 7f48853b1..ed90e307d 100644 --- a/src/components/Modals/SaveCourseModal.vue +++ b/src/components/Modals/SaveCourseModal.vue @@ -2,7 +2,7 @@
@@ -64,6 +64,7 @@