diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e0219db4..4921ee64 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,8 @@ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "oderwat.indent-rainbow", - "mhutchie.git-graph" + "mhutchie.git-graph", + "kenomaru.thunder" ] } }, diff --git a/.github/labeler.yml b/.github/labeler.yml index e4ac1cb8..3480590c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,59 +1,59 @@ -# Add 'root' label to any root file changes -# Quotation marks are required for the leading asterisk -root: -- changed-files: - - any-glob-to-any-file: '*' - -# Add 'Documentation' label to any change to .md files within the entire repository -Documentation: -- changed-files: - - any-glob-to-any-file: '**/*.md' - -# Add 'source' label to any change to src files within the source dir -source: -- all: - - changed-files: - - any-glob-to-any-file: 'src/**/*' - -# Add 'pages' label to any changes within the entire repository -pages: -- changed-files: - - any-glob-to-any-file: 'pages/**/*' - -# Add 'public' label to any changes within the entire repository -public: -- changed-files: - - any-glob-to-any-file: 'public/**/*' - -# Add 'globals' label to any changes within the entire repository -globals: -- changed-files: - - any-glob-to-any-file: 'globals/**/*' - -# Add 'firebase' label to any changes within the entire repository -firebase: -- changed-files: - - any-glob-to-any-file: 'firebase/**/*' - -# Add 'context' label to any changes within the entire repository -context: -- changed-files: - - any-glob-to-any-file: 'context/**/*' - -# Add 'workflows' label to any changes within the entire repository -workflows: -- changed-files: - - any-glob-to-any-file: '.github/**/*' - -# Add 'devcontainer' label to any changes within the entire repository -devcontainer: -- changed-files: - - any-glob-to-any-file: '.devcontainer/**/*' - -# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name -feature: - - head-branch: ['^feature', 'feature'] - -# Add 'release' label to any PR that is opened against the `main` branch -release: - - base-branch: 'main' +# Add 'root' label to any root file changes +# Quotation marks are required for the leading asterisk +root: + - changed-files: + - any-glob-to-any-file: '*' + +# Add 'Documentation' label to any change to .md files within the entire repository +Documentation: + - changed-files: + - any-glob-to-any-file: '**/*.md' + +# Add 'source' label to any change to src files within the source dir +source: + - all: + - changed-files: + - any-glob-to-any-file: 'src/**/*' + +# Add 'pages' label to any changes within the entire repository +pages: + - changed-files: + - any-glob-to-any-file: 'pages/**/*' + +# Add 'public' label to any changes within the entire repository +public: + - changed-files: + - any-glob-to-any-file: 'public/**/*' + +# Add 'globals' label to any changes within the entire repository +globals: + - changed-files: + - any-glob-to-any-file: 'globals/**/*' + +# Add 'firebase' label to any changes within the entire repository +firebase: + - changed-files: + - any-glob-to-any-file: 'firebase/**/*' + +# Add 'context' label to any changes within the entire repository +context: + - changed-files: + - any-glob-to-any-file: 'context/**/*' + +# Add 'workflows' label to any changes within the entire repository +workflows: + - changed-files: + - any-glob-to-any-file: '.github/**/*' + +# Add 'devcontainer' label to any changes within the entire repository +devcontainer: + - changed-files: + - any-glob-to-any-file: '.devcontainer/**/*' + +# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name +feature: + - head-branch: ['^feature', 'feature'] + +# Add 'release' label to any PR that is opened against the `main` branch +release: + - base-branch: 'main' diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 2ade0474..d9eb0049 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -1,16 +1,16 @@ -name: Greetings - -on: [pull_request_target, issues] - -jobs: - greeting: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: "Congratulations on your first issue! We will look into it!" - pr-message: "Congratulations on your first PR! We will be reviewing it! " +name: Greetings + +on: [pull_request_target, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Congratulations on your first issue! We will look into it!' + pr-message: 'Congratulations on your first PR! We will be reviewing it! ' diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index be398307..7faee1ef 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -1,25 +1,24 @@ -# This workflow will triage pull requests and apply a label based on the -# paths that are modified in the pull request. -# -# To use this workflow, you will need to set up a .github/labeler.yml -# file with configuration. For more information, see: -# https://github.com/actions/labeler - -name: Labeler -on: [pull_request_target] - -jobs: - label: - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Add label - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" +# This workflow will triage pull requests and apply a label based on the +# paths that are modified in the pull request. +# +# To use this workflow, you will need to set up a .github/labeler.yml +# file with configuration. For more information, see: +# https://github.com/actions/labeler + +name: Labeler +on: [pull_request_target] + +jobs: + label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Add label + uses: actions/labeler@v5 + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a86dc954..662cc671 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,34 +1,33 @@ -# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. -# -# You can adjust the behavior by modifying this file. -# For more information, see: -# https://github.com/actions/stale -name: Mark stale issues and pull requests - -on: - schedule: - - cron: '36 0 * * *' - -jobs: - stale: - - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - - steps: - - uses: actions/stale@v9 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' - exempt-issue-labels: Not Stale - exempt-pr-labels: Not Stale - stale-issue-message: > - This issue is stale because it has been open for 60 days with no - activity. - stale-pr-message: > - This pull request is stale because it has been open for 60 days - with no activity. - days-before-close: -1 +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '36 0 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' + exempt-issue-labels: Not Stale + exempt-pr-labels: Not Stale + stale-issue-message: > + This issue is stale because it has been open for 60 days with no + activity. + stale-pr-message: > + This pull request is stale because it has been open for 60 days + with no activity. + days-before-close: -1 diff --git a/README.md b/README.md index abe20e53..008c4a3c 100755 --- a/README.md +++ b/README.md @@ -81,7 +81,8 @@ yarn build yarn dev ``` -***Note***: A cloud-based Firebase project must still exist in order to run the emulator locally in this manner. Simply create a blank/empty [Firebase](https://firebase.google.com/) project using a Google account for this purpose, and then populate file `website/.env` accordingly with corresponding API key and project information from Firebase (see `website/example.env` for additional reference, as well as `website/__seed__/README.md` for more information regarding seeding a cloud-based Firebase Firestore database). +> [!NOTE] +> A cloud-based Firebase project must still exist in order to run the emulator locally in this manner. Simply create a blank/empty [Firebase](https://firebase.google.com/) project using a Google account for this purpose, and then populate file `website/.env` accordingly with corresponding API key and project information from Firebase (see `website/example.env` for additional reference, as well as `website/__seed__/README.md` for more information regarding seeding a cloud-based Firebase Firestore database). Open `http://localhost:3000` with your browser to see the result. The local data will be seeded from scratch. Furthermore, you can use the local auth service by simply logging in via any of the provided services (e.g., Google) with auto-generated credentials, which will simulate a logged in user account. diff --git a/__seed__/README.md b/__seed__/README.md index b9d622d3..161405d3 100644 --- a/__seed__/README.md +++ b/__seed__/README.md @@ -15,17 +15,19 @@ recentsData reviewsData /{courseId} - /{year}-{SemesterTerm}/data: + /{year}-{SemesterTerm} + /data: usersData /{userId}: ``` - -***N.B.*** The canonical path format for Firebase Firestore is `{collectionName}/{documentId}/{subCollectionName}/{subDocumentId}/...` and so on, with the path terminating at a (sub)document. +> [!NOTE] +> The canonical path format for Firebase Firestore is `{collectionName}/{documentId}/{subCollectionName}/{subDocumentId}/...` and so on, with the path terminating at a (sub)document. ## Seeding a Firebase project -***Note***: All paths indicated in this section are relative to the top-level directory (i.e., `website`). Furthermore, all commands (i.e., `yarn ...`) should also be issued from the top-level directory accordingly. +> [!NOTE] +> All paths indicated in this section are relative to the top-level directory (i.e., `website`). Furthermore, all commands (i.e., `yarn ...`) should also be issued from the top-level directory accordingly. ### Seeding or updating a cloud Firebase project @@ -35,7 +37,8 @@ To seed the data in a development cloud-based Firebase Firestore database, defin yarn fb:seed-db-cloud ``` -***NOTE***: Do **NOT** use this method in **production**!!! Production Firebase database must be updated manually via the Firebase UI/console; otherwise, this seeding approach will wipe all of the live data **without** ability to recover it! +> [!WARNING] +> Do **NOT** use this method in **production**!!! Production Firebase database must be updated manually via the Firebase UI/console; otherwise, this seeding approach will wipe all of the live data **without** ability to recover it! ### Seeding or updating local *Firebase Emulator Suite* @@ -78,7 +81,8 @@ This will update the corresponding files in directory `/__seed__/firebase-seed`. ## Adding a new course -***Note***: All paths indicated here are relative to the top-level directory (i.e., `website`). +> [!NOTE] +> All paths indicated here are relative to the top-level directory (i.e., `website`). To add a new course, update the following files: diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index 1f98d162..1cda99f3 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -1,171 +1,171 @@ -import { auth } from '@backend/FirebaseConfig'; -import { useAlert } from '@context/AlertContext'; -import { - FirebaseAuthUser, - TContextProviderProps, - TSignInAction, -} from '@context/types'; -import { TNullable } from '@globals/types'; -import { isGTEmail, isOutlookEmail } from '@globals/utilities'; -import { - FacebookAuthProvider, - fetchSignInMethodsForEmail, - GithubAuthProvider, - GoogleAuthProvider, - isSignInWithEmailLink, - OAuthProvider, - onAuthStateChanged, - sendSignInLinkToEmail, - signInWithEmailLink, - signInWithPopup, - signOut, -} from 'firebase/auth'; -import router from 'next/router'; -import { createContext, useContext, useEffect, useState } from 'react'; - -export type TAuthContext = { - user: TNullable; - loading: Boolean; - signInWithProvider: TSignInAction; - signWithMagic: TSignInAction; - logout: () => void; -}; - -// local storage key -const EMAIL_FOR_SIGN_IN = 'emailForSignIn'; - -const AuthContext = createContext>(null); - -export const useAuth = () => useContext(AuthContext); - -// eslint-disable-next-line no-undef -export const AuthContextProvider = ({ children }: TContextProviderProps) => { - const [user, setUser] = useState>(null); - const [loading, setLoading] = useState(true); - const { setAlert } = useAlert(); - - useEffect(() => { - setLoading(true); - const unsubscribe = onAuthStateChanged( - auth, - (user: TNullable) => { - if (user) { - setUser(user); - setLoading(false); - } else { - setUser(null); - setLoading(false); - } - }, - ); - // OAuth Providers - const email = window.localStorage.getItem(EMAIL_FOR_SIGN_IN); - - if (isSignInWithEmailLink(auth, window.location.href) && !!email) { - // Sign the user in - signInWithEmailLink(auth, email, window.location.href); - } - - // Remove all listeners from firebase when unmounting - return () => unsubscribe(); - }, []); - // Providers - const providerMap: any = { - Google: [GoogleAuthProvider, new GoogleAuthProvider()], - Apple: [OAuthProvider, new OAuthProvider('apple.com')], - Github: [GithubAuthProvider, new GithubAuthProvider()], - Facebook: [GithubAuthProvider, new FacebookAuthProvider()], - }; - const logout = async () => { - setUser(null); - window.localStorage.removeItem(EMAIL_FOR_SIGN_IN); - await signOut(auth); - router.push('/'); - }; - const signWithMagic = (email: string) => { - // If the user is re-entering their email address but already has a code - sendSignInLinkToEmail(auth, email, { - url: window.location.origin, - handleCodeInApp: true, - }).then(() => { - // Save the users email to verify it after they access their email - window.localStorage.setItem(EMAIL_FOR_SIGN_IN, email); - const additionalInstructions = - isGTEmail(email) || isOutlookEmail(email) - ? ' NOTE: gatech.edu or outlook.com domain may require release from Quarantine. See https://security.microsoft.com/quarantine' - : ''; - const text = `A Magic Link was sent to ${email}! Check your spam folder just in-case.${additionalInstructions}`; - setAlert({ - severity: 'success', - text, - variant: 'outlined', - }); - }); - }; - const signInWithProvider = (provider: string) => { - const [currentProvider, currentProviderAuth] = providerMap[provider]; - signInWithPopup(auth, currentProviderAuth) - .then((result) => { - // This gives you a Google Access Token. You can use it to access the Google API. - currentProvider.credentialFromResult(result); - // const token = credential.accessToken; - // The signed-in user info. - // const user = result.user; - // ... - }) - .catch((error) => { - // Handle Errors here. - const errorCode = error.code; - // const errorMessage = error.message; - const email = error.customData.email; - // The email of the user's account used. - switch (errorCode) { - case 'auth/account-exists-with-different-credential': { - fetchSignInMethodsForEmail(auth, email).then( - (providers: string[]) => { - const providersArray = providers.map((provider: string) => { - const lowerCaseName = provider.split('.')[0]; - const normalCaseName = - lowerCaseName.charAt(0).toUpperCase() + - lowerCaseName.slice(1).toLowerCase(); - if (normalCaseName === 'Emaillink') { - return 'Email'; - } - return normalCaseName; - }); - - setAlert({ - severity: 'error', - text: - `You already have an account${ - providersArray.length > 1 ? 's' : '' - } with the following sign-in method${ - providersArray.length > 1 ? 's' : '' - }: ${providersArray.join(', ')}.` + - '\n' + - `Please login with ${ - providersArray.length > 1 - ? 'one of those methods' - : 'that method' - } to link the new OAuth method for future use.`, - variant: 'outlined', - }); - }, - ); - } - } - // The credential that was used. - currentProvider.credentialFromError(error); - - // ... - }); - }; - - return ( - - {children} - - ); -}; +import { auth } from '@backend/FirebaseConfig'; +import { useAlert } from '@context/AlertContext'; +import { + FirebaseAuthUser, + TContextProviderProps, + TSignInAction, +} from '@context/types'; +import { TNullable } from '@globals/types'; +import { isGTEmail, isOutlookEmail } from '@globals/utilities'; +import { + FacebookAuthProvider, + fetchSignInMethodsForEmail, + GithubAuthProvider, + GoogleAuthProvider, + isSignInWithEmailLink, + OAuthProvider, + onAuthStateChanged, + sendSignInLinkToEmail, + signInWithEmailLink, + signInWithPopup, + signOut, +} from 'firebase/auth'; +import router from 'next/router'; +import { createContext, useContext, useEffect, useState } from 'react'; + +export type TAuthContext = { + user: TNullable; + loading: Boolean; + signInWithProvider: TSignInAction; + signWithMagic: TSignInAction; + logout: () => void; +}; + +// local storage key +const EMAIL_FOR_SIGN_IN = 'emailForSignIn'; + +const AuthContext = createContext>(null); + +export const useAuth = () => useContext(AuthContext); + +// eslint-disable-next-line no-undef +export const AuthContextProvider = ({ children }: TContextProviderProps) => { + const [user, setUser] = useState>(null); + const [loading, setLoading] = useState(true); + const { setAlert } = useAlert(); + + useEffect(() => { + setLoading(true); + const unsubscribe = onAuthStateChanged( + auth, + (user: TNullable) => { + if (user) { + setUser(user); + setLoading(false); + } else { + setUser(null); + setLoading(false); + } + }, + ); + // OAuth Providers + const email = window.localStorage.getItem(EMAIL_FOR_SIGN_IN); + + if (isSignInWithEmailLink(auth, window.location.href) && !!email) { + // Sign the user in + signInWithEmailLink(auth, email, window.location.href); + } + + // Remove all listeners from firebase when unmounting + return () => unsubscribe(); + }, []); + // Providers + const providerMap: any = { + Google: [GoogleAuthProvider, new GoogleAuthProvider()], + Apple: [OAuthProvider, new OAuthProvider('apple.com')], + Github: [GithubAuthProvider, new GithubAuthProvider()], + Facebook: [GithubAuthProvider, new FacebookAuthProvider()], + }; + const logout = async () => { + setUser(null); + window.localStorage.removeItem(EMAIL_FOR_SIGN_IN); + await signOut(auth); + router.push('/'); + }; + const signWithMagic = (email: string) => { + // If the user is re-entering their email address but already has a code + sendSignInLinkToEmail(auth, email, { + url: window.location.origin, + handleCodeInApp: true, + }).then(() => { + // Save the users email to verify it after they access their email + window.localStorage.setItem(EMAIL_FOR_SIGN_IN, email); + const additionalInstructions = + isGTEmail(email) || isOutlookEmail(email) + ? ' NOTE: gatech.edu or outlook.com domain may require release from Quarantine. See https://security.microsoft.com/quarantine' + : ''; + const text = `A Magic Link was sent to ${email}! Check your spam folder just in-case.${additionalInstructions}`; + setAlert({ + severity: 'success', + text, + variant: 'outlined', + }); + }); + }; + const signInWithProvider = (provider: string) => { + const [currentProvider, currentProviderAuth] = providerMap[provider]; + signInWithPopup(auth, currentProviderAuth) + .then((result) => { + // This gives you a Google Access Token. You can use it to access the Google API. + currentProvider.credentialFromResult(result); + // const token = credential.accessToken; + // The signed-in user info. + // const user = result.user; + // ... + }) + .catch((error) => { + // Handle Errors here. + const errorCode = error.code; + // const errorMessage = error.message; + const email = error.customData.email; + // The email of the user's account used. + switch (errorCode) { + case 'auth/account-exists-with-different-credential': { + fetchSignInMethodsForEmail(auth, email).then( + (providers: string[]) => { + const providersArray = providers.map((provider: string) => { + const lowerCaseName = provider.split('.')[0]; + const normalCaseName = + lowerCaseName.charAt(0).toUpperCase() + + lowerCaseName.slice(1).toLowerCase(); + if (normalCaseName === 'Emaillink') { + return 'Email'; + } + return normalCaseName; + }); + + setAlert({ + severity: 'error', + text: + `You already have an account${ + providersArray.length > 1 ? 's' : '' + } with the following sign-in method${ + providersArray.length > 1 ? 's' : '' + }: ${providersArray.join(', ')}.` + + '\n' + + `Please login with ${ + providersArray.length > 1 + ? 'one of those methods' + : 'that method' + } to link the new OAuth method for future use.`, + variant: 'outlined', + }); + }, + ); + } + } + // The credential that was used. + currentProvider.credentialFromError(error); + + // ... + }); + }; + + return ( + + {children} + + ); +}; diff --git a/firebase.json b/firebase.json index 73eacf5c..e14ac7fb 100755 --- a/firebase.json +++ b/firebase.json @@ -35,5 +35,18 @@ }, "storage": { "rules": "storage.rules" - } + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ] + } + ] } diff --git a/firebase/README.md b/firebase/README.md index e8dad554..dc3dfd07 100755 --- a/firebase/README.md +++ b/firebase/README.md @@ -2,9 +2,11 @@ ## Preliminaries -***N.B.*** All file-path references assume a starting point of the root directory of the project unless noted otherwise. +> [!NOTE] +> All file-path references assume a starting point of the root directory of the project unless noted otherwise. -***N.B.*** Requires Node v. 16+. +> [!NOTE] +> Requires Node v. 20+. ## Overview @@ -46,7 +48,8 @@ The data documents defined in this app, and their corresponding CRUD operations' | `Review` | `getReviews(courseId, year, semesterTerm)` | `getReview(reviewId)` | `addReview(userId, reviewId, data)` | `updateReview(userId, reviewId, data)` | `deleteReview(userId, reviewId)` | | `User` | (N/A) | `getUser(id)` | `addUser(id)` | `updateUser(id)` | `deleteUser(id)` | -***N.B.*** See `/globals/types.ts` for definition of document data fields (i.e., argument `data` per above). +> [!NOTE] +> See `/globals/types.ts` for definition of document data fields (i.e., argument `data` per above). Example usage via `courses` document (and similarly for the others): @@ -95,4 +98,5 @@ await updateCourse(courseId, updatedCourseData) await deleteCourse(courseId) ``` -***N.B.*** All non-"`GET`" operations require authorization/authentication via Firebase Authentication and corresponding permissions. +> [!NOTE] +> All non-"`GET`" operations require authorization/authentication via Firebase Authentication and corresponding permissions. diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 00000000..21ee8d3d --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.local \ No newline at end of file diff --git a/functions/README.md b/functions/README.md new file mode 100644 index 00000000..16679dce --- /dev/null +++ b/functions/README.md @@ -0,0 +1,44 @@ +# Firebase Cloud Functions + +This sub-repo contains the definitions for the Firebase Cloud Functions, intended primarily to read-dump the data into JSON output. + +## Deploying the Cloud Functions + +### Cloud Firebase Project + +> [!WARNING] +> Deploying Cloud Functions requires a ***paid plan*** Firebase project, which must be preconfigured as such accordingly prior to deployment. + +To deploy the cloud functions as defined in `/website/functions/index.js` to a cloud-based Firebase project, ensure that the terminal is in location `.../website/functions/` and then perform the following steps via terminal: + +1) Log into Firebase account. +```bash +firebase login +``` +2) Identify the target `` for the cloud-based Firebase project in question. +```bash +firebase projects:list +``` +3) Set the Firebase CLI to the target project. +```bash +firebase use +``` +4) Deploy the Cloud Functions to the target project. +```bash +yarn deploy +``` + +## Local Firebase Emulators + +If running local Firebase Emulators, the functions will be detected accordingly. + +## Using the Cloud Functions + +Once deployed (or running locally), simply make HTTP GET requests to the corresponding URLs as provided. + +The following functions are defined: + +| GET Route | Query Params | Result | +|:--:|:--:|:--:| +| `/getReviewsFlat` | `isLegacy` | Returns an array of objects containing the reviews in ascending order by created time. Param `?isLegacy=true` returns the legacy reviews, otherwise omission of this param returns only the reviews collected since inception of OMSHub (i.e., post-legacy). | +| `/getUsers` | (N/A) | Returns an array of objects containing the users. | diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 00000000..9a1656b2 --- /dev/null +++ b/functions/index.js @@ -0,0 +1,149 @@ +/** + * Import function triggers from their respective submodules: + * + * const {onCall} = require("firebase-functions/v2/https"); + * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ + +const logger = require('firebase-functions/logger'); +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); + +// Create and deploy your first functions +// https://firebase.google.com/docs/functions/get-started + +admin.initializeApp(); + +// constants +const httpMethods = { + GET: 'GET', +}; +const httpStatuses = { + OK: 200, + NOT_ALLOWED: 405, + SERVER_ERROR: 500, +}; +const collections = { + reviews: '_reviewsDataFlat', + users: 'usersData', +}; +const fields = { + // reviews + isLegacy: 'isLegacy', + created: 'created', + // users + userId: 'userId', +}; +const sortOrder = { + ASC: 'asc', + DESC: 'desc', +}; + +// methods/routes +exports.getReviewsFlat = functions.https.onRequest(async (req, res) => { + logger.info('Requesting reviews'); + + if (req.method !== httpMethods.GET) { + logger.warn(`Method '${req.method}' not allowed`); + return res.status(httpStatuses.NOT_ALLOWED).send('Method Not Allowed'); + } + + // default: send non-legacy reviews + let isLegacy = false; + + // parse legacy vs. non-legacy from query params + if (req.query?.isLegacy) { + isLegacy = req.query.isLegacy.toLowerCase() === 'true'; + } + + try { + const db = admin.firestore(); + const querySnapshot = await db + .collection(collections.reviews) + .where(fields.isLegacy, '==', isLegacy) + .orderBy(fields.created, sortOrder.ASC) + .get(); + + const reviews = []; + querySnapshot.forEach((doc) => { + const { + reviewId, + courseId, + year, + semesterId, + isLegacy, + reviewerId, + isGTVerifiedReviewer, + created, + modified, + workload, + difficulty, + overall, + upvotes, + downvotes, + body, + } = doc.data(); + + reviews.push({ + reviewId, + courseId, + year, + semesterId, + isLegacy, + reviewerId, + isGTVerifiedReviewer, + created, + modified, + workload, + difficulty, + overall, + upvotes, + downvotes, + body, + }); + }); + + logger.info(`Sending ${isLegacy ? 'legacy' : 'non-legacy'} reviews`); + res.status(httpStatuses.OK).json(reviews); + } catch (error) { + console.error('Error getting reviews:', error); + logger.error('Error getting reviews:', error); + res.status(httpStatuses.SERVER_ERROR).send('Internal Server Error'); + } +}); + +exports.getUsers = functions.https.onRequest(async (req, res) => { + logger.info('Requesting users'); + + if (req.method !== httpMethods.GET) { + logger.warn(`Method '${req.method}' not allowed`); + return res.status(httpStatuses.NOT_ALLOWED).send('Method Not Allowed'); + } + + try { + const db = admin.firestore(); + const querySnapshot = await db + .collection(collections.users) + .orderBy(fields.userId, sortOrder.ASC) + .get(); + + const users = []; + querySnapshot.forEach((doc) => { + const { userId, hasGTEmail } = doc.data(); + + users.push({ + userId, + hasGTEmail, + }); + }); + + logger.info(`Sending users`); + res.status(httpStatuses.OK).json(users); + } catch (error) { + console.error('Error getting users:', error); + logger.error('Error getting users:', error); + res.status(httpStatuses.SERVER_ERROR).send('Internal Server Error'); + } +}); diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 00000000..d2fa884f --- /dev/null +++ b/functions/package.json @@ -0,0 +1,23 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "yarn shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "20" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0" + }, + "private": true +} diff --git a/functions/yarn.lock b/functions/yarn.lock new file mode 100644 index 00000000..8db859da --- /dev/null +++ b/functions/yarn.lock @@ -0,0 +1,1563 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@fastify/busboy@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-3.0.0.tgz#328a4639cdd9282c1d1f56aa84943f153df8839d" + integrity sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w== + +"@firebase/app-check-interop-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" + integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== + +"@firebase/app-types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" + integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== + +"@firebase/auth-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" + integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== + +"@firebase/component@0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.8.tgz#899b9318c0ce0586580e8cda7eaf61296f7fb43b" + integrity sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g== + dependencies: + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/database-compat@^1.0.2": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.7.tgz#5c761bea1a78daea76fecc4bf5de5d6915a1c3b4" + integrity sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/database" "1.0.7" + "@firebase/database-types" "1.0.4" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/database-types@1.0.4", "@firebase/database-types@^1.0.0": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.4.tgz#dc507f7838ed29ac3235c68ebae5fd42a562e3e8" + integrity sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ== + dependencies: + "@firebase/app-types" "0.9.2" + "@firebase/util" "1.9.7" + +"@firebase/database@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.7.tgz#0794801ab1e63336eda69401131228bb85aa6776" + integrity sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/logger@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" + integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== + dependencies: + tslib "^2.1.0" + +"@firebase/util@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.7.tgz#c03b0ae065b3bba22800da0bd5314ef030848038" + integrity sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA== + dependencies: + tslib "^2.1.0" + +"@google-cloud/firestore@^7.7.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-7.9.0.tgz#fcacef51c07e39c083bad3a4801814a7a60a878a" + integrity sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^4.3.3" + protobufjs "^7.2.6" + +"@google-cloud/paginator@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" + integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== + +"@google-cloud/promisify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" + integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== + +"@google-cloud/storage@^7.7.0": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.12.1.tgz#7963bdc3b5266e4698047f6158a043d53231ec9c" + integrity sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ== + dependencies: + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" + abort-controller "^3.0.0" + async-retry "^1.3.3" + duplexify "^4.1.3" + fast-xml-parser "^4.4.1" + gaxios "^6.0.2" + google-auth-library "^9.6.3" + html-entities "^2.5.2" + mime "^3.0.0" + p-limit "^3.0.1" + retry-request "^7.0.0" + teeny-request "^9.0.0" + uuid "^8.0.0" + +"@grpc/grpc-js@^1.10.9": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.1.tgz#a92f33e98f1959feffcd1b25a33b113d2c977b70" + integrity sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw== + dependencies: + "@grpc/proto-loader" "^0.7.13" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.7.13": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.5": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@4.17.3": + version "4.17.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.3.tgz#38e4458ce2067873b09a73908df488870c303bd9" + integrity sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.17": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/jsonwebtoken@^9.0.2": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + +"@types/lodash@^4.14.104": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== + +"@types/long@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.0.1": + version "22.4.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.2.tgz#55fefb1c3dba2ecd7eb76738c6b80da75760523f" + integrity sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw== + dependencies: + undici-types "~6.19.2" + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/request@^2.48.8": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.3.4: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +duplexify@^4.0.0, duplexify@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +express@^4.17.1: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +farmhash-modern@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/farmhash-modern/-/farmhash-modern-1.1.0.tgz#c36b34ad196290d57b0b482dc89e637d0b59835f" + integrity sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-xml-parser@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== + dependencies: + strnum "^1.0.5" + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +firebase-admin@^12.1.0: + version "12.3.1" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-12.3.1.tgz#425ca09e439003043ef7ddbae28d6af214025543" + integrity sha512-vEr3s3esl8nPIA9r/feDT4nzIXCfov1CyyCSpMQWp6x63Q104qke0MEGZlrHUZVROtl8FLus6niP/M9I1s4VBA== + dependencies: + "@fastify/busboy" "^3.0.0" + "@firebase/database-compat" "^1.0.2" + "@firebase/database-types" "^1.0.0" + "@types/node" "^22.0.1" + farmhash-modern "^1.1.0" + jsonwebtoken "^9.0.0" + jwks-rsa "^3.1.0" + node-forge "^1.3.1" + uuid "^10.0.0" + optionalDependencies: + "@google-cloud/firestore" "^7.7.0" + "@google-cloud/storage" "^7.7.0" + +firebase-functions-test@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/firebase-functions-test/-/firebase-functions-test-3.3.0.tgz#63566f0ea71d9fd0651c756b29a8b50ec5f408d8" + integrity sha512-X+OOA34MGrsTimFXTDnWT0psAqnmBkJ85bGCoLMwjgei5Prfkqh3bv5QASnXC/cmIVBSF2Qw9uW1+mF/t3kFlw== + dependencies: + "@types/lodash" "^4.14.104" + lodash "^4.17.5" + ts-deepmerge "^2.0.1" + +firebase-functions@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-5.1.0.tgz#9fc3f7596736df403c88afb97acd3dc7b5e49933" + integrity sha512-VO46n9lqljrNiqOv4BbnFHYxY+yYCdZcOeUIF1t9DbFxBbVPztHdMM9MvpfCDp0nzXP2PugdmghSgM0hORrNvw== + dependencies: + "@types/cors" "^2.8.5" + "@types/express" "4.17.3" + cors "^2.8.5" + express "^4.17.1" + protobufjs "^7.2.2" + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + +gcp-metadata@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" + integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg== + dependencies: + gaxios "^6.0.0" + json-bigint "^1.0.0" + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +google-auth-library@^9.3.0, google-auth-library@^9.6.3: + version "9.14.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.14.0.tgz#79e568b5cf3493a8470968a5d099eabe465cb950" + integrity sha512-Y/eq+RWVs55Io/anIsm24sDS8X79Tq948zVLGaa7+KlJYYqaGwp1YI37w48nzrNi12RgnzMrQD4NzdmCowT90g== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + +google-gax@^4.3.3: + version "4.3.9" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.3.9.tgz#44e2920d13e1e12934da1c74f3e9bb8ac7e4f197" + integrity sha512-tcjQr7sXVGMdlvcG25wSv98ap1dtF4Z6mcV0rztGIddOcezw4YMb/uTXg72JPrLep+kXcVjaJjg6oo3KLf4itQ== + dependencies: + "@grpc/grpc-js" "^1.10.9" + "@grpc/proto-loader" "^0.7.13" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + google-auth-library "^9.3.0" + node-fetch "^2.7.0" + object-hash "^3.0.0" + proto3-json-serializer "^2.0.2" + protobufjs "^7.3.2" + retry-request "^7.0.0" + uuid "^9.0.1" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +html-entities@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +jose@^4.14.6: + version "4.15.9" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" + integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== + +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jwks-rsa@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.1.0.tgz#50406f23e38c9b2682cd437f824d7d61aa983171" + integrity sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg== + dependencies: + "@types/express" "^4.17.17" + "@types/jsonwebtoken" "^9.0.2" + debug "^4.3.4" + jose "^4.14.6" + limiter "^1.1.5" + lru-memoizer "^2.2.0" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.17.5: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + +lru-cache@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru-memoizer@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.3.0.tgz#ef0fbc021bceb666794b145eefac6be49dc47f31" + integrity sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "6.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-fetch@^2.6.9, node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proto3-json-serializer@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz#5b705203b4d58f3880596c95fad64902617529dd" + integrity sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ== + dependencies: + protobufjs "^7.2.5" + +protobufjs@^7.2.2, protobufjs@^7.2.5, protobufjs@^7.2.6, protobufjs@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" + integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^3.1.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +retry-request@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" + integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w== + dependencies: + "@types/request" "^2.48.8" + extend "^3.0.2" + teeny-request "^9.0.0" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + +teeny-request@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-9.0.0.tgz#18140de2eb6595771b1b02203312dfad79a4716d" + integrity sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.9" + stream-events "^1.0.5" + uuid "^9.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-deepmerge@^2.0.1: + version "2.0.7" + resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz#36786a9a10b5f3a6f5154007cf17bfba7251e0a7" + integrity sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg== + +tslib@^2.1.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + +uuid@^8.0.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0, uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/package.json b/package.json index 1ca38be3..a04e1205 100644 --- a/package.json +++ b/package.json @@ -1,110 +1,110 @@ -{ - "name": "nextjs-with-typescript", - "version": "1.0.0", - "private": true, - "engines": { - "node": ">=20.0", - "yarn": ">=1.22.0", - "npm": "please-use-yarn" - }, - "scripts": { - "dev": "cross-env NODE_OPTIONS='--inspect' next dev", - "build": "next build", - "build-sitemap": "next-sitemap", - "start": "next start", - "test": "jest --watch", - "test:ci": "jest --ci", - "lint": "next lint", - "prepare": "husky install", - "precommit": "lint-staged", - "prettier": "prettier --write .", - "fmt": "yarn prettier && yarn lint", - "post-update": "echo \"codesandbox preview only, need an update\" && yarn upgrade --latest", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "fb:seed-db": "node ./__seed__/script.js", - "fb:seed-db-cloud": "node ./__seed__/script.js SEED_FIREBASE_FIRESTORE_IN_CLOUD", - "fb:login": "firebase login", - "fb:emu": "firebase emulators:start --import=./__seed__/firebase-seed --project website", - "fb:exp-seed": "firebase emulators:export ./__seed__/firebase-seed" - }, - "dependencies": { - "@babel/core": "^7.21.0", - "@emotion/cache": "^11.10.5", - "@emotion/react": "^11.11.1", - "@emotion/server": "^11.4.0", - "@emotion/styled": "^11.11.0", - "@fontsource/roboto": "^5.0.14", - "@mui/icons-material": "^5.11.11", - "@mui/material": "^5.14.5", - "@mui/x-data-grid": "^7.13.0", - "@supabase/supabase-js": "^2.45.2", - "@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0", - "@toast-ui/react-editor": "^3.2.2", - "@types/prismjs": "^1.26.4", - "firebase": "^10.13.0", - "html-to-image": "^1.11.11", - "isomorphic-dompurify": "^1.8.0", - "next": "^14.2.3", - "prismjs": "^1.29.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.45.2", - "react-markdown": "^9.0.1", - "rehype-katex": "^7.0.1", - "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.0", - "remark-math": "^6.0.0", - "string-width": "^6.1.0", - "swr": "^2.2.4", - "tsconfig-paths-webpack-plugin": "^4.0.0" - }, - "devDependencies": { - "@commitlint/cli": "^19.4.0", - "@commitlint/config-conventional": "^17.4.2", - "@next/bundle-analyzer": "^14.2.5", - "@storybook/addon-actions": "^8.1.11", - "@storybook/addon-essentials": "^7.0.18", - "@storybook/addon-interactions": "^7.0.18", - "@storybook/addon-links": "^8.1.11", - "@storybook/blocks": "^7.0.18", - "@storybook/nextjs": "^7.0.18", - "@storybook/react": "^7.0.18", - "@storybook/testing-library": "^0.2.2", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^15.0.7", - "@types/node": "^20.8.4", - "@types/react": "^18.0.26", - "babel-loader": "^9.1.2", - "babel-plugin-import": "^1.13.8", - "commitizen": "^4.3.0", - "cross-env": "^7.0.3", - "cz-conventional-changelog": "3.3.0", - "eslint": "8.50.0", - "eslint-config-next": "^14.2.3", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-storybook": "^0.6.11", - "husky": "^9.1.5", - "jest": "^29.4.3", - "jest-environment-jsdom": "^29.3.1", - "jest-fetch-mock": "^3.0.3", - "lint-staged": "^13.1.0", - "next-sitemap": "^4.1.8", - "prettier": "^3.0.3", - "storybook": "^7.1.1", - "typescript": "^5.1.3" - }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } - }, - "lint-staged": { - "{ts,tsx}": [ - "next lint" - ], - "*": [ - "prettier --write" - ] - } -} +{ + "name": "nextjs-with-typescript", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=20.0", + "yarn": ">=1.22.0", + "npm": "please-use-yarn" + }, + "scripts": { + "dev": "cross-env NODE_OPTIONS='--inspect' next dev", + "build": "next build", + "build-sitemap": "next-sitemap", + "start": "next start", + "test": "jest --watch", + "test:ci": "jest --ci", + "lint": "next lint", + "prepare": "husky install", + "precommit": "lint-staged", + "prettier": "prettier --write .", + "fmt": "yarn prettier && yarn lint", + "post-update": "echo \"codesandbox preview only, need an update\" && yarn upgrade --latest", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "fb:seed-db": "node ./__seed__/script.js", + "fb:seed-db-cloud": "node ./__seed__/script.js SEED_FIREBASE_FIRESTORE_IN_CLOUD", + "fb:login": "firebase login", + "fb:emu": "firebase emulators:start --import=./__seed__/firebase-seed --project website", + "fb:exp-seed": "firebase emulators:export ./__seed__/firebase-seed" + }, + "dependencies": { + "@babel/core": "^7.21.0", + "@emotion/cache": "^11.10.5", + "@emotion/react": "^11.11.1", + "@emotion/server": "^11.4.0", + "@emotion/styled": "^11.11.0", + "@fontsource/roboto": "^5.0.14", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.14.5", + "@mui/x-data-grid": "^7.13.0", + "@supabase/supabase-js": "^2.45.2", + "@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0", + "@toast-ui/react-editor": "^3.2.2", + "@types/prismjs": "^1.26.4", + "firebase": "^10.13.0", + "html-to-image": "^1.11.11", + "isomorphic-dompurify": "^1.8.0", + "next": "^14.2.3", + "prismjs": "^1.29.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.45.2", + "react-markdown": "^9.0.1", + "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", + "string-width": "^6.1.0", + "swr": "^2.2.4", + "tsconfig-paths-webpack-plugin": "^4.0.0" + }, + "devDependencies": { + "@commitlint/cli": "^19.4.0", + "@commitlint/config-conventional": "^17.4.2", + "@next/bundle-analyzer": "^14.2.5", + "@storybook/addon-actions": "^8.1.11", + "@storybook/addon-essentials": "^7.0.18", + "@storybook/addon-interactions": "^7.0.18", + "@storybook/addon-links": "^8.1.11", + "@storybook/blocks": "^7.0.18", + "@storybook/nextjs": "^7.0.18", + "@storybook/react": "^7.0.18", + "@storybook/testing-library": "^0.2.2", + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^15.0.7", + "@types/node": "^20.8.4", + "@types/react": "^18.0.26", + "babel-loader": "^9.1.2", + "babel-plugin-import": "^1.13.8", + "commitizen": "^4.3.0", + "cross-env": "^7.0.3", + "cz-conventional-changelog": "3.3.0", + "eslint": "8.50.0", + "eslint-config-next": "^14.2.3", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-storybook": "^0.6.11", + "husky": "^9.1.5", + "jest": "^29.4.3", + "jest-environment-jsdom": "^29.3.1", + "jest-fetch-mock": "^3.0.3", + "lint-staged": "^13.1.0", + "next-sitemap": "^4.1.8", + "prettier": "^3.0.3", + "storybook": "^7.1.1", + "typescript": "^5.1.3" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "lint-staged": { + "{ts,tsx}": [ + "next lint" + ], + "*": [ + "prettier --write" + ] + } +} diff --git a/pages/course/[courseid].tsx b/pages/course/[courseid].tsx index 7aa7dcbc..21b96ba0 100755 --- a/pages/course/[courseid].tsx +++ b/pages/course/[courseid].tsx @@ -1,525 +1,525 @@ -import backend from '@backend/index'; -import ReviewCard from '@components/ReviewCard'; -import ReviewForm from '@components/ReviewForm'; -import { useAuth } from '@context/AuthContext'; -import { FirebaseAuthUser } from '@context/types'; -import { DESC, EMOJI_NO_REVIEWS, REVIEW_ID } from '@globals/constants'; -import { - Course, - Review, - TCourseId, - TPayloadReviews, - TNullable, -} from '@globals/types'; -import { mapDynamicCoursesDataToCourses } from '@globals/utilities'; -import FileCopyIcon from '@mui/icons-material/FileCopyOutlined'; -import LinkIcon from '@mui/icons-material/Link'; -import RateReviewIcon from '@mui/icons-material/RateReview'; -import ShareIcon from '@mui/icons-material/Share'; -import { - useMediaQuery, - Box, - Button, - Card, - CardContent, - CircularProgress, - Container, - Dialog, - Grid, - Snackbar, - SpeedDial, - SpeedDialAction, - SpeedDialIcon, - ToggleButton, - ToggleButtonGroup, - Typography, - useTheme, -} from '@mui/material'; -import Link from '@src/Link'; -import { - mapPayloadToArray, - mapRatingToColor, - mapRatingToColorInverted, - mapSemesterTermToEmoji, - mapSemesterTermToName, - roundNumber, -} from '@src/utilities'; -import type { NextPage } from 'next'; -import React, { useEffect, useState } from 'react'; -import useSWR, { useSWRConfig } from 'swr'; - -type TActiveSemesters = { - [semesterTerm: number]: boolean; -}; - -interface CoursePageProps { - courseData: Course; - courseTimeline: number[]; - courseYears: number[]; - defaultYear: number; - defaultSemester: string; - defaultSemesterToggles: boolean[]; - defaultReviews: TPayloadReviews; - numberOfReviews: number; -} - -const { getCourses, getReviews } = backend; - -const CourseId: NextPage = ({ - courseData, - courseTimeline, - defaultYear, - courseYears, - defaultSemester, - defaultSemesterToggles, - defaultReviews, -}) => { - const { - courseId: courseId, - name: courseName, - numReviews: courseNumReviews, - url: courseUrl, - avgWorkload: courseAvgWorkload, - avgDifficulty: courseAvgDifficulty, - avgOverall: courseAvgOverall, - } = courseData; - const [loading, setLoading] = useState(false); - const [snackBarOpen, setSnackBarOpen] = useState(false); - const [snackBarMessage, setSnackBarMessage] = useState(''); - const [reviewModalOpen, setReviewModalOpen] = useState(false); - const handleReviewModalOpen = () => setReviewModalOpen(true); - const handleReviewModalClose = () => setReviewModalOpen(false); - - const authContext: TNullable = useAuth(); - const user: TNullable = authContext.user; - - const theme = useTheme(); - - const [activeSemesters, setActiveSemesters] = useState( - defaultSemesterToggles, - ); - const [selectedSemester, setSelectedSemester] = - useState(defaultSemester); - const [selectedYear, setSelectedYear] = useState(defaultYear); - const [courseReviews, setCourseReviews] = - useState(defaultReviews); - const orientation = useMediaQuery('(min-width:600px)'); - - const { mutate } = useSWRConfig(); - const { data: course_reviews } = useSWR( - `/course/${courseId}/${selectedYear}/${selectedSemester}`, - ); - - const actions = [ - { - icon: , - enabled: true, - name: 'Share Course URL', - clickAction: () => { - navigator.clipboard.writeText(window.location.href); - setSnackBarMessage('Copied Course URL to Clipboard'); - setSnackBarOpen(true); - }, - }, - { - icon: , - enabled: true, - name: 'Copy Course Name', - clickAction: () => { - navigator.clipboard.writeText(`${courseId}: ${courseName}`); - setSnackBarMessage('Copied Course Name to Clipboard'); - setSnackBarOpen(true); - }, - }, - { - icon: , - enabled: user ? true : false, - name: 'Add Review', - clickAction: user - ? () => { - handleReviewModalOpen(); - } - : () => {}, - }, - ]; - - const handleSemester = ( - event: React.MouseEvent, - newSemester: string, - ) => { - setSelectedSemester(newSemester); - }; - - const handleYear = ( - event: React.MouseEvent, - newYear: number, - ) => { - setSelectedYear(newYear); - }; - const handleClose = ( - event: React.SyntheticEvent | Event, - reason?: string, - ) => { - if (reason === 'clickaway') { - return; - } - - setSnackBarOpen(false); - }; - useEffect(() => { - if (course_reviews) { - setCourseReviews(course_reviews); - } - }, [course_reviews]); - useEffect(() => { - if (selectedYear && selectedSemester) { - setLoading(true); - const newAvailableSemesters: any = Object.keys( - courseTimeline[selectedYear], - ); - const newActiveSemesters: any = Object.keys(mapSemesterTermToName).reduce( - (attrs, key) => ({ - ...attrs, - [key]: !(newAvailableSemesters.indexOf(key.toString()) > -1), - }), - {}, - ); - if (newActiveSemesters[selectedSemester]) { - setSelectedSemester( - newAvailableSemesters[newAvailableSemesters.length - 1], - ); - } - setActiveSemesters(newActiveSemesters); - } - mutate( - selectedYear && selectedSemester - ? `/course/${courseId}/${selectedYear}/${selectedSemester}` - : null, - () => { - return getReviews( - courseId, - String(selectedYear), - String(selectedSemester), - ); - }, - ); - setLoading(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedYear, selectedSemester]); - return ( - - - - {courseName} - - {courseUrl && ( - - - - - {'Course Website'} - - - - )} - {courseAvgWorkload && courseAvgDifficulty && courseAvgOverall && ( - - - - - - {`Average Workload`} - - - {roundNumber(Number(courseAvgWorkload), 1) + ' hrs/wk'} - - - - - - - - - {`Average Difficulty`} - - - {roundNumber(Number(courseAvgDifficulty), 1) + ' /5'} - - - - - - - - - {`Average Overall`} - - - {roundNumber(Number(courseAvgOverall), 1) + ' /5'} - - - - - - )} - - - {activeSemesters && - Object.entries(activeSemesters).map( - ([key, value]: [string, boolean], index: number) => ( - - - {mapSemesterTermToName[Number(key)]}{' '} - {mapSemesterTermToEmoji[Number(key)]} - - - ), - )} - - - {courseYears && - courseYears.map((year: number, index: number) => { - return ( - - {year} - - ); - })} - - - {loading ? ( - - - - ) : ( - <> - {courseNumReviews ? ( - <> - {courseReviews && ( - - {mapPayloadToArray(courseReviews, REVIEW_ID, DESC).map( - (value: Review) => ( - - - - ), - )} - - )} - - ) : ( - <> - - {`Aww shucks no reviews ${EMOJI_NO_REVIEWS}`} - - - )} - - )} - - - - - } - FabProps={{ - sx: { - border: `1px solid ${theme.palette.secondary.contrastText}`, - backgroundColor: `${theme.palette.secondary.main}`, - color: `${theme.palette.secondary.contrastText}`, - '&:hover': { - backgroundColor: `${theme.palette.secondary.contrastText}`, - color: `${theme.palette.secondary.main}`, - }, - }, - }} - > - {actions - .flatMap((action) => { - if (!action.enabled) { - return []; - } - return action; - }) - .map((action) => ( - - ))} - - - Close - - } - message={snackBarMessage} - /> - - ); -}; - -export default CourseId; - -interface PageProps { - query: { courseid: string }; -} - -export async function getServerSideProps(context: PageProps) { - const { courseid } = context.query; - const courseId = courseid as TCourseId; - const allCourseDataDynamic = await getCourses(); - const allCourseData = mapDynamicCoursesDataToCourses(allCourseDataDynamic); - const currentCourseData = allCourseData[courseId]; - if (currentCourseData.numReviews) { - const courseTimeline = currentCourseData.reviewsCountsByYearSem; - const courseYears = Object.keys(courseTimeline) - .map((year) => Number(year)) - .reverse(); - const mostRecentYear = courseYears[0]; - const mostRecentYearSemesters = Object.keys(courseTimeline[mostRecentYear]); - const mostRecentSemester = - mostRecentYearSemesters[mostRecentYearSemesters.length - 1]; - const availableSemesters = Object.keys(courseTimeline[mostRecentYear]); - const activeSemesters = Object.keys(mapSemesterTermToName).reduce( - (attrs, key) => ({ - ...attrs, - [key]: !(availableSemesters.indexOf(key.toString()) > -1), - }), - {}, - ); - const courseReviews = await getReviews( - courseId, - String(mostRecentYear), - String(mostRecentSemester), - ); - return { - props: { - courseData: currentCourseData, - courseTimeline: courseTimeline, - courseYears: courseYears, - defaultYear: mostRecentYear, - defaultSemester: mostRecentSemester, - defaultSemesterToggles: activeSemesters, - defaultReviews: courseReviews, - numReviews: currentCourseData.numReviews, - }, - }; - } else { - return { - props: { - courseData: currentCourseData, - }, - }; - } -} +import backend from '@backend/index'; +import ReviewCard from '@components/ReviewCard'; +import ReviewForm from '@components/ReviewForm'; +import { useAuth } from '@context/AuthContext'; +import { FirebaseAuthUser } from '@context/types'; +import { DESC, EMOJI_NO_REVIEWS, REVIEW_ID } from '@globals/constants'; +import { + Course, + Review, + TCourseId, + TPayloadReviews, + TNullable, +} from '@globals/types'; +import { mapDynamicCoursesDataToCourses } from '@globals/utilities'; +import FileCopyIcon from '@mui/icons-material/FileCopyOutlined'; +import LinkIcon from '@mui/icons-material/Link'; +import RateReviewIcon from '@mui/icons-material/RateReview'; +import ShareIcon from '@mui/icons-material/Share'; +import { + useMediaQuery, + Box, + Button, + Card, + CardContent, + CircularProgress, + Container, + Dialog, + Grid, + Snackbar, + SpeedDial, + SpeedDialAction, + SpeedDialIcon, + ToggleButton, + ToggleButtonGroup, + Typography, + useTheme, +} from '@mui/material'; +import Link from '@src/Link'; +import { + mapPayloadToArray, + mapRatingToColor, + mapRatingToColorInverted, + mapSemesterTermToEmoji, + mapSemesterTermToName, + roundNumber, +} from '@src/utilities'; +import type { NextPage } from 'next'; +import React, { useEffect, useState } from 'react'; +import useSWR, { useSWRConfig } from 'swr'; + +type TActiveSemesters = { + [semesterTerm: number]: boolean; +}; + +interface CoursePageProps { + courseData: Course; + courseTimeline: number[]; + courseYears: number[]; + defaultYear: number; + defaultSemester: string; + defaultSemesterToggles: boolean[]; + defaultReviews: TPayloadReviews; + numberOfReviews: number; +} + +const { getCourses, getReviews } = backend; + +const CourseId: NextPage = ({ + courseData, + courseTimeline, + defaultYear, + courseYears, + defaultSemester, + defaultSemesterToggles, + defaultReviews, +}) => { + const { + courseId: courseId, + name: courseName, + numReviews: courseNumReviews, + url: courseUrl, + avgWorkload: courseAvgWorkload, + avgDifficulty: courseAvgDifficulty, + avgOverall: courseAvgOverall, + } = courseData; + const [loading, setLoading] = useState(false); + const [snackBarOpen, setSnackBarOpen] = useState(false); + const [snackBarMessage, setSnackBarMessage] = useState(''); + const [reviewModalOpen, setReviewModalOpen] = useState(false); + const handleReviewModalOpen = () => setReviewModalOpen(true); + const handleReviewModalClose = () => setReviewModalOpen(false); + + const authContext: TNullable = useAuth(); + const user: TNullable = authContext.user; + + const theme = useTheme(); + + const [activeSemesters, setActiveSemesters] = useState( + defaultSemesterToggles, + ); + const [selectedSemester, setSelectedSemester] = + useState(defaultSemester); + const [selectedYear, setSelectedYear] = useState(defaultYear); + const [courseReviews, setCourseReviews] = + useState(defaultReviews); + const orientation = useMediaQuery('(min-width:600px)'); + + const { mutate } = useSWRConfig(); + const { data: course_reviews } = useSWR( + `/course/${courseId}/${selectedYear}/${selectedSemester}`, + ); + + const actions = [ + { + icon: , + enabled: true, + name: 'Share Course URL', + clickAction: () => { + navigator.clipboard.writeText(window.location.href); + setSnackBarMessage('Copied Course URL to Clipboard'); + setSnackBarOpen(true); + }, + }, + { + icon: , + enabled: true, + name: 'Copy Course Name', + clickAction: () => { + navigator.clipboard.writeText(`${courseId}: ${courseName}`); + setSnackBarMessage('Copied Course Name to Clipboard'); + setSnackBarOpen(true); + }, + }, + { + icon: , + enabled: user ? true : false, + name: 'Add Review', + clickAction: user + ? () => { + handleReviewModalOpen(); + } + : () => {}, + }, + ]; + + const handleSemester = ( + event: React.MouseEvent, + newSemester: string, + ) => { + setSelectedSemester(newSemester); + }; + + const handleYear = ( + event: React.MouseEvent, + newYear: number, + ) => { + setSelectedYear(newYear); + }; + const handleClose = ( + event: React.SyntheticEvent | Event, + reason?: string, + ) => { + if (reason === 'clickaway') { + return; + } + + setSnackBarOpen(false); + }; + useEffect(() => { + if (course_reviews) { + setCourseReviews(course_reviews); + } + }, [course_reviews]); + useEffect(() => { + if (selectedYear && selectedSemester) { + setLoading(true); + const newAvailableSemesters: any = Object.keys( + courseTimeline[selectedYear], + ); + const newActiveSemesters: any = Object.keys(mapSemesterTermToName).reduce( + (attrs, key) => ({ + ...attrs, + [key]: !(newAvailableSemesters.indexOf(key.toString()) > -1), + }), + {}, + ); + if (newActiveSemesters[selectedSemester]) { + setSelectedSemester( + newAvailableSemesters[newAvailableSemesters.length - 1], + ); + } + setActiveSemesters(newActiveSemesters); + } + mutate( + selectedYear && selectedSemester + ? `/course/${courseId}/${selectedYear}/${selectedSemester}` + : null, + () => { + return getReviews( + courseId, + String(selectedYear), + String(selectedSemester), + ); + }, + ); + setLoading(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedYear, selectedSemester]); + return ( + + + + {courseName} + + {courseUrl && ( + + + + + {'Course Website'} + + + + )} + {courseAvgWorkload && courseAvgDifficulty && courseAvgOverall && ( + + + + + + {`Average Workload`} + + + {roundNumber(Number(courseAvgWorkload), 1) + ' hrs/wk'} + + + + + + + + + {`Average Difficulty`} + + + {roundNumber(Number(courseAvgDifficulty), 1) + ' /5'} + + + + + + + + + {`Average Overall`} + + + {roundNumber(Number(courseAvgOverall), 1) + ' /5'} + + + + + + )} + + + {activeSemesters && + Object.entries(activeSemesters).map( + ([key, value]: [string, boolean], index: number) => ( + + + {mapSemesterTermToName[Number(key)]}{' '} + {mapSemesterTermToEmoji[Number(key)]} + + + ), + )} + + + {courseYears && + courseYears.map((year: number, index: number) => { + return ( + + {year} + + ); + })} + + + {loading ? ( + + + + ) : ( + <> + {courseNumReviews ? ( + <> + {courseReviews && ( + + {mapPayloadToArray(courseReviews, REVIEW_ID, DESC).map( + (value: Review) => ( + + + + ), + )} + + )} + + ) : ( + <> + + {`Aww shucks no reviews ${EMOJI_NO_REVIEWS}`} + + + )} + + )} + + + + + } + FabProps={{ + sx: { + border: `1px solid ${theme.palette.secondary.contrastText}`, + backgroundColor: `${theme.palette.secondary.main}`, + color: `${theme.palette.secondary.contrastText}`, + '&:hover': { + backgroundColor: `${theme.palette.secondary.contrastText}`, + color: `${theme.palette.secondary.main}`, + }, + }, + }} + > + {actions + .flatMap((action) => { + if (!action.enabled) { + return []; + } + return action; + }) + .map((action) => ( + + ))} + + + Close + + } + message={snackBarMessage} + /> + + ); +}; + +export default CourseId; + +interface PageProps { + query: { courseid: string }; +} + +export async function getServerSideProps(context: PageProps) { + const { courseid } = context.query; + const courseId = courseid as TCourseId; + const allCourseDataDynamic = await getCourses(); + const allCourseData = mapDynamicCoursesDataToCourses(allCourseDataDynamic); + const currentCourseData = allCourseData[courseId]; + if (currentCourseData.numReviews) { + const courseTimeline = currentCourseData.reviewsCountsByYearSem; + const courseYears = Object.keys(courseTimeline) + .map((year) => Number(year)) + .reverse(); + const mostRecentYear = courseYears[0]; + const mostRecentYearSemesters = Object.keys(courseTimeline[mostRecentYear]); + const mostRecentSemester = + mostRecentYearSemesters[mostRecentYearSemesters.length - 1]; + const availableSemesters = Object.keys(courseTimeline[mostRecentYear]); + const activeSemesters = Object.keys(mapSemesterTermToName).reduce( + (attrs, key) => ({ + ...attrs, + [key]: !(availableSemesters.indexOf(key.toString()) > -1), + }), + {}, + ); + const courseReviews = await getReviews( + courseId, + String(mostRecentYear), + String(mostRecentSemester), + ); + return { + props: { + courseData: currentCourseData, + courseTimeline: courseTimeline, + courseYears: courseYears, + defaultYear: mostRecentYear, + defaultSemester: mostRecentSemester, + defaultSemesterToggles: activeSemesters, + defaultReviews: courseReviews, + numReviews: currentCourseData.numReviews, + }, + }; + } else { + return { + props: { + courseData: currentCourseData, + }, + }; + } +} diff --git a/pages/user/reviews.tsx b/pages/user/reviews.tsx index 76fd71b2..5442b7b1 100644 --- a/pages/user/reviews.tsx +++ b/pages/user/reviews.tsx @@ -1,111 +1,111 @@ -import ReviewCard from '@components/ReviewCard'; -import { useAuth } from '@context/AuthContext'; -import { FirebaseAuthUser } from '@context/types'; -import { DESC, EMOJI_NO_REVIEWS, reviewFields } from '@globals/constants'; -import { Review, TUserReviews, TNullable } from '@globals/types'; -import { mapPayloadToArray } from '@src/utilities'; -import type { NextPage } from 'next'; -import { useEffect, useState } from 'react'; - -import backend from '@backend/index'; -import { isGTEmail } from '@globals/utilities'; - -import { - Box, - CircularProgress, - Container, - Grid, - Typography, -} from '@mui/material'; - -const { addUser, getUser } = backend; - -const UserReviews: NextPage = () => { - const [loading, setLoading] = useState(true); - - const authContext = useAuth(); - - const [userReviews, setUserReviews] = useState({}); - - let user: TNullable = null; - if (authContext) { - ({ user } = authContext); - } - - useEffect(() => { - if (user) { - getUser(user.uid).then((results) => { - if (results.userId) { - setUserReviews(results['reviews']); - } else if (user && user.uid && user.email) { - const hasGTEmail = isGTEmail(user.email); - addUser(user.uid, hasGTEmail); - setUserReviews({}); - } - setLoading(false); - }); - } else { - setUserReviews({}); - setLoading(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user]); - - return ( - - - <> - - {`My Reviews`} - - {loading ? ( - - - - ) : ( - <> - {Object.keys(userReviews)?.length ? ( - <> - {userReviews && ( - - {mapPayloadToArray( - userReviews, - reviewFields.CREATED, - DESC, - ).map((value: Review) => ( - - - - ))} - - )} - - ) : ( - <> - - {`Aww shucks no reviews ${EMOJI_NO_REVIEWS}`} - - - )} - - )} - - - - ); -}; - -export default UserReviews; +import ReviewCard from '@components/ReviewCard'; +import { useAuth } from '@context/AuthContext'; +import { FirebaseAuthUser } from '@context/types'; +import { DESC, EMOJI_NO_REVIEWS, reviewFields } from '@globals/constants'; +import { Review, TUserReviews, TNullable } from '@globals/types'; +import { mapPayloadToArray } from '@src/utilities'; +import type { NextPage } from 'next'; +import { useEffect, useState } from 'react'; + +import backend from '@backend/index'; +import { isGTEmail } from '@globals/utilities'; + +import { + Box, + CircularProgress, + Container, + Grid, + Typography, +} from '@mui/material'; + +const { addUser, getUser } = backend; + +const UserReviews: NextPage = () => { + const [loading, setLoading] = useState(true); + + const authContext = useAuth(); + + const [userReviews, setUserReviews] = useState({}); + + let user: TNullable = null; + if (authContext) { + ({ user } = authContext); + } + + useEffect(() => { + if (user) { + getUser(user.uid).then((results) => { + if (results.userId) { + setUserReviews(results['reviews']); + } else if (user && user.uid && user.email) { + const hasGTEmail = isGTEmail(user.email); + addUser(user.uid, hasGTEmail); + setUserReviews({}); + } + setLoading(false); + }); + } else { + setUserReviews({}); + setLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user]); + + return ( + + + <> + + {`My Reviews`} + + {loading ? ( + + + + ) : ( + <> + {Object.keys(userReviews)?.length ? ( + <> + {userReviews && ( + + {mapPayloadToArray( + userReviews, + reviewFields.CREATED, + DESC, + ).map((value: Review) => ( + + + + ))} + + )} + + ) : ( + <> + + {`Aww shucks no reviews ${EMOJI_NO_REVIEWS}`} + + + )} + + )} + + + + ); +}; + +export default UserReviews; diff --git a/src/components/FormEditor.tsx b/src/components/FormEditor.tsx index 2b71e588..07b21d72 100644 --- a/src/components/FormEditor.tsx +++ b/src/components/FormEditor.tsx @@ -1,64 +1,64 @@ -import { Box } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Editor } from '@toast-ui/react-editor'; -import DOMPurify from 'isomorphic-dompurify'; -import { useEffect, useRef } from 'react'; -import '@toast-ui/editor/dist/toastui-editor.css'; -import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; -import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight'; -import Prism from 'prismjs'; -import 'prismjs/themes/prism.css'; -//SSR is currently not supported for toastui - -export default function FormEditor({ - initialValue, - onChange, -}: { - initialValue: any; - onChange: any; -}) { - const editorRef = useRef(null); - const theme = useTheme(); - - function handleChange() { - const dirty = editorRef?.current - ? editorRef?.current.getInstance().getMarkdown() - : ''; - const clean = DOMPurify.sanitize(dirty, { FORBID_TAGS: ['img'] }); - onChange(clean); - } - - useEffect(() => { - //Set initial value this way because theres a character limit the other way when doing it via prop - if (!editorRef?.current?.getInstance().getMarkdown()) { - editorRef?.current?.getInstance().setMarkdown(initialValue); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editorRef?.current?.getInstance().getMarkdown(), initialValue]); - - return ( - - - - ); -} +import { Box } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { Editor } from '@toast-ui/react-editor'; +import DOMPurify from 'isomorphic-dompurify'; +import { useEffect, useRef } from 'react'; +import '@toast-ui/editor/dist/toastui-editor.css'; +import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; +import codeSyntaxHighlight from '@toast-ui/editor-plugin-code-syntax-highlight'; +import Prism from 'prismjs'; +import 'prismjs/themes/prism.css'; +//SSR is currently not supported for toastui + +export default function FormEditor({ + initialValue, + onChange, +}: { + initialValue: any; + onChange: any; +}) { + const editorRef = useRef(null); + const theme = useTheme(); + + function handleChange() { + const dirty = editorRef?.current + ? editorRef?.current.getInstance().getMarkdown() + : ''; + const clean = DOMPurify.sanitize(dirty, { FORBID_TAGS: ['img'] }); + onChange(clean); + } + + useEffect(() => { + //Set initial value this way because theres a character limit the other way when doing it via prop + if (!editorRef?.current?.getInstance().getMarkdown()) { + editorRef?.current?.getInstance().setMarkdown(initialValue); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorRef?.current?.getInstance().getMarkdown(), initialValue]); + + return ( + + + + ); +} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 213256f8..70056285 100755 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,192 +1,191 @@ -import Login from '@components/LoginContent'; -import MobileMenu from '@components/MobileMenu'; -import ProfileMenu from '@components/ProfileMenu'; -import { useAuth } from '@context/AuthContext'; -import { useMenu } from '@context/MenuContext'; -import { FirebaseAuthUser } from '@context/types'; -import { TNullable } from '@globals/types'; -import Brightness4Icon from '@mui/icons-material/Brightness4'; -import Brightness7Icon from '@mui/icons-material/Brightness7'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import { - IconButton, - AppBar, - Box, - Button, - Toolbar, - Tooltip, - Typography, -} from '@mui/material'; -import { useColorScheme } from '@mui/material/styles'; -import { useTheme } from '@mui/material/styles'; - -import Link from '@src/Link'; -interface NavBarProps {} - -export interface MenuLinksProps { - [key: string]: any; -} - -export const NavBar = ({ ...props }: NavBarProps) => { - const authContext: TNullable = useAuth(); - const user: TNullable = authContext.user; - const loading: TNullable = authContext.loading; - - const { handleLoginOpen } = useMenu(); - - const navigationMenuItems: MenuLinksProps = { - Recents: { - url: '/recents', - tooltip: 'Recent 50 reviews', - }, - About: { url: '/about', tooltip: 'Our background' }, - }; - - const profileMenuItems: MenuLinksProps = { - // 'My Account': '/user/', - 'My Reviews': '/user/reviews', - }; - - const theme = useTheme(); - const { mode, setMode } = useColorScheme(); - - return ( - - - - - - - - OMSHub - - - - - - {Object.keys(navigationMenuItems).map( - (name: string, index: number) => ( - - - {name} - - - ), - )} - - setMode(mode == 'light' ? 'dark' : 'light')} - color='inherit' - > - {theme.palette.mode === 'dark' ? ( - - ) : ( - - )} - - - - - - - - - - - - setMode(mode == 'light' ? 'dark' : 'light')} - color='inherit' - > - {theme.palette.mode === 'dark' ? ( - - ) : ( - - )} - - - {/* User Profile Side */} - {!loading ? ( - <> - {' '} - {!user ? ( - <> - - - - ) : ( - - - - )} - - ) : ( - - )} - - - - ); -}; +import Login from '@components/LoginContent'; +import MobileMenu from '@components/MobileMenu'; +import ProfileMenu from '@components/ProfileMenu'; +import { useAuth } from '@context/AuthContext'; +import { useMenu } from '@context/MenuContext'; +import { FirebaseAuthUser } from '@context/types'; +import { TNullable } from '@globals/types'; +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import { + IconButton, + AppBar, + Box, + Button, + Toolbar, + Tooltip, + Typography, +} from '@mui/material'; +import { useColorScheme } from '@mui/material/styles'; +import { useTheme } from '@mui/material/styles'; + +import Link from '@src/Link'; +interface NavBarProps {} + +export interface MenuLinksProps { + [key: string]: any; +} + +export const NavBar = ({ ...props }: NavBarProps) => { + const authContext: TNullable = useAuth(); + const user: TNullable = authContext.user; + const loading: TNullable = authContext.loading; + + const { handleLoginOpen } = useMenu(); + + const navigationMenuItems: MenuLinksProps = { + Recents: { + url: '/recents', + tooltip: 'Recent 50 reviews', + }, + About: { url: '/about', tooltip: 'Our background' }, + }; + + const profileMenuItems: MenuLinksProps = { + // 'My Account': '/user/', + 'My Reviews': '/user/reviews', + }; + + const theme = useTheme(); + const { mode, setMode } = useColorScheme(); + + return ( + + + + + + + + OMSHub + + + + + + {Object.keys(navigationMenuItems).map( + (name: string, index: number) => ( + + + {name} + + + ), + )} + + setMode(mode == 'light' ? 'dark' : 'light')} + color='inherit' + > + {theme.palette.mode === 'dark' ? ( + + ) : ( + + )} + + + + + + + + + + + setMode(mode == 'light' ? 'dark' : 'light')} + color='inherit' + > + {theme.palette.mode === 'dark' ? ( + + ) : ( + + )} + + + {/* User Profile Side */} + {!loading ? ( + <> + {' '} + {!user ? ( + <> + + + + ) : ( + + + + )} + + ) : ( + + )} + + + + ); +}; diff --git a/src/components/ProfileMenu.tsx b/src/components/ProfileMenu.tsx index cf2b13ef..3cc7358d 100644 --- a/src/components/ProfileMenu.tsx +++ b/src/components/ProfileMenu.tsx @@ -1,81 +1,81 @@ -import { useAuth } from '@context/AuthContext'; -import { useMenu } from '@context/MenuContext'; -import { FirebaseAuthUser } from '@context/types'; -import { TNullable } from '@globals/types'; -import Link from '@src/Link'; - -import { isGTEmail } from '@globals/utilities'; -import { Avatar, Container, Menu, MenuItem, Tooltip } from '@mui/material'; - -export interface MenuLinksProps { - [key: string]: any; -} - -const ProfileMenu = (profileMenuItems: MenuLinksProps) => { - const authContext = useAuth(); - - let user: TNullable = null; - let logout = () => {}; - - if (authContext) { - ({ user, logout } = authContext); - } - - const { profileMenuAnchorEl, handleProfileMenuOpen, handleProfileMenuClose } = - useMenu(); - - const isGatech = isGTEmail(user?.email!); - const BuzzProfile = '/buzz-profile.jpg'; - const LamaProfile = '/lama-profile.png'; - const isProfileMenuOpen = Boolean(profileMenuAnchorEl); - const menuId = 'primary-search-account-menu'; - return ( - <> - - - - - - {Object.keys(profileMenuItems).map((key: string, index: number) => ( - - - {key} - - - ))} - { - handleProfileMenuClose(); - logout(); - }} - > - Logout - - - - - ); -}; - -export default ProfileMenu; +import { useAuth } from '@context/AuthContext'; +import { useMenu } from '@context/MenuContext'; +import { FirebaseAuthUser } from '@context/types'; +import { TNullable } from '@globals/types'; +import Link from '@src/Link'; + +import { isGTEmail } from '@globals/utilities'; +import { Avatar, Container, Menu, MenuItem, Tooltip } from '@mui/material'; + +export interface MenuLinksProps { + [key: string]: any; +} + +const ProfileMenu = (profileMenuItems: MenuLinksProps) => { + const authContext = useAuth(); + + let user: TNullable = null; + let logout = () => {}; + + if (authContext) { + ({ user, logout } = authContext); + } + + const { profileMenuAnchorEl, handleProfileMenuOpen, handleProfileMenuClose } = + useMenu(); + + const isGatech = isGTEmail(user?.email!); + const BuzzProfile = '/buzz-profile.jpg'; + const LamaProfile = '/lama-profile.png'; + const isProfileMenuOpen = Boolean(profileMenuAnchorEl); + const menuId = 'primary-search-account-menu'; + return ( + <> + + + + + + {Object.keys(profileMenuItems).map((key: string, index: number) => ( + + + {key} + + + ))} + { + handleProfileMenuClose(); + logout(); + }} + > + Logout + + + + + ); +}; + +export default ProfileMenu; diff --git a/src/components/ReviewCard.tsx b/src/components/ReviewCard.tsx index 8d196525..ebcac601 100755 --- a/src/components/ReviewCard.tsx +++ b/src/components/ReviewCard.tsx @@ -1,358 +1,358 @@ -import backend from '@backend/index'; -import ReviewForm from '@components/ReviewForm'; -import { useAuth } from '@context/AuthContext'; -import { FirebaseAuthUser } from '@context/types'; -import { Review, TNullable } from '@globals/types'; -import { getCourseDataStatic } from '@globals/utilities'; -import { PhotoCamera, ErrorOutline, Edit, Delete } from '@mui/icons-material'; -import stringWidth from 'string-width'; -import remarkGfm from 'remark-gfm'; -import rehypeKatex from 'rehype-katex'; -import rehypeRaw from 'rehype-raw'; -import remarkMath from 'remark-math'; -import { - Box, - Button, - Card, - CardContent, - Chip, - CircularProgress, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, - IconButton, - Snackbar, - Tooltip, - Typography, -} from '@mui/material'; - -import { grey } from '@mui/material/colors'; - -import { techGold } from '@src/colorPalette'; - -import { - mapDifficulty, - mapOverall, - mapRatingToColor, - mapRatingToColorInverted, - mapSemesterIdToName, -} from '@src/utilities'; -import { toBlob } from 'html-to-image'; -import { useRouter } from 'next/router'; -import { SyntheticEvent, useEffect, useRef, useState } from 'react'; -import Markdown from 'react-markdown'; - -const { deleteReview } = backend; - -const ReviewCard = ({ - reviewId, - body, - overall, - difficulty, - workload, - semesterId, - created, - year, - courseId, - reviewerId, - isLegacy, - modified, - upvotes, - downvotes, - isGTVerifiedReviewer = false, -}: Review) => { - const router = useRouter(); - const authContext: TNullable = useAuth(); - const user: TNullable = authContext.user; - const timestamp = new Date(created).toLocaleDateString(); - const clipboardRef = useRef(null); - const { name: courseName } = getCourseDataStatic(courseId); - const [snackBarOpen, setSnackBarOpen] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isFirefox, setIsFirefox] = useState(false); - const [reviewModalOpen, setReviewModalOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const handleReviewModalOpen = () => setReviewModalOpen(true); - const handleReviewModalClose = () => setReviewModalOpen(false); - - useEffect(() => { - navigator.userAgent.match(`Firefox`) - ? setIsFirefox(true) - : setIsFirefox(false); - }, []); - - const handleClose = (event: SyntheticEvent | Event, reason?: string) => { - if (reason === 'clickaway') { - return; - } - setSnackBarOpen(false); - }; - - const handleCopyToClipboard = async () => { - const blob: any = await toBlob(clipboardRef?.current!); - const item: any = { [blob.type]: blob }; - // eslint-disable-next-line no-undef - const clipboardItem = new ClipboardItem(item); - await navigator.clipboard.write([clipboardItem]); - setSnackBarOpen(true); - }; - - const handleDeleteDialogOpen = () => { - setDeleteDialogOpen(true); - }; - - const handleDeleteDialogClose = () => { - setDeleteDialogOpen(false); - }; - - const handleDeleteReview = async () => { - setIsSubmitting(true); - if (user && user.uid && reviewId) { - await deleteReview(user.uid, reviewId); - handleDeleteDialogClose(); - router.reload(); - } - setIsSubmitting(false); - }; - - return ( -
- - - - - - {courseId} - - - {courseName} - - - - Taken {mapSemesterIdToName[semesterId]} {year} - - - - - Reviewed on {timestamp} - - - {isGTVerifiedReviewer && ( - - Verified GT Email - - )} - - - - - {isLegacy && ( - - } - color='warning' - label='Legacy' - variant='outlined' - /> - - )} - - - - - - - - - - - - - {body} - - {/* {body} */} - - {/* Screenshot button*/} - {!isFirefox && ( - - - - - - )} - - Close - - } - message={'Screenshotted Review to Clipboard!'} - /> - {!isLegacy && reviewerId == user?.uid ? ( - <> - {/* Update Button */} - - - - - - - - - {/* Delete Button */} - - - - - - - {`Delete ${mapSemesterIdToName[semesterId]} ${year} review for ${courseName}?`} - - - {`Your ${mapSemesterIdToName[semesterId]} ${year} review for ${courseName} will forever be deleted. Note: this process is unrecoverable!`} - - - - {isSubmitting ? ( - - ) : ( - <> - - - - )} - - - - ) : ( - <> - )} - - - -
- ); -}; - -export default ReviewCard; +import backend from '@backend/index'; +import ReviewForm from '@components/ReviewForm'; +import { useAuth } from '@context/AuthContext'; +import { FirebaseAuthUser } from '@context/types'; +import { Review, TNullable } from '@globals/types'; +import { getCourseDataStatic } from '@globals/utilities'; +import { PhotoCamera, ErrorOutline, Edit, Delete } from '@mui/icons-material'; +import stringWidth from 'string-width'; +import remarkGfm from 'remark-gfm'; +import rehypeKatex from 'rehype-katex'; +import rehypeRaw from 'rehype-raw'; +import remarkMath from 'remark-math'; +import { + Box, + Button, + Card, + CardContent, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid, + IconButton, + Snackbar, + Tooltip, + Typography, +} from '@mui/material'; + +import { grey } from '@mui/material/colors'; + +import { techGold } from '@src/colorPalette'; + +import { + mapDifficulty, + mapOverall, + mapRatingToColor, + mapRatingToColorInverted, + mapSemesterIdToName, +} from '@src/utilities'; +import { toBlob } from 'html-to-image'; +import { useRouter } from 'next/router'; +import { SyntheticEvent, useEffect, useRef, useState } from 'react'; +import Markdown from 'react-markdown'; + +const { deleteReview } = backend; + +const ReviewCard = ({ + reviewId, + body, + overall, + difficulty, + workload, + semesterId, + created, + year, + courseId, + reviewerId, + isLegacy, + modified, + upvotes, + downvotes, + isGTVerifiedReviewer = false, +}: Review) => { + const router = useRouter(); + const authContext: TNullable = useAuth(); + const user: TNullable = authContext.user; + const timestamp = new Date(created).toLocaleDateString(); + const clipboardRef = useRef(null); + const { name: courseName } = getCourseDataStatic(courseId); + const [snackBarOpen, setSnackBarOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isFirefox, setIsFirefox] = useState(false); + const [reviewModalOpen, setReviewModalOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const handleReviewModalOpen = () => setReviewModalOpen(true); + const handleReviewModalClose = () => setReviewModalOpen(false); + + useEffect(() => { + navigator.userAgent.match(`Firefox`) + ? setIsFirefox(true) + : setIsFirefox(false); + }, []); + + const handleClose = (event: SyntheticEvent | Event, reason?: string) => { + if (reason === 'clickaway') { + return; + } + setSnackBarOpen(false); + }; + + const handleCopyToClipboard = async () => { + const blob: any = await toBlob(clipboardRef?.current!); + const item: any = { [blob.type]: blob }; + // eslint-disable-next-line no-undef + const clipboardItem = new ClipboardItem(item); + await navigator.clipboard.write([clipboardItem]); + setSnackBarOpen(true); + }; + + const handleDeleteDialogOpen = () => { + setDeleteDialogOpen(true); + }; + + const handleDeleteDialogClose = () => { + setDeleteDialogOpen(false); + }; + + const handleDeleteReview = async () => { + setIsSubmitting(true); + if (user && user.uid && reviewId) { + await deleteReview(user.uid, reviewId); + handleDeleteDialogClose(); + router.reload(); + } + setIsSubmitting(false); + }; + + return ( +
+ + + + + + {courseId} + + + {courseName} + + + + Taken {mapSemesterIdToName[semesterId]} {year} + + + + + Reviewed on {timestamp} + + + {isGTVerifiedReviewer && ( + + Verified GT Email + + )} + + + + + {isLegacy && ( + + } + color='warning' + label='Legacy' + variant='outlined' + /> + + )} + + + + + + + + + + + + + {body} + + {/* {body} */} + + {/* Screenshot button*/} + {!isFirefox && ( + + + + + + )} + + Close + + } + message={'Screenshotted Review to Clipboard!'} + /> + {!isLegacy && reviewerId == user?.uid ? ( + <> + {/* Update Button */} + + + + + + + + + {/* Delete Button */} + + + + + + + {`Delete ${mapSemesterIdToName[semesterId]} ${year} review for ${courseName}?`} + + + {`Your ${mapSemesterIdToName[semesterId]} ${year} review for ${courseName} will forever be deleted. Note: this process is unrecoverable!`} + + + + {isSubmitting ? ( + + ) : ( + <> + + + + )} + + + + ) : ( + <> + )} + + + +
+ ); +}; + +export default ReviewCard; diff --git a/src/components/ReviewForm.tsx b/src/components/ReviewForm.tsx index 3050cb1e..838fd4cd 100644 --- a/src/components/ReviewForm.tsx +++ b/src/components/ReviewForm.tsx @@ -1,481 +1,481 @@ -import { addUser, getUser } from '@backend/dbOperations'; -import backend from '@backend/index'; -import { useAlert } from '@context/AlertContext'; -import { useAuth } from '@context/AuthContext'; -import { FirebaseAuthUser } from '@context/types'; -import { SEMESTER_ID } from '@globals/constants'; - -import { - Review, - TCourseId, - TCourseName, - TNullable, - TRatingScale, - TSemesterId, - TUserReviews, -} from '@globals/types'; -import { isGTEmail } from '@globals/utilities'; -import { - Button, - TextField, - CircularProgress, - Alert, - Grid, - InputAdornment, - InputLabel, - MenuItem, - Rating, - Select, - Typography, -} from '@mui/material'; -import { mapSemesterIdToName, mapSemsterIdToTerm } from '@src/utilities'; -import dynamic from 'next/dynamic'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { - Controller, - DefaultValues, - SubmitHandler, - useForm, -} from 'react-hook-form'; - -const { addReview, updateReview } = backend; - -const DynamicEditor = dynamic(() => import('@components/FormEditor'), { - ssr: false, -}); - -interface ReviewFormInputs { - year: TNullable; - semesterId: TNullable; - body: string; - workload: TNullable; - overall: TNullable; - difficulty: TNullable; -} - -type TPropsReviewForm = { - courseId: TCourseId; - courseName: TCourseName; - reviewInput: TNullable; - handleReviewModalClose: () => void; -}; - -type TSemesterMap = { - // eslint-disable-next-line no-unused-vars - [semesterId in TSemesterId]: Date; -}; - -const fallbackDates = { - sp: 'Feb 01', - sm: 'June 01', - fa: 'Sept 01', -}; - -const ReviewFormDefaults: DefaultValues = { - year: null, - semesterId: null, - body: ' ', - workload: null, - overall: null, - difficulty: null, -}; - -const ReviewForm = ({ - courseId, - courseName, - reviewInput, - handleReviewModalClose, -}: TPropsReviewForm) => { - const authContext: TNullable = useAuth(); - - const user: TNullable = authContext.user; - - const { setAlert } = useAlert(); - const [userReviews, setUserReviews] = useState({}); - const router = useRouter(); - - const yearRange = getYearRange(); - - const { - control, - handleSubmit, - getValues, - trigger, - reset, - setValue, - formState: { errors, isDirty, isValid, isSubmitting }, - } = useForm({ - mode: 'onChange', - reValidateMode: 'onChange', - resolver: undefined, - defaultValues: ReviewFormDefaults, - context: undefined, - criteriaMode: 'firstError', - shouldFocusError: true, - shouldUnregister: true, - }); - const onSubmit: SubmitHandler = async ( - data: ReviewFormInputs, - ) => { - const isGoodSubmission = await trigger(); - - const hasNonNullDataValues = Boolean( - courseId && - data.year && - data.semesterId && - data.difficulty && - data.overall && - data.workload, - ); - - const isLoggedIn = Boolean(user && user.uid && user.email); - - if (isGoodSubmission && isLoggedIn && hasNonNullDataValues) { - const currentTime = Date.now(); - const semesterId = data.semesterId as TSemesterId; - const year = Number(data.year); - const body = data.body; - const reviewerId = user?.uid!; - const reviewId = `${courseId}-${data.year}-${mapSemsterIdToTerm[semesterId]}-${currentTime}`; - const workload = Number(data.workload); - const difficulty = Number(data.difficulty) as TRatingScale; - const overall = Number(data.overall) as TRatingScale; - const isGTVerifiedReviewer = isGTEmail(user?.email!); - - const reviewValues = { - ['courseId']: reviewInput ? reviewInput.courseId : courseId, - ['reviewerId']: reviewInput ? reviewInput.reviewerId : reviewerId, - ['reviewId']: reviewInput ? reviewInput.reviewId : reviewId, - ['created']: reviewInput ? reviewInput.created : currentTime, - ['modified']: currentTime, - ['semesterId']: reviewInput ? reviewInput.semesterId : semesterId, - ['upvotes']: reviewInput ? reviewInput.upvotes : 0, - ['downvotes']: reviewInput ? reviewInput.downvotes : 0, - ['isLegacy']: reviewInput ? reviewInput.isLegacy : false, - ['year']: reviewInput ? reviewInput.year : year, - ['isGTVerifiedReviewer']: reviewInput - ? reviewInput.isGTVerifiedReviewer - : isGTVerifiedReviewer, - body, - workload, - difficulty, - overall, - }; - - reviewInput?.reviewId - ? await updateReview(user?.uid!, reviewInput?.reviewId, reviewValues) - : await addReview(user?.uid!, reviewId, reviewValues); - - setAlert({ - severity: 'success', - text: `Successful review submission for ${courseId} for ${mapSemesterIdToName[semesterId]} ${year}`, - variant: 'outlined', - }); - - handleReviewModalClose(); - router.reload(); - } - }; - - useEffect(() => { - getUser(user?.uid!).then((results) => { - if (results.userId) { - setUserReviews(results['reviews']); - } else if (user && user.uid && user.email) { - const hasGTEmail = isGTEmail(user.email); - addUser(user.uid, hasGTEmail); - setUserReviews({}); - } else { - setUserReviews({}); - } - }); - }, [user]); - - useEffect(() => { - reset({ ...reviewInput }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reviewInput, reset]); - - return ( - - {`Add Review for ${courseId}: ${courseName}`} - - - - - - Year - - ( - - )} - rules={{ - required: true, - validate: reviewInput?.reviewId - ? {} - : { - validateYearGivenSemester: (year) => { - return validateSemesterYear(getValues()?.semesterId, year); - }, - validateNotTakenCourse: (year) => { - return validateUserNotTakenCourse( - userReviews, - courseId, - getValues()?.semesterId, - year, - ); - }, - }, - }} - > - {errors.year && errors.year.type === 'validateYearGivenSemester' && ( - {`Please wait until ${ - fallbackDates[getValues()?.semesterId!] - } to review ${courseId} for semester ${ - mapSemesterIdToName[`${getValues()?.semesterId!}`] - } ${getValues()['year']}`} - )} - {errors.year && errors.year.type === 'validateNotTakenCourse' && ( - - {`You've already reviewed this course for the semester and year!`} - - )} - - - - Semester - - ( - - )} - rules={{ - required: true, - validate: reviewInput?.reviewId - ? {} - : { - validateSemesterGivenYear: (semester) => { - return validateSemesterYear(semester, getValues()['year']); - }, - validateNotTakenCourse: (semester) => { - return validateUserNotTakenCourse( - userReviews, - courseId, - semester, - getValues()?.year, - ); - }, - }, - }} - > - {errors.semesterId && - errors.semesterId.type === 'validateSemesterGivenYear' && ( - {`Please wait until ${ - fallbackDates[getValues()?.semesterId!] - } to review ${courseId} for semester ${ - mapSemesterIdToName[`${getValues()?.semesterId!}`] - } ${getValues()?.year!}`} - )} - {errors.semesterId && - errors.semesterId.type === 'validateNotTakenCourse' && ( - {`You've already reviewed this course for the semester and year!`} - )} - - - - Workload - - ( - { - const double = parseFloat(event.target.value); - if (double) { - return field.onChange(double); - } - return; - }} - InputProps={{ - inputMode: 'numeric', - endAdornment: ( - hr/wk - ), - }} - fullWidth - /> - )} - rules={{ - min: '1', - max: '168', - required: true, - validate: { - validateIsNumber: (value: TNullable) => - value ? value > 0 : false, - }, - }} - > - {errors.workload && errors.workload.type === 'min' && ( - - {`Please enter a workload greater than 0`} - - )} - {errors.workload && errors.workload.type === 'max' && ( - - {`Please enter a workload less than 168`} - - )} - - - - Difficulty - } - rules={{ - required: true, - min: '1', - }} - > - - - Overall - ( - - )} - rules={{ - required: true, - min: '1', - }} - > - - - - Review - - ( - { - setValue('body', body, { shouldDirty: true }); - }} - initialValue={field.value} - /> - )} - > - - - {isSubmitting ? ( - - ) : ( - - )} - - - ); -}; - -const getYearRange = () => { - const currentYear = new Date().getFullYear(); - const programStart = 2013; - const limitYear = 5; - return Array.from( - { length: currentYear - programStart - limitYear + 1 }, - (_, i) => currentYear + i * -1, - ); -}; - -const validateSemesterYear = ( - semester: TNullable, - year: TNullable, -) => { - if (semester && year) { - const currentYear = new Date().getFullYear(); - const semesterMap: TSemesterMap = { - sp: new Date(`02/01/${currentYear}`), - sm: new Date(`06/01/${currentYear}`), - fa: new Date(`09/01/${currentYear}`), - }; - // @ts-ignore -- semester is TSemesterId in this usage/context - const compareDate = semesterMap[semester] as Date; - if (year < new Date().getFullYear()) { - return true; - } - if (new Date() < compareDate) { - return false; - } - return true; - } -}; - -const validateUserNotTakenCourse = ( - userReviews: TUserReviews | {}, - courseId: TCourseId, - semester: TNullable, - year: TNullable, -) => { - if (semester && year) { - const objKey = `${courseId}-${year}-${mapSemsterIdToTerm[semester]}`; - return Object.keys(userReviews).find((key) => key.includes(objKey)) - ? false - : true; - } -}; - -export default ReviewForm; +import { addUser, getUser } from '@backend/dbOperations'; +import backend from '@backend/index'; +import { useAlert } from '@context/AlertContext'; +import { useAuth } from '@context/AuthContext'; +import { FirebaseAuthUser } from '@context/types'; +import { SEMESTER_ID } from '@globals/constants'; + +import { + Review, + TCourseId, + TCourseName, + TNullable, + TRatingScale, + TSemesterId, + TUserReviews, +} from '@globals/types'; +import { isGTEmail } from '@globals/utilities'; +import { + Button, + TextField, + CircularProgress, + Alert, + Grid, + InputAdornment, + InputLabel, + MenuItem, + Rating, + Select, + Typography, +} from '@mui/material'; +import { mapSemesterIdToName, mapSemsterIdToTerm } from '@src/utilities'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { + Controller, + DefaultValues, + SubmitHandler, + useForm, +} from 'react-hook-form'; + +const { addReview, updateReview } = backend; + +const DynamicEditor = dynamic(() => import('@components/FormEditor'), { + ssr: false, +}); + +interface ReviewFormInputs { + year: TNullable; + semesterId: TNullable; + body: string; + workload: TNullable; + overall: TNullable; + difficulty: TNullable; +} + +type TPropsReviewForm = { + courseId: TCourseId; + courseName: TCourseName; + reviewInput: TNullable; + handleReviewModalClose: () => void; +}; + +type TSemesterMap = { + // eslint-disable-next-line no-unused-vars + [semesterId in TSemesterId]: Date; +}; + +const fallbackDates = { + sp: 'Feb 01', + sm: 'June 01', + fa: 'Sept 01', +}; + +const ReviewFormDefaults: DefaultValues = { + year: null, + semesterId: null, + body: ' ', + workload: null, + overall: null, + difficulty: null, +}; + +const ReviewForm = ({ + courseId, + courseName, + reviewInput, + handleReviewModalClose, +}: TPropsReviewForm) => { + const authContext: TNullable = useAuth(); + + const user: TNullable = authContext.user; + + const { setAlert } = useAlert(); + const [userReviews, setUserReviews] = useState({}); + const router = useRouter(); + + const yearRange = getYearRange(); + + const { + control, + handleSubmit, + getValues, + trigger, + reset, + setValue, + formState: { errors, isDirty, isValid, isSubmitting }, + } = useForm({ + mode: 'onChange', + reValidateMode: 'onChange', + resolver: undefined, + defaultValues: ReviewFormDefaults, + context: undefined, + criteriaMode: 'firstError', + shouldFocusError: true, + shouldUnregister: true, + }); + const onSubmit: SubmitHandler = async ( + data: ReviewFormInputs, + ) => { + const isGoodSubmission = await trigger(); + + const hasNonNullDataValues = Boolean( + courseId && + data.year && + data.semesterId && + data.difficulty && + data.overall && + data.workload, + ); + + const isLoggedIn = Boolean(user && user.uid && user.email); + + if (isGoodSubmission && isLoggedIn && hasNonNullDataValues) { + const currentTime = Date.now(); + const semesterId = data.semesterId as TSemesterId; + const year = Number(data.year); + const body = data.body; + const reviewerId = user?.uid!; + const reviewId = `${courseId}-${data.year}-${mapSemsterIdToTerm[semesterId]}-${currentTime}`; + const workload = Number(data.workload); + const difficulty = Number(data.difficulty) as TRatingScale; + const overall = Number(data.overall) as TRatingScale; + const isGTVerifiedReviewer = isGTEmail(user?.email!); + + const reviewValues = { + ['courseId']: reviewInput ? reviewInput.courseId : courseId, + ['reviewerId']: reviewInput ? reviewInput.reviewerId : reviewerId, + ['reviewId']: reviewInput ? reviewInput.reviewId : reviewId, + ['created']: reviewInput ? reviewInput.created : currentTime, + ['modified']: currentTime, + ['semesterId']: reviewInput ? reviewInput.semesterId : semesterId, + ['upvotes']: reviewInput ? reviewInput.upvotes : 0, + ['downvotes']: reviewInput ? reviewInput.downvotes : 0, + ['isLegacy']: reviewInput ? reviewInput.isLegacy : false, + ['year']: reviewInput ? reviewInput.year : year, + ['isGTVerifiedReviewer']: reviewInput + ? reviewInput.isGTVerifiedReviewer + : isGTVerifiedReviewer, + body, + workload, + difficulty, + overall, + }; + + reviewInput?.reviewId + ? await updateReview(user?.uid!, reviewInput?.reviewId, reviewValues) + : await addReview(user?.uid!, reviewId, reviewValues); + + setAlert({ + severity: 'success', + text: `Successful review submission for ${courseId} for ${mapSemesterIdToName[semesterId]} ${year}`, + variant: 'outlined', + }); + + handleReviewModalClose(); + router.reload(); + } + }; + + useEffect(() => { + getUser(user?.uid!).then((results) => { + if (results.userId) { + setUserReviews(results['reviews']); + } else if (user && user.uid && user.email) { + const hasGTEmail = isGTEmail(user.email); + addUser(user.uid, hasGTEmail); + setUserReviews({}); + } else { + setUserReviews({}); + } + }); + }, [user]); + + useEffect(() => { + reset({ ...reviewInput }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reviewInput, reset]); + + return ( + + {`Add Review for ${courseId}: ${courseName}`} + + + + + + Year + + ( + + )} + rules={{ + required: true, + validate: reviewInput?.reviewId + ? {} + : { + validateYearGivenSemester: (year) => { + return validateSemesterYear(getValues()?.semesterId, year); + }, + validateNotTakenCourse: (year) => { + return validateUserNotTakenCourse( + userReviews, + courseId, + getValues()?.semesterId, + year, + ); + }, + }, + }} + > + {errors.year && errors.year.type === 'validateYearGivenSemester' && ( + {`Please wait until ${ + fallbackDates[getValues()?.semesterId!] + } to review ${courseId} for semester ${ + mapSemesterIdToName[`${getValues()?.semesterId!}`] + } ${getValues()['year']}`} + )} + {errors.year && errors.year.type === 'validateNotTakenCourse' && ( + + {`You've already reviewed this course for the semester and year!`} + + )} + + + + Semester + + ( + + )} + rules={{ + required: true, + validate: reviewInput?.reviewId + ? {} + : { + validateSemesterGivenYear: (semester) => { + return validateSemesterYear(semester, getValues()['year']); + }, + validateNotTakenCourse: (semester) => { + return validateUserNotTakenCourse( + userReviews, + courseId, + semester, + getValues()?.year, + ); + }, + }, + }} + > + {errors.semesterId && + errors.semesterId.type === 'validateSemesterGivenYear' && ( + {`Please wait until ${ + fallbackDates[getValues()?.semesterId!] + } to review ${courseId} for semester ${ + mapSemesterIdToName[`${getValues()?.semesterId!}`] + } ${getValues()?.year!}`} + )} + {errors.semesterId && + errors.semesterId.type === 'validateNotTakenCourse' && ( + {`You've already reviewed this course for the semester and year!`} + )} + + + + Workload + + ( + { + const double = parseFloat(event.target.value); + if (double) { + return field.onChange(double); + } + return; + }} + InputProps={{ + inputMode: 'numeric', + endAdornment: ( + hr/wk + ), + }} + fullWidth + /> + )} + rules={{ + min: '1', + max: '168', + required: true, + validate: { + validateIsNumber: (value: TNullable) => + value ? value > 0 : false, + }, + }} + > + {errors.workload && errors.workload.type === 'min' && ( + + {`Please enter a workload greater than 0`} + + )} + {errors.workload && errors.workload.type === 'max' && ( + + {`Please enter a workload less than 168`} + + )} + + + + Difficulty + } + rules={{ + required: true, + min: '1', + }} + > + + + Overall + ( + + )} + rules={{ + required: true, + min: '1', + }} + > + + + + Review + + ( + { + setValue('body', body, { shouldDirty: true }); + }} + initialValue={field.value} + /> + )} + > + + + {isSubmitting ? ( + + ) : ( + + )} + + + ); +}; + +const getYearRange = () => { + const currentYear = new Date().getFullYear(); + const programStart = 2013; + const limitYear = 5; + return Array.from( + { length: currentYear - programStart - limitYear + 1 }, + (_, i) => currentYear + i * -1, + ); +}; + +const validateSemesterYear = ( + semester: TNullable, + year: TNullable, +) => { + if (semester && year) { + const currentYear = new Date().getFullYear(); + const semesterMap: TSemesterMap = { + sp: new Date(`02/01/${currentYear}`), + sm: new Date(`06/01/${currentYear}`), + fa: new Date(`09/01/${currentYear}`), + }; + // @ts-ignore -- semester is TSemesterId in this usage/context + const compareDate = semesterMap[semester] as Date; + if (year < new Date().getFullYear()) { + return true; + } + if (new Date() < compareDate) { + return false; + } + return true; + } +}; + +const validateUserNotTakenCourse = ( + userReviews: TUserReviews | {}, + courseId: TCourseId, + semester: TNullable, + year: TNullable, +) => { + if (semester && year) { + const objKey = `${courseId}-${year}-${mapSemsterIdToTerm[semester]}`; + return Object.keys(userReviews).find((key) => key.includes(objKey)) + ? false + : true; + } +}; + +export default ReviewForm;