diff --git a/.gitignore b/.gitignore index 1fa4844..02f2ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* .env .env.** !.env.example + +# tsc +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/components/TopListItem.module.scss b/components/TopListItem.module.scss new file mode 100644 index 0000000..a4352c8 --- /dev/null +++ b/components/TopListItem.module.scss @@ -0,0 +1,52 @@ + +$topItemSpace: 7px; + +.topItemIcon { + min-width: 35px!important; + margin-top: $topItemSpace!important; + padding-right: $topItemSpace!important; + text-align: right; +} + +.topItemText { + max-width: 80%!important; + //background-color: darkorchid; +} + +.topItemTitle { + @media (max-width: 768px) { + line-height: 1.15!important; + } + padding-top: 0; +} + +.topItemSubtitle { + line-height: 1.2!important; + padding: calc(0.75em / 2) 0; + padding-top: 0; +} + +.hintedMetric { + font-size: 0.75rem!important; + font-style: italic; + margin-top: calc($topItemSpace + 1px)!important; + padding-right: 2px; + white-space: break-spaces!important; + text-align: right; + min-width: 45px; + margin-left: auto!important; + align-self: stretch; + //background-color: darkgreen; + @media(min-width: 991px) { + flex-shrink: 0; + } + @media (max-width: 450px) { + max-width: 60px; + } +} + +.hintedMetricExtended { + @media (max-width: 990px) { + display: none; + } +} diff --git a/components/TopListItem.tsx b/components/TopListItem.tsx new file mode 100644 index 0000000..2d4978a --- /dev/null +++ b/components/TopListItem.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react' +import Link from 'next/link' +import { TopResult } from '../lib/data/useTopResults' +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import Typography from '@mui/material/Typography'; +import ListItemText from '@mui/material/ListItemText'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import { Badge } from './badge' +import { TopMetric } from '../lib/top_back'; + +import styles from './TopListItem.module.scss' +//import interactivity from '../../styles/interactivity.module.scss' +import instructorCardStyles from './instructorcard.module.scss' + +interface TopListItemProps { + data: TopResult; + index: number; + viewMetric: TopMetric; +} + +export function TopListItem({ data: item, index, viewMetric }: TopListItemProps) { + return ( + + + + + { + index + 1 <= 10 + ? `#${index + 1}` + : #{index + 1} + } + + + + + {item.title} + + } + secondary={<> + + { + item.subtitle.length <= 50 + ? `${item.subtitle}` + : {item.subtitle} + } + + + { item.badges.map(b => ( + + {b.text} + + ))} + + } + /> + + {item.metricFormatted}{ viewMetric === 'totalEnrolled' ? {' '}since {item.metricTimeSpanFormatted} : null} + + + + ) +} diff --git a/components/appcheck.tsx b/components/appcheck.tsx deleted file mode 100644 index 3690ebf..0000000 --- a/components/appcheck.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react' -import { useFirebaseApp } from 'reactfire' -import { buildArgs, firebaseConfig } from '../lib/environment' - -/** - * https://firebase.google.com/docs/app-check/web/recaptcha-provider?authuser=0 - */ -export function AppCheck() { - const app = useFirebaseApp(); - - useEffect(() => { - if(buildArgs.vercelEnv === 'production' && firebaseConfig.recaptchaSiteKey !== undefined) { - const appCheck = app.appCheck(); - appCheck.activate(firebaseConfig.recaptchaSiteKey) - } - },[]) - - return <>; -} diff --git a/components/auth/CustomClaimsCheck.tsx b/components/auth/CustomClaimsCheck.tsx deleted file mode 100644 index 96ac0ef..0000000 --- a/components/auth/CustomClaimsCheck.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react' -import { useSigninCheck } from 'reactfire' -import { useJWT } from './RealtimeClaimUpdater' -import { Emoji } from '../emoji' - -interface CustomClaimsCheckProps { - requiredClaims: { [key: string]: any; }; - children: React.ReactNode; - fallback?: React.ReactNode; -} - -/** - * Re-implementation of: https://github.com/FirebaseExtended/reactfire/blob/b4f22bc0a84729245db87861d5190a0483b19348/src/auth.tsx#L74-L95 - */ -export function CustomClaimsCheck({ requiredClaims, children, fallback }: CustomClaimsCheckProps) { - const { status, data } = useSigninCheck({ requiredClaims }); - const jwt = useJWT() - - // typical SWR stuff - if (status === 'error') return
Error
; - if (status === 'loading') return
Loading
; - - // if we're signed in and have a token to reference - if(data.signedIn && jwt) { - let missingClaims: { [key: string]: { expected: string, actual: string}; } = {}; - - for(let claim of Object.keys(requiredClaims)) { - if (requiredClaims[claim] !== jwt.claims[claim]) { - missingClaims[claim] = { - expected: requiredClaims[claim], - actual: jwt.claims[claim] - }; - } - } - - // if the user is unauthorized - if(Object.keys(missingClaims).length > 0) { - if(fallback) { - return <>{fallback}; - } - else { - // if no fallback is provided, show the actual missing claims - return ( - <> -

Unauthorized

-

You are missing these claims to gain access:

- - - ); - } - } - else { - // if they're authorized, show the intended content - return <>{children}; - } - } - else { - return ( - <> -

Unauthorized

-

You are not signed in!

- - ); - } -} diff --git a/components/auth/LoginForm.tsx b/components/auth/LoginForm.tsx deleted file mode 100644 index 7accee3..0000000 --- a/components/auth/LoginForm.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import { useAuth } from 'reactfire' - -export function LoginForm() { - const auth = useAuth(); - const fbAuth = useAuth; - const provider = new fbAuth.GoogleAuthProvider(); - provider.addScope('profile'); - provider.addScope('email'); - - const signIn = async () => { - await auth.signInWithPopup(provider) - }; - - // If already signed in, offer account options - if (auth.currentUser) return ( -
-

You're already signed in!

-
- ); - - // If not yet signed in, offer login options - return ( -
-

Authentication required

-

Only authorized users will be permitted to access. Before proceeding, please sign in.

-
- - - By signing in, certain information about your Google account is preserved in our database. What does this mean? - - -

- - This is limited to: your display name, your email, and the URL of your Google account picture. - Users will have the option of having their account deleted from our database. - CougarGrades.io is open-source and not for profit. We are not interested in contacting you with this information or sharing it with third-parties. - -

-
- -
- ); -} diff --git a/components/auth/RealtimeClaimUpdater.tsx b/components/auth/RealtimeClaimUpdater.tsx deleted file mode 100644 index 99bbc91..0000000 --- a/components/auth/RealtimeClaimUpdater.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { User } from '@cougargrades/types' -import { useEffect, useState } from 'react'; -import { useFirestore, useFirestoreDoc, useSigninCheck } from 'reactfire' -import { useRecoilState } from 'recoil' -import { FirestoreGuard } from '../../lib/firebase'; -import { jwtAtom } from '../../lib/recoil' - -export function useJWT() { - const [jwt, _] = useRecoilState(jwtAtom); - return jwt; -} - -/** - * Insert this somewhere in your DOM tree. It will always just render 1 empty React.Fragment. - * @returns - */ -export function RealtimeClaimUpdater() { - const { status, data: signInCheckResult } = useSigninCheck(); - const shouldSubscribe = status === 'success' && signInCheckResult.signedIn - const [jwt, setJwt] = useRecoilState(jwtAtom); - - //console.log('shouldSubscribe?', shouldSubscribe) - - useEffect(() => { - // this removes outdated JWTs if the user unsubscribes - if(! shouldSubscribe) { - //console.log('jwt nullified') - setJwt(null) - } - }, [shouldSubscribe, jwt]) - - return shouldSubscribe ? : <>; -} - -interface UserDocumentSubscriberProps { - user: firebase.default.User; - delay: number; -} - -/** - * This should only be called if the user is already signed in - * We use a separate component instead of 1 hook because hooks can't be - * conditionally called, but components can. - */ -function UserDocumentSubscriber({ user, delay }: UserDocumentSubscriberProps) { - const db = useFirestore(); - const [forceRefresh, setForceRefresh] = useState(false); - const [jwt, setJwt] = useRecoilState(jwtAtom); - const { status: docStatus, data: doc } = useFirestoreDoc(db.doc(`/users/${user.uid}`)) - - // initialize jwt - useEffect(() => { - (async () => { - if(jwt === null) { - setJwt(await user.getIdTokenResult()); - //console.log('jwt initialized') - } - })(); - }, [jwt]) - - // trigger a token refresh using a brief state change - useEffect(() => { - (async () => { - if(forceRefresh) { - //console.log('forceRefresh tripped') - setJwt(await user.getIdTokenResult(true)); - setTimeout(() => setForceRefresh(false), delay) - } - })(); - //console.log('refresh effect') - }, [forceRefresh]) - - // compare changes - useEffect(() => { - // don't try another refresh until the effect hook above catches up - if(! forceRefresh) { - // verify that both are loaded before comparing - if(docStatus === 'success' && doc.exists && jwt !== null) { - const docData = doc.data() - //console.log('JWT (doc): ', docData.custom_claims) - //console.log('JWT (recoil): ', onlyClaims(jwt.claims, ['admin','hello'])); - // compare if token claims are different from the doc - for(const key of Object.keys(docData.custom_claims)) { - if(docData.custom_claims[key] !== jwt.claims[key]) { - //console.log('discrepancy!',docData.custom_claims[key],' vs ',jwt.claims[key]) - setForceRefresh(true); - break; - } - } - //console.log('comparator done') - } - } - }, [forceRefresh, docStatus, doc, jwt]) - - return <> -} - - -const onlyClaims = (raw: { [key: string]: any }, allowed: string[]) => Object.keys(raw) - .filter(e => allowed.includes(e)) - .reduce((obj, key) => { - obj[key] = raw[key]; - return obj; - }, {}); - diff --git a/components/auth/UserAccountControl.module.scss b/components/auth/UserAccountControl.module.scss deleted file mode 100644 index 9d96c34..0000000 --- a/components/auth/UserAccountControl.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.buttonSpread { - button { - margin: 0.25em; - } - button:nth-child(1) { - margin-left: 0; - } -} diff --git a/components/auth/UserAccountControl.tsx b/components/auth/UserAccountControl.tsx deleted file mode 100644 index 9977153..0000000 --- a/components/auth/UserAccountControl.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState } from 'react' -import { useAuth, useSigninCheck } from 'reactfire' -import { CustomClaimNames as isCustomClaim } from '@cougargrades/types/dist/is' -import { Collaborator } from '../collaborator' - -import styles from './UserAccountControl.module.scss' -import { useJWT } from './RealtimeClaimUpdater' -import FormControlLabel from '@mui/material/FormControlLabel' -import Switch from '@mui/material/Switch' - - -export function UserAccountControl() { - // get the current user, identified by the SDK-managed JWT - const { status, data: signInCheckResult } = useSigninCheck(); - const jwt = useJWT(); - const [showAllClaims, setShowAllClaims] = useState(false) - - // for signing out - const auth = useAuth(); - const fbAuth = useAuth; - const provider = new fbAuth.GoogleAuthProvider(); - provider.addScope('profile'); - provider.addScope('email'); - - // typical SWR stuff - if (status === 'error') return
failed to load
; - if (status === 'loading') return
loading...
; - if (! signInCheckResult.signedIn || jwt === null) return
not signed in
; - - // for rendering claims - const rows = Object.keys(jwt.claims) - // dont present other stuff present in the OpenID spec - // see: https://openid.net/specs/openid-connect-core-1_0.html#IDToken - .filter(e => showAllClaims ? true : isCustomClaim(e)) - .map(key => - - {key} - {JSON.stringify(jwt.claims[key])} - - ); - - const signOut = () => { - auth.signOut(); - } - - const deleteAccount = async () => { - if(auth.currentUser) { - // this is a sensitive operation so we want to revalidate their identity - const cred = await auth.currentUser.reauthenticateWithPopup(provider); - // if reauthentication was successful - if(cred.credential) { - await auth.currentUser.delete(); - } - } - }; - - // If already signed in, offer account options - if (auth.currentUser) return ( -
-
-

Signed in as:

- -
-
- - -
- setShowAllClaims(e.target.checked)} />} label="Show all claims" /> - - - - - - - - - - {rows} - -
{showAllClaims ? 'All Claims' : 'Custom Claims'}
FieldValue
-
- ); - - // If not yet signed in, offer login options - return ( -
-

You're not signed in!

-
- ); -} diff --git a/components/badge.module.scss b/components/badge.module.scss index 35150c1..7bb6595 100644 --- a/components/badge.module.scss +++ b/components/badge.module.scss @@ -1,7 +1,7 @@ .badge { display: inline-block; padding: 0.25em 0.4em; - font-size: 80%; + font-size: 0.8em; font-weight: bold; line-height: 1; text-align: center; @@ -14,8 +14,8 @@ } .skeleton { - display: inline-block; - border-radius: 0.3rem; + display: inline-block!important; + border-radius: 0.3rem!important; } .new { diff --git a/components/badge.tsx b/components/badge.tsx index 75c8ce4..e51abc4 100644 --- a/components/badge.tsx +++ b/components/badge.tsx @@ -1,5 +1,6 @@ import React, { CSSProperties } from 'react' import Skeleton from '@mui/material/Skeleton' +import type { Property } from 'csstype' import styles from './badge.module.scss' type BadgeProps = React.DetailedHTMLProps, HTMLSpanElement> & { @@ -32,19 +33,22 @@ export function BadgeSkeleton({ style }: SkeletonProps) { ) } -export type Grade = 'A' | 'B' | 'C' | 'D' | 'F' | 'I' | 'W' | 'S' | 'U' | 'NCR'; -export const grade2Color = new Map([ - ['A', '#87cefa'], - ['B', '#90ee90'], - ['C', '#ffff00'], - ['D', '#ffa07a'], - ['F', '#cd5c5c'], - ['I', '#d3d3d3'], - ['W', '#9370D8'], - ['S', '#8fbc8f'], - ['NCR', '#d87093'], -]); +export enum LetterGrade { A, B, C, D, F, I, W, S, U, NCR } +export type Grade = keyof typeof LetterGrade; + +export const grade2Color: Record = { + 'A': '#87cefa', + 'B': '#90ee90', + 'C': '#ffff00', + 'D': '#ffa07a', + 'F': '#cd5c5c', + 'I': '#d3d3d3', + 'W': '#9370D8', + 'S': '#8fbc8f', + 'U': '#d87093', + 'NCR': '#d87093' +} // Based on https://github.com/cougargrades/web/blob/3d511fc56b0a90f2038883a71852245b726af7e3/src/components/instructors/GPABadge.js export function getGradeForGPA(n: number): Grade { diff --git a/components/blog.tsx b/components/blog.tsx index 3a78abe..aa8046e 100644 --- a/components/blog.tsx +++ b/components/blog.tsx @@ -106,12 +106,12 @@ export interface BlogNotice { environments?: (VercelEnv & '*')[]; } -function getNotices(feed: AtomFeed): BlogNotice[] { +function getNotices(feed?: AtomFeed): BlogNotice[] { if(feed !== undefined) { - const notices = feed.entries.filter(e => e.link.filter(e => e.title === 'notice').length > 0) + const notices = feed.entries.filter(e => e.link && e.link.filter(e => e.title === 'notice').length > 0) return notices.map(e => - e.link.filter(e => e.title === 'notice') + e.link!.filter(e => e.title === 'notice') .map((link, index) => ({ // extract what we can from the primary post information id: `${e.id}|${index}`, diff --git a/components/datatable.tsx b/components/datatable.tsx index b5c0ec7..d881f3b 100644 --- a/components/datatable.tsx +++ b/components/datatable.tsx @@ -154,14 +154,14 @@ export function EnhancedTable({ title, column {/* stableSort(rows, getComparator(order, orderBy)) */} {stableSort(rows, (a: T, b: T) => { - const col = columns.find(e => e.field === orderBy); + const col = columns.find(e => e.field === orderBy)!; // If we're ordering by a string or a number, we can use the built-in comparator if(['string','number'].includes(col.type)) { return order === 'desc' ? descendingComparator(a, b, orderBy) : -descendingComparator(a, b, orderBy); } else { // Otherwise, we have to rely on the user-supplied comparator - return order === 'desc' ? col.sortComparator(a, b) : -col.sortComparator(a, b); + return order === 'desc' ? col.sortComparator!(a, b)! : -col.sortComparator!(a, b); } }) .map(row => ( diff --git a/components/faqpostbody.tsx b/components/faqpostbody.tsx index 4b0867a..91e7bcc 100644 --- a/components/faqpostbody.tsx +++ b/components/faqpostbody.tsx @@ -1,6 +1,6 @@ import markdownStyles from './faqpostbody.module.scss' -export function FaqPostBody({ content }) { +export function FaqPostBody({ content }: { content: string }) { return (
('https://github-org-stats-au5ton.vercel.app/api/sponsors'); + const { data, error, isLoading } = useSWR('https://github-org-stats-au5ton.vercel.app/api/sponsors'); + const status: ObservableStatus = error ? 'error' : (isLoading || !data) ? 'loading' : 'success' const { commitHash, version, buildDate, vercelEnv } = buildArgs; // hopefully works return (
@@ -64,9 +66,9 @@ export default function Footer(props: { hideDisclaimer?: boolean }) {

{ - isValidating ? '' : - `${data.totalSponsorCount} people sponsor CougarGrades - totalling $${data.monthlyEstimatedSponsorsIncomeFormatted} per month. Thank you!` + status === 'success' ? + `${data!.totalSponsorCount} people sponsor CougarGrades + totalling $${data!.monthlyEstimatedSponsorsIncomeFormatted} per month. Thank you!` : '' }

diff --git a/components/groupcontent.module.scss b/components/groupcontent.module.scss index 2d33cfe..5201700 100644 --- a/components/groupcontent.module.scss +++ b/components/groupcontent.module.scss @@ -20,6 +20,7 @@ .chartWrap { overflow: auto; + margin-bottom: 30px; div { display: inline-block } diff --git a/components/groupcontent.tsx b/components/groupcontent.tsx index aa0c498..542ddfd 100644 --- a/components/groupcontent.tsx +++ b/components/groupcontent.tsx @@ -6,35 +6,40 @@ import Chip from '@mui/material/Chip' import Skeleton from '@mui/material/Skeleton' import Typography from '@mui/material/Typography' import useMediaQuery from '@mui/material/useMediaQuery' -import Tilty from 'react-tilty' +import Tilty from '@au5ton/react-tilty' +import { Chart } from 'react-google-charts' import { InstructorCard, InstructorCardSkeleton } from './instructorcard' -import { GroupResult } from '../lib/data/useAllGroups' +import { GroupResult, PopulatedGroupResult } from '../lib/data/useAllGroups' import { CoursePlus, useGroupData } from '../lib/data/useGroupData' import { Carousel } from './carousel' import { EnhancedTable } from './datatable' import { CustomSkeleton } from './skeleton' +import { LoadingBoxIndeterminate } from './loading' import styles from './groupcontent.module.scss' import interactivity from '../styles/interactivity.module.scss' -import { LinearProgressWithLabel } from './uploader/progress' interface GroupContentProps { - data: GroupResult; + data: PopulatedGroupResult; } +// This can be expensive to run, so here's a simple toggle +export const ENABLE_GROUP_SECTIONS: boolean = false + export function GroupContent({ data }: GroupContentProps) { const router = useRouter() const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') - const { data: { topEnrolled, dataGrid, dataChart, sectionLoadingProgress }, status } = useGroupData(data) + const { data: data2, status } = useGroupData(data) + const { topEnrolled, dataGrid, dataChart, sectionLoadingProgress } = data2 ?? {}; const RELATED_COURSE_LIMIT = 4 < data.courses.length ? 4 : data.courses.length; - const REMAINING_COURSES = status === 'success' ? topEnrolled.length - RELATED_COURSE_LIMIT : data.courses.length - RELATED_COURSE_LIMIT; + const REMAINING_COURSES = status === 'success' ? topEnrolled!.length - RELATED_COURSE_LIMIT : data.courses.length - RELATED_COURSE_LIMIT; const LINK_TEXT = REMAINING_COURSES <= 0 || isNaN(REMAINING_COURSES) ? 'Show All' : `Show ${REMAINING_COURSES.toLocaleString()} More`; const isCoreGroup = data.categories.includes('#UHCoreCurriculum') // Used for prefetching all options which are presented useEffect(() => { - for(let item of topEnrolled) { + for(let item of topEnrolled!) { router.prefetch(item.href); } }, [topEnrolled]); @@ -55,36 +60,33 @@ export function GroupContent({ data }: GroupContentProps) { }

Most Enrolled

- { topEnrolled.length > 0 ? topEnrolled.slice(0,RELATED_COURSE_LIMIT).map(e => + { topEnrolled!.length > 0 ? topEnrolled!.slice(0,RELATED_COURSE_LIMIT).map(e => ) : Array.from(new Array(RELATED_COURSE_LIMIT).keys()).map(e => )} -

Data

- {/* { - (status !== 'error' && dataChart.data.length > 1) ? +

Data

+ { + ENABLE_GROUP_SECTIONS ? + (status === 'success' && dataChart!.data.length > 1) ?
} - data={dataChart.data} - options={dataChart.options} + data={dataChart!.data} + options={dataChart!.options} // prevent ugly red box when there's no data yet on first-mount chartEvents={[{ eventName: 'error', callback: (event) => event.google.visualization.errors.removeError(event.eventArgs[0].id) }]} />
: - - Loading {data.sections.length.toLocaleString()} sections... -
- -
-
- } */} + + : null + } title="Courses" - columns={status !== 'error' ? dataGrid.columns : []} - rows={status !== 'error' ? dataGrid.rows : []} + columns={status === 'success' ? dataGrid!.columns : []} + rows={status === 'success' ? dataGrid!.rows : []} defaultOrderBy="id" /> diff --git a/components/header.tsx b/components/header.tsx index f1a56f3..a19cba9 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -4,12 +4,12 @@ import Button from '@mui/material/Button' //import { useSigninCheck } from 'reactfire' import Search, { SearchBarSkeleton } from './search' import { Emoji } from './emoji' -import { FirestoreGuard } from '../lib/firebase' +import { LinkProps } from './link' import styles from './header.module.scss' import interactivity from '../styles/interactivity.module.scss' -export const NavLink = ({ href, children }) => ; +export const NavLink = ({ href, children }: LinkProps) => ; export default function Header() { //const { status, data: signInCheckResult } = useSigninCheck({ requiredClaims: { admin: true }}); @@ -26,6 +26,7 @@ export default function Header() { - }> + + {/* }> - + */} ) diff --git a/components/instructorcard.module.scss b/components/instructorcard.module.scss index 3716d76..390c5b8 100644 --- a/components/instructorcard.module.scss +++ b/components/instructorcard.module.scss @@ -22,12 +22,17 @@ } .badgeRow { + display: flex!important; + flex-direction: row; + justify-content: flex-start; + flex-wrap: wrap; + gap: 0.25rem; margin-bottom: 5px; } .badgeRowBadge { font-size: inherit!important; - margin-right: 0.25rem; + // margin-right: 0.25rem; } .badgeStackListItem { diff --git a/components/instructorcard.tsx b/components/instructorcard.tsx index 15a6c96..9eac177 100644 --- a/components/instructorcard.tsx +++ b/components/instructorcard.tsx @@ -106,16 +106,16 @@ export function InstructorCardShowMore({ courseName, data }: InstructorCardShowM return ( <> - - setOpen(true)}> - - - View all {courseName} instructors - - - - - + setOpen(true)}> + + + View all {courseName} instructors + + + + + +export const InternalLink = ({ href, children }: LinkProps) => -export const ExternalLink = ({ href, children }) => +export const ExternalLink = ({ href, children }: LinkProps) => -export const FakeLink = ({ href, children }) => { - return e.preventDefault()} className="nostyle">{children} +export const FakeLink = ({ href, children }: LinkProps) => { + return e.preventDefault()} className="nostyle">{children} } \ No newline at end of file diff --git a/components/loading.module.scss b/components/loading.module.scss new file mode 100644 index 0000000..910b9a5 --- /dev/null +++ b/components/loading.module.scss @@ -0,0 +1,9 @@ + +.loadingFlex { + display: flex!important; + flex-direction: column; + justify-content: center; + align-items: center; + padding-bottom: 10px; + row-gap: 10px; +} diff --git a/components/loading.tsx b/components/loading.tsx new file mode 100644 index 0000000..70e2b21 --- /dev/null +++ b/components/loading.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import Box from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' +import Typography from '@mui/material/Typography' +import { LinearProgressWithLabel } from './uploader/progress' + +import styles from './loading.module.scss' + + +export interface LoadingBoxLinearProgressProps { + title: string; + progress: number; +} + +export function LoadingBoxLinearProgress({ title, progress }: LoadingBoxLinearProgressProps) { + return ( + + {title} +
+ +
+
+ ) +} + +export interface LoadingBoxIndeterminateProps { + title: string; +} + +export function LoadingBoxIndeterminate({ title }: LoadingBoxIndeterminateProps) { + return ( + + {title} + + + ) +} + +export interface ErrorBoxIndeterminateProps { + errorMessage?: string; +} + +export function ErrorBoxIndeterminate({ errorMessage }: ErrorBoxIndeterminateProps) { + return ( + + An unexpected error occurred + + {errorMessage ?? 'Please try again later'} + + + โš ๏ธ + + + ) +} diff --git a/components/pageviewlogger.tsx b/components/pageviewlogger.tsx index 1a6ec2a..3cb2d15 100644 --- a/components/pageviewlogger.tsx +++ b/components/pageviewlogger.tsx @@ -1,12 +1,39 @@ -import { useEffect } from 'react' +import { useEffect, useRef, useState } from 'react' import { useRouter } from 'next/router' -import { useAnalyticsRef } from '../lib/hook' +import { AnalyticsProvider, FirebaseAppProvider, useAnalytics, useFirebaseApp } from 'reactfire' +import { getAnalytics, logEvent, isSupported, Analytics } from 'firebase/analytics' +//import { useAnalyticsRef } from '../lib/hook' import { buildArgs, firebaseConfig } from '../lib/environment' +import { useAnalyticsRef } from '../lib/firebase' + + + +// export function PageViewLogger() { +// const app = useFirebaseApp() +// const [wasSupported, setWasSupported] = useState(false) + +// useEffect(() => { +// (async () => { +// setWasSupported(await isSupported()) +// })(); +// }, []) + +// if (wasSupported) { +// return ( +// +// +// +// ) +// } + +// return <> +// } /** * https://firebase.google.com/docs/app-check/web/recaptcha-provider?authuser=0 */ export function PageViewLogger() { + //const analytics = useAnalytics() const analyticsRef = useAnalyticsRef() const router = useRouter() @@ -15,9 +42,12 @@ export function PageViewLogger() { if(buildArgs.vercelEnv !== 'development') { const analytics = analyticsRef.current console.log(`event logged: ${router.asPath}`); - analytics.logEvent('page_view', { - path_name: router.asPath, - }); + logEvent(analytics, 'page_view', { + page_path: router.asPath + }) + // analytics.logEvent('page_view', { + // path_name: router.asPath, + // }); } } }, [router.asPath, analyticsRef]); diff --git a/components/panko.tsx b/components/panko.tsx index d376897..5825368 100644 --- a/components/panko.tsx +++ b/components/panko.tsx @@ -10,6 +10,7 @@ import { copyText } from '../vendor/clipboard' import { useIsMobile } from '../lib/hook' import { Emoji } from './emoji' import { BlogNotifications } from './blog' +import { POPULAR_TABS } from '../lib/top_front' import styles from './panko.module.scss' import interactivity from '../styles/interactivity.module.scss' @@ -56,7 +57,7 @@ export function PankoRow() { } useEffect(() => { - if(navigator.share) { + if('share' in navigator) { const inUserAgent = (x: string) => navigator.userAgent.toLowerCase().indexOf(x.toLocaleLowerCase()) >= 0 const isMac = inUserAgent('Macintosh'); @@ -136,6 +137,9 @@ export function generateBreadcrumbs(path: string) { if(value.toLowerCase() === 'faq') { return FAQ } + if(value.toLowerCase() === 'top') { + return Popular + } } if(index === 2) { if(array[1].toLowerCase() === 'g') { @@ -144,11 +148,14 @@ export function generateBreadcrumbs(path: string) { if(array[1].toLowerCase() === 'faq') { return {capitalizeFirstLetter(decodeURI(value).split('-').join(' '))} } + if(array[1].toLowerCase() === 'top') { + return {POPULAR_TABS.find(e => e.slug === value)?.title ?? '???'} + } } return {decodeURI(value)} }) } -function capitalizeFirstLetter(string) { +function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } diff --git a/components/search.module.scss b/components/search.module.scss index a5e9e0e..82ac70f 100644 --- a/components/search.module.scss +++ b/components/search.module.scss @@ -23,9 +23,13 @@ } .badgeList { - span { - margin-left: 0.25rem; - } + display: flex; + flex-direction: row; + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.25rem; + //font-size: 0.9em; + padding-left: 0.5rem; } .highlight { diff --git a/components/search.tsx b/components/search.tsx index cd7b7b6..6c646b5 100644 --- a/components/search.tsx +++ b/components/search.tsx @@ -2,16 +2,18 @@ import React, { useState, useEffect, useRef } from 'react' import { useRouter } from 'next/router' import { useRecoilState } from 'recoil' import TextField from '@mui/material/TextField' -import Autocomplete from '@mui/material/Autocomplete' +import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete' import Backdrop from '@mui/material/Backdrop' import CircularProgress from '@mui/material/CircularProgress' import Highlighter from 'react-highlight-words' import NProgress from 'nprogress' +import { logEvent } from 'firebase/analytics' import { searchInputAtom } from '../lib/recoil' import { SearchResult, useSearchResults } from '../lib/data/useSearchResults' import { Badge } from './badge' import { isMobile } from '../lib/util' -import { useAnalyticsRef } from '../lib/hook' +import { useAnalyticsRef } from '../lib/firebase' + import styles from './search.module.scss' @@ -43,7 +45,7 @@ export default function SearchBar() { // For state management const [open, setOpen] = useState(false); - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); const [inputValue, setInputValue] = useState(''); // For actually performing searches const { data, status } = useSearchResults(inputValue) @@ -52,7 +54,7 @@ export default function SearchBar() { // For analytics useEffect(() => { if(analyticsRef.current !== null && inputValue.length > 0) { - analyticsRef.current.logEvent('search', { + logEvent(analyticsRef.current, 'search', { search_term: inputValue }) } @@ -62,7 +64,7 @@ export default function SearchBar() { const router = useRouter(); // Used for displaying rerouting progress bar useEffect(() => { - const handleStart = (url) => { + const handleStart = () => { console.time('reroute') NProgress.start() } @@ -84,19 +86,21 @@ export default function SearchBar() { // Used for prefetching all options which are presented useEffect(() => { - for(let item of data) { - router.prefetch(item.href); + if (data) { + for(let item of data) { + router.prefetch(item.href); + } } }, [data]); // Used for actually issuing the redirect - const handleChange = (_, x: string | SearchResult) => { + const handleChange: AutocompleteProps['onChange'] = (event, x) => { if(typeof x !== 'string' && x !== null) { // update the state setInputValue(x.title) setValue(null) // unselect the searchbar after choosing a result - elementRef.current.blur() + elementRef.current?.blur() // redirect to the selected result router.push(x.href, undefined, { scroll: false }) } @@ -156,7 +160,7 @@ export default function SearchBar() { isOptionEqualToValue={(option, value) => option.key === value.key} getOptionLabel={(option) => typeof option === 'string' ? option : option.title} groupBy={(option) => option.group} - options={data} + options={data ?? []} loading={loading} filterOptions={(x) => x} renderOption={(props, option) => } diff --git a/components/skeleton.tsx b/components/skeleton.tsx index f0d63ef..12508a4 100644 --- a/components/skeleton.tsx +++ b/components/skeleton.tsx @@ -1,19 +1,21 @@ import Skeleton from '@mui/material/Skeleton' import styles from './skeleton.module.scss' +import type { Property } from 'csstype' interface SkeletonProps { width: string | number; height: string | number; margin?: number; + display?: Property.Display; } const DEFAULT_MARGIN = 4 -export const CustomSkeleton = ({ width, height, margin }: SkeletonProps) => ( +export const CustomSkeleton = ({ width, height, margin, display }: SkeletonProps) => ( diff --git a/components/uploader/AsyncFileReader.ts b/components/uploader/AsyncFileReader.ts deleted file mode 100644 index 78c2afa..0000000 --- a/components/uploader/AsyncFileReader.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Patchfile } from '@cougargrades/types/dist/Patchfile'; -import * as is from '@cougargrades/types/dist/is'; - -export type FileReaderMode = 'text' | 'dataURL' | 'binaryString' | 'arrayBuffer'; -export type FileReaderResult = string | ArrayBuffer | null; - -export function readFile(file: File, mode: FileReaderMode): Promise { - return new Promise((resolve, reject) => { - // make file reader - const reader = new FileReader(); - - // setup callbacks - reader.onabort = (err) => reject(err) - reader.onerror = (err) => reject(err) - reader.onload = () => { - // Do whatever you want with the file contents - resolve(reader.result) - } - - if(mode === 'text') { - reader.readAsText(file); - } - else if(mode === 'dataURL') { - reader.readAsDataURL(file); - } - else if(mode === 'binaryString') { - reader.readAsBinaryString(file); - } - else if(mode === 'arrayBuffer') { - reader.readAsArrayBuffer(file); - } - }); -} - -export const readFileAsText = (file: File) => readFile(file, 'text'); -export const readFileAsDataURL = (file: File) => readFile(file, 'dataURL'); -export const readFileAsBinaryString = (file: File) => readFile(file, 'binaryString'); -export const readFileAsArrayBuffer = (file: File) => readFile(file, 'arrayBuffer'); - -export async function readPatchfile(file: File): Promise { - try { - const contents = await readFileAsText(file); - if(typeof contents === 'string') { - const decoded = JSON.parse(contents); - return is.Patchfile(decoded) ? decoded : null; - } - else { - return null; - } - } - catch(err) { - return null; - } -} diff --git a/components/uploader/dropzone.module.scss b/components/uploader/dropzone.module.scss deleted file mode 100644 index 396c6f1..0000000 --- a/components/uploader/dropzone.module.scss +++ /dev/null @@ -1,16 +0,0 @@ -.dropzone { - display: flex; - flex-direction: column; - align-items: center; - padding: 1.5rem; // 20px - border-radius: 0.5rem; // 2 - border: 2px dashed rgb(200, 200, 200, 0.4); - background-color: rgba(200,200,200,0.2); - color: #a5a5a5; - outline: none; - transition: border .24s ease-in-out, background-color .24s ease-in-out; - - &:active { - border-color: #00e676; - } -} \ No newline at end of file diff --git a/components/uploader/dropzone.tsx b/components/uploader/dropzone.tsx deleted file mode 100644 index 8782851..0000000 --- a/components/uploader/dropzone.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, { useMemo } from 'react' -import { useDropzone } from 'react-dropzone' - -import styles from './dropzone.module.scss' - -export function Dropzone(props: { onDrop: (acceptedFiles: File[]) => void }) { - const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ onDrop: props.onDrop }); - - const style = useMemo(() => { - const activeStyle: React.CSSProperties = { - borderColor: '#2196f3' - }; - - const acceptStyle: React.CSSProperties = { - borderColor: '#00e676', - backgroundColor: 'rgba(0, 230, 119, 0.2)' - }; - - const rejectStyle: React.CSSProperties = { - borderColor: '#ff1744' - }; - - return ({ - ...(isDragActive ? activeStyle : {}), - ...(isDragAccept ? acceptStyle : {}), - ...(isDragReject ? rejectStyle : {}) - }); - }, [ isDragActive, isDragAccept, isDragReject ]); - - return ( -
- - { - isDragActive ? -
Drop the files here
: -
Drag and drop some files here, or click to select files
- } -
- ); -} \ No newline at end of file diff --git a/components/uploader/queuemanager.tsx b/components/uploader/queuemanager.tsx deleted file mode 100644 index 37ae8f2..0000000 --- a/components/uploader/queuemanager.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { useState } from 'react' -import { useFirestore } from 'reactfire' -import Button from '@mui/material/Button' -import ArrowForwardIcon from '@mui/icons-material/ArrowForward' -import { LinearProgressWithLabel } from './progress' - -//import styles from './uploader.module.scss' - -export function QueueManager() { - /** - * Firebase stuff - */ - const firestore = useFirestore(); - const FieldValue = useFirestore.FieldValue; - - /** - * UI - */ - const [uploadQueueRecycleInProgress, setUploadQueueRecycleInProgress] = useState(false) - const [uploadQueueRecycleDone, setUploadQueueRecycleDone] = useState(0); - const [uploadQueueRecycleMax, setUploadQueueRecycleMax] = useState(0); - const [uploadQueueTripInProgress, setUploadQueueTripInProgress] = useState(false) - const [uploadQueueTripDone, setUploadQueueTripDone] = useState(0); - const [uploadQueueTripMax, setUploadQueueTripMax] = useState(0); - - const handleClick = async () => { - console.log('clicked') - setUploadQueueRecycleInProgress(true) - // get upload_queue docs - const upload_queue_snap = await firestore.collection('upload_queue').get(); - setUploadQueueRecycleMax(upload_queue_snap.size) - // save a copy of the data - const data = upload_queue_snap.docs.map(e => e.data()) - // delete items in upload_queue - for(const doc of upload_queue_snap.docs) { - await doc.ref.delete() - } - // add extracted data to backlog - for(const item of data) { - await firestore.collection('upload_queue_backlog').add(item) - setUploadQueueRecycleDone(x => x + 1) - } - setUploadQueueRecycleInProgress(false) - } - - const handleClick2 = async () => { - console.log('clicked') - setUploadQueueTripInProgress(true) - // get upload_queue docs - const upload_queue_snap = await firestore.collection('upload_queue').get(); - setUploadQueueTripMax(upload_queue_snap.size) - // save a copy of the data - const data = upload_queue_snap.docs.map(e => e.data()) - // delete items in upload_queue - for(const doc of upload_queue_snap.docs) { - await doc.ref.delete() - } - // add extracted data to backlog - for(const item of data) { - await firestore.collection('upload_queue_backlog').add(item) - setUploadQueueTripDone(x => x + 1) - } - setUploadQueueTripInProgress(false) - } - - return ( -
-

Database Utilities

-
- {/* upload_queue recycle */} - -

- Recycled {uploadQueueRecycleDone} of {uploadQueueRecycleMax} -

-
- - -
- {/* upload_queue trip */} - -

- Tripped {uploadQueueTripDone} of {uploadQueueTripMax} -

-
- - - {/* boop */} -
- ); -} \ No newline at end of file diff --git a/components/uploader/uploader.module.scss b/components/uploader/uploader.module.scss deleted file mode 100644 index e69de29..0000000 diff --git a/components/uploader/uploader.tsx b/components/uploader/uploader.tsx deleted file mode 100644 index c863378..0000000 --- a/components/uploader/uploader.tsx +++ /dev/null @@ -1,575 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react' -import prettyBytes from 'pretty-bytes' -import TimeAgo from 'timeago-react' -import * as timeago from 'timeago.js' -import { TransitionGroup, CSSTransition } from 'react-transition-group' -import Papa from 'papaparse' -import { useFirestore } from 'reactfire' -import type { GradeDistributionCSVRow } from '@cougargrades/types/dist/GradeDistributionCSVRow' -import { tryFromRaw } from '@cougargrades/types/dist/GradeDistributionCSVRow' -import { executePatchFile } from '@cougargrades/types/dist/PatchfileUtil' -import Button from '@mui/material/Button' -import TextField from '@mui/material/TextField' -import Switch from '@mui/material/Switch' -import FormControlLabel from '@mui/material/FormControlLabel' -import Alert from '@mui/material/Alert' -import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' -import { Dropzone } from './dropzone' -import { LinearProgressWithLabel, SliderWithLabel } from './progress' -import { readPatchfile } from './AsyncFileReader' -import { AsyncSemaphore } from './AsyncSemaphore' -import { localeFunc } from './timeago' - -//import styles from './uploader.module.scss' - -export function Uploader() { - timeago.register('en_US_custom', localeFunc); - - /** - * UI - */ - const [uploadStartedAt, setUploadStartedAt] = useState(new Date(0)); - const [uploadFinishedAt, setUploadFinishedAt] = useState(new Date(0)); - const [recordProcessingStartedAt, setRecordProcessingStartedAt] = useState(new Date(0)); - const [recordProcessingEstimate, setRecordProcessingEstimate] = useState(new Date(0)); - const [recordProcessingFinishedAt, setRecordProcessingFinishedAt] = useState(new Date(0)); - const [patchfileProcessingStartedAt, setPatchfileProcessingStartedAt] = useState(new Date(0)); - const [patchfileProcessingEstimate, setPatchfileProcessingEstimate] = useState(new Date(0)); - const [patchfileProcessingFinishedAt, setPatchfileProcessingFinishedAt] = useState(new Date(0)); - const [isButtonEnabled, setIsButtonEnabled] = useState(false); - const [allowUploading, setAllowUploading] = useState(false); - const [showBundleInfo, setShowBundleInfo] = useState(false); - const [showPendingUploads, setShowPendingUploads] = useState(false); - - /** - * Upload logic - */ - // File handles - const [recordsFile, setRecordsFile] = useState(); - const [patchFiles, setPatchFiles] = useState([]); - - // number of rows processed so far - const [uploadQueueProcessed, setUploadQueueProcessed] = useState(0); - // total number of rows - const [uploadQueueMax, setUploadQueueMax] = useState(0); - // number of PF processed so far - const [patchfilesProcessed, setPatchfilesProcessed] = useState(0); - // total number of PF - const [patchfilesMax, setPatchfilesMax] = useState(0); - // current phase of patchfiles - const [patchfilesPhase, setPatchfilesPhase] = useState(-1); - // max number of phases - const [patchfilesMaxPhase, setPatchfilesMaxPhase] = useState(-1); - // max number to do in parallel - //const [patchfileConcurrencyLimit, setPatchfileConcurrencyLimit] = useState(64); - // max number to do in parallel (per phase) - const [patchfileConcurrencyLimitPerPhase, setPatchfileConcurrencyLimitPerPhase] = useState<{ prefix: string, limit: number}[]>([]); - // number of patchfiles which failed to execute - const [patchfilesFailed, setPatchfilesFailed] = useState(0); - // easier to list why a patchfile failed - const [patchfilesFailedReasons, setPatchfilesFailedReasons] = useState([]); - - // max number to do in parallel - const [recordConcurrencyLimit, setRecordConcurrencyLimit] = useState(64); - // current amount pending - const [recordConcurrencyCount, setRecordConcurrencyCount] = useState(0); - // has the original snapshot loaded? - const [initialSnapshotLoaded, setInitialSnapshotLoaded] = useState(false); - // firebase document IDs of the pending uploads - const [pendingUploads, setPendingUploads] = useState([]); - // decoded CSV - const [loadedRecords, setLoadedRecords] = useState([]); - - /** - * Firebase stuff - */ - const firestore = useFirestore(); - const FieldValue = useFirestore.FieldValue; - useEffect(() => { - /** - * Subscribe to the upload_queue collection - * - * We only want to subscribe once because we don't want to miss uploads. However, in order for React to - * only run something once, we need to do `useEffect(..., [])` with an empty array. This means that whenever - * `.onShapshot()` is called, we're reading the old/out-of-date values of the state. This means that in - * `onCollectionSnapshot` we can only do WRITE operations, but no read operations. To respond to these write - * operations with current/up-to-date state data, we use a separate `useEffect()` hook. - */ - - console.log('another!', firestore); - - const unsubscribe = firestore.collection('upload_queue').onSnapshot(onCollectionSnapshot); - - return () => { unsubscribe() }; - }, [ firestore ]); - useEffect(() => { - /** - * Do the same thing as above, but for the backlog - */ - const unsubscribe = firestore.collection('upload_queue_backlog').onSnapshot(onAddedToBacklog); - - return () => { unsubscribe() }; - }, [ firestore ]); - - /** - * Methods - */ - - // What happens when the Upload button is clicked - const handleClick = () => { - // run phase 0 before doing courses - if(patchfilesMaxPhase > -1) { - setPatchfilesPhase(0); - } - else { - setAllowUploading(true); - } - // This allows us to benchmark upload times - setUploadStartedAt(new Date()); - setRecordProcessingStartedAt(new Date()); - // This triggers the useEffect() that listens to `allowUploading`, which will start the upload queue - // setAllowUploading(true); // this is done after phase 0 completes instead - setIsButtonEnabled(false); - }; - - // What happens when files are dropped into the page - const handleDrop = useCallback((acceptedFiles: File[]) => { - /** - * we identify what a file is depending on its name, - * because we don't have access to its full path - * see: - * - https://stackoverflow.com/a/23005925/2275818 - * - https://developer.mozilla.org/en-US/docs/Web/API/File - * - * then we save references to the native File so we can read it later - */ - - // Update UI - setIsButtonEnabled(false); - setShowBundleInfo(true); - - // Save reference to records - acceptedFiles.forEach(file => { - // identify primary CSV data source - if(file.name.startsWith('records') && file.name.endsWith('.csv')) { - setRecordsFile(_ => { - // parse CSV file - Papa.parse(file, { - worker: true, - header: true, - dynamicTyping: false, - skipEmptyLines: true, - complete: results => { - // Reformat and validate decoded CSV - let validated = results.data - .map(e => tryFromRaw(e)) - .filter((e): e is GradeDistributionCSVRow => e !== null); - - // Update state - setLoadedRecords(v => [...validated]); - setUploadQueueMax(validated.length); - setIsButtonEnabled(true); - }, - }); - return file; - }); - } - }); - - // Set only the patchfiles, and in the correct order - const temp = acceptedFiles - .filter(e => e.name.startsWith('patch-') && e.name.endsWith('.json')) - .sort((a,b) => a.name.localeCompare(b.name)); - - if(temp.length > 0) { - // get final patchfile phase - setPatchfilesMaxPhase(x => Number( // convert to a number - temp[temp.length - 1] // last patchfile after sorting - .name // access name - .split('-') // [ "patch", "0", "groupdefaults", "1617828381961927207.json" ] - [1] // access phase - )); - } - - // Save references to patchfiles - setPatchFiles(pf => [ ...temp ]); - setPatchfilesMax(temp.length) - setIsButtonEnabled(true); - }, []); - - // Return a random entry from the loaded records, then delete it from our memory - const popRecordFromQueue = (): GradeDistributionCSVRow => { - // generate random index - let idx = Math.floor(Math.random() * loadedRecords.length); - // fetch random index - let copy = loadedRecords[idx]; - // remove random index - setLoadedRecords(x => [...x.slice(0, idx).concat(x.slice(idx + 1))]); - // send back - return copy; - }; - - /** - * What happens whenever a new file enters or leaves the server-side processing queue - * NOTE: ALL STATE READS FROM THIS CONTEXT WILL BE OUT OF DATE. - * (see "Firebase stuff" above for details) - */ - const onCollectionSnapshot = (snapshot: firebase.default.firestore.QuerySnapshot) => { - // get the document IDs of the additions and removals - let removals = snapshot.docChanges().filter(e => e.type === 'removed').map(e => e.doc.id); - let additions = snapshot.docChanges().filter(e => e.type === 'added').map(e => e.doc.id); - // adjust pending upload queue - setPendingUploads(x => { - let copy = x.slice(); - // remove all removals - for(let r of removals) { - copy.splice(copy.indexOf(r), 1); - } - // add all additions - return [...copy, ...additions]; - }); - - // Update count - setRecordConcurrencyCount(x => x + additions.length - removals.length); - setUploadQueueProcessed(x => x + removals.length); - setInitialSnapshotLoaded(true); - }; - - /** - * Same as above - */ - const onAddedToBacklog = async (snapshot: firebase.default.firestore.QuerySnapshot) => { - // we want all the additions - let additions = snapshot.docChanges().filter(e => e.type === 'added'); - - // validate additions by attempting to map back to GradeDistributionCSVRow - let validated = additions - .map(e => tryFromRaw(e)) - .filter((e): e is GradeDistributionCSVRow => e !== null); - - // actually attempt to delete these from the upload_queue_backlog, because we've already staged them to be added again - for(let item of additions) { - await item.doc.ref.delete(); - } - - // Update state - setLoadedRecords(v => [...v, ...validated]); // add backlogged records to end of loaded records - setUploadQueueMax(x => x + validated.length); // expand the length of the queue because we're executing these again - }; - - /** - * Ran whenever one of these state values change - * Respond to vacancies in the upload_queue - */ - useEffect(() => { - // if the first snapshot has loaded already, then it's safe to trust the state data we have access to - if(initialSnapshotLoaded) { - // computes number of times we can add to the current queue - console.log('recordConcurrencyLimit: ', recordConcurrencyLimit); - console.log('recordConcurrencyCount: ', recordConcurrencyCount); - const can_upload_next = recordConcurrencyLimit - recordConcurrencyCount; - console.log('can_upload_next: ', can_upload_next); - /** - * if: - * - there is room in the upload_queue - * - if we have records loaded - * - if we're allowed to upload - */ - if(can_upload_next > 0 && loadedRecords.length > 0 && allowUploading) { - // actually do the firestore upload - firestore.collection('upload_queue').doc().set(popRecordFromQueue()); - } - console.log('---'); - } - }, [recordConcurrencyCount, recordConcurrencyLimit, initialSnapshotLoaded, allowUploading]); - - /** - * Detect when records upload is completed - */ - useEffect(() => { - // estimate when upload will be completed - // (TimeTaken / linesProcessed) * linesLeft = timeLeft - const now = new Date().valueOf(); - const timeTaken = now - recordProcessingStartedAt.valueOf(); - const itemsRemaining = uploadQueueMax - uploadQueueProcessed; - const timeRemaining = (timeTaken / uploadQueueProcessed) * itemsRemaining; - setRecordProcessingEstimate(new Date(now + timeRemaining)); - - if(uploadStartedAt.valueOf() > 0 && ((uploadQueueMax > 0 && uploadQueueProcessed === uploadQueueMax) || (uploadQueueMax === 0))) { - console.log('UPLOAD FINISHED MAYBE?'); - - // mark records as completed - setRecordProcessingFinishedAt(new Date()); - - // update patchFile phase to begin processing patchfiles - if(patchfilesPhase === 0 && patchfilesMaxPhase > 0) { - setPatchfilesPhase(1) - } - else { - console.log('no patchfiles to process after records'); - // mark all uploads as finished - setUploadFinishedAt(new Date()); - setPatchfileProcessingFinishedAt(new Date()); - } - setPatchfileProcessingStartedAt(new Date()); - } - }, [ uploadQueueProcessed, uploadQueueMax, uploadStartedAt, patchfilesPhase, patchfilesMaxPhase ]); - - /** - * Execute an individual patchfile - */ - const processPatchfile = async (file: File) => { - //console.log(`reading: ${file.name}`); - const contents = await readPatchfile(file); - //console.log(`read: ${file.name}`); - if(contents !== null) { - //console.log(`executing: ${file.name}`); - try { - await executePatchFile(firestore, FieldValue as any, contents); - setPatchfilesProcessed(x => x + 1); - } - catch(err) { - console.warn('Patchfile failed: ', err); - setPatchfilesProcessed(x => x + 1); - setPatchfilesFailed(x => x + 1); - setPatchfilesFailedReasons(x => [...x, `For target '${contents.target.path}' in phase=${patchfilesPhase}: ${err}`]) - } - //console.log(`DONE: ${file.name}`); - } - } - - /** - * Execute each Patchfile phase - */ - useEffect(() => { - (async () => { - // phases start at 0 - if(patchfilesPhase >= 0 && patchfilesPhase <= patchfilesMaxPhase) { - // only run phases which have patchfiles, skip phases that are missing them - if(patchfileConcurrencyLimitPerPhase[patchfilesPhase] !== undefined) { - //console.log(patchfilesPhase, 'well defined') - const filesForCurrentPhase = patchFiles.filter(e => e.name.startsWith(`patch-${patchfilesPhase}`)); - - // parallel processing - const semaphore = new AsyncSemaphore(patchfileConcurrencyLimitPerPhase[patchfilesPhase].limit); - - for(let file of filesForCurrentPhase) { - await semaphore.withLockRunAndForget(async () => await processPatchfile(file)); - } - - await semaphore.awaitTerminate(); - console.log(`phase ${patchfilesPhase} queue done!`); - - // remove current phase to prevent double executions - setPatchFiles(x => [...patchFiles.filter(e => ! e.name.startsWith(`patch-${patchfilesPhase}`))]); - } - else { - console.log('phase skipped: files for phase', patchfilesPhase, 'are missing'); - } - - // kick off to process next phase, but only - if(patchfilesPhase < patchfilesMaxPhase) { - // but only if we've already done phase 0 (special phase) - if(patchfilesPhase >= 1) { - console.log('attempting kick off of next phase: ', patchfilesPhase + 1); - setPatchfilesPhase(x => x + 1); - } - else { - console.log(`phase kickoff skipped (finished phase ${patchfilesPhase})`) - setAllowUploading(true); - } - } - else { - console.log('no more phases!'); - // mark all uploads as finished - setUploadFinishedAt(new Date()); - setPatchfileProcessingFinishedAt(new Date()); - } - } - })(); - // do nothing if phase == -1 - }, [ patchfilesPhase, patchfilesMaxPhase, patchfileConcurrencyLimitPerPhase ]) - - /** - * Estimate when Patchfile queue will be completed - */ - useEffect(() => { - // (TimeTaken / linesProcessed) * linesLeft = timeLeft - const now = new Date().valueOf(); - const timeTaken = now - patchfileProcessingStartedAt.valueOf(); - const itemsRemaining = patchfilesMax - patchfilesProcessed; - const timeRemaining = (timeTaken / patchfilesProcessed) * itemsRemaining; - setPatchfileProcessingEstimate(new Date(now + timeRemaining)); - }, [patchfilesProcessed, patchfilesMax, patchfileProcessingStartedAt]) - - /** - * - */ - useEffect(() => { - if(patchfilesMaxPhase >= 0 && patchfilesPhase === -1) { - // temporarily get sorted list of patchfiles - const temp = patchFiles.slice().sort((a,b) => a.name.localeCompare(b.name)); - // [ "patch", "0", "groupdefaults", "1617828381961927207.json" ] - const prefixes = Array.from(new Set(temp.map(e => { - const phase = e.name.split('-')[1] - const prefix = e.name.split('-')[2] - return `${phase}-${prefix}` - }))) - // properly set the limits per phase if phases are missing - const limitPerPhase: { prefix: string, limit: number}[] = []; - for(const item of prefixes) { - const [phase, prefix] = item.split('-'); - limitPerPhase[parseInt(phase)] = { prefix, limit: 64 }; - } - - setPatchfileConcurrencyLimitPerPhase([...limitPerPhase]) - } - }, [patchFiles, patchfilesPhase, patchfilesMaxPhase]) - - return ( -
-

Database Uploader

-

To use, grab the latest public data bundle, unzip it, and drop the files onto this webpage.

-

Public data bundles have a lot of files, so this will likely lag your web browser.

- - { - ! showBundleInfo ? <> : - <> - {/* File info area */} -
{Number(uploadQueueMax).toLocaleString()} rows ({prettyBytes(recordsFile ? recordsFile.size : 0)}), {Number(patchfilesMax).toLocaleString()} Patchfiles ({prettyBytes(patchFiles.length > 0 ? patchFiles.map(e => e.size).reduce((sum, x) => sum + x) : 0)})
-
recordsFile: {recordsFile?.name}
-
- {/* Input area */} -
- setRecordConcurrencyLimit(parseInt(e.target.value))} - /> -
-
-
Patchfile Concurrency Limits
- - Maximum number of patchfiles to process concurrently (the initial value of the semaphore), customizable per phase. Default: 64 - - {patchfileConcurrencyLimitPerPhase.map(({ prefix, limit }, index) => ( - -1} - defaultValue={64} - value={limit} - onChange={(_, value) => setPatchfileConcurrencyLimitPerPhase(x => { - const temp = x.slice(); - temp[index].limit = value as number; - return temp; - })} - min={1} - max={64} - step={4} - marks - /> - ))} - { - patchfileConcurrencyLimitPerPhase.length > 0 ? <> : - - Patchfiles not added - - } -
-
-
- setShowPendingUploads(e.target.checked)} />} label="Show pending uploads animation" /> -
-
-
- -

- { - uploadStartedAt.valueOf() === 0 || uploadFinishedAt.valueOf() > 0 ? <> : - <> - Upload started at {uploadStartedAt.toLocaleString()} (). -
- - } - { - uploadFinishedAt.valueOf() === 0 ? <> : - <> - Upload finished . - - } -

-
- {/* Upload status area */} - { - uploadStartedAt.valueOf() === 0 ? <> : - <> -

Uploading records

- - {/* {`${Math.round(uploadQueueProcessed/uploadQueueMax*100)}%`} */} - -

Executing Patchfiles

- - {/* {`${Math.round(patchfilesProcessed/patchfilesMax*100)}%`} */} - - { - patchfilesFailed === 0 ? <> : - <> - {patchfilesFailed} Patchfiles failed to execute. -
- See reasons -
    - { patchfilesFailedReasons.map((value, index) =>
  • {value}
  • )} -
-
- - } -
Debugging
-
    -
  • recordConcurrencyCount: {recordConcurrencyCount}
  • -
  • patchfilesPhase: {patchfilesPhase}
  • -
  • patchfilesMaxPhase: {patchfilesMaxPhase}
  • -
- { - ! showPendingUploads ? <> : - <> -

Active Uploads

-
    - - {pendingUploads.slice().sort().map(id => ( - -
  • {id}
  • -
    - ))} -
    -
- - } - - } - - } -
- ); -} \ No newline at end of file diff --git a/i18n/en_US.ts b/i18n/en_US.ts index 26931dc..ec43205 100644 --- a/i18n/en_US.ts +++ b/i18n/en_US.ts @@ -4,10 +4,10 @@ export const en_US = { "hello": "Hello", "meta": { "course": { - "description": ({ courseName, description }) => isEmpty(courseName) ? 'A course at the University of Houston. View grade distribution data at CougarGrades.io.' : `${courseName} ${! isEmpty(description) ? `(${description}) ` : ''}is a course at the University of Houston. View grade distribution data at CougarGrades.io.` + "description": ({ courseName, description }: any) => isEmpty(courseName) ? 'A course at the University of Houston. View grade distribution data at CougarGrades.io.' : `${courseName} ${! isEmpty(description) ? `(${description}) ` : ''}is a course at the University of Houston. View grade distribution data at CougarGrades.io.` }, "instructor": { - "description": ({instructorName, departmentText}) => isEmpty(instructorName) ? `An instructor at the University of Houston. View grade distribution data at CougarGrades.io.` : `${instructorName} is ${isEmpty(departmentText) || isVowel(departmentText) ? 'an ' : 'a ' }${`${unwrap(departmentText)} `}instructor at the University of Houston. View grade distribution data at CougarGrades.io.` + "description": ({instructorName, departmentText}: any) => isEmpty(instructorName) ? `An instructor at the University of Houston. View grade distribution data at CougarGrades.io.` : `${instructorName} is ${isEmpty(departmentText) || isVowel(departmentText) ? 'an ' : 'a ' }${`${unwrap(departmentText)} `}instructor at the University of Houston. View grade distribution data at CougarGrades.io.` }, "groups": { "description": "View courses which satisfy different areas of the UH Core Curriculum. View grade distribution data at CougarGrades.io." diff --git a/i18n/es.ts b/i18n/es.ts index fbde1f8..627b5f6 100644 --- a/i18n/es.ts +++ b/i18n/es.ts @@ -4,10 +4,10 @@ export const es = { "hello": "Hola", "meta": { "course": { - "description": ({ courseName, description }) => isEmpty(courseName) ? 'Un curso en la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.' : `${courseName} ${! isEmpty(description) ? `(${description}) ` : ''}es un curso de la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` + "description": ({ courseName, description }: any) => isEmpty(courseName) ? 'Un curso en la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.' : `${courseName} ${! isEmpty(description) ? `(${description}) ` : ''}es un curso de la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` }, "instructor": { - "description": ({instructorName, departmentText}) => isEmpty(instructorName) ? `Instructora de la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` : `${instructorName} es instructor ${departmentText !== '' ? `de ${departmentText} ` : ''}en la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` + "description": ({instructorName, departmentText}: any) => isEmpty(instructorName) ? `Instructora de la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` : `${instructorName} es instructor ${departmentText !== '' ? `de ${departmentText} ` : ''}en la Universidad de Houston. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io.` }, "groups": { "description": "Ver cursos que satisfacen diferentes รกreas del plan de estudios bรกsico de UH. Vea los datos de distribuciรณn de calificaciones en CougarGrades.io." diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000..1623c87 --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,9 @@ +import { buildArgs } from './environment'; +import { days_to_seconds, hours_to_seconds, minutes_to_seconds } from './to_seconds' + +export const PROD_CACHE_CONTROL = `public, must-revalidate, max-age=${days_to_seconds(1)}, s-maxage=${days_to_seconds(7)}, stale-while-revalidate=${days_to_seconds(14)}`; +export const PREVIEW_CACHE_CONTROL = `public, must-revalidate, max-age=${days_to_seconds(1)}, s-maxage=${days_to_seconds(3)}, stale-while-revalidate=${days_to_seconds(7)}`; +export const DEV_CACHE_CONTROL = `public, must-revalidate, max-age=${minutes_to_seconds(15)}, stale-while-revalidate=${hours_to_seconds(1)}`; + +const vercelEnv = buildArgs.vercelEnv +export const CACHE_CONTROL = vercelEnv === 'development' ? DEV_CACHE_CONTROL : vercelEnv === 'preview' ? PREVIEW_CACHE_CONTROL : PROD_CACHE_CONTROL; diff --git a/lib/data/Observable.ts b/lib/data/Observable.ts index 4b28ef7..1c494ba 100644 --- a/lib/data/Observable.ts +++ b/lib/data/Observable.ts @@ -12,7 +12,7 @@ export interface Observable { status: ObservableStatus; //hasEmitted: boolean; //isComplete: boolean; - data: T; + data: T | undefined; error: Error | undefined; //firstValuePromise: Promise; } \ No newline at end of file diff --git a/lib/data/back/getAllGroups.ts b/lib/data/back/getAllGroups.ts new file mode 100644 index 0000000..1d63c7d --- /dev/null +++ b/lib/data/back/getAllGroups.ts @@ -0,0 +1,54 @@ +import { Group } from '@cougargrades/types' +import { defaultComparator } from '../../../components/datatable' +import { firebase } from '../../firebase_admin' +import { AllGroupsResult, AllGroupsResultItem, ALL_GROUPS_SENTINEL, group2Result } from '../useAllGroups' + +export async function getAllGroups(): Promise { + const db = firebase.firestore() + const query = db.collection('groups').where('categories', 'array-contains', '#UHCoreCurriculum') + const querySnap = await query.get() + const data: Group[] = querySnap.docs.filter(e => e.exists).map(e => e.data() as Group); + + // sanitize output + for(let i = 0; i < data.length; i++) { + data[i].courses = [] + data[i].sections = [] + } + + // make categories + const categories = [ + ...( + Array.from(new Set(data.map(e => Array.isArray(e.categories) ? e.categories.filter(cat => !cat.startsWith('#')) : []).flat())) + .sort((a,b) => defaultComparator(a,b)) // [ '(All)', '(2022-2023)', '(2021-2022)', '(2020-2021)' ] + .slice(0,2) // don't endlessly list the groups, they're still accessible from a course directly + ), + //ALL_GROUPS_SENTINEL + ]; + + // make a key/value store of category -> GroupResult[] + const results = categories + .reduce((obj, key) => { + if(key === ALL_GROUPS_SENTINEL) { + // obj[key] = [ + // ...(status === 'success' ? data.filter(e => Array.isArray(e.categories) && e.categories.length === 0).map(e => group2Result(e)) : []) + // ]; + } + else { + obj[key] = [ + ...(data.filter(e => Array.isArray(e.categories) && e.categories.includes(key)).map(e => group2Result(e))) + ]; + } + return obj; + }, {} as AllGroupsResultItem); + + return { + categories, + results, + core_curriculum: [ + ...(data.filter(e => Array.isArray(e.categories) && e.categories.includes('UH Core Curriculum')).map(e => group2Result(e))) + ], + all_groups: [ + ...(data.filter(e => Array.isArray(e.categories) && ! e.categories.includes('UH Core Curriculum')).map(e => group2Result(e))) + ], + } +} diff --git a/lib/data/back/getCourseData.ts b/lib/data/back/getCourseData.ts new file mode 100644 index 0000000..db381c8 --- /dev/null +++ b/lib/data/back/getCourseData.ts @@ -0,0 +1,162 @@ +import { Course, Enrollment, Group, Instructor, Section, Util } from '@cougargrades/types'; +import { Grade, grade2Color } from '../../../components/badge'; +import { Column, descendingComparator } from '../../../components/datatable'; +import { getRosetta } from '../../i18n'; +import { getYear, seasonCode } from '../../util'; +import { getBadges } from '../getBadges'; +import { getChartData } from '../getChartData'; +import { CourseResult, group2Result, instructor2Result, SectionPlus } from '../useCourseData' +import { getFirestoreDocument } from './getFirestoreData'; + +/** + * Used in serverless functions + * @param courseName + * @returns + */ +export async function getCourseData(courseName: string): Promise { + const stone = getRosetta() + const data = await getFirestoreDocument(`/catalog/${courseName}`) + const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 + + const settledData = await Promise.allSettled([ + (data && Array.isArray(data.groups) && Util.isDocumentReferenceArray(data.groups) ? Util.populate(data.groups) : Promise.resolve([])), + (data && Array.isArray(data.instructors) && Util.isDocumentReferenceArray(data.instructors) ? Util.populate(data.instructors) : Promise.resolve([])), + (data && Array.isArray(data?.sections) && Util.isDocumentReferenceArray(data.sections) ? Util.populate
(data.sections, 10, true) : Promise.resolve([])), + ]); + + const [groupDataSettled, instructorDataSettled, sectionDataSettled] = settledData; + const groupData = groupDataSettled.status === 'fulfilled' ? groupDataSettled.value : []; + const instructorData = instructorDataSettled.status === 'fulfilled' ? instructorDataSettled.value : []; + const sectionData = sectionDataSettled.status === 'fulfilled' ? sectionDataSettled.value : []; + + return { + badges: [ + ...(didLoadCorrectly ? getBadges(data.GPA, data.enrollment) : []), + ], + publications: [ + ...(didLoadCorrectly && data.publications !== undefined && Array.isArray(data.publications) ? data.publications.map(e => ( + { + ...e, + key: `${e.catoid}|${e.coid}` + } + )).sort((a,b) => descendingComparator(a, b, 'catoid')).slice(0,3) : []) + ], + tccnsUpdates: [ + ...(didLoadCorrectly && data.tccnsUpdates !== undefined && Array.isArray(data.tccnsUpdates) ? data.tccnsUpdates : []), + ], + firstTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.firstTaught)}`)} ${getYear(data.firstTaught)}` : '', + lastTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.lastTaught)}`)} ${getYear(data.lastTaught)}` : '', + relatedGroups: [ + ...(didLoadCorrectly ? groupData.map(e => group2Result(e)) : []) + ], + relatedInstructors: [ + ...(didLoadCorrectly ? instructorData.sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled).map(e => instructor2Result(e)) : []) + ], + dataGrid: { + columns: [ + { + field: 'term', + headerName: 'Term', + type: 'number', + width: 65, + //valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, + }, + { + field: 'sectionNumber', + headerName: 'Section #', + type: 'number', + width: 90, + }, + { + field: 'primaryInstructorName', + headerName: 'Instructor', + type: 'string', + width: 95, + }, + ...(['A','B','C','D','F','W','S','NCR']).map>(e => ({ + field: e as any, + headerName: e, + description: `Number of ${e}s given for this section`, + type: 'number', + width: e !== 'NCR' ? 30 : 60, + padding: 6, + })), + { + field: 'semesterGPA', + headerName: 'GPA', + description: 'Grade Point Average for just this section', + type: 'number', + width: 60, + padding: 8, + } as any, + ], + rows: [ + ...(didLoadCorrectly ? sectionData.sort((a,b) => b.term - a.term).map(e => ({ + ...e, + id: e._id, + primaryInstructorName: Array.isArray(e.instructorNames) ? `${e.instructorNames[0].lastName}, ${e.instructorNames[0].firstName}` : '', + instructors: [], + })) : []) + ], + }, + dataChart: { + data: [ + ...(didLoadCorrectly ? getChartData(sectionData) : []) + ], + // https://developers.google.com/chart/interactive/docs/gallery/linechart?hl=en#configuration-options + options: { + title: `${courseName} Average GPA Over Time by Instructor`, + vAxis: { + title: 'Average GPA', + gridlines: { + count: -1 //auto + }, + maxValue: 4.0, + minValue: 0.0 + }, + hAxis: { + title: 'Semester', + gridlines: { + count: -1 //auto + }, + textStyle: { + fontSize: 12 + }, + }, + chartArea: { + //width: '100%', + //width: '55%', + //width: '65%', + left: 'auto', + //left: 65, // default 'auto' or 65 + right: 'auto', + //right: 35, // default 'auto' or 65 + //left: (window.innerWidth < 768 ? 55 : (window.innerWidth < 992 ? 120 : null)) + }, + legend: { + position: 'bottom' + }, + pointSize: 5, + interpolateNulls: true //lines between point gaps + } + }, + enrollment: [ + ...(didLoadCorrectly ? + data.enrollment.totalEnrolled === 0 ? + [{ key: 'nodata', title: 'No data', color: grade2Color['I'], value: -1, percentage: 100 }] : + (['totalA','totalB','totalC','totalD','totalF','totalS','totalNCR','totalW'] as (keyof Enrollment)[]) + .map(k => ({ + key: k, + title: k.substring(5), // 'totalA' => 'A' + color: grade2Color[k.substring(5) as Grade] ?? grade2Color['I'], + value: data.enrollment[k], + percentage: data.enrollment[k] !== undefined && data.enrollment.totalEnrolled !== 0 ? data.enrollment[k] / data.enrollment.totalEnrolled * 100 : 0, + }) + ) : []), + ], + instructorCount: didLoadCorrectly ? Array.isArray(data.instructors) ? data.instructors.length : 0 : 0, + sectionCount: didLoadCorrectly ? Array.isArray(data.sections) ? data.sections.length : 0 : 0, + classSize: didLoadCorrectly && Array.isArray(data.sections) ? data.enrollment.totalEnrolled / data.sections.length : 0, + sectionLoadingProgress: 100, + }; +} diff --git a/lib/data/back/getFirestoreData.ts b/lib/data/back/getFirestoreData.ts new file mode 100644 index 0000000..ef9dea8 --- /dev/null +++ b/lib/data/back/getFirestoreData.ts @@ -0,0 +1,13 @@ +import { firebase } from '../../firebase_admin' + +export async function getFirestoreDocument(documentPath: string): Promise { + const db = firebase.firestore() + const snap = await db.doc(documentPath).get(); + return snap.exists ? snap.data() as T : undefined +} + +export async function getFirestoreCollection(collectionPath: string): Promise { + const db = firebase.firestore() + const docs = await db.collection(collectionPath).get() + return docs.docs.filter(e => e.exists).map(e => e.data() as T); +} diff --git a/lib/data/back/getInstructorData.ts b/lib/data/back/getInstructorData.ts new file mode 100644 index 0000000..b595ba6 --- /dev/null +++ b/lib/data/back/getInstructorData.ts @@ -0,0 +1,308 @@ +import { Course, Enrollment, Group, Instructor, Section, Util } from '@cougargrades/types' +import { firebase } from '../../firebase_admin' +import { getFirestoreDocument } from '../../data/back/getFirestoreData' +import { InstructorResult } from '../useInstructorData'; +import { getRosetta } from '../../i18n'; +import { getBadges } from '../getBadges'; +import { Grade, grade2Color } from '../../../components/badge'; +import { getYear, seasonCode } from '../../util'; +import { group2Result, SectionPlus } from '../useCourseData'; +import { course2Result } from '../useAllGroups'; +import { Column } from '../../../components/datatable'; +import { getChartDataForInstructor } from '../getChartDataForInstructor'; + +/** + * Used server-side + */ +export async function getInstructorData(instructorName: string): Promise { + const stone = getRosetta() + const db = firebase.firestore(); + const data = await getFirestoreDocument(`/instructors/${instructorName}`) + const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 + const groupRefs = ! didLoadCorrectly ? [] : Object + .entries(data.departments) + .sort((a, b) => b[1] - a[1]) + .map(e => e[0]) + .map(e => db.doc(`/groups/${e}`) as FirebaseFirestore.DocumentReference); + + // const searchSnapshots = await Promise.allSettled([ + // courseQuery.get(), + // courseByDeptQuery.get(), + // instructorQuery.get(), + // groupQuery.get(), + // ]) + + const settledData = await Promise.allSettled([ + (data && Array.isArray(data.courses) && Util.isDocumentReferenceArray(data.courses) ? Util.populate(data.courses) : Promise.resolve([])), + (data && Array.isArray(data?.sections) && Util.isDocumentReferenceArray(data.sections) ? Util.populate
(data.sections, 10, true) : Promise.resolve([])), + (data && Array.isArray(groupRefs) && Util.isDocumentReferenceArray(groupRefs) ? Util.populate(groupRefs) : Promise.resolve([])), + ]); + + const [courseDataSettled, sectionDataSettled, groupDataSettled] = settledData; + const courseData = courseDataSettled.status === 'fulfilled' ? courseDataSettled.value : []; + const sectionData = sectionDataSettled.status === 'fulfilled' ? sectionDataSettled.value : []; + const groupData = groupDataSettled.status === 'fulfilled' ? groupDataSettled.value : []; + + // const courseData: Course[] = [...( + // data && Array.isArray(data.courses) && Util.isDocumentReferenceArray(data.courses) + // ? await Util.populate(data.courses) + // : [] + // )]; + // const sectionData: Section[] = [...( + // data && Array.isArray(data?.sections) && Util.isDocumentReferenceArray(data.sections) + // ? await Util.populate
(data.sections, 10, true) + // : [] + // )]; + // const groupData: Group[] = [...( + // data && Array.isArray(groupRefs) && Util.isDocumentReferenceArray(groupRefs) + // ? await Util.populate(groupRefs) + // : [] + // )]; + + return { + badges: [ + ...(didLoadCorrectly ? getBadges(data.GPA, data.enrollment) : []), + ], + enrollment: [ + ...(didLoadCorrectly ? + data.enrollment.totalEnrolled === 0 ? + [{ key: 'nodata', title: 'No data', color: grade2Color['I'], value: -1, percentage: 100 }] : + (['totalA','totalB','totalC','totalD','totalF','totalS','totalNCR','totalW'] as (keyof Enrollment)[]) + .map(k => ({ + key: k, + title: k.substring(5), // 'totalA' => 'A' + color: grade2Color[k.substring(5) as Grade] ?? grade2Color['I'], + value: data.enrollment[k], + percentage: data.enrollment[k] !== undefined && data.enrollment.totalEnrolled !== 0 ? data.enrollment[k] / data.enrollment.totalEnrolled * 100 : 0, + }) + ) : []), + ], + firstTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.firstTaught)}`)} ${getYear(data.firstTaught)}` : '', + lastTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.lastTaught)}`)} ${getYear(data.lastTaught)}` : '', + relatedGroups: [ + ...(didLoadCorrectly ? groupData.map(e => group2Result(e)) : []) + ], + relatedCourses: [ + ...(didLoadCorrectly ? courseData.sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled).map(e => course2Result(e)) : []) + ], + sectionDataGrid: { + columns: [ + { + field: 'term', + headerName: 'Term', + type: 'number', + width: 65, + //valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, + }, + { + field: 'courseName', + headerName: 'Course', + type: 'string', + width: 75, + padding: 8, + // eslint-disable-next-line react/display-name + //valueFormatter: value => {value}, + }, + { + field: 'sectionNumber', + headerName: 'Section #', + type: 'number', + width: 90, + }, + ...(['A','B','C','D','F','W','S','NCR']).map>(e => ({ + field: e as any, + headerName: e, + description: `Number of ${e}s given for this section`, + type: 'number', + width: e !== 'NCR' ? 30 : 60, + padding: 6, + })), + { + field: 'semesterGPA', + headerName: 'GPA', + description: 'Grade Point Average for just this section', + type: 'number', + width: 60, + padding: 8, + } as any, + ], + rows: [ + ...(didLoadCorrectly ? sectionData.sort((a,b) => b.term - a.term).map(e => ({ + ...e, + id: e._id, + primaryInstructorName: Array.isArray(e.instructorNames) ? `${e.instructorNames[0].lastName}, ${e.instructorNames[0].firstName}` : '', + instructors: [], + })) : []) + ], + }, + courseDataGrid: { + columns: [ + { + field: 'id', + headerName: 'Name', + type: 'string', + width: 65, + padding: 8, + // eslint-disable-next-line react/display-name + //valueFormatter: value => {value}, + }, + { + field: 'description', + headerName: 'Description', + type: 'string', + width: 105, + padding: 8, + }, + { + field: 'firstTaught', + headerName: 'First Taught', + description: 'The oldest semester that this course was taught', + type: 'number', + width: 75, + padding: 6, + //valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, + }, + { + field: 'lastTaught', + headerName: 'Last Taught', + description: 'The most recent semester that this course was taught', + type: 'number', + width: 75, + padding: 6, + //valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, + }, + { + field: 'instructorCount', + headerName: '# Instructors', + description: 'Number of instructors', + type: 'number', + width: 110, + padding: 4, + //valueFormatter: value => value.toLocaleString(), + }, + { + field: 'sectionCount', + headerName: '# Sections', + description: 'Number of sections', + type: 'number', + width: 95, + padding: 8, + //valueFormatter: value => value.toLocaleString(), + }, + { + field: 'totalEnrolled', + headerName: '# Enrolled', + description: 'Total number of students who have been enrolled in this course', + type: 'number', + width: 95, + padding: 8, + //valueFormatter: value => isNaN(value) ? 'No data' : value.toLocaleString(), + }, + { + field: 'enrolledPerSection', + headerName: 'Class Size', + description: 'Estimated average size of each section, # of total enrolled รท # of sections', + type: 'number', + width: 80, + padding: 6, + //valueFormatter: value => isNaN(value) ? 'No data' : `~ ${value.toFixed(1)}`, + }, + { + field: 'gradePointAverage', + headerName: 'GPA', + description: 'Average of Grade Point Average', + type: 'number', + width: 60, + padding: 8, + // eslint-disable-next-line react/display-name + //valueFormatter: value => value !== 0 ? {formatGPAValue(value)} : '', + }, + { + field: 'standardDeviation', + headerName: 'SD', + description: 'Standard deviation of GPA across all sections in a course', + type: 'number', + width: 60, + padding: 8, + // eslint-disable-next-line react/display-name + //valueFormatter: value => value !== 0 ? {formatSDValue(value)} : '', + }, + { + field: 'dropRate', + headerName: '% W', + description: 'Drop rate, # of total Ws รท # of total enrolled', + type: 'number', + width: 60, + padding: 8, + // eslint-disable-next-line react/display-name + //valueFormatter: value => isNaN(value) ? 'No data' : {formatDropRateValue(value)}, + }, + ], + rows: [ + ...(courseData.sort((a,b) => b._id.localeCompare(a._id)).map(e => ({ + ...e, + id: e._id, + instructorCount: Array.isArray(e.instructors) ? e.instructors.length : 0, + instructors: [], + sectionCount: Array.isArray(e.sections) ? e.sections.length : 0, + sections: [], + groups: [], + gradePointAverage: e.GPA.average, + standardDeviation: e.GPA.standardDeviation, + dropRate: e.enrollment !== undefined ? (e.enrollment.totalW/e.enrollment.totalEnrolled*100) : NaN, + totalEnrolled: e.enrollment !== undefined ? e.enrollment.totalEnrolled : NaN, + enrolledPerSection: e.enrollment !== undefined && Array.isArray(e.sections) ? (e.enrollment.totalEnrolled / e.sections.length) : NaN, + }))) + ], + }, + dataChart: { + data: [ + ...(didLoadCorrectly ? getChartDataForInstructor(sectionData) : []) + ], + // https://developers.google.com/chart/interactive/docs/gallery/linechart?hl=en#configuration-options + options: { + title: `${instructorName} Average GPA Over Time by Course`, + vAxis: { + title: 'Average GPA', + gridlines: { + count: -1 //auto + }, + maxValue: 4.0, + minValue: 0.0 + }, + hAxis: { + title: 'Semester', + gridlines: { + count: -1 //auto + }, + //slantedText: false, + //showTextEvery: 1, + textStyle: { + fontSize: 12 + }, + }, + chartArea: { + //width: '100%', + //width: '55%', + //width: '65%', + left: 'auto', + //left: 65, // default 'auto' or 65 + right: 'auto', + //right: 35, // default 'auto' or 65 + //left: (window.innerWidth < 768 ? 55 : (window.innerWidth < 992 ? 120 : null)) + }, + legend: { + position: 'bottom' + }, + pointSize: 5, + interpolateNulls: true //lines between point gaps + } + }, + courseCount: didLoadCorrectly ? Array.isArray(data.courses) ? data.courses.length : 0 : 0, + sectionCount: didLoadCorrectly ? Array.isArray(data.sections) ? data.sections.length : 0 : 0, + classSize: didLoadCorrectly && Array.isArray(data.sections) ? data.enrollment.totalEnrolled / data.sections.length : 0, + //sectionLoadingProgress: didLoadCorrectly ? Array.isArray(data.sections) ? (sectionLoadingProgress/data.sections.length*100) : 0 : 0, + sectionLoadingProgress: 100, + rmpHref: didLoadCorrectly && data.rmpLegacyId !== undefined ? `https://www.ratemyprofessors.com/ShowRatings.jsp?tid=${data.rmpLegacyId}` : undefined, + } +} diff --git a/lib/data/back/getOneGroup.ts b/lib/data/back/getOneGroup.ts new file mode 100644 index 0000000..0eeb2d9 --- /dev/null +++ b/lib/data/back/getOneGroup.ts @@ -0,0 +1,51 @@ +import { Section, Util } from '@cougargrades/types'; +import { CoursePlus, group2PopResult, GroupPlus, PopulatedGroupResult } from '../useAllGroups'; +import { getFirestoreDocument } from './getFirestoreData'; + +export async function getOneGroup(groupId: string, includeSections: boolean = false): Promise { + const data = await getFirestoreDocument(`/groups/${groupId}`) + const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 + + const settledData = await Promise.allSettled([ + (data && Array.isArray(data.courses) && Util.isDocumentReferenceArray(data.courses) ? Util.populate(data.courses) : Promise.resolve([])), + (data && includeSections && Array.isArray(data?.sections) && Util.isDocumentReferenceArray(data.sections) ? Util.populate
(data.sections, 10, true) : Promise.resolve([])), + ]); + const [courseDataSettled, sectionDataSettled] = settledData + const courseData = courseDataSettled.status === 'fulfilled' ? courseDataSettled.value : []; + const sectionData = sectionDataSettled.status === 'fulfilled' ? sectionDataSettled.value : []; + + if (didLoadCorrectly) { + data.keywords = [] + data.courseCount = Array.isArray(data.courses) ? data.courses.length : 0 + data.courses = courseData + // filter out undefined because there might be some empty references + .filter(e => e !== undefined) + // sort courses by total enrolled + .sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled) + // sanitize unwanted document references + .map(course => ({ + ...course, + // property necessary for some client-side calculations + sectionCount: Array.isArray(course.sections) ? course.sections.length : 0, + sections: [], + instructorCount: Array.isArray(course.instructors) ? course.instructors.length : 0, + instructors: [], + // property not needed + //sections: Array.isArray(course.sections) ? course.sections.map(sec => ({ id: sec?.id as any as string })) as any : [], + //instructors: Array.isArray(course.instructors) ? course.instructors.map(ins => ({ id: ins?.id as any as string })) as any : [], + groups: [], + keywords: [], + })); + data.sectionCount = Array.isArray(data.sections) ? data.sections.length : 0 + data.sections = sectionData + // filter out undefined because there might be some empty references + .filter(e => e !== undefined) + // sanitize unwanted document references + .map(sec => ({ + ...sec, + instructors: [], + })); + } + + return didLoadCorrectly ? group2PopResult(data) : undefined +} diff --git a/lib/data/back/getSearchResults.ts b/lib/data/back/getSearchResults.ts new file mode 100644 index 0000000..9422b4a --- /dev/null +++ b/lib/data/back/getSearchResults.ts @@ -0,0 +1,45 @@ +import { Course, Group, Instructor } from '@cougargrades/types' +import { firebase } from '../../firebase_admin' +import { course2Result, group2Result, instructor2Result, SearchResult, sortByTitle } from '../useSearchResults' + +export async function getSearchResults(inputValue: string): Promise { + const SEARCH_RESULT_LIMIT = 5; + const COURSE_EXACT_SEARCH_RESULT_LIMIT = 3; + const COURSE_SEARCH_RESULT_LIMIT = 2; + + const db = firebase.firestore(); + // Search for courses + const courseQuery = db.collection('catalog').where('keywords', 'array-contains', inputValue.toLowerCase()).limit(COURSE_SEARCH_RESULT_LIMIT) + // Search for courses that start with the given department code + // reference: https://stackoverflow.com/a/57290806/4852536 + const courseByDeptQuery = db.collection('catalog').where('department', '>=', inputValue.toUpperCase()).where('department', '<', inputValue.toUpperCase().replace(/.$/, c => String.fromCharCode(c.charCodeAt(0) + 1))).limit(COURSE_EXACT_SEARCH_RESULT_LIMIT) + // Search for instructors + const instructorQuery = db.collection('instructors').where('keywords', 'array-contains', inputValue.toLowerCase()).orderBy('lastName').limit(SEARCH_RESULT_LIMIT) + // Search for groups + const groupQuery = db.collection('groups').where('keywords', 'array-contains', inputValue.toLowerCase()).orderBy('name').limit(SEARCH_RESULT_LIMIT) + + const searchSnapshots = await Promise.allSettled([ + courseQuery.get(), + courseByDeptQuery.get(), + instructorQuery.get(), + groupQuery.get(), + ]) + + const [courseSnap, courseByDeptSnap, instructorSnap, groupSnap] = searchSnapshots; + + return [ + ...[ + ...(courseByDeptSnap.status === 'fulfilled' ? courseByDeptSnap.value.docs.map(e => e.data() as Course).map(e => course2Result(e)) : []), + ...(courseSnap.status === 'fulfilled' ? courseSnap.value.docs.map(e => e.data() as Course).map(e => course2Result(e)) : []), + ] + // remove duplicates + // reference: https://stackoverflow.com/a/56757215/4852536 + .filter((item, index, self) => index === self.findIndex(e => (e.key === item.key))) + // put exact title matches first + .sort(sortByTitle(inputValue)), + ...(instructorSnap.status === 'fulfilled' ? instructorSnap.value.docs.map(e => e.data() as Instructor).map(e => instructor2Result(e)).sort(sortByTitle(inputValue)) : []), + ...(groupSnap.status === 'fulfilled' ? groupSnap.value.docs.map(e => e.data() as Group).map(e => group2Result(e)).sort(sortByTitle(inputValue)) : []), + ] +} + + diff --git a/lib/data/getBadges.ts b/lib/data/getBadges.ts index 0a964ca..0de21d2 100644 --- a/lib/data/getBadges.ts +++ b/lib/data/getBadges.ts @@ -13,7 +13,7 @@ export function getBadges(gpa: GPA.GPA, enrollment?: Enrollment): SearchResultBa { key: 'gpa', text: formatGPAValue(gpa.average), - color: grade2Color.get(getGradeForGPA(gpa.average)), + color: grade2Color[getGradeForGPA(gpa.average)], caption: 'Grade Point Average', } ] : []), @@ -21,7 +21,7 @@ export function getBadges(gpa: GPA.GPA, enrollment?: Enrollment): SearchResultBa { key: 'sd', text: formatSDValue(gpa.standardDeviation), - color: grade2Color.get(getGradeForStdDev(gpa.standardDeviation)), + color: grade2Color[getGradeForStdDev(gpa.standardDeviation)], caption: 'Standard Deviation', } ] : []), @@ -29,7 +29,7 @@ export function getBadges(gpa: GPA.GPA, enrollment?: Enrollment): SearchResultBa { key: 'droprate', text: formatDropRateValue(enrollment.totalW/enrollment.totalEnrolled*100), - color: grade2Color.get('W'), + color: grade2Color['W'], caption: 'Drop Rate', } ] : []), diff --git a/lib/data/getChartData.ts b/lib/data/getChartData.ts index 0783dc9..01874de 100644 --- a/lib/data/getChartData.ts +++ b/lib/data/getChartData.ts @@ -54,10 +54,12 @@ export function getChartData(sections: Section[]) { else { rowID = rowsMap.get(term); } - if(typeof graphArray[rowID][colsMap.get(instructor)] === 'undefined') { //initialize cell - graphArray[rowID][colsMap.get(instructor)] = 0; + if(typeof graphArray[rowID][colsMap.get(instructor)!] === 'undefined') { //initialize cell + graphArray[rowID][colsMap.get(instructor)!] = 0; } - graphArray[rowID][colsMap.get(instructor)] += gpa*students; //increment student-weighted GPA + // TODO: fix? null * number => 0 + //graphArray[rowID][colsMap.get(instructor)!] += gpa*students; //increment student-weighted GPA + graphArray[rowID][colsMap.get(instructor)!] += (gpa ?? 0)*students; //increment student-weighted GPA } for(let i = 1; i < graphArray.length; ++i) { for(let j = 1; j < graphArray[i].length; ++j) { @@ -110,8 +112,9 @@ export function expandSections(sections: Section[]) { } } // move instructor data to `primaryInstructor` - item.primaryInstructor = item.instructorNames[0] - delete item.instructorNames; + //item.primaryInstructor = item.instructorNames[0] + item.primaryInstructor = Array.isArray(item.instructorNames) ? item.instructorNames[0] : { firstName: '???', lastName: '???' }; + //delete item.instructorNames; } // expands instructor data diff --git a/lib/data/getChartDataForInstructor.ts b/lib/data/getChartDataForInstructor.ts index 1e901b6..0464eb3 100644 --- a/lib/data/getChartDataForInstructor.ts +++ b/lib/data/getChartDataForInstructor.ts @@ -55,10 +55,12 @@ export function getChartDataForInstructor(sections: Section[]) { else { rowID = rowsMap.get(term); } - if(typeof graphArray[rowID][colsMap.get(courseName)] === 'undefined') { //initialize cell - graphArray[rowID][colsMap.get(courseName)] = 0; + if(typeof graphArray[rowID][colsMap.get(courseName)!] === 'undefined') { //initialize cell + graphArray[rowID][colsMap.get(courseName)!] = 0; } - graphArray[rowID][colsMap.get(courseName)] += gpa*students; //increment student-weighted GPA + // TODO: fix? null * number => 0 + //graphArray[rowID][colsMap.get(courseName)!] += gpa*students; //increment student-weighted GPA + graphArray[rowID][colsMap.get(courseName)!] += (gpa ?? 0)*students; //increment student-weighted GPA } for(let i = 1; i < graphArray.length; ++i) { for(let j = 1; j < graphArray[i].length; ++j) { @@ -111,8 +113,9 @@ export function expandSections(sections: Section[]) { } } // move instructor data to `primaryInstructor` - item.primaryInstructor = item.instructorNames[0] - delete item.instructorNames; + //item.primaryInstructor = item.instructorNames[0] + item.primaryInstructor = Array.isArray(item.instructorNames) ? item.instructorNames[0] : { firstName: '???', lastName: '???' }; + //delete item.instructorNames; } // expands instructor data diff --git a/lib/data/useAllGroups.ts b/lib/data/useAllGroups.ts index 67558fd..dfb4b72 100644 --- a/lib/data/useAllGroups.ts +++ b/lib/data/useAllGroups.ts @@ -1,13 +1,10 @@ -import { useFirestore, useFirestoreCollectionData, useFirestoreDoc } from 'reactfire' -import { Course, Group, LabeledLink, Section } from '@cougargrades/types' +import { Course, Group, LabeledLink, Section, Util } from '@cougargrades/types' import { DocumentReference } from '@cougargrades/types/dist/FirestoreStubs' -import { Observable } from './Observable' -import { CourseInstructorResult } from './useCourseData'; +import { CourseInstructorResult } from './useCourseData' import { getBadges } from './getBadges' -import { useFakeFirestore } from '../firebase' -import { defaultComparator, descendingComparator } from '../../components/datatable'; +import { defaultComparator, descendingComparator } from '../../components/datatable' -type AllGroupsResultItem = { [key: string]: GroupResult[] }; +export type AllGroupsResultItem = { [key: string]: GroupResult[] }; export interface AllGroupsResult { categories: string[]; @@ -27,6 +24,19 @@ export interface GroupResult { sources: LabeledLink[]; } +export interface PopulatedGroupResult { + key: string; + href: string; + title: string; + description: string; + categories: string[]; + courses: CoursePlus[]; + courseCount?: number; + sections: Section[]; + sectionCount?: number; + sources: LabeledLink[]; +} + export function group2Result(data: Group): GroupResult { return { key: data.identifier, @@ -40,13 +50,30 @@ export function group2Result(data: Group): GroupResult { } } -export function course2Result(data: Course): CourseInstructorResult { +export function group2PopResult(data: GroupPlus): PopulatedGroupResult { + return { + key: data.identifier, + href: `/g/${data.identifier}`, + title: data.name, + description: data.description, + categories: Array.isArray(data.categories) ? data.categories : [], + courses: data.courses as CoursePlus[], + courseCount: data.courseCount, + sections: data.sections as Section[], + sectionCount: data.sectionCount, + sources: Array.isArray(data.sources) ? data.sources.sort((a,b) => descendingComparator(a, b, 'title')).slice(0,3) : [] + } +} + +export function course2Result(data: CoursePlus): CourseInstructorResult { + const numInstructors: number = data.instructorCount !== undefined ? data.instructorCount : Array.isArray(data.instructors) ? data.instructors.length : 0; + const numSections: number = data.sectionCount !== undefined ? data.sectionCount : Array.isArray(data.sections) ? data.sections.length : 0; return { key: data._path, href: `/c/${data._id}`, title: `${data.department} ${data.catalogNumber}`, subtitle: data.description, - caption: `${Array.isArray(data.instructors) ? data.instructors.length : 0} instructors โ€ข ${Array.isArray(data.sections) ? data.sections.length : 0} sections`, + caption: `${numInstructors} instructors โ€ข ${numSections} sections`, badges: getBadges(data.GPA, data.enrollment), id: data._id, lastInitial: '', @@ -55,65 +82,14 @@ export function course2Result(data: Course): CourseInstructorResult { export const ALL_GROUPS_SENTINEL = 'All Groups' -export function useAllGroups(): Observable { - const db = useFakeFirestore(); - const query = (db.collection('groups') as any).where('categories', 'array-contains', '#UHCoreCurriculum') - const { data, status, error } = useFirestoreCollectionData(query) - - const categories = [ - ...( - status === 'success' ? - Array.from(new Set(data.map(e => Array.isArray(e.categories) ? e.categories.filter(cat => !cat.startsWith('#')) : []).flat())) - .sort((a,b) => defaultComparator(a,b)) // [ '(All)', '(2022-2023)', '(2021-2022)', '(2020-2021)' ] - .slice(0,2) // don't endlessly list the groups, they're still accessible from a course directly - : [] - ), - //ALL_GROUPS_SENTINEL - ]; - - // make a key/value store of category -> GroupResult[] - const results = categories - .reduce((obj, key) => { - if(key === ALL_GROUPS_SENTINEL) { - // obj[key] = [ - // ...(status === 'success' ? data.filter(e => Array.isArray(e.categories) && e.categories.length === 0).map(e => group2Result(e)) : []) - // ]; - } - else { - obj[key] = [ - ...(status === 'success' ? data.filter(e => Array.isArray(e.categories) && e.categories.includes(key)).map(e => group2Result(e)) : []) - ]; - } - return obj; - }, {} as AllGroupsResultItem); +export interface GroupPlus extends Group { + courseCount?: number; + sectionCount?: number; +} - return { - data: { - categories, - results, - core_curriculum: [ - ...(status === 'success' ? data.filter(e => Array.isArray(e.categories) && e.categories.includes('UH Core Curriculum')).map(e => group2Result(e)) : []) - ], - all_groups: [ - ...(status === 'success' ? data.filter(e => Array.isArray(e.categories) && ! e.categories.includes('UH Core Curriculum')).map(e => group2Result(e)) : []) - ], - }, - error, - status, - } +export interface CoursePlus extends Course { + sectionCount?: number; + instructorCount?: number; } -export function useOneGroup(groupId: string): Observable { - const db = useFakeFirestore(); - const { data, status, error } = useFirestoreDoc(db.doc(`/groups/${groupId}`) as any) - const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 - const isBadObject = typeof data === 'object' && Object.keys(data).length === 1 - const isActualError = typeof groupId === 'string' && groupId !== '' && status !== 'loading' && isBadObject - const good = status === 'success' && didLoadCorrectly && !isActualError && data.exists; - return { - data: good ? group2Result(data.data()) : undefined, - error, - status, - } -} diff --git a/lib/data/useCourseData.ts b/lib/data/useCourseData.ts deleted file mode 100644 index 9e78e95..0000000 --- a/lib/data/useCourseData.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { useEffect, useState } from 'react' -import { useFirestore, useFirestoreDocData } from 'reactfire' -import { usePrevious } from 'react-use' -import { Course, Group, Instructor, PublicationInfo, TCCNSUpdateInfo, Section, Util } from '@cougargrades/types' -import abbreviationMap from '@cougargrades/publicdata/bundle/edu.uh.publications.subjects/subjects.json' -import { Observable } from './Observable' -import { SearchResultBadge } from './useSearchResults' -import { Grade, grade2Color } from '../../components/badge' -import { Column, defaultComparator, descendingComparator } from '../../components/datatable' -import { useRosetta } from '../i18n' -import { getYear, seasonCode } from '../util' -import { getChartData } from './getChartData' -import { EnrollmentInfoResult } from '../../components/enrollment' -import { getBadges } from './getBadges' -import { useFakeFirestore } from '../firebase' - -export type SectionPlus = Section & { id: string }; - -export interface CourseResult { - //course: Course; - badges: SearchResultBadge[]; - publications: (PublicationInfo & { key: string })[]; - firstTaught: string; - lastTaught: string; - relatedGroups: CourseGroupResult[]; - relatedInstructors: CourseInstructorResult[]; - dataGrid: { - columns: Column[]; - rows: SectionPlus[]; - }; - dataChart: { - data: any[], - options: { [key: string ]: any } - }; - enrollment: EnrollmentInfoResult[]; - instructorCount: number; - sectionCount: number; - classSize: number; - sectionLoadingProgress: number; - tccnsUpdates: TCCNSUpdateInfo[]; -} - -export interface CourseGroupResult { - key: string; - href: string; - title: string; - description: string; - count: number; -} - -export interface CourseInstructorResult { - key: string; // used for react, same as document path - href: string; // where to redirect the user when selected - title: string; // typically the instructor's full name (ex: Tyler James Beck) - subtitle: string; // typically the instructor's associated departments (ex: Applied Music, Music) - caption: string; // typically the number of courses and sections (ex: 4 courses โ€ข 5 sections) - badges: SearchResultBadge[]; - id: string; - lastInitial: string; -} - -export function group2Result(data: Group): CourseGroupResult { - const isCoreCurrGroup = Array.isArray(data.categories) && data.categories.includes('#UHCoreCurriculum'); - const isSubjectGroup = Array.isArray(data.categories) && data.categories.includes('#UHSubject'); - const suffix = isCoreCurrGroup ? ' (Core)' : isSubjectGroup ? ' (Subject)' : ''; - return { - key: data.identifier, - href: `/g/${data.identifier}`, - title: `${data.name}${suffix}`, - description: data.description, - count: Array.isArray(data.courses) ? data.courses.length : 0 - }; -} - -export function instructor2Result(data: Instructor): CourseInstructorResult { - return { - key: data._path, - href: `/i/${data._id}`, - title: data.fullName, - subtitle: generateSubjectString(data), - caption: `${Array.isArray(data.courses) ? data.courses.length : 0} courses โ€ข ${Array.isArray(data.sections) ? data.sections.length : 0} sections`, - badges: getBadges(data.GPA, data.enrollment), - id: data._id, - lastInitial: data.lastName.charAt(0).toUpperCase() - }; -} - -function generateSubjectString(data: Instructor | undefined): string { - if(data !== undefined && data !== null && data.departments !== undefined && data.departments !== null) { - const entries = Object.entries(data.departments).sort((a, b) => b[1] - a[1]) - if(entries.length > 0) { - return entries.map(e => abbreviationMap[e[0]]).filter(e => e !== undefined).join(', ') - } - } - return ''; -} - -/** - * React hook for accessing the course data client-side - * @param courseName - * @returns - */ -export function useCourseData(courseName: string): Observable { - const stone = useRosetta() - const db = useFakeFirestore() - const { data, error, status } = useFirestoreDocData(db.doc(`/catalog/${courseName}`) as any) - const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 - const isBadObject = typeof data === 'object' && Object.keys(data).length === 1 - const isActualError = typeof courseName === 'string' && courseName !== '' && status !== 'loading' && isBadObject - const [instructorData, setInstructorData] = useState([]); - const [groupData, setGroupData] = useState([]); - const [sectionData, setSectionData] = useState([]); - const [sectionLoadingProgress, setSectionLoadingProgress] = useState(0); - const previous = usePrevious(data?._id) - const sharedStatus = status === 'success' ? isActualError ? 'error' : isBadObject ? 'loading' : didLoadCorrectly ? 'success' : 'error' : status - - // load courses + section + group data - useEffect(() => { - const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1; - // prevent loading the same data again - if(didLoadCorrectly && previous !== data._id) { - setInstructorData([]); - setGroupData([]); - setSectionData([]); - setSectionLoadingProgress(0); - (async () => { - if(Array.isArray(data.groups) && Util.isDocumentReferenceArray(data.groups)) { - setGroupData(await Util.populate(data.groups)) - } - if(Array.isArray(data.instructors) && Util.isDocumentReferenceArray(data.instructors)) { - setInstructorData(await Util.populate(data.instructors)) - } - if(Array.isArray(data.sections) && Util.isDocumentReferenceArray(data.sections)) { - console.count('course populate section') - setSectionData(await Util.populate
(data.sections, 10, true, (p,total) => setSectionLoadingProgress(p/total*100), false, false)) - } - })(); - } - }, [data,previous]) - - try { - return { - data: { - badges: [ - ...(didLoadCorrectly ? getBadges(data.GPA, data.enrollment) : []), - ], - publications: [ - ...(didLoadCorrectly && data.publications !== undefined && Array.isArray(data.publications) ? data.publications.map(e => ( - { - ...e, - key: `${e.catoid}|${e.coid}` - } - )).sort((a,b) => descendingComparator(a, b, 'catoid')).slice(0,3) : []) - ], - tccnsUpdates: [ - ...(didLoadCorrectly && data.tccnsUpdates !== undefined && Array.isArray(data.tccnsUpdates) ? data.tccnsUpdates : []), - ], - firstTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.firstTaught)}`)} ${getYear(data.firstTaught)}` : '', - lastTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.lastTaught)}`)} ${getYear(data.lastTaught)}` : '', - relatedGroups: [ - ...(didLoadCorrectly ? groupData.map(e => group2Result(e)) : []) - ], - relatedInstructors: [ - ...(didLoadCorrectly ? instructorData.sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled).map(e => instructor2Result(e)) : []) - ], - dataGrid: { - columns: [ - { - field: 'term', - headerName: 'Term', - type: 'number', - width: 65, - valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, - }, - { - field: 'sectionNumber', - headerName: 'Section #', - type: 'number', - width: 90, - }, - { - field: 'instructorNames', - headerName: 'Instructor', - type: 'string', - width: 95, - sortComparator: (a, b) => defaultComparator(`${a[0].lastName}, ${a[0].firstName}`, `${b[0].lastName}, ${b[0].firstName}`), - valueFormatter: value => `${value[0].lastName}, ${value[0].firstName}`, - }, - ...(['A','B','C','D','F','W','S','NCR']).map>(e => ({ - field: e as any, - headerName: e, - description: `Number of ${e}s given for this section`, - type: 'number', - width: e !== 'NCR' ? 30 : 60, - padding: 6, - })), - { - field: 'semesterGPA', - headerName: 'GPA', - description: 'Grade Point Average for just this section', - type: 'number', - width: 60, - padding: 8, - }, - ], - rows: [ - ...(didLoadCorrectly ? sectionData.sort((a,b) => b.term - a.term).map(e => ({ - id: e._id, - ...e, - })) : []) - ], - }, - dataChart: { - data: [ - ...(didLoadCorrectly ? getChartData(sectionData) : []) - ], - // https://developers.google.com/chart/interactive/docs/gallery/linechart?hl=en#configuration-options - options: { - title: `${courseName} Average GPA Over Time by Instructor`, - vAxis: { - title: 'Average GPA', - gridlines: { - count: -1 //auto - }, - maxValue: 4.0, - minValue: 0.0 - }, - hAxis: { - title: 'Semester', - gridlines: { - count: -1 //auto - }, - textStyle: { - fontSize: 12 - }, - }, - chartArea: { - //width: '100%', - //width: '55%', - //width: '65%', - left: 'auto', - //left: 65, // default 'auto' or 65 - right: 'auto', - //right: 35, // default 'auto' or 65 - //left: (window.innerWidth < 768 ? 55 : (window.innerWidth < 992 ? 120 : null)) - }, - legend: { - position: 'bottom' - }, - pointSize: 5, - interpolateNulls: true //lines between point gaps - } - }, - enrollment: [ - ...(didLoadCorrectly ? - data.enrollment.totalEnrolled === 0 ? - [{ key: 'nodata', title: 'No data', color: grade2Color.get('I'), value: -1, percentage: 100 }] : - ['totalA','totalB','totalC','totalD','totalF','totalS','totalNCR','totalW'] - .map(k => ({ - key: k, - title: k.substring(5), // 'totalA' => 'A' - color: grade2Color.get(k.substring(5) as Grade), - value: data.enrollment[k], - percentage: data.enrollment[k] !== undefined && data.enrollment[k].totalEnrolled !== 0 ? data.enrollment[k] / data.enrollment.totalEnrolled * 100 : 0, - }) - ) : []), - ], - instructorCount: didLoadCorrectly ? Array.isArray(data.instructors) ? data.instructors.length : 0 : 0, - sectionCount: didLoadCorrectly ? Array.isArray(data.sections) ? data.sections.length : 0 : 0, - classSize: didLoadCorrectly && Array.isArray(data.sections) ? data.enrollment.totalEnrolled / data.sections.length : 0, - //sectionLoadingProgress: didLoadCorrectly ? Array.isArray(data.sections) ? (sectionLoadingProgress/data.sections.length*100) : 0 : 0, - sectionLoadingProgress, - }, - error, - status: sharedStatus, - } - } - catch(error) { - console.error(`[useCourseData] Error:`, error) - return { - data: undefined, - error, - status: 'error', - } - } -} diff --git a/lib/data/useCourseData.tsx b/lib/data/useCourseData.tsx new file mode 100644 index 0000000..d3a4c05 --- /dev/null +++ b/lib/data/useCourseData.tsx @@ -0,0 +1,178 @@ +import React from 'react' +import Link from 'next/link' +import useSWR from 'swr/immutable' +import { Group, Instructor, PublicationInfo, TCCNSUpdateInfo, Section } from '@cougargrades/types' +import abbreviationMap from '@cougargrades/publicdata/bundle/edu.uh.publications.subjects/subjects.json' +import { Observable, ObservableStatus } from './Observable' +import { SearchResultBadge } from './useSearchResults' +import { Column } from '../../components/datatable' +import { useRosetta } from '../i18n' +import { getYear, seasonCode } from '../util' +import { EnrollmentInfoResult } from '../../components/enrollment' +import { getBadges } from './getBadges' + +export type SectionPlus = Section & { + id: string, + primaryInstructorName: string, +}; + +export interface CourseResult { + //course: Course; + badges: SearchResultBadge[]; + publications: (PublicationInfo & { key: string })[]; + firstTaught: string; + lastTaught: string; + relatedGroups: CourseGroupResult[]; + relatedInstructors: CourseInstructorResult[]; + dataGrid: { + columns: Column[]; + rows: SectionPlus[]; + }; + dataChart: { + data: any[], + options: { [key: string ]: any } + }; + enrollment: EnrollmentInfoResult[]; + instructorCount: number; + sectionCount: number; + classSize: number; + sectionLoadingProgress: number; + tccnsUpdates: TCCNSUpdateInfo[]; +} + +export interface CourseGroupResult { + key: string; + href: string; + title: string; + description: string; + count: number; +} + +export interface CourseInstructorResult { + key: string; // used for react, same as document path + href: string; // where to redirect the user when selected + title: string; // typically the instructor's full name (ex: Tyler James Beck) + subtitle: string; // typically the instructor's associated departments (ex: Applied Music, Music) + caption: string; // typically the number of courses and sections (ex: 4 courses โ€ข 5 sections) + badges: SearchResultBadge[]; + id: string; + lastInitial: string; +} + +export function group2Result(data: Group): CourseGroupResult { + const isCoreCurrGroup = Array.isArray(data.categories) && data.categories.includes('#UHCoreCurriculum'); + const isSubjectGroup = Array.isArray(data.categories) && data.categories.includes('#UHSubject'); + const suffix = isCoreCurrGroup ? ' (Core)' : isSubjectGroup ? ' (Subject)' : ''; + return { + key: data.identifier, + href: `/g/${data.identifier}`, + title: `${data.name}${suffix}`, + description: data.description, + count: Array.isArray(data.courses) ? data.courses.length : 0 + }; +} + +export function instructor2Result(data: Instructor): CourseInstructorResult { + return { + key: data._path, + href: `/i/${data._id}`, + title: data.fullName, + subtitle: generateSubjectString(data), + caption: `${Array.isArray(data.courses) ? data.courses.length : 0} courses โ€ข ${Array.isArray(data.sections) ? data.sections.length : 0} sections`, + badges: getBadges(data.GPA, data.enrollment), + id: data._id, + lastInitial: data.lastName.charAt(0).toUpperCase() + }; +} + +export function generateSubjectString(data: Instructor | undefined): string { + if(data !== undefined && data !== null && data.departments !== undefined && data.departments !== null) { + const entries = Object.entries(data.departments).sort((a, b) => b[1] - a[1]) + if(entries.length > 0) { + // The following attempts to prevent Instructor descriptions from being too long + const CHARACTER_LIMIT = 70 + let numAllowedEntries = entries.length + const try_attempt = () => entries.slice(0, numAllowedEntries).map(e => (abbreviationMap as any)[e[0]]).filter(e => e !== undefined).join(', '); + while(try_attempt().length > CHARACTER_LIMIT) { + numAllowedEntries--; + } + + return try_attempt(); + } + } + return ''; +} + +/** + * React hook for accessing the course data client-side + * @param courseName + * @returns + */ +export function useCourseData(courseName: string): Observable { + const stone = useRosetta() + + const { data, error, isLoading } = useSWR(`/api/course/${courseName}`) + const status: ObservableStatus = error ? 'error' : (isLoading || !data || !courseName) ? 'loading' : 'success' + + try { + return { + data: { + ...(status === 'success' ? data : {} as any), + dataGrid: { + columns: [ + { + field: 'term', + headerName: 'Term', + type: 'number', + width: 65, + valueFormatter: value => `${stone.t(`season.${seasonCode(value)}`)} ${getYear(value)}`, + }, + { + field: 'sectionNumber', + headerName: 'Section #', + type: 'number', + width: 90, + }, + { + field: 'primaryInstructorName', + headerName: 'Instructor', + type: 'string', + width: 95, + valueFormatter: value => {value}, + }, + ...(['A','B','C','D','F','W','S','NCR']).map>(e => ({ + field: e as any, + headerName: e, + description: `Number of ${e}s given for this section`, + type: 'number', + width: e !== 'NCR' ? 30 : 60, + padding: 6, + })), + { + field: 'semesterGPA', + headerName: 'GPA', + description: 'Grade Point Average for just this section', + type: 'number', + width: 60, + padding: 8, + } as any, + ], + rows: [ + ...(status === 'success' ? data!.dataGrid.rows : []), + ], + }, + }, + error, + status, + } + } + catch(error) { + console.error(`[useCourseData] Error:`, error) + return { + data: undefined, + error: error as any, + status: 'error', + } + } +} + diff --git a/lib/data/useGroupData.tsx b/lib/data/useGroupData.tsx index 01dbc58..e389af6 100644 --- a/lib/data/useGroupData.tsx +++ b/lib/data/useGroupData.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react' import Link from 'next/link' -import { usePrevious } from 'react-use' -import { Course, Section, Util } from '@cougargrades/types' -import { Observable } from './Observable' -import { GroupResult, course2Result, } from './useAllGroups' +import useSWR from 'swr/immutable' +import { Course } from '@cougargrades/types' +import { Observable, ObservableStatus } from './Observable' +import { course2Result, PopulatedGroupResult, } from './useAllGroups' import { CourseInstructorResult } from './useCourseData' import { Column, defaultComparator } from '../../components/datatable' import { Badge, getGradeForGPA, getGradeForStdDev, grade2Color } from '../../components/badge' @@ -11,6 +11,7 @@ import { useRosetta } from '../i18n' import { getYear, seasonCode } from '../util' import { formatDropRateValue, formatGPAValue, formatSDValue } from './getBadges' import { getChartDataForInstructor } from './getChartDataForInstructor' +import { ENABLE_GROUP_SECTIONS } from '../../components/groupcontent' export type CoursePlus = Course & { @@ -37,46 +38,23 @@ export interface GroupDataResult { sectionLoadingProgress: number; } -export function useGroupData(data: GroupResult): Observable { +/** + * Necessary for remapping data into client-usable nodes + * @param data + * @returns + */ +export function useGroupData(data: PopulatedGroupResult): Observable { const stone = useRosetta() - const [courseData, setCourseData] = useState([]); - const [sectionData, setSectionData] = useState([]); - const [loading, setLoading] = useState(false); + const { data: oneGroupData, error, isLoading } = useSWR(`/api/group/${data.key}${ENABLE_GROUP_SECTIONS ? '/sections' : ''}`) + const sectionStatus: ObservableStatus = error ? 'error' : (isLoading || !oneGroupData || !data.key) ? 'loading' : 'success' + const sectionData = sectionStatus === 'success' ? oneGroupData!.sections : [] const [sectionLoadingProgress, setSectionLoadingProgress] = useState(0); - const previous = usePrevious(data.key) - - // load courses + section data - useEffect(() => { - // prevent loading the same data again - if(previous !== data.key) { - setCourseData([]); - setSectionData([]); - setLoading(true); - setSectionLoadingProgress(0); - (async () => { - if(Array.isArray(data.courses) && Util.isDocumentReferenceArray(data.courses)) { - setCourseData( - (await Util.populate(data.courses)) - // filter out undefined because there might be some empty references - .filter(e => e !== undefined) - // sort courses by total enrolled - .sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled) - ) - } - // if(Array.isArray(data.sections) && Util.isDocumentReferenceArray(data.sections)) { - // console.count('group populate section') - // setSectionData(await Util.populate
(data.sections, 10, true, (p, total) => setSectionLoadingProgress(p/total*100))) - // } - setLoading(false) - })(); - } - }, [data,previous]) try { return { data: { topEnrolled: [ - ...courseData.map(e => course2Result(e)) + ...data.courses.sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled).map(e => course2Result(e)) ], dataGrid: { columns: [ @@ -158,7 +136,7 @@ export function useGroupData(data: GroupResult): Observable { width: 60, padding: 8, // eslint-disable-next-line react/display-name - valueFormatter: value => value !== 0 ? {formatGPAValue(value)} : '', + valueFormatter: value => value !== 0 ? {formatGPAValue(value)} : '', }, { field: 'standardDeviation', @@ -168,7 +146,7 @@ export function useGroupData(data: GroupResult): Observable { width: 60, padding: 8, // eslint-disable-next-line react/display-name - valueFormatter: value => value !== 0 ? {formatSDValue(value)} : '', + valueFormatter: value => value !== 0 ? {formatSDValue(value)} : '', }, { field: 'dropRate', @@ -178,20 +156,23 @@ export function useGroupData(data: GroupResult): Observable { width: 60, padding: 8, // eslint-disable-next-line react/display-name - valueFormatter: value => isNaN(value) ? 'No data' : {formatDropRateValue(value)}, + valueFormatter: value => isNaN(value) ? 'No data' : {formatDropRateValue(value)}, }, ], rows: [ - ...(courseData.sort((a,b) => b._id.localeCompare(a._id)).map(e => ({ + ...(data.courses.sort((a,b) => b._id.localeCompare(a._id)).map(e => ({ + ...e, id: e._id, - instructorCount: Array.isArray(e.instructors) ? e.instructors.length : 0, - sectionCount: Array.isArray(e.sections) ? e.sections.length : 0, + //instructorCount: Array.isArray(e.instructors) ? e.instructors.length : 0, + instructorCount: e.instructorCount ?? 0, + //sectionCount: Array.isArray(e.sections) ? e.sections.length : 0, + sectionCount: e.sectionCount ?? 0, gradePointAverage: e.GPA.average, standardDeviation: e.GPA.standardDeviation, dropRate: e.enrollment !== undefined ? (e.enrollment.totalW/e.enrollment.totalEnrolled*100) : NaN, totalEnrolled: e.enrollment !== undefined ? e.enrollment.totalEnrolled : NaN, - enrolledPerSection: e.enrollment !== undefined && Array.isArray(e.sections) ? (e.enrollment.totalEnrolled / e.sections.length) : NaN, - ...e, + //enrolledPerSection: e.enrollment !== undefined && Array.isArray(e.sections) ? (e.enrollment.totalEnrolled / e.sections.length) : NaN, + enrolledPerSection: e.enrollment !== undefined && e.sectionCount !== undefined ? (e.enrollment.totalEnrolled / e.sectionCount) : NaN, }))) ], //rows: [], @@ -241,13 +222,13 @@ export function useGroupData(data: GroupResult): Observable { sectionLoadingProgress, }, error: undefined, - status: loading ? 'loading' : 'success', + status: sectionStatus, } } catch(error) { return { data: undefined, - error, + error: error as any, status: 'error', } } diff --git a/lib/data/useInstructorData.tsx b/lib/data/useInstructorData.tsx index 3053784..27176c8 100644 --- a/lib/data/useInstructorData.tsx +++ b/lib/data/useInstructorData.tsx @@ -1,13 +1,12 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' import Link from 'next/link' -import { useFirestore, useFirestoreDocData } from 'reactfire' -import { usePrevious } from 'react-use' -import { Course, Group, Instructor, Section, Util } from '@cougargrades/types' -import { Observable } from './Observable' +import useSWR from 'swr/immutable' +import { Course, Enrollment, Group, Instructor, Section, Util } from '@cougargrades/types' +import { Observable, ObservableStatus } from './Observable' import { SearchResultBadge } from './useSearchResults' import { Badge, getGradeForGPA, getGradeForStdDev, Grade, grade2Color } from '../../components/badge' import { Column, defaultComparator } from '../../components/datatable' -import { useRosetta } from '../i18n' +import { getRosetta, useRosetta } from '../i18n' import { getYear, seasonCode } from '../util' import { EnrollmentInfoResult } from '../../components/enrollment' import { formatDropRateValue, formatGPAValue, formatSDValue, getBadges } from './getBadges' @@ -15,7 +14,8 @@ import { CourseGroupResult, CourseInstructorResult, group2Result, SectionPlus } import { course2Result } from './useAllGroups' import { CoursePlus } from './useGroupData' import { getChartDataForInstructor } from './getChartDataForInstructor' -import { useFakeFirestore } from '../firebase' +//import { firebaseApp, getFirestoreDocument } from '../ssg' + export interface InstructorResult { badges: SearchResultBadge[]; @@ -44,81 +44,19 @@ export interface InstructorResult { } /** - * React hook for accessing the instructor data client-side - * @param instructorName - * @returns + * Necessary for including `valueFormatter` in table results + * Used client-side */ export function useInstructorData(instructorName: string): Observable { const stone = useRosetta() - const db = useFakeFirestore() - const { data, error, status } = useFirestoreDocData(db.doc(`/instructors/${instructorName}`) as any) - const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1 - const isBadObject = typeof data === 'object' && Object.keys(data).length === 1 - const isActualError = typeof instructorName === 'string' && instructorName !== '' && status !== 'loading' && isBadObject - const groupRefs = ! didLoadCorrectly ? [] : Object - .entries(data.departments) - .sort((a, b) => b[1] - a[1]) - .map(e => e[0]) - .map(e => db.doc(`/groups/${e}`) as firebase.default.firestore.DocumentReference); - const [courseData, setCourseData] = useState([]); - const [sectionData, setSectionData] = useState([]); - const [groupData, setGroupData] = useState([]); - const [sectionLoadingProgress, setSectionLoadingProgress] = useState(0); - const previous = usePrevious(data?._id) - const sharedStatus = status === 'success' ? isActualError ? 'error' : isBadObject ? 'loading' : didLoadCorrectly ? 'success' : 'error' : status - // load courses + section + group data - useEffect(() => { - const didLoadCorrectly = data !== undefined && typeof data === 'object' && Object.keys(data).length > 1; - // prevent loading the same data again - if(didLoadCorrectly && previous !== data._id) { - setCourseData([]); - setSectionData([]); - setGroupData([]); - setSectionLoadingProgress(0); - (async () => { - if(Array.isArray(data.courses) && Util.isDocumentReferenceArray(data.courses)) { - setCourseData(await Util.populate(data.courses)) - } - if(Array.isArray(data.sections) && Util.isDocumentReferenceArray(data.sections)) { - console.count('instructor populate section') - setSectionData(await Util.populate
(data.sections, 10, true, (p, total) => setSectionLoadingProgress(p/total*100), false, false)) - } - if(Array.isArray(groupRefs) && Util.isDocumentReferenceArray(groupRefs)) { - setGroupData(await Util.populate(groupRefs)) - } - })(); - } - },[data,previous]) + const { data, error, isLoading } = useSWR(`/api/instructor/${instructorName}`) + const status: ObservableStatus = error ? 'error' : (isLoading || !data || !instructorName) ? 'loading' : 'success' try { return { data: { - badges: [ - ...(didLoadCorrectly ? getBadges(data.GPA, data.enrollment) : []), - ], - enrollment: [ - ...(didLoadCorrectly ? - data.enrollment.totalEnrolled === 0 ? - [{ key: 'nodata', title: 'No data', color: grade2Color.get('I'), value: -1, percentage: 100 }] : - ['totalA','totalB','totalC','totalD','totalF','totalS','totalNCR','totalW'] - .map(k => ({ - key: k, - title: k.substring(5), // 'totalA' => 'A' - color: grade2Color.get(k.substring(5) as Grade), - value: data.enrollment[k], - percentage: data.enrollment[k] !== undefined && data.enrollment[k].totalEnrolled !== 0 ? data.enrollment[k] / data.enrollment.totalEnrolled * 100 : 0, - }) - ) : []), - ], - firstTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.firstTaught)}`)} ${getYear(data.firstTaught)}` : '', - lastTaught: didLoadCorrectly ? `${stone.t(`season.${seasonCode(data.lastTaught)}`)} ${getYear(data.lastTaught)}` : '', - relatedGroups: [ - ...(didLoadCorrectly ? groupData.map(e => group2Result(e)) : []) - ], - relatedCourses: [ - ...(didLoadCorrectly ? courseData.sort((a,b) => b.enrollment.totalEnrolled - a.enrollment.totalEnrolled).map(e => course2Result(e)) : []) - ], + ...(status === 'success' ? data : {} as any), sectionDataGrid: { columns: [ { @@ -161,10 +99,7 @@ export function useInstructorData(instructorName: string): Observable b.term - a.term).map(e => ({ - id: e._id, - ...e, - })) : []) + ...(status === 'success' ? data!.sectionDataGrid.rows : []), ], }, courseDataGrid: { @@ -247,7 +182,7 @@ export function useInstructorData(instructorName: string): Observable value !== 0 ? {formatGPAValue(value)} : '', + valueFormatter: value => value !== 0 ? {formatGPAValue(value)} : '', }, { field: 'standardDeviation', @@ -257,7 +192,7 @@ export function useInstructorData(instructorName: string): Observable value !== 0 ? {formatSDValue(value)} : '', + valueFormatter: value => value !== 0 ? {formatSDValue(value)} : '', }, { field: 'dropRate', @@ -267,81 +202,22 @@ export function useInstructorData(instructorName: string): Observable isNaN(value) ? 'No data' : {formatDropRateValue(value)}, + valueFormatter: value => isNaN(parseFloat(`${value}`)) ? 'No data' : {formatDropRateValue(value)}, }, ], rows: [ - ...(courseData.sort((a,b) => b._id.localeCompare(a._id)).map(e => ({ - id: e._id, - instructorCount: Array.isArray(e.instructors) ? e.instructors.length : 0, - sectionCount: Array.isArray(e.sections) ? e.sections.length : 0, - gradePointAverage: e.GPA.average, - standardDeviation: e.GPA.standardDeviation, - dropRate: e.enrollment !== undefined ? (e.enrollment.totalW/e.enrollment.totalEnrolled*100) : NaN, - totalEnrolled: e.enrollment !== undefined ? e.enrollment.totalEnrolled : NaN, - enrolledPerSection: e.enrollment !== undefined && Array.isArray(e.sections) ? (e.enrollment.totalEnrolled / e.sections.length) : NaN, - ...e, - }))) + ...(status === 'success' ? data!.courseDataGrid.rows : []), ], }, - dataChart: { - data: [ - ...(didLoadCorrectly ? getChartDataForInstructor(sectionData) : []) - ], - // https://developers.google.com/chart/interactive/docs/gallery/linechart?hl=en#configuration-options - options: { - title: `${instructorName} Average GPA Over Time by Course`, - vAxis: { - title: 'Average GPA', - gridlines: { - count: -1 //auto - }, - maxValue: 4.0, - minValue: 0.0 - }, - hAxis: { - title: 'Semester', - gridlines: { - count: -1 //auto - }, - //slantedText: false, - //showTextEvery: 1, - textStyle: { - fontSize: 12 - }, - }, - chartArea: { - //width: '100%', - //width: '55%', - //width: '65%', - left: 'auto', - //left: 65, // default 'auto' or 65 - right: 'auto', - //right: 35, // default 'auto' or 65 - //left: (window.innerWidth < 768 ? 55 : (window.innerWidth < 992 ? 120 : null)) - }, - legend: { - position: 'bottom' - }, - pointSize: 5, - interpolateNulls: true //lines between point gaps - } - }, - courseCount: didLoadCorrectly ? Array.isArray(data.courses) ? data.courses.length : 0 : 0, - sectionCount: didLoadCorrectly ? Array.isArray(data.sections) ? data.sections.length : 0 : 0, - classSize: didLoadCorrectly && Array.isArray(data.sections) ? data.enrollment.totalEnrolled / data.sections.length : 0, - //sectionLoadingProgress: didLoadCorrectly ? Array.isArray(data.sections) ? (sectionLoadingProgress/data.sections.length*100) : 0 : 0, - sectionLoadingProgress, - rmpHref: didLoadCorrectly && data.rmpLegacyId !== undefined ? `https://www.ratemyprofessors.com/ShowRatings.jsp?tid=${data.rmpLegacyId}` : undefined, }, error, - status: sharedStatus, + status, } } catch(error) { return { data: undefined, - error, + error: error as any, status: 'error', } } diff --git a/lib/data/useSearchResults.ts b/lib/data/useSearchResults.ts index 90eaab0..67d9067 100644 --- a/lib/data/useSearchResults.ts +++ b/lib/data/useSearchResults.ts @@ -1,9 +1,9 @@ -import { useFirestore, useFirestoreCollectionData } from 'reactfire' import { Course, Instructor, Group } from '@cougargrades/types' -import useSWR from 'swr' -import { getGradeForGPA, getGradeForStdDev, grade2Color } from '../../components/badge' +import useSWR from 'swr/immutable' import { Observable } from './Observable' import { getBadges } from './getBadges'; +//import { firebaseApp } from '../ssg' +//import { firebase } from '../firebase_admin' export interface SearchResultBadge { key: string; @@ -65,8 +65,13 @@ function getFirst(arr: (T | undefined)[]): T | undefined { return undefined } -// reference: https://stackoverflow.com/a/1129270/4852536 -const sortByTitle = (inputValue: string) => (a: SearchResult, b: SearchResult) => { +/** + * If compareFunction(a, b) returns value > than 0, sort b before a. + * If compareFunction(a, b) returns value โ‰ค 0, leave a and b in the same order. + * If inconsistent results are returned, then the sort order is undefined. + * reference: https://stackoverflow.com/a/1129270/4852536 + */ +export const sortByTitle = (inputValue: string) => (a: SearchResult, b: SearchResult) => { // if A matches title but B doesn't if(a.title.toUpperCase().startsWith(inputValue.toUpperCase()) && !b.title.toUpperCase().startsWith(inputValue.toUpperCase())) { return -2; @@ -82,56 +87,24 @@ const sortByTitle = (inputValue: string) => (a: SearchResult, b: SearchResult) = }; export function useSearchResults(inputValue: string): Observable { - const SEARCH_RESULT_LIMIT = 5; - const COURSE_EXACT_SEARCH_RESULT_LIMIT = 3; - const COURSE_SEARCH_RESULT_LIMIT = 2; - const db = useFirestore() - // Search for courses - const courseQuery = db.collection('catalog').where('keywords', 'array-contains', inputValue.toLowerCase()).limit(COURSE_SEARCH_RESULT_LIMIT) - const courseData = useFirestoreCollectionData(courseQuery) - // Search for courses that start with the given department code - // reference: https://stackoverflow.com/a/57290806/4852536 - const courseByDeptQuery = db.collection('catalog').where('department', '>=', inputValue.toUpperCase()).where('department', '<', inputValue.toUpperCase().replace(/.$/, c => String.fromCharCode(c.charCodeAt(0) + 1))).limit(COURSE_EXACT_SEARCH_RESULT_LIMIT) - const courseByDeptData = useFirestoreCollectionData(courseByDeptQuery); - // Search for instructors - const instructorQuery = db.collection('instructors').where('keywords', 'array-contains', inputValue.toLowerCase()).orderBy('lastName').limit(SEARCH_RESULT_LIMIT) - const instructorData = useFirestoreCollectionData(instructorQuery) - // Search for groups - const groupQuery = db.collection('groups').where('keywords', 'array-contains', inputValue.toLowerCase()).orderBy('name').limit(SEARCH_RESULT_LIMIT) - const groupData = useFirestoreCollectionData(groupQuery) - // Get "Trending" data - const { data: trendingData, error, isValidating } = useSWR('/api/trending'); + const { data: searchData, error: searchError } = useSWR(`/api/search?${new URLSearchParams({ q: inputValue.toLowerCase() })}`); + const { data: trendingData, error: trendingError } = useSWR('/api/trending'); try { return { data: [ - ...(!isValidating && Array.isArray(trendingData) ? trendingData : []) + ...(Array.isArray(trendingData) ? trendingData : []) .filter(trend => trend.title.includes(inputValue)), - ...[ - ...(courseByDeptData.status === 'success' ? courseByDeptData.data.map(e => course2Result(e)) : []), - ...(courseData.status === 'success' ? courseData.data.map(e => course2Result(e)) : []) - ] - // remove duplicates - // reference: https://stackoverflow.com/a/56757215/4852536 - .filter((item, index, self) => index === self.findIndex(e => (e.key === item.key))) - // put exact title matches first - .sort(sortByTitle(inputValue)), - ...(instructorData.status === 'success' ? instructorData.data.map(e => instructor2Result(e)) : []).sort(sortByTitle(inputValue)), - ...(groupData.status === 'success' ? groupData.data.map(e => group2Result(e)) : []).sort(sortByTitle(inputValue)), + ...(Array.isArray(searchData) ? searchData : []), ], - /** - * If compareFunction(a, b) returns value > than 0, sort b before a. - * If compareFunction(a, b) returns value โ‰ค 0, leave a and b in the same order. - * If inconsistent results are returned, then the sort order is undefined. - */ - error: getFirst([courseData.error, instructorData.error, groupData.error]), - status: inputValue === '' ? 'success' : [courseData.status, instructorData.status, groupData.status].some(e => e === 'loading') ? 'loading' : 'success' + error: getFirst([ trendingError, searchError ]), + status: inputValue === '' ? 'success' : [trendingData, searchData].some(e => !Array.isArray(e)) ? 'loading' : 'success' } } catch(error) { return { data: [], - error, + error: error as any, status: 'error', } } diff --git a/lib/data/useTopResults.ts b/lib/data/useTopResults.ts new file mode 100644 index 0000000..6f7772a --- /dev/null +++ b/lib/data/useTopResults.ts @@ -0,0 +1,55 @@ +import React from 'react' +import useSWR from 'swr/immutable' +import { TopMetric, TopOptions } from '../../lib/top_front' +import { getRosetta, useRosetta } from '../i18n' +import { CoursePlusMetrics, InstructorPlusMetrics } from '../trending' +import { formatTermCode, getYear, seasonCode } from '../util' +import { Observable, ObservableStatus } from './Observable' +import { course2Result } from './useAllGroups' +import { CourseInstructorResult, instructor2Result } from './useCourseData' + +export interface TopResult extends CourseInstructorResult { + metricValue: number; + metricFormatted: string; + metricTimeSpanFormatted: string; +} + +/** + * This has nothing to do with the "metric" numbering system + * https://en.wikipedia.org/wiki/Metric_system + */ +export function formatMetric(value: number, metric: TopMetric): string { + const phrase = metric === 'totalEnrolled' ? 'enrolled' : 'views' + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}K ${phrase}` + } + else { + return `${value} ${phrase}` + } +} + +export function useTopResults({ metric, topic, limit, time }: TopOptions): Observable { + const queryString = new URLSearchParams({ metric, topic, limit: `${limit}`, time }) + const { data, error, isLoading } = useSWR<(CoursePlusMetrics | InstructorPlusMetrics)[]>(`/api/top?${queryString}`); + const status: ObservableStatus = error ? 'error' : (isLoading || !data) ? 'loading' : 'success' + + const get_value = (e: CoursePlusMetrics | InstructorPlusMetrics) => + metric === 'totalEnrolled' + ? (e.enrollment?.totalEnrolled ?? 0) + : metric === 'activeUsers' ? (e.activeUsers ?? 0) : (e.screenPageViews ?? 0); + + return { + data: [ + ...(status === 'success' ? data!.map(e => ({ + ...('catalogNumber' in e ? course2Result(e) : instructor2Result(e)), + metricValue: get_value(e), + metricFormatted: formatMetric(get_value(e), metric), + //metricTimeSpanFormatted: `${getYear(e.firstTaught)}`, + metricTimeSpanFormatted: formatTermCode(e.firstTaught), + })) : []) + ], + status, + error, + } +} + diff --git a/lib/date.d.ts b/lib/date.d.ts new file mode 100644 index 0000000..57cbbef --- /dev/null +++ b/lib/date.d.ts @@ -0,0 +1,9 @@ +// From: https://javascript.plainenglish.io/type-safe-date-strings-66b6dc58658a + +type d = 1|2|3|4|5|6|7|8|9|0; +type YYYY = `19${d}${d}` | `20${d}${d}`; +type oneToNine = 1|2|3|4|5|6|7|8|9; +type MM = `0${oneToNine}` | `1${0|1|2}`; +type DD = `${0}${oneToNine}` | `${1|2}${d}` | `3${0|1}`; +export type DateYMString = `${YYYY}-${MM}`; +export type DateYMDString = `${DateYMString}-${DD}`; \ No newline at end of file diff --git a/lib/environment.ts b/lib/environment.ts index f066a30..bae0e97 100644 --- a/lib/environment.ts +++ b/lib/environment.ts @@ -20,8 +20,8 @@ export const buildArgs: { buildDate: string, vercelEnv: VercelEnv } = { - commitHash: process.env.NEXT_PUBLIC_GIT_SHA, - version: process.env.NEXT_PUBLIC_VERSION, - buildDate: process.env.NEXT_PUBLIC_BUILD_DATE, + commitHash: process.env.NEXT_PUBLIC_GIT_SHA ?? '', + version: process.env.NEXT_PUBLIC_VERSION ?? '', + buildDate: process.env.NEXT_PUBLIC_BUILD_DATE ?? '', vercelEnv: (process.env.NEXT_PUBLIC_VERCEL_ENV as any) ?? 'development' }; diff --git a/lib/faq.ts b/lib/faq.ts index 2ddd15b..afbf7fb 100644 --- a/lib/faq.ts +++ b/lib/faq.ts @@ -44,7 +44,7 @@ export function getPostBySlug(slug: string, fields: string[] = []): FaqPostData } if (typeof data[field] !== 'undefined') { - items[field] = data[field] + items[field as keyof FaqPostData] = data[field] } }) @@ -58,16 +58,16 @@ export function getAllPosts(fields: string[] = []): FaqPostData[] { // sort posts by date in descending order //.sort((post1, post2) => (post1.date > post2.date ? -1 : 1)) // sort by post "id" - .sort((a,b) => a.id - b.id) + .sort((a,b) => (a.id ?? 0) - (b.id ?? 0)) return posts } export async function markdownToHtml(markdown: string) { const schema = defaultSchema - schema.tagNames.push('iframe') - if(!Array.isArray(schema.attributes['iframe'])) - schema.attributes['iframe'] = [] - schema.attributes['iframe'].push('src') + schema!.tagNames!.push('iframe') + if(!Array.isArray(schema!.attributes!['iframe'])) + schema!.attributes!['iframe'] = [] + schema!.attributes!['iframe'].push('src') const result = await remark() .use(remarkRehype, { allowDangerousHtml: true }) .use(rehypeRaw) diff --git a/lib/firebase.tsx b/lib/firebase.tsx index 4dc3f9a..62536e9 100644 --- a/lib/firebase.tsx +++ b/lib/firebase.tsx @@ -1,25 +1,25 @@ -import React, { useState, useEffect } from 'react' -import { useRecoilState } from 'recoil' -import { FirebaseAppProvider, preloadFirestore, useFirebaseApp } from 'reactfire' -import { Firestore as FirestoreStub } from '@cougargrades/types/dist/FirestoreStubs' +import React, { useEffect, useRef } from 'react' +import { FirebaseAppProvider, useFirebaseApp } from 'reactfire' +import { Analytics, getAnalytics, isSupported } from 'firebase/analytics' import { firebaseConfig } from './environment' -import * as localstorage from './localstorage' -import { isOverNDaysOld } from './util' -import { isFirestoreLoadedAtom } from './recoil' -import firebase from 'firebase/app' -//import 'firebase/auth' -import 'firebase/performance' -import 'firebase/analytics' -//import 'firebase/firestore' -//import 'firebase/app-check' +export function useAnalyticsRef() { + const app = useFirebaseApp() + const analyticsRef = useRef(null as any) -export interface WrapperProps { - children: React.ReactNode; + useEffect(() => { + (async () => { + if(await isSupported()) { + analyticsRef.current = getAnalytics(app) + } + })(); + }, []) + + return analyticsRef } -export interface WrapperWithFallback extends WrapperProps { - fallback?: React.ReactNode; +export interface WrapperProps { + children: React.ReactNode; } export const FirebaseAppProviderWrapper = (props: WrapperProps) => ( @@ -28,71 +28,3 @@ export const FirebaseAppProviderWrapper = (props: WrapperProps) => ( ); -export function FirestorePreloader() { - const firebaseApp = useFirebaseApp() - const [, setIsFirestoreLoaded] = useRecoilState(isFirestoreLoadedAtom) - useEffect(() => { - preloadFirestore({ - firebaseApp, - setup: async firestore => { - try { - // set cache size to 300 megabytes - firestore().settings({ cacheSizeBytes: 300e6 }) - - // determine if cache is too old to keep around (7 day age limit) - if(isOverNDaysOld(new Date(localstorage.get('cacheLastCleared')), 7)) { - console.debug('cache could potentially be out of date, clearing') - await firestore().clearPersistence() - localstorage.set('cacheLastCleared', new Date().toISOString()) - } - else { - console.debug('cache is still fresh') - } - - // enable cache - await firestore().enablePersistence({ synchronizeTabs: true }); - console.log('[firebase.tsx] Persistence enabled') - } - catch(err) { - console.warn('[firebase.tsx] There was an issue calling enablePersistence. This is unfortunate, but safe to ignore. More:',err) - } - setIsFirestoreLoaded(true) - } - }) - }, []) - - return null; -} - -export function FirestoreGuard(props: WrapperWithFallback) { - const [isFirestoreLoaded, _] = useRecoilState(isFirestoreLoadedAtom) - - return isFirestoreLoaded ? <>{props.children} : props.fallback ? <>{props.fallback} : null -} - -export const firestoreStub: FirestoreStub = { - // somehow this works - doc: (x: any) => ({ - firestore: { app: { name: '[DEFAULT]' }}, onSnapshot: (x: any) => (() => undefined) - }), - // somehow this works - collection: (x: any) => ({ - where: (a: any, b: any, c: any) => ({ - isEqual: () => false, - onSnapshot: (x: any) => (() => undefined) - }), - isEqual: () => false, - onSnapshot: (x: any) => (() => undefined) - }), - runTransaction: (x: any) => undefined, -} as any - -export function useFakeFirestore() { - const app = useFirebaseApp() - const [isFirestoreLoaded, _] = useRecoilState(isFirestoreLoadedAtom) - return isFirestoreLoaded ? app.firestore() : firestoreStub -} - -export function isFakeFirestore(db: ReturnType) { - return typeof db['useEmulator'] === 'undefined'; -} diff --git a/lib/firebase_admin.ts b/lib/firebase_admin.ts new file mode 100644 index 0000000..97c1b89 --- /dev/null +++ b/lib/firebase_admin.ts @@ -0,0 +1,5 @@ +import * as admin from 'firebase-admin' + +const serviceAccount = JSON.parse(Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS as any, 'base64').toString()); + +export const firebase = !admin.apps.length ? admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }) : admin.app(); diff --git a/lib/firestoreHooks.tsx b/lib/firestoreHooks.tsx deleted file mode 100644 index 31f3467..0000000 --- a/lib/firestoreHooks.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState, useEffect, useRef } from 'react' -import { useRecoilState } from 'recoil' -import { useFirebaseApp, useFirestoreDoc as useFirestoreDocRF, useFirestoreDocData as useFirestoreDocDataRF } from 'reactfire' -import { usePrevious } from 'react-use' -import { DocumentReference as DocumentReferenceStub, Firestore as FirestoreStub } from '@cougargrades/types/dist/FirestoreStubs' -import { isFirestoreLoadedAtom } from './recoil' -import { Observable, ObservableStatus } from './data/Observable' - -// export function useFirestoreDoc(ref: string | undefined): Observable> { -// // try { -// // return useFirestoreDocRF(ref as any) -// // } -// // catch(err) { -// // return { -// // data: undefined, -// // error: err, -// // status: 'error', -// // } -// // } -// const db = useFirestore() -// //const [data, setData] = useState>(undefined) -// const dataRef = useRef>(undefined) -// const [error, setError] = useState(undefined) -// const [status, setStatus] = useState('loading') -// // const [loaded, setLoaded] = useState(false) -// const refsLoaded = db['settings'] !== undefined && ref !== undefined -// const previousRefPath = usePrevious(ref) -// //console.log('previousRefPath?',previousRefPath) -// console.log('fuckin anything') - -// useEffect(() => { -// console.log('DB HOOK') -// },[db]) - -// //console.log('db?',db) -// //console.log('ref?',ref) -// //console.log('refsLoaded?',refsLoaded,'path different?',previousRefPath !== ref?.path) - - -// useEffect(() => { -// if(refsLoaded && previousRefPath !== ref) { -// console.log('once pls: ',ref); -// (async () => { -// try { -// console.log('ASKING AGAIN') -// dataRef.current = await db.doc(ref).get() as firebase.default.firestore.DocumentSnapshot -// //setData(await db.doc(ref).get() as firebase.default.firestore.DocumentSnapshot) -// setStatus('success') -// } -// catch(err) { -// console.error('ERROR',err) -// setStatus('error') -// setError(err) -// } -// })(); -// } -// }, [refsLoaded, previousRefPath, ref, db]) - -// return { -// data: dataRef.current, -// error, -// status, -// } -// } - -// // firebase.default.firestore.DocumentReference | DocumentReferenceStub - -// export function useFirestoreDocData(ref: string | undefined): Observable { -// const { data: snap, status, error } = useFirestoreDoc(ref) - -// return { -// data: status === 'success' && snap.exists ? snap.data() : undefined, -// error, -// status, -// } - -// // console.log(ref) -// // try { - -// // const obvs = useFirestoreDocDataRF(ref) -// // console.log(obvs) -// // return obvs -// // } -// // catch(err) { -// // return { -// // data: undefined, -// // error: err, -// // status: 'error', -// // } -// // } -// } diff --git a/lib/hook.tsx b/lib/hook.tsx index d3b9c37..cd077f4 100644 --- a/lib/hook.tsx +++ b/lib/hook.tsx @@ -1,6 +1,5 @@ -import { useEffect, useRef } from 'react' +import React from 'react' import { useMedia } from 'react-use' -import { useFirebaseApp } from 'reactfire' export function useIsMobile() { return useMedia('(max-width: 576px)'); @@ -9,14 +8,3 @@ export function useIsMobile() { export function useIsCondensed() { return useMedia('(max-width: 768px)'); } - -export function useAnalyticsRef() { - const firebaseApp = useFirebaseApp() - const analyticsRef = useRef(null) - - useEffect(() => { - analyticsRef.current = firebaseApp.analytics() - },[]) - - return analyticsRef; -} diff --git a/lib/i18n.tsx b/lib/i18n.tsx index 8d51b5a..8ca49b5 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -12,12 +12,12 @@ export function useRosetta() { // create and maintain an instance of rosetta const [stone, _] = useState(rosetta(config)); // set the default locale, but replace hyphens with underscores because variable names cant contain hyphens in JS - stone.locale(locale.replace('-','_')); + stone.locale(locale?.replace('-','_')); // return the instance return stone; } -export function getRosetta(locale: string) { +export function getRosetta(locale: string = 'en_US') { const stone = rosetta(config); stone.locale(locale.replace('-','_')); return stone; diff --git a/lib/recoil.ts b/lib/recoil.ts index 2212a73..400f89a 100644 --- a/lib/recoil.ts +++ b/lib/recoil.ts @@ -1,17 +1,10 @@ import { atom } from 'recoil' -//import { Firestore as FirestoreStub } from '@cougargrades/types/dist/FirestoreStubs' -//import { firestoreStub } from './firebase' export const searchInputAtom = atom({ key: 'searchInputAtom', default: null, }) -export const jwtAtom = atom({ - key: 'jwtAtom', - default: null, -}) - export const tocAtom = atom({ key: 'tocAtom', default: false, @@ -21,8 +14,3 @@ export const isFirestoreLoadedAtom = atom({ key: 'isFirestoreLoadedAtom', default: false, }) - -// export const firestoreInstanceAtom = atom({ -// key: 'firestoreInstanceAtom', -// default: firestoreStub, -// }) diff --git a/lib/ssg.tsx b/lib/ssg.tsx index 0cd7a25..22d707b 100644 --- a/lib/ssg.tsx +++ b/lib/ssg.tsx @@ -1,10 +1,15 @@ -import firebase from 'firebase' -import 'firebase/firestore' +// import firebase from 'firebase' +// import 'firebase/firestore' +//import { firebase } from './firebase_admin' + import { firebaseConfig } from '../lib/environment' -export const onlyOne = (value: string | string[]) => Array.isArray(value) ? value[0] : value; -export async function getStaticData(func: string, fallback: T = undefined) { +/** + * + * @deprecated + */ +export async function getStaticData(func: string, fallback: T | undefined = undefined) { try { const { projectId } = firebaseConfig const res = await fetch(`https://us-central1-${projectId}.cloudfunctions.net/${func}`) @@ -16,16 +21,29 @@ export async function getStaticData(func: string, fallback: T = undefined) { } } -export async function getFirestoreDocument(documentPath: string): Promise { - const app = !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : firebase.app() - const db = app.firestore() - const snap = await db.doc(documentPath).get(); - return snap.exists ? snap.data() as T : undefined -} +/** + * @deprecated + */ +//export const firebaseApp = !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : firebase.app() -export async function getFirestoreCollection(collectionPath: string): Promise { - const app = !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : firebase.app() - const db = app.firestore() - const docs = await db.collection(collectionPath).get() - return docs.docs.filter(e => e.exists).map(e => e.data() as T); -} +/** + * + * @deprecated + */ +// export async function getFirestoreDocument(documentPath: string): Promise { +// const app = !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : firebase.app() +// const db = app.firestore() +// const snap = await db.doc(documentPath).get(); +// return snap.exists ? snap.data() as T : undefined +// } + +/** + * + * @deprecated + */ +// export async function getFirestoreCollection(collectionPath: string): Promise { +// const app = !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : firebase.app() +// const db = app.firestore() +// const docs = await db.collection(collectionPath).get() +// return docs.docs.filter(e => e.exists).map(e => e.data() as T); +// } diff --git a/lib/to_seconds.ts b/lib/to_seconds.ts new file mode 100644 index 0000000..f0aaaa0 --- /dev/null +++ b/lib/to_seconds.ts @@ -0,0 +1,11 @@ + +const SECONDS_PER_MINUTE = 60 +const MINUTES_PER_HOUR = 60 +const HOURS_PER_DAY = 24 +const DAYS_PER_WEEK = 7 + + +export const minutes_to_seconds = (minutes: number) => minutes * SECONDS_PER_MINUTE; +export const hours_to_seconds = (hours: number) => hours * MINUTES_PER_HOUR * SECONDS_PER_MINUTE; +export const days_to_seconds = (days: number) => days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE; +export const weeks_to_seconds = (weeks: number) => weeks * DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE; diff --git a/lib/top_back.ts b/lib/top_back.ts new file mode 100644 index 0000000..6846627 --- /dev/null +++ b/lib/top_back.ts @@ -0,0 +1,60 @@ +import { Course, Instructor } from '@cougargrades/types'; +//import { firebaseApp } from './ssg'; +import { firebase } from './firebase_admin' +import { AvailableMetric, CoursePlusMetrics, getAnalyticsReports, InstructorPlusMetrics, RelativeDate, resolveReport } from './trending' +import { notNullish } from './util' + +export type TopMetric = 'totalEnrolled' | AvailableMetric +export type TopTopic = 'course' | 'instructor'; +export type TopLimit = number; +export type TopTime = 'all' | 'lastMonth' +export interface TopOptions { + metric: TopMetric; + topic: TopTopic; + limit: TopLimit; + time: TopTime; +} + +export async function getTopResults({ metric, topic, limit, time }: TopOptions): Promise<(CoursePlusMetrics | InstructorPlusMetrics)[]> { + if (metric === 'totalEnrolled') { + const db = firebase.firestore(); + const query = db.collection(topic === 'course' ? 'catalog' : 'instructors').orderBy('enrollment.totalEnrolled', 'desc').limit(limit) + const snap = await query.get() + + return [ + ...( + snap.docs + .map(doc => doc.data() as (Course | Instructor)) + .map(e => ({ + ...e, + courses: [], + sections: [], + instructors: [], + groups: [], + keywords: [], + })) + ) + ] + } + else { + const reportTime: RelativeDate = time === 'all' ? '2019-01-01' : '30daysAgo'; + const cleanedReports = await getAnalyticsReports(limit, metric, reportTime, topic); + const resolved = await Promise.all(cleanedReports.map(row => resolveReport(row))); + + return [ + ...( + resolved + .filter(notNullish) + .slice(0, limit) + .map(e => ({ + ...e, + courses: [], + sections: [], + instructors: [], + groups: [], + keywords: [], + })) + ) + ]; + } +} diff --git a/lib/top_front.tsx b/lib/top_front.tsx new file mode 100644 index 0000000..7367a2d --- /dev/null +++ b/lib/top_front.tsx @@ -0,0 +1,30 @@ +import { FaqPostData } from './faq'; + +export type { TopOptions, TopMetric, TopTopic, TopLimit, TopTime } from './top_back' + +export const POPULAR_TABS: FaqPostData[] = [ + { + id: 1, + slug: 'enrolled-courses', + title: 'Top Enrolled Courses', + content: 'The most enrolled Courses at the University of Houston.', + }, + { + id: 2, + slug: 'viewed-courses', + title: 'Most Viewed Courses', + content: 'The Courses which are most often viewed on CougarGrades.', + }, + { + id: 3, + slug: 'enrolled-instructors', + title: 'Top Enrolled Instructors', + content: 'The most enrolled Instructors at the University of Houston.', + }, + { + id: 4, + slug: 'viewed-instructors', + title: 'Most Viewed Instructors', + content: 'The Instructors which are most often viewed on CougarGrades.', + }, +]; diff --git a/lib/trending.ts b/lib/trending.ts index 5fff0f4..bb15af6 100644 --- a/lib/trending.ts +++ b/lib/trending.ts @@ -1,7 +1,11 @@ import { BetaAnalyticsDataClient } from '@google-analytics/data' -import { course2Result, instructor2Result, SearchResult } from './data/useSearchResults'; -import { getFirestoreDocument } from './ssg'; +import type { google } from '@google-analytics/data/build/protos/protos' import { Course, Instructor } from '@cougargrades/types'; +import { course2Result, instructor2Result, SearchResult } from './data/useSearchResults'; +//import { getFirestoreDocument } from './ssg'; +import { getFirestoreDocument } from './data/back/getFirestoreData' +import { DateYMDString } from './date' +import { notNullish } from './util' const credential = JSON.parse(Buffer.from(process.env.GOOGLE_APPLICATION_CREDENTIALS as any, 'base64').toString()); const propertyId = process.env.GA4_PROPERTY_ID @@ -14,21 +18,35 @@ const analyticsDataClient = new BetaAnalyticsDataClient({ } }); -type RelativeDate = 'today' | 'yesterday' | `${bigint}daysAgo` -type AvailableDimension = 'pagePath' -type AvailableMetric = 'activeUsers' | 'screenPageViews' + +export type RelativeDate = 'today' | 'yesterday' | `${bigint}daysAgo` | DateYMDString; +export type AvailableDimension = 'pagePath' | '' +export type AvailableMetric = 'activeUsers' | 'screenPageViews' +export type DimensionFilterStringMatchType = google.analytics.data.v1beta.Filter.IStringFilter['matchType'] export interface ReportOptions { startDate: RelativeDate endDate: RelativeDate dimensions: AvailableDimension[] metrics: AvailableMetric[], + dimensionFilter?: { + fieldName: AvailableDimension, + stringFilter: { + matchType: DimensionFilterStringMatchType, + value: string, + }, + }, limit: number, offset: number, orderBy: AvailableMetric, orderDescending: boolean } +/** + * Underlying call to the Google Analytics API + * @param options + * @returns + */ export async function runReport(options: ReportOptions) { const [response] = await analyticsDataClient.runReport({ property: `properties/${propertyId}`, @@ -40,6 +58,9 @@ export async function runReport(options: ReportOptions) { ], dimensions: options.dimensions.map(dim => ({ name: dim })), metrics: options.metrics.map(met => ({ name: met })), + dimensionFilter: !options.dimensionFilter ? undefined : { + filter: options.dimensionFilter + }, orderBys: [ { metric: { @@ -62,27 +83,42 @@ export interface TrendingItemInfo { type CleanedReport = {[key in (AvailableDimension | AvailableMetric)]: string}; -export async function getTrending(limit: number = 5, criteria: AvailableMetric = 'activeUsers'): Promise { +/** + * Query the Google Analytics API by some different criteria + * @param limit + * @param criteria + * @param startDate + * @returns + */ +export async function getAnalyticsReports(limit: number = 5, criteria: AvailableMetric = 'activeUsers', startDate: RelativeDate = '30daysAgo', filterOnly: 'course' | 'instructor' | undefined = undefined): Promise { const dimensions: AvailableDimension[] = ['pagePath'] const metrics: AvailableMetric[] = ['activeUsers', 'screenPageViews'] + const dimensionFilter: ReportOptions['dimensionFilter'] | undefined = filterOnly === undefined ? undefined : { + fieldName: 'pagePath', + stringFilter: { + matchType: 'BEGINS_WITH', + value: filterOnly === 'course' ? '/c/' : '/i/' + } + }; const rawReport = await runReport({ - startDate: '30daysAgo', + startDate: startDate, endDate: 'today', dimensions: dimensions, metrics: metrics, - limit: limit * 2, + dimensionFilter, + limit: Math.round(limit * 1.5), // sometimes results don't resolve, so we want some buffer space offset: 0, orderBy: criteria, orderDescending: true, }); - const cleanedReport = rawReport.rows.map(row => { + const cleanedReport = rawReport.rows!.map(row => { let temp: CleanedReport = {} as any; dimensions.forEach((item, i) => { - temp[item] = row.dimensionValues[i].value + temp[item] = row.dimensionValues![i].value ?? '' }); metrics.forEach((item, i) => { - temp[item] = row.metricValues[i].value + temp[item] = row.metricValues![i].value ?? '' }); if(temp['pagePath']) { temp['pagePath'] = decodeURI(temp['pagePath']); @@ -90,28 +126,67 @@ export async function getTrending(limit: number = 5, criteria: AvailableMetric = return temp; }).filter(item => item.pagePath.startsWith('/c/') || item.pagePath.startsWith('/i/')); - async function resolveReport(report: CleanedReport): Promise { - if(report.pagePath.startsWith('/c/')) { - const courseName = report.pagePath.substring('/c/'.length).trim(); - const courseData = await getFirestoreDocument(`/catalog/${courseName}`); - if(courseData) { - const result = course2Result(courseData); - result.group = '๐Ÿ”ฅ Trending'; + return cleanedReport; +} + +export interface PlusMetrics { + activeUsers?: number; + screenPageViews?: number; +} + +export interface CoursePlusMetrics extends Course, PlusMetrics {} +export interface InstructorPlusMetrics extends Instructor, PlusMetrics {} + +const parseIntOrUndefined = (x: string | undefined): number | undefined => x === undefined ? undefined : isNaN(parseInt(x)) ? undefined : parseInt(x) + +/** + * Turn a report into a Course, Instructor, or undefined if the report's URL didn't correspond to a valid course + * @param report + * @returns + */ +export async function resolveReport(report: CleanedReport): Promise { + if(report.pagePath.startsWith('/c/')) { + const courseName = report.pagePath.substring('/c/'.length).trim(); + const courseData = await getFirestoreDocument(`/catalog/${courseName}`); + if(courseData) courseData.activeUsers = parseIntOrUndefined(report?.activeUsers) + if(courseData) courseData.screenPageViews = parseIntOrUndefined(report?.screenPageViews) + return courseData; + } + else if(report.pagePath.startsWith('/i/')) { + const instructorName = report.pagePath.substring('/i/'.length).trim(); + const instructorData = await getFirestoreDocument(`/instructors/${instructorName}`); + if (instructorData) instructorData.activeUsers = parseIntOrUndefined(report?.activeUsers) + if (instructorData) instructorData.screenPageViews = parseIntOrUndefined(report?.screenPageViews) + return instructorData; + } + return undefined; +} + +/** + * Transform + * @param limit + * @param criteria + * @returns + */ +export async function getTrendingResults(limit: number = 5, criteria: AvailableMetric = 'activeUsers'): Promise { + const cleanedReport = await getAnalyticsReports(limit, criteria); + const TRENDING_TEXT = '๐Ÿ”ฅ Popular'; + + const resolved: (Course | Instructor | undefined)[] = await Promise.all(cleanedReport.map(row => resolveReport(row))); + return resolved + .filter(notNullish) + .map(item => { + if ('catalogNumber' in item) { + const result = course2Result(item); + result.group = TRENDING_TEXT; return result; } - } - else if(report.pagePath.startsWith('/i/')) { - const instructorName = report.pagePath.substring('/i/'.length).trim(); - const instructorData = await getFirestoreDocument(`/instructors/${instructorName}`); - if(instructorData) { - const result = instructor2Result(instructorData); - result.group = '๐Ÿ”ฅ Trending'; + else { + const result = instructor2Result(item); + result.group = TRENDING_TEXT; return result; } - } - return undefined; - } - - const resolved = await Promise.all(cleanedReport.map(row => resolveReport(row))); - return resolved.filter(item => item !== null && item !== undefined).slice(0,limit); + }) + .slice(0,limit); } + diff --git a/lib/util.ts b/lib/util.ts index 1abd492..f8970e7 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -1,3 +1,4 @@ +import { getRosetta } from "./i18n"; export const randRange = (min: number, max: number) => Math.random() * (max - min) + min; @@ -11,6 +12,11 @@ export const seasonCode = (termCode: number): string => { return `${first}${second}` } +export function formatTermCode(termCode: number): string { + const stone = getRosetta() + return `${stone.t(`season.${seasonCode(termCode)}`)} ${getYear(termCode)}` +} + export const getYear = (termCode: number) => Math.floor(termCode / 100) export const sum = (x: number[]) => x.reduce((a, b) => a + b, 0) @@ -21,3 +27,11 @@ export function isOverNDaysOld(d: Date, n: number): boolean { return d.valueOf() < n_days_ago.valueOf(); } + +export const extract = (x: string | string[] | undefined): string => x === undefined ? '' : Array.isArray(x) ? x[0] : x; + +export const truncateWithEllipsis = (x: string, maxLength: number): string => x.length <= maxLength ? x : `${x.slice(0,maxLength-1)}\u2026` + +export function notNullish(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined +} diff --git a/next.config.js b/next.config.js index 5328203..bf2758e 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,4 @@ -const withTM = require('next-transpile-modules')(['rxfire','reactfire','fitty', 'react-fitty', 'react-tilty']); +const withTM = require('next-transpile-modules')(['fitty', 'react-fitty', '@au5ton/react-tilty']); /** @type {import('next/dist/next-server/server/config').NextConfig} */ module.exports = withTM({ @@ -8,6 +8,9 @@ module.exports = withTM({ images: { domains: ['avatars.githubusercontent.com', 'lh3.googleusercontent.com'], }, + api: { + responseLimit: false, + }, headers: async function() { return [ { @@ -29,7 +32,7 @@ module.exports = withTM({ i18n: { // These are all the locales you want to support in // your application - locales: ['en-US', 'es'], + locales: ['en-US'], // This is the default locale you want to be used when visiting // a non-locale prefixed path e.g. `/hello` defaultLocale: 'en-US', @@ -51,6 +54,6 @@ module.exports = withTM({ // }, // ], // Automatically redirect based on the user's preferred locale - localeDetection: true, + localeDetection: false, }, }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 826cb86..1053ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@cougargrades/web", - "version": "1.0.7", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cougargrades/web", - "version": "1.0.7", + "version": "1.1.0", "license": "MIT", "dependencies": { + "@au5ton/react-tilty": "^2.0.4", "@au5ton/use-atom-feed": "^1.0.4", "@cougargrades/publicdata": "^1.0.0-20220805004439", "@cougargrades/types": "0.0.91", @@ -20,7 +21,8 @@ "@mui/material": "^5.2.6", "@primer/css": "^19.1.1", "bootstrap": "^5.0.1", - "firebase": "^8.10.0", + "firebase": "^9.16.0", + "firebase-admin": "^9.12.0", "fitty": "^2.3.5", "gray-matter": "^4.0.3", "next": "~12.0.3", @@ -32,13 +34,12 @@ "react-dom": "17.0.2", "react-dropzone": "^11.3.4", "react-fitty": "^1.0.1", - "react-google-charts": "^3.0.15", + "react-google-charts": "^4.0.0", "react-highlight-words": "^0.17.0", "react-swipeable": "^6.1.2", - "react-tilty": "^2.0.3", "react-transition-group": "^4.4.2", "react-use": "^17.2.4", - "reactfire": "^3.0.0", + "reactfire": "^4.2.2", "recoil": "^0.5.2", "rehype-raw": "^6.1.1", "rehype-sanitize": "^5.0.1", @@ -48,12 +49,14 @@ "remark-rehype": "^10.1.0", "rosetta": "^1.1.0", "sass": "^1.45.2", + "swr": "^2.0.1", "timeago-react": "^3.0.2" }, "devDependencies": { "@au5ton/snooze": "^1.0.3", "@types/nprogress": "^0.2.0", "@types/react": "^17.0.11", + "@types/react-highlight-words": "^0.16.4", "eslint": "^7.28.0", "eslint-config-next": "^12.1.6", "next-transpile-modules": "^9.0.0", @@ -73,6 +76,14 @@ "node": ">=6.0.0" } }, + "node_modules/@au5ton/react-tilty": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@au5ton/react-tilty/-/react-tilty-2.0.4.tgz", + "integrity": "sha512-e6Iu1NkC2kfu5N5vLojBq7mALF9POqnjERXfklDHPN1yCKCHhGDKi7Uh3ztHr04a/UPVS3ERVb9+pLpYc4SyCg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@au5ton/snooze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@au5ton/snooze/-/snooze-1.0.3.tgz", @@ -92,6 +103,17 @@ "node": ">=12" } }, + "node_modules/@au5ton/use-atom-feed/node_modules/swr": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-0.5.7.tgz", + "integrity": "sha512-Jh1Efgu8nWZV9rU4VLUMzBzcwaZgi4znqbVXvAtUy/0JzSiN6bNjLaJK8vhY/Rtp7a83dosz5YuehfBNwC/ZoQ==", + "dependencies": { + "dequal": "2.0.2" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -608,67 +630,103 @@ "integrity": "sha512-qhbGfqBRwUlM6MCSaJdUfjq86opNCMvM+6kVvs6S0kYhy0V8dKbe4rDMIklEJGuMc5QH5OuPjdCReu9I0tim2w==" }, "node_modules/@firebase/analytics": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.6.18.tgz", - "integrity": "sha512-FXNtYDxbs9ynPbzUVuG94BjFPOPpgJ7156660uvCBuKgoBCIVcNqKkJQQ7TH8384fqvGjbjdcgARY9jgAHbtog==", - "dependencies": { - "@firebase/analytics-types": "0.6.0", - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.9.1.tgz", + "integrity": "sha512-ARXtNHDrjDhVrs5MqmFDpr5yyCw89r1eHLd+Dw9fotAufxL1WTmo6O9bJqKb7QulIJaA84vsFokA3NaO2DNCnQ==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.1.tgz", + "integrity": "sha512-qfFAGS4YFsBbmZwVa7xaDnGh7k9BKF4o/piyjySAv0lxRYd74/tSrm3kMk1YM7GCti7PdbgKvl6oSR70zMFQjw==", + "dependencies": { + "@firebase/analytics": "0.9.1", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/analytics-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.6.0.tgz", - "integrity": "sha512-kbMawY0WRPyL/lbknBkme4CNLl+Gw+E9G4OpNeXAauqoQiNkBgpIvZYy7BRT4sNGhZbxdxXxXbruqUwDzLmvTw==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" }, "node_modules/@firebase/app": { - "version": "0.6.30", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.30.tgz", - "integrity": "sha512-uAYEDXyK0mmpZ8hWQj5TNd7WVvfsU8PgsqKpGljbFBG/HhsH8KbcykWAAA+c1PqL7dt/dbt0Reh1y9zEdYzMhg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.1.tgz", + "integrity": "sha512-Z8wOSol+pvp4CFyY1mW+aqdZlrwhW/ha2YXQ6/avJ56c5Hnvt4k6GktZE6o5NyzvfJTgNHryhMtnEJMIuLaT4w==", "dependencies": { - "@firebase/app-types": "0.6.3", - "@firebase/component": "0.5.6", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "dom-storage": "2.1.0", - "tslib": "^2.1.0", - "xmlhttprequest": "1.8.0" + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "idb": "7.0.1", + "tslib": "^2.1.0" } }, "node_modules/@firebase/app-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.3.2.tgz", - "integrity": "sha512-YjpsnV1xVTO1B836IKijRcDeceLgHQNJ/DWa+Vky9UHkm1Mi4qosddX8LZzldaWRTWKX7BN1MbZOLY8r7M/MZQ==", - "dependencies": { - "@firebase/app-check-interop-types": "0.1.0", - "@firebase/app-check-types": "0.3.1", - "@firebase/component": "0.5.6", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.6.1.tgz", + "integrity": "sha512-gDG4Gr4n3MnBZAAwLMynU9u/b+f1y87lCezfwlmN1gUxD85mJcvp4hLf87fACTyRkdVfe8hqSXm+MOYn2bMGLg==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.1.tgz", + "integrity": "sha512-IaSYdmaoQgWUrN6rjAYJs1TGXj38Wl9damtrDEyJBf7+rrvKshPAP/CP6e2bd89XOMZKbvy8rKoe1CqX1K3ZjQ==", + "dependencies": { + "@firebase/app-check": "0.6.1", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/app-check-interop-types": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz", - "integrity": "sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.2.0.tgz", + "integrity": "sha512-+3PQIeX6/eiVK+x/yg8r6xTNR97fN7MahFDm+jiQmDjcyvSefoGuTTNQuuMScGyx3vYUBeZn+Cp9kC0yY/9uxQ==" }, "node_modules/@firebase/app-check-types": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.3.1.tgz", - "integrity": "sha512-KJ+BqJbdNsx4QT/JIT1yDj5p6D+QN97iJs3GuHnORrqL+DU3RWc9nSYQsrY6Tv9jVWcOkMENXAgDT484vzsm2w==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.1.tgz", + "integrity": "sha512-UgPy2ZO0li0j4hAkaZKY9P1TuJEx5RylhUWPzCb8DZhBm+uHdfsFI9Yr+wMlu6qQH2sWoweFtYU6ljGzxwdctw==", + "dependencies": { + "@firebase/app": "0.9.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } }, "node_modules/@firebase/app-types": { "version": "0.6.3", @@ -676,16 +734,36 @@ "integrity": "sha512-/M13DPPati7FQHEQ9Minjk1HGLm/4K4gs9bR4rzLCWJg64yGtVC0zNg9gDpkw9yc2cvol/mNFxqTtd4geGrwdw==" }, "node_modules/@firebase/auth": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.8.tgz", - "integrity": "sha512-mR0UXG4LirWIfOiCWxVmvz1o23BuKGxeItQ2cCUgXLTjNtWJXdcky/356iTUsd7ZV5A78s2NHeN5tIDDG6H4rg==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.21.1.tgz", + "integrity": "sha512-/ap7eT9X7kZTD4Fn2m+nJyC1a9DfFo0H4euoJDN8U+JCMN+GOqkPbkMWCey7wV510WNoPCZQ05+nsAqKkbEVJw==", "dependencies": { - "@firebase/auth-types": "0.10.3" + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" }, "peerDependencies": { "@firebase/app": "0.x" } }, + "node_modules/@firebase/auth-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.3.1.tgz", + "integrity": "sha512-Ndcaam+IL1TuJ6hZ0EcQ+v261cK3kPm4mvUtouoTfl3FNinm9XvhccN8ojuaRtIV9TiY18mzGjONKF5ZCXLIZw==", + "dependencies": { + "@firebase/auth": "0.21.1", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, "node_modules/@firebase/auth-interop-types": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", @@ -696,244 +774,412 @@ } }, "node_modules/@firebase/auth-types": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz", - "integrity": "sha512-zExrThRqyqGUbXOFrH/sowuh2rRtfKHp9SBVY2vOqKWdCX1Ztn682n9WLtlUDsiYVIbBcwautYWk2HyCGFv0OA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "node_modules/@firebase/component": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.6.tgz", - "integrity": "sha512-GyQJ+2lrhsDqeGgd1VdS7W+Y6gNYyI0B51ovNTxeZVG/W8I7t9MwEiCWsCvfm5wQgfsKp9dkzOcJrL5k8oVO/Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.1.tgz", + "integrity": "sha512-yvKthG0InjFx9aOPnh6gk0lVNfNVEtyq3LwXgZr+hOwD0x/CtXq33XCpqv0sQj5CA4FdMy8OO+y9edI+ZUw8LA==", "dependencies": { - "@firebase/util": "1.3.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "node_modules/@firebase/database": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.11.0.tgz", - "integrity": "sha512-b/kwvCubr6G9coPlo48PbieBDln7ViFBHOGeVt/bt82yuv5jYZBEYAac/mtOVSxpf14aMo/tAN+Edl6SWqXApw==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.1.tgz", + "integrity": "sha512-iX6/p7hoxUMbYAGZD+D97L05xQgpkslF2+uJLZl46EdaEfjVMEwAdy7RS/grF96kcFZFg502LwPYTXoIdrZqOA==", + "dependencies": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", + "dependencies": { + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "dependencies": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/database": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", "dependencies": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.6", - "@firebase/database-types": "0.8.0", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "faye-websocket": "0.11.3", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/database-types": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", + "dependencies": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.5.2" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/logger": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", + "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat/node_modules/@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@firebase/database-types": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.8.0.tgz", - "integrity": "sha512-7IdjAFRfPWyG3b4wcXyghb3Y1CLCSJFZIg1xl5GbTVMttSQFT4B5NYdhsfA34JwAsv5pMzPpjOaS3/K9XJ2KiA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.1.tgz", + "integrity": "sha512-UgUx9VakTHbP2WrVUdYrUT2ofTFVfWjGW2O1fwuvvMyo6WSnuSyO5nB1u0cyoMPvO25dfMIUVerfK7qFfwGL3Q==", "dependencies": { - "@firebase/app-types": "0.6.3", - "@firebase/util": "1.3.0" + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.0" } }, + "node_modules/@firebase/database-types/node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/@firebase/database/node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + }, "node_modules/@firebase/firestore": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.4.1.tgz", - "integrity": "sha512-S51XnILdhNt0ZA6bPnbxpqKPI5LatbGY9RQjA2TmATrjSPE3aWndJsLIrutI6aS9K+YFwy5+HLDKVRFYQfmKAw==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/firestore-types": "2.4.0", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "@firebase/webchannel-wrapper": "0.5.1", - "@grpc/grpc-js": "^1.3.2", - "@grpc/proto-loader": "^0.6.0", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.8.1.tgz", + "integrity": "sha512-oc2HMkUnq/zF+g9o974tp5RVCdXCnrU8e5S98ajfWG/hGV+8pr4i6vIa4z0yEXKWGi4X0FguxrC69z1dxEJbNg==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "@firebase/webchannel-wrapper": "0.9.0", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", "node-fetch": "2.6.7", "tslib": "^2.1.0" }, "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=10.10.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.1.tgz", + "integrity": "sha512-7eE4O2ASyy5X2h4a+KCRt0ZpliUAKo2jrKxKl1ZVCnOOjSCkXXeRVRG9eNZRqBwukhdwskJTM9acs0WxmKOYLA==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/firestore": "3.8.1", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/firestore-types": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.4.0.tgz", - "integrity": "sha512-0dgwfuNP7EN6/OlK2HSNSQiQNGLGaRBH0gvgr1ngtKKJuJFuq0Z48RBMeJX9CGjV4TP9h2KaB+KrUKJ5kh1hMg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "node_modules/@firebase/functions": { - "version": "0.6.16", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.6.16.tgz", - "integrity": "sha512-KDPjLKSjtR/zEH06YXXbdWTi8gzbKHGRzL/+ibZQA/1MLq0IilfM+1V1Fh8bADsMCUkxkqoc1yiA4SUbH5ajJA==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/functions-types": "0.4.0", - "@firebase/messaging-types": "0.5.0", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.9.1.tgz", + "integrity": "sha512-xCSSU4aVSqYU+lCqhn9o5jJcE1KLUOOKyJfCTdCSCyTn2J3vl9Vk4TDm3JSb1Eu6XsNWtxeMW188F/GYxuMWcw==", + "dependencies": { + "@firebase/app-check-interop-types": "0.2.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.0", "node-fetch": "2.6.7", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.1.tgz", + "integrity": "sha512-f2D2XoRN+QCziCrUL7UrLaBEoG3v2iAeyNwbbOQ3vv0rI0mtku2/yeB2OINz5/iI6oIrBPUMNLr5fitofj7FpQ==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/functions": "0.9.1", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/functions-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.4.0.tgz", - "integrity": "sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" + }, + "node_modules/@firebase/functions/node_modules/@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" }, "node_modules/@firebase/installations": { - "version": "0.4.32", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.4.32.tgz", - "integrity": "sha512-K4UlED1Vrhd2rFQQJih+OgEj8OTtrtH4+Izkx7ip2bhXSc+unk8ZhnF69D0kmh7zjXAqEDJrmHs9O5fI3rV6Tw==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/installations-types": "0.3.4", - "@firebase/util": "1.3.0", - "idb": "3.0.2", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.1.tgz", + "integrity": "sha512-gpobP09LLLakBfNCL04fyblfyb3oX1pn+iNmELygrcAkXTO13IAMuOzThI+Xk4NHQZMX1p5GFSAiGbG4yfsSUQ==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", + "idb": "7.0.1", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.1.tgz", + "integrity": "sha512-X4IBVKajEeaE45zWX0Y1q8ey39aPFLa+BsUoYzsduMzCxcMBIPZd5/lV1EVGt8SN3+unnC2J75flYkxXVlhBoQ==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/installations-types": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.3.4.tgz", - "integrity": "sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", "peerDependencies": { "@firebase/app-types": "0.x" } }, "node_modules/@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dependencies": { + "tslib": "^2.1.0" + } }, "node_modules/@firebase/messaging": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.8.0.tgz", - "integrity": "sha512-hkFHDyVe1kMcY9KEG+prjCbvS6MtLUgVFUbbQqq7JQfiv58E07YCzRUcMrJolbNi/1QHH6Jv16DxNWjJB9+/qA==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/messaging-types": "0.5.0", - "@firebase/util": "1.3.0", - "idb": "3.0.2", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.1.tgz", + "integrity": "sha512-/F+2OWarR8TcJJVlQS6zBoHHfXMgfgR0/ukQ3h7Ow3WZ3WZ9+Sj/gvxzothXZm+WtBylfXuhiANFgHEDFL0J0w==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.0", + "idb": "7.0.1", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" } }, - "node_modules/@firebase/messaging-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/messaging-types/-/messaging-types-0.5.0.tgz", - "integrity": "sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg==", + "node_modules/@firebase/messaging-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.1.tgz", + "integrity": "sha512-BykvXtAWOs0W4Ik79lNfMKSxaUCtOJ47PJ9Vw2ySHZ14vFFNuDAtRTOBOlAFhUpsHqRoQFvFCkBGsRIQYq8hzw==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/messaging": "0.12.1", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, "peerDependencies": { - "@firebase/app-types": "0.x" + "@firebase/app-compat": "0.x" } }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" + }, "node_modules/@firebase/performance": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.4.18.tgz", - "integrity": "sha512-lvZW/TVDne2TyOpWbv++zjRn277HZpbjxbIPfwtnmKjVY1gJ+H77Qi1c2avVIc9hg80uGX/5tNf4pOApNDJLVg==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/performance-types": "0.0.13", - "@firebase/util": "1.3.0", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.1.tgz", + "integrity": "sha512-mT/CWz3CLgyn/a3sO/TJgrTt+RA3DfuvWwGXY9zmIiuBZY2bDi1M2uMefJdJKc9sBUPRajNF6RL10nGYq3BAuQ==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.1.tgz", + "integrity": "sha512-4mn6eS7r2r+ZAHvU0OHE+3ZO+x6gOVhf2ypBoijuDNaRNjSn9GcvA8udD4IbJ8FNv/k7mbbtA9AdxVb701Lr1g==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.1", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/performance-types": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.0.13.tgz", - "integrity": "sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" }, - "node_modules/@firebase/polyfill": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.36.tgz", - "integrity": "sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==", + "node_modules/@firebase/remote-config": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.1.tgz", + "integrity": "sha512-RCzBH3FjAPRSP3M1T7jdxLYBesIdLtNIQ0fR9ywJpGSSa0kIXEJ9iSZMTP+9pJtaCxz8db07FvjEqg7Y+lgjzg==", "dependencies": { - "core-js": "3.6.5", - "promise-polyfill": "8.1.3", - "whatwg-fetch": "2.0.4" + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@firebase/remote-config": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.1.43.tgz", - "integrity": "sha512-laNM4MN0CfeSp7XCVNjYOC4DdV6mj0l2rzUh42x4v2wLTweCoJ/kc1i4oWMX9TI7Jw8Am5Wl71Awn1J2pVe5xA==", - "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/remote-config-types": "0.1.9", - "@firebase/util": "1.3.0", + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.1.tgz", + "integrity": "sha512-RPCj7c2Q3QxMgJH3YCt0iD57KppFApghxAGETzlr6Jm6vT7k0vqvk2KgRBgKa4koJBsgwlUtRn2roaCqUEadyg==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.1", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/remote-config-types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz", - "integrity": "sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA==" + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" }, "node_modules/@firebase/storage": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.7.1.tgz", - "integrity": "sha512-T7uH6lAgNs/Zq8V3ElvR3ypTQSGWon/R7WRM2I5Td/d0PTsNIIHSAGB6q4Au8mQEOz3HDTfjNQ9LuQ07R6S2ug==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.10.1.tgz", + "integrity": "sha512-eN4ME+TFCh5KfyG9uo8PhE6cgKjK5Rb9eucQg1XEyLHMiaZiUv2xSuWehJn0FaL+UdteoaWKuRUZ4WXRDskXrA==", "dependencies": { - "@firebase/component": "0.5.6", - "@firebase/storage-types": "0.5.0", - "@firebase/util": "1.3.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", "node-fetch": "2.6.7", "tslib": "^2.1.0" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.2.1.tgz", + "integrity": "sha512-H0oFdYsMn2Z6tP9tlVERBkJiZsCbFAcl3Li1dnpvDg9g323egdjCnUUgH/tJODRR/Y84iZSNRkg4FvHDVI/o7Q==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/storage": "0.10.1", + "@firebase/storage-types": "0.7.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, "node_modules/@firebase/storage-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.5.0.tgz", - "integrity": "sha512-6Wv3Lu7s18hsgW7HG4BFwycTquZ3m/C8bjBoOsmPu0TD6M1GKwCzOC7qBdN7L6tRYPh8ipTj5+rPFrmhGfUVKA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.7.0.tgz", + "integrity": "sha512-n/8pYd82hc9XItV3Pa2KGpnuJ/2h/n/oTAaBberhe6GeyWQPnsmwwRK94W3GxUwBA/ZsszBAYZd7w7tTE+6XXA==", "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "node_modules/@firebase/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.3.0.tgz", - "integrity": "sha512-SESvmYwuKOVCZ1ZxLbberbx+9cnbxpCa4CG2FUSQYqN6Ab8KyltegMDIsqMw5KyIBZ4n1phfHoOa22xo5NzAlQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.0.tgz", + "integrity": "sha512-oeoq/6Sr9btbwUQs5HPfeww97bf7qgBbkknbDTXpRaph2LZ23O9XLCE5tJy856SBmGQfO4xBZP8dyryLLM2nSQ==", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/@firebase/webchannel-wrapper": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.5.1.tgz", - "integrity": "sha512-dZMzN0uAjwJXWYYAcnxIwXqRTZw3o14hGe7O6uhwjD1ZQWPVYA5lASgnNskEBra0knVBsOXB4KXg+HnlKewN/A==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.9.0.tgz", + "integrity": "sha512-BpiZLBWdLFw+qFel9p3Zs1jD6QmH7Ii4aTDu6+vx8ShdidChZUXqDhYJly4ZjSgQh54miXbBgBrk0S+jTIh/Qg==" }, "node_modules/@google-analytics/data": { "version": "3.1.0", @@ -946,6 +1192,387 @@ "node": ">=12.0.0" } }, + "node_modules/@google-cloud/firestore": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/@grpc/grpc-js": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.12.tgz", + "integrity": "sha512-JmvQ03OTSpVd9JTlj/K3IWHSz4Gk/JMLUTtW7Zb0KvO1LcOYGATh5cNuRYzCAeDR3O8wq+q8FZe97eO9MBrkUw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "optional": true, + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@google-cloud/firestore/node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.0.tgz", + "integrity": "sha512-hYCqTDuII4iJ4stZqiuGCSU8xxWl5JeXYpwARGtn/tWcKCAro6h3WQz+xpsNbXW0UYqpmTQFEyFWO0G0Kjt64g==", + "hasInstallScript": true, + "optional": true, + "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" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@google-cloud/firestore/node_modules/@grpc/grpc-js/node_modules/protobufjs/node_modules/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + }, + "node_modules/@google-cloud/firestore/node_modules/gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "optional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "optional": true, + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/google-gax": { + "version": "2.30.5", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.5.tgz", + "integrity": "sha512-Jey13YrAN2hfpozHzbtrwEfEHdStJh1GwaQ2+Akh1k0Tv/EuNVSuBtHZoKSBm5wBMvNsxTsEIZ/152NrYyZgxQ==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "~1.6.0", + "@grpc/proto-loader": "^0.6.12", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.14.0", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^0.1.8", + "protobufjs": "6.11.3", + "retry-request": "^4.0.0" + }, + "bin": { + "compileProtos": "build/tools/compileProtos.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "optional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/firestore/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "node_modules/@google-cloud/firestore/node_modules/proto3-json-serializer": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz", + "integrity": "sha512-A60IisqvnuI45qNRygJjrnNjX2TMdQGMY+57tR3nul3ZgO2zXkR9OGR8AXxJhkqx84g0FTnrfi3D5fWMSdANdQ==", + "optional": true, + "dependencies": { + "protobufjs": "^6.11.2" + } + }, + "node_modules/@google-cloud/firestore/node_modules/retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage": { + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.20.5.tgz", + "integrity": "sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.14.1", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "retry-request": "^4.2.2", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "uuid": "^8.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "optional": true, + "dependencies": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "optional": true, + "dependencies": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "optional": true, + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "optional": true, + "dependencies": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@google-cloud/storage/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@google-cloud/storage/node_modules/retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", @@ -1010,14 +1637,14 @@ "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==" }, "node_modules/@grpc/proto-loader": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.11.tgz", - "integrity": "sha512-MRiPjTjNgKxMupQ0M8mM9Mcljb2aZvE3Y/oEv+dacozIs2TwTdiPbvfkZpMeghfjGtoDJhDjyCtmFzJcjdDTUQ==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "long": "^4.0.0 || ^5.2.0", - "protobufjs": "^6.10.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "bin": { @@ -1027,6 +1654,11 @@ "node": ">=6" } }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -1539,6 +2171,14 @@ "node": ">= 8" } }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@popperjs/core": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", @@ -1624,6 +2264,32 @@ "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", "dev": true }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -1640,6 +2306,27 @@ "@types/trusted-types": "*" } }, + "node_modules/@types/express": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", + "integrity": "sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -1659,6 +2346,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", @@ -1691,6 +2386,11 @@ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -1727,6 +2427,16 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, "node_modules/@types/react": { "version": "17.0.44", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.44.tgz", @@ -1737,6 +2447,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-highlight-words": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/react-highlight-words/-/react-highlight-words-0.16.4.tgz", + "integrity": "sha512-KITBX3xzheQLu2s3bUgLmRE7ekmhc52zRjRTwkKayQARh30L4fjEGzGm7ULK9TuX2LgxWWavZqyQGDGjAHbL3w==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -1758,6 +2477,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", @@ -2103,6 +2831,15 @@ "node": ">=8" } }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -2278,9 +3015,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001335", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz", - "integrity": "sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==", + "version": "1.0.30001449", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", + "integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==", "funding": [ { "type": "opencollective", @@ -2426,11 +3163,40 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "optional": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -2447,16 +3213,6 @@ "toggle-selection": "^1.0.6" } }, - "node_modules/core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-js-pure": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.4.tgz", @@ -2496,6 +3252,15 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/css-in-js-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", @@ -2593,6 +3358,17 @@ "node": ">=6" } }, + "node_modules/dicer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", + "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -2639,19 +3415,23 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-storage": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", - "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==", - "engines": { - "node": "*" - } - }, "node_modules/dompurify": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "optional": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -2716,6 +3496,12 @@ "node": ">=8.6" } }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, "node_modules/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -3534,9 +4320,9 @@ } }, "node_modules/faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -3596,28 +4382,91 @@ } }, "node_modules/firebase": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-8.10.1.tgz", - "integrity": "sha512-84z/zqF8Y5IpUYN8nREZ/bxbGtF5WJDOBy4y0hAxRzGpB5+2tw9PQgtTnUzk6MQiVEf/WOniMUL3pCVXKsxALw==", - "dependencies": { - "@firebase/analytics": "0.6.18", - "@firebase/app": "0.6.30", - "@firebase/app-check": "0.3.2", - "@firebase/app-types": "0.6.3", - "@firebase/auth": "0.16.8", - "@firebase/database": "0.11.0", - "@firebase/firestore": "2.4.1", - "@firebase/functions": "0.6.16", - "@firebase/installations": "0.4.32", - "@firebase/messaging": "0.8.0", - "@firebase/performance": "0.4.18", - "@firebase/polyfill": "0.3.36", - "@firebase/remote-config": "0.1.43", - "@firebase/storage": "0.7.1", - "@firebase/util": "1.3.0" + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.16.0.tgz", + "integrity": "sha512-nNLpDwJvfP3crRc6AjnHH46TAkFzk8zimNVMJfYRCwAf5amOSGyU8duuc3IsJF6dQGiYLSfzfr2tMCsQa+rhKQ==", + "dependencies": { + "@firebase/analytics": "0.9.1", + "@firebase/analytics-compat": "0.2.1", + "@firebase/app": "0.9.1", + "@firebase/app-check": "0.6.1", + "@firebase/app-check-compat": "0.3.1", + "@firebase/app-compat": "0.2.1", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.21.1", + "@firebase/auth-compat": "0.3.1", + "@firebase/database": "0.14.1", + "@firebase/database-compat": "0.3.1", + "@firebase/firestore": "3.8.1", + "@firebase/firestore-compat": "0.3.1", + "@firebase/functions": "0.9.1", + "@firebase/functions-compat": "0.3.1", + "@firebase/installations": "0.6.1", + "@firebase/installations-compat": "0.2.1", + "@firebase/messaging": "0.12.1", + "@firebase/messaging-compat": "0.2.1", + "@firebase/performance": "0.6.1", + "@firebase/performance-compat": "0.2.1", + "@firebase/remote-config": "0.4.1", + "@firebase/remote-config-compat": "0.2.1", + "@firebase/storage": "0.10.1", + "@firebase/storage-compat": "0.2.1", + "@firebase/util": "1.9.0" + } + }, + "node_modules/firebase-admin": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.12.0.tgz", + "integrity": "sha512-AtA7OH5RbIFGoc0gZOQgaYC6cdjdhZv4w3XgWoupkPKO1HY+0GzixOuXDa75kFeoVyhIyo4PkLg/GAC1dC1P6w==", + "dependencies": { + "@firebase/database-compat": "^0.1.1", + "@firebase/database-types": "^0.7.2", + "@types/node": ">=12.12.47", + "dicer": "^0.3.0", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.2", + "node-forge": "^0.10.0" }, "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=10.13.0" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^4.5.0", + "@google-cloud/storage": "^5.3.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/database-types": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.3.tgz", + "integrity": "sha512-dSOJmhKQ0nL8O4EQMRNGpSExWCXeHtH57gGg0BfNAdWcKhC8/4Y+qfKLfWXzyHvrSecpLmO0SmAi/iK2D5fp5A==", + "dependencies": { + "@firebase/app-types": "0.6.3" + } + }, + "node_modules/firebase-admin/node_modules/node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/firebase/node_modules/@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "node_modules/firebase/node_modules/@firebase/database-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.1.tgz", + "integrity": "sha512-sI7LNh0C8PCq9uUKjrBKLbZvqHTSjsf2LeZRxin+rHVegomjsOAYk9OzYwxETWh3URhpMkCM8KcTl7RVwAldog==", + "dependencies": { + "@firebase/component": "0.6.1", + "@firebase/database": "0.14.1", + "@firebase/database-types": "0.10.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" } }, "node_modules/fitty": { @@ -3671,7 +4520,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "devOptional": true }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -4211,6 +5060,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true + }, "node_modules/hast-to-hyperscript": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz", @@ -4402,6 +5257,20 @@ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4420,9 +5289,9 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "node_modules/idb": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", - "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" }, "node_modules/ignore": { "version": "4.0.6", @@ -4457,7 +5326,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -4677,6 +5546,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz", @@ -4762,6 +5640,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "optional": true + }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -4796,6 +5680,20 @@ "@babel/runtime": "^7.15.4" } }, + "node_modules/jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -4911,6 +5809,54 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "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": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "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" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", @@ -4934,6 +5880,22 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.5.tgz", + "integrity": "sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==", + "dependencies": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^8.5.9", + "debug": "^4.3.4", + "jose": "^2.0.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, "node_modules/jws": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", @@ -4995,6 +5957,11 @@ "node": ">= 0.8.0" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5031,12 +5998,52 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -5079,6 +6086,44 @@ "node": ">=10" } }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -5690,6 +6735,39 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6196,11 +7274,6 @@ "node": ">=0.4.0" } }, - "node_modules/promise-polyfill": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz", - "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6289,6 +7362,32 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6374,14 +7473,10 @@ } }, "node_modules/react-google-charts": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-3.0.15.tgz", - "integrity": "sha512-78s5xOQOJvL+jIewrWQZEHtlVk+5Yh4zZy+ODA1on1o1FaRjKWXxoo4n4JQl1XuqkF/A9NWque3KqM6pMggjzQ==", - "dependencies": { - "react-load-script": "^0.0.6" - }, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-4.0.0.tgz", + "integrity": "sha512-9OG0EkBb9JerKEPQYdhmAXnhGLzOdOHOPS9j7l+P1a3z1kcmq9mGDa7PUoX/VQUY4IjZl2/81nsO4o+1cuYsuw==", "peerDependencies": { - "prop-types": ">=15", "react": ">=16.3.0", "react-dom": ">=16.3.0" } @@ -6404,16 +7499,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-load-script": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/react-load-script/-/react-load-script-0.0.6.tgz", - "integrity": "sha512-aRGxDGP9VoLxcsaYvKWIW+LRrMOzz2eEcubTS4NvQPPugjk2VvMhow0wWTkSl7RxookomD1MwcP4l5UStg5ShQ==", - "deprecated": "abandoned and unmaintained", - "peerDependencies": { - "prop-types": ">=15", - "react": ">=0.14.9" - } - }, "node_modules/react-swipeable": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.2.2.tgz", @@ -6422,14 +7507,6 @@ "react": "^16.8.3 || ^17 || ^18" } }, - "node_modules/react-tilty": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/react-tilty/-/react-tilty-2.0.3.tgz", - "integrity": "sha512-/z23wWNndBpo6bMl5ktVj+Ght1bn+IMjSBIlCn7Fd3Te51MiSU/MRPcyefa6ghK01SxCO0ISf5xaVfTcjdTOag==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -6480,18 +7557,18 @@ } }, "node_modules/reactfire": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/reactfire/-/reactfire-3.0.0.tgz", - "integrity": "sha512-syL0m52kMZQwlNYJ/dTOlZ6jxmMbl0+te64PBMcXl/eVOBndV7qP3h8sTE7UG+dZHIkVpV3U3cFU5+grYT1QJA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/reactfire/-/reactfire-4.2.2.tgz", + "integrity": "sha512-lxDFyr/Ugu907F/BRbRb7WRTmLSIlO528xeIChAhFipCsqtKZ8m3qceucACepUc2rcikzi3JYVhrMhzqpGOzHQ==", "dependencies": { - "rxfire": "5.0.0-rc.3", + "rxfire": "^6.0.3", "rxjs": "^6.6.3 || ^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=14" }, "peerDependencies": { - "firebase": "^8.1.1", + "firebase": "^9.0.0", "react": ">=16 || experimental" } }, @@ -6748,6 +7825,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/retry-request": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", @@ -6828,14 +7914,14 @@ } }, "node_modules/rxfire": { - "version": "5.0.0-rc.3", - "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-5.0.0-rc.3.tgz", - "integrity": "sha512-OkQQa/zZW6QWZTudNyz3U0DBEtdgmm+CodawPGq670K9OKQpQ0yN/wh645qnSRR/Z53bNLHe05ZWCnwK7LPQUQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", "dependencies": { "tslib": "^1.9.0 || ~2.1.0" }, "peerDependencies": { - "firebase": "^8.0.0", + "firebase": "^9.0.0", "rxjs": "^6.0.0 || ^7.0.0" } }, @@ -6845,9 +7931,9 @@ "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "node_modules/rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", "dependencies": { "tslib": "^2.1.0" } @@ -6967,6 +8053,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7101,11 +8193,28 @@ "stacktrace-gps": "^3.0.4" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7250,6 +8359,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -7305,14 +8420,17 @@ } }, "node_modules/swr": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/swr/-/swr-0.5.7.tgz", - "integrity": "sha512-Jh1Efgu8nWZV9rU4VLUMzBzcwaZgi4znqbVXvAtUy/0JzSiN6bNjLaJK8vhY/Rtp7a83dosz5YuehfBNwC/ZoQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.0.1.tgz", + "integrity": "sha512-6z4FpS9dKAay7axedlStsPahEw25nuMlVh4GHkuPpGptbmEEP8v/+kr0GkAE/7ErUs25U2VFOnZQz3AWfkmXdw==", "dependencies": { - "dequal": "2.0.2" + "use-sync-external-store": "^1.2.0" + }, + "engines": { + "pnpm": "7" }, "peerDependencies": { - "react": "^16.11.0 || ^17.0.0" + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/table": { @@ -7367,6 +8485,22 @@ "node": ">=6" } }, + "node_modules/teeny-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/templite": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/templite/-/templite-1.2.0.tgz", @@ -7528,6 +8662,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "optional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", @@ -7612,6 +8755,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "optional": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/unist-builder": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz", @@ -7713,11 +8868,28 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/uvu": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz", @@ -7817,11 +8989,6 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -7921,19 +9088,32 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" }, - "node_modules/xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7980,6 +9160,18 @@ "node": ">=10" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zwitch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", @@ -8001,6 +9193,12 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@au5ton/react-tilty": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@au5ton/react-tilty/-/react-tilty-2.0.4.tgz", + "integrity": "sha512-e6Iu1NkC2kfu5N5vLojBq7mALF9POqnjERXfklDHPN1yCKCHhGDKi7Uh3ztHr04a/UPVS3ERVb9+pLpYc4SyCg==", + "requires": {} + }, "@au5ton/snooze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@au5ton/snooze/-/snooze-1.0.3.tgz", @@ -8015,6 +9213,16 @@ "@types/dompurify": "^2.2.2", "dompurify": "^2.2.8", "swr": "^0.5.6" + }, + "dependencies": { + "swr": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/swr/-/swr-0.5.7.tgz", + "integrity": "sha512-Jh1Efgu8nWZV9rU4VLUMzBzcwaZgi4znqbVXvAtUy/0JzSiN6bNjLaJK8vhY/Rtp7a83dosz5YuehfBNwC/ZoQ==", + "requires": { + "dequal": "2.0.2" + } + } } }, "@babel/code-frame": { @@ -8413,59 +9621,91 @@ "integrity": "sha512-qhbGfqBRwUlM6MCSaJdUfjq86opNCMvM+6kVvs6S0kYhy0V8dKbe4rDMIklEJGuMc5QH5OuPjdCReu9I0tim2w==" }, "@firebase/analytics": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.6.18.tgz", - "integrity": "sha512-FXNtYDxbs9ynPbzUVuG94BjFPOPpgJ7156660uvCBuKgoBCIVcNqKkJQQ7TH8384fqvGjbjdcgARY9jgAHbtog==", - "requires": { - "@firebase/analytics-types": "0.6.0", - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.9.1.tgz", + "integrity": "sha512-ARXtNHDrjDhVrs5MqmFDpr5yyCw89r1eHLd+Dw9fotAufxL1WTmo6O9bJqKb7QulIJaA84vsFokA3NaO2DNCnQ==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.1.tgz", + "integrity": "sha512-qfFAGS4YFsBbmZwVa7xaDnGh7k9BKF4o/piyjySAv0lxRYd74/tSrm3kMk1YM7GCti7PdbgKvl6oSR70zMFQjw==", + "requires": { + "@firebase/analytics": "0.9.1", + "@firebase/analytics-types": "0.8.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/analytics-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.6.0.tgz", - "integrity": "sha512-kbMawY0WRPyL/lbknBkme4CNLl+Gw+E9G4OpNeXAauqoQiNkBgpIvZYy7BRT4sNGhZbxdxXxXbruqUwDzLmvTw==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.0.tgz", + "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" }, "@firebase/app": { - "version": "0.6.30", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.30.tgz", - "integrity": "sha512-uAYEDXyK0mmpZ8hWQj5TNd7WVvfsU8PgsqKpGljbFBG/HhsH8KbcykWAAA+c1PqL7dt/dbt0Reh1y9zEdYzMhg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.1.tgz", + "integrity": "sha512-Z8wOSol+pvp4CFyY1mW+aqdZlrwhW/ha2YXQ6/avJ56c5Hnvt4k6GktZE6o5NyzvfJTgNHryhMtnEJMIuLaT4w==", "requires": { - "@firebase/app-types": "0.6.3", - "@firebase/component": "0.5.6", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "dom-storage": "2.1.0", - "tslib": "^2.1.0", - "xmlhttprequest": "1.8.0" + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "idb": "7.0.1", + "tslib": "^2.1.0" } }, "@firebase/app-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.3.2.tgz", - "integrity": "sha512-YjpsnV1xVTO1B836IKijRcDeceLgHQNJ/DWa+Vky9UHkm1Mi4qosddX8LZzldaWRTWKX7BN1MbZOLY8r7M/MZQ==", - "requires": { - "@firebase/app-check-interop-types": "0.1.0", - "@firebase/app-check-types": "0.3.1", - "@firebase/component": "0.5.6", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.6.1.tgz", + "integrity": "sha512-gDG4Gr4n3MnBZAAwLMynU9u/b+f1y87lCezfwlmN1gUxD85mJcvp4hLf87fACTyRkdVfe8hqSXm+MOYn2bMGLg==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.1.tgz", + "integrity": "sha512-IaSYdmaoQgWUrN6rjAYJs1TGXj38Wl9damtrDEyJBf7+rrvKshPAP/CP6e2bd89XOMZKbvy8rKoe1CqX1K3ZjQ==", + "requires": { + "@firebase/app-check": "0.6.1", + "@firebase/app-check-types": "0.5.0", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/app-check-interop-types": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz", - "integrity": "sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.2.0.tgz", + "integrity": "sha512-+3PQIeX6/eiVK+x/yg8r6xTNR97fN7MahFDm+jiQmDjcyvSefoGuTTNQuuMScGyx3vYUBeZn+Cp9kC0yY/9uxQ==" }, "@firebase/app-check-types": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.3.1.tgz", - "integrity": "sha512-KJ+BqJbdNsx4QT/JIT1yDj5p6D+QN97iJs3GuHnORrqL+DU3RWc9nSYQsrY6Tv9jVWcOkMENXAgDT484vzsm2w==" + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.0.tgz", + "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" + }, + "@firebase/app-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.1.tgz", + "integrity": "sha512-UgPy2ZO0li0j4hAkaZKY9P1TuJEx5RylhUWPzCb8DZhBm+uHdfsFI9Yr+wMlu6qQH2sWoweFtYU6ljGzxwdctw==", + "requires": { + "@firebase/app": "0.9.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } }, "@firebase/app-types": { "version": "0.6.3", @@ -8473,11 +9713,28 @@ "integrity": "sha512-/M13DPPati7FQHEQ9Minjk1HGLm/4K4gs9bR4rzLCWJg64yGtVC0zNg9gDpkw9yc2cvol/mNFxqTtd4geGrwdw==" }, "@firebase/auth": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.8.tgz", - "integrity": "sha512-mR0UXG4LirWIfOiCWxVmvz1o23BuKGxeItQ2cCUgXLTjNtWJXdcky/356iTUsd7ZV5A78s2NHeN5tIDDG6H4rg==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.21.1.tgz", + "integrity": "sha512-/ap7eT9X7kZTD4Fn2m+nJyC1a9DfFo0H4euoJDN8U+JCMN+GOqkPbkMWCey7wV510WNoPCZQ05+nsAqKkbEVJw==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + } + }, + "@firebase/auth-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.3.1.tgz", + "integrity": "sha512-Ndcaam+IL1TuJ6hZ0EcQ+v261cK3kPm4mvUtouoTfl3FNinm9XvhccN8ojuaRtIV9TiY18mzGjONKF5ZCXLIZw==", "requires": { - "@firebase/auth-types": "0.10.3" + "@firebase/auth": "0.21.1", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" } }, "@firebase/auth-interop-types": { @@ -8487,200 +9744,361 @@ "requires": {} }, "@firebase/auth-types": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz", - "integrity": "sha512-zExrThRqyqGUbXOFrH/sowuh2rRtfKHp9SBVY2vOqKWdCX1Ztn682n9WLtlUDsiYVIbBcwautYWk2HyCGFv0OA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", "requires": {} }, "@firebase/component": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.6.tgz", - "integrity": "sha512-GyQJ+2lrhsDqeGgd1VdS7W+Y6gNYyI0B51ovNTxeZVG/W8I7t9MwEiCWsCvfm5wQgfsKp9dkzOcJrL5k8oVO/Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.1.tgz", + "integrity": "sha512-yvKthG0InjFx9aOPnh6gk0lVNfNVEtyq3LwXgZr+hOwD0x/CtXq33XCpqv0sQj5CA4FdMy8OO+y9edI+ZUw8LA==", "requires": { - "@firebase/util": "1.3.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/database": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.11.0.tgz", - "integrity": "sha512-b/kwvCubr6G9coPlo48PbieBDln7ViFBHOGeVt/bt82yuv5jYZBEYAac/mtOVSxpf14aMo/tAN+Edl6SWqXApw==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.1.tgz", + "integrity": "sha512-iX6/p7hoxUMbYAGZD+D97L05xQgpkslF2+uJLZl46EdaEfjVMEwAdy7RS/grF96kcFZFg502LwPYTXoIdrZqOA==", + "requires": { + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + } + } + }, + "@firebase/database-compat": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", "requires": { - "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.6", - "@firebase/database-types": "0.8.0", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "faye-websocket": "0.11.3", + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", + "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + }, + "@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "requires": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.13", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.5.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", + "requires": { + "@firebase/app-types": "0.7.0", + "@firebase/util": "1.5.2" + } + }, + "@firebase/logger": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.2.tgz", + "integrity": "sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-types": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.8.0.tgz", - "integrity": "sha512-7IdjAFRfPWyG3b4wcXyghb3Y1CLCSJFZIg1xl5GbTVMttSQFT4B5NYdhsfA34JwAsv5pMzPpjOaS3/K9XJ2KiA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.1.tgz", + "integrity": "sha512-UgUx9VakTHbP2WrVUdYrUT2ofTFVfWjGW2O1fwuvvMyo6WSnuSyO5nB1u0cyoMPvO25dfMIUVerfK7qFfwGL3Q==", "requires": { - "@firebase/app-types": "0.6.3", - "@firebase/util": "1.3.0" + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.0" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + } } }, "@firebase/firestore": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-2.4.1.tgz", - "integrity": "sha512-S51XnILdhNt0ZA6bPnbxpqKPI5LatbGY9RQjA2TmATrjSPE3aWndJsLIrutI6aS9K+YFwy5+HLDKVRFYQfmKAw==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/firestore-types": "2.4.0", - "@firebase/logger": "0.2.6", - "@firebase/util": "1.3.0", - "@firebase/webchannel-wrapper": "0.5.1", - "@grpc/grpc-js": "^1.3.2", - "@grpc/proto-loader": "^0.6.0", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.8.1.tgz", + "integrity": "sha512-oc2HMkUnq/zF+g9o974tp5RVCdXCnrU8e5S98ajfWG/hGV+8pr4i6vIa4z0yEXKWGi4X0FguxrC69z1dxEJbNg==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "@firebase/webchannel-wrapper": "0.9.0", + "@grpc/grpc-js": "~1.7.0", + "@grpc/proto-loader": "^0.6.13", "node-fetch": "2.6.7", "tslib": "^2.1.0" } }, + "@firebase/firestore-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.1.tgz", + "integrity": "sha512-7eE4O2ASyy5X2h4a+KCRt0ZpliUAKo2jrKxKl1ZVCnOOjSCkXXeRVRG9eNZRqBwukhdwskJTM9acs0WxmKOYLA==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/firestore": "3.8.1", + "@firebase/firestore-types": "2.5.1", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, "@firebase/firestore-types": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.4.0.tgz", - "integrity": "sha512-0dgwfuNP7EN6/OlK2HSNSQiQNGLGaRBH0gvgr1ngtKKJuJFuq0Z48RBMeJX9CGjV4TP9h2KaB+KrUKJ5kh1hMg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", + "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", "requires": {} }, "@firebase/functions": { - "version": "0.6.16", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.6.16.tgz", - "integrity": "sha512-KDPjLKSjtR/zEH06YXXbdWTi8gzbKHGRzL/+ibZQA/1MLq0IilfM+1V1Fh8bADsMCUkxkqoc1yiA4SUbH5ajJA==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/functions-types": "0.4.0", - "@firebase/messaging-types": "0.5.0", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.9.1.tgz", + "integrity": "sha512-xCSSU4aVSqYU+lCqhn9o5jJcE1KLUOOKyJfCTdCSCyTn2J3vl9Vk4TDm3JSb1Eu6XsNWtxeMW188F/GYxuMWcw==", + "requires": { + "@firebase/app-check-interop-types": "0.2.0", + "@firebase/auth-interop-types": "0.2.1", + "@firebase/component": "0.6.1", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.0", "node-fetch": "2.6.7", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/auth-interop-types": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz", + "integrity": "sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg==" + } + } + }, + "@firebase/functions-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.1.tgz", + "integrity": "sha512-f2D2XoRN+QCziCrUL7UrLaBEoG3v2iAeyNwbbOQ3vv0rI0mtku2/yeB2OINz5/iI6oIrBPUMNLr5fitofj7FpQ==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/functions": "0.9.1", + "@firebase/functions-types": "0.6.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" } }, "@firebase/functions-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.4.0.tgz", - "integrity": "sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.0.tgz", + "integrity": "sha512-hfEw5VJtgWXIRf92ImLkgENqpL6IWpYaXVYiRkFY1jJ9+6tIhWM7IzzwbevwIIud/jaxKVdRzD7QBWfPmkwCYw==" }, "@firebase/installations": { - "version": "0.4.32", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.4.32.tgz", - "integrity": "sha512-K4UlED1Vrhd2rFQQJih+OgEj8OTtrtH4+Izkx7ip2bhXSc+unk8ZhnF69D0kmh7zjXAqEDJrmHs9O5fI3rV6Tw==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/installations-types": "0.3.4", - "@firebase/util": "1.3.0", - "idb": "3.0.2", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.1.tgz", + "integrity": "sha512-gpobP09LLLakBfNCL04fyblfyb3oX1pn+iNmELygrcAkXTO13IAMuOzThI+Xk4NHQZMX1p5GFSAiGbG4yfsSUQ==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", + "idb": "7.0.1", + "tslib": "^2.1.0" + } + }, + "@firebase/installations-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.1.tgz", + "integrity": "sha512-X4IBVKajEeaE45zWX0Y1q8ey39aPFLa+BsUoYzsduMzCxcMBIPZd5/lV1EVGt8SN3+unnC2J75flYkxXVlhBoQ==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/installations-types": "0.5.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/installations-types": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.3.4.tgz", - "integrity": "sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.0.tgz", + "integrity": "sha512-9DP+RGfzoI2jH7gY4SlzqvZ+hr7gYzPODrbzVD82Y12kScZ6ZpRg/i3j6rleto8vTFC8n6Len4560FnV1w2IRg==", "requires": {} }, "@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "requires": { + "tslib": "^2.1.0" + } }, "@firebase/messaging": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.8.0.tgz", - "integrity": "sha512-hkFHDyVe1kMcY9KEG+prjCbvS6MtLUgVFUbbQqq7JQfiv58E07YCzRUcMrJolbNi/1QHH6Jv16DxNWjJB9+/qA==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/messaging-types": "0.5.0", - "@firebase/util": "1.3.0", - "idb": "3.0.2", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.1.tgz", + "integrity": "sha512-/F+2OWarR8TcJJVlQS6zBoHHfXMgfgR0/ukQ3h7Ow3WZ3WZ9+Sj/gvxzothXZm+WtBylfXuhiANFgHEDFL0J0w==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/messaging-interop-types": "0.2.0", + "@firebase/util": "1.9.0", + "idb": "7.0.1", "tslib": "^2.1.0" } }, - "@firebase/messaging-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/messaging-types/-/messaging-types-0.5.0.tgz", - "integrity": "sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg==", - "requires": {} + "@firebase/messaging-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.1.tgz", + "integrity": "sha512-BykvXtAWOs0W4Ik79lNfMKSxaUCtOJ47PJ9Vw2ySHZ14vFFNuDAtRTOBOlAFhUpsHqRoQFvFCkBGsRIQYq8hzw==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/messaging": "0.12.1", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/messaging-interop-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", + "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" }, "@firebase/performance": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.4.18.tgz", - "integrity": "sha512-lvZW/TVDne2TyOpWbv++zjRn277HZpbjxbIPfwtnmKjVY1gJ+H77Qi1c2avVIc9hg80uGX/5tNf4pOApNDJLVg==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/performance-types": "0.0.13", - "@firebase/util": "1.3.0", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.1.tgz", + "integrity": "sha512-mT/CWz3CLgyn/a3sO/TJgrTt+RA3DfuvWwGXY9zmIiuBZY2bDi1M2uMefJdJKc9sBUPRajNF6RL10nGYq3BAuQ==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, + "@firebase/performance-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.1.tgz", + "integrity": "sha512-4mn6eS7r2r+ZAHvU0OHE+3ZO+x6gOVhf2ypBoijuDNaRNjSn9GcvA8udD4IbJ8FNv/k7mbbtA9AdxVb701Lr1g==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/performance": "0.6.1", + "@firebase/performance-types": "0.2.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/performance-types": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.0.13.tgz", - "integrity": "sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.0.tgz", + "integrity": "sha512-kYrbr8e/CYr1KLrLYZZt2noNnf+pRwDq2KK9Au9jHrBMnb0/C9X9yWSXmZkFt4UIdsQknBq8uBB7fsybZdOBTA==" }, - "@firebase/polyfill": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.3.36.tgz", - "integrity": "sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==", + "@firebase/remote-config": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.1.tgz", + "integrity": "sha512-RCzBH3FjAPRSP3M1T7jdxLYBesIdLtNIQ0fR9ywJpGSSa0kIXEJ9iSZMTP+9pJtaCxz8db07FvjEqg7Y+lgjzg==", "requires": { - "core-js": "3.6.5", - "promise-polyfill": "8.1.3", - "whatwg-fetch": "2.0.4" + "@firebase/component": "0.6.1", + "@firebase/installations": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" } }, - "@firebase/remote-config": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.1.43.tgz", - "integrity": "sha512-laNM4MN0CfeSp7XCVNjYOC4DdV6mj0l2rzUh42x4v2wLTweCoJ/kc1i4oWMX9TI7Jw8Am5Wl71Awn1J2pVe5xA==", - "requires": { - "@firebase/component": "0.5.6", - "@firebase/installations": "0.4.32", - "@firebase/logger": "0.2.6", - "@firebase/remote-config-types": "0.1.9", - "@firebase/util": "1.3.0", + "@firebase/remote-config-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.1.tgz", + "integrity": "sha512-RPCj7c2Q3QxMgJH3YCt0iD57KppFApghxAGETzlr6Jm6vT7k0vqvk2KgRBgKa4koJBsgwlUtRn2roaCqUEadyg==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/logger": "0.4.0", + "@firebase/remote-config": "0.4.1", + "@firebase/remote-config-types": "0.3.0", + "@firebase/util": "1.9.0", "tslib": "^2.1.0" } }, "@firebase/remote-config-types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz", - "integrity": "sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA==" + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.0.tgz", + "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" }, "@firebase/storage": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.7.1.tgz", - "integrity": "sha512-T7uH6lAgNs/Zq8V3ElvR3ypTQSGWon/R7WRM2I5Td/d0PTsNIIHSAGB6q4Au8mQEOz3HDTfjNQ9LuQ07R6S2ug==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.10.1.tgz", + "integrity": "sha512-eN4ME+TFCh5KfyG9uo8PhE6cgKjK5Rb9eucQg1XEyLHMiaZiUv2xSuWehJn0FaL+UdteoaWKuRUZ4WXRDskXrA==", "requires": { - "@firebase/component": "0.5.6", - "@firebase/storage-types": "0.5.0", - "@firebase/util": "1.3.0", + "@firebase/component": "0.6.1", + "@firebase/util": "1.9.0", "node-fetch": "2.6.7", "tslib": "^2.1.0" } }, + "@firebase/storage-compat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.2.1.tgz", + "integrity": "sha512-H0oFdYsMn2Z6tP9tlVERBkJiZsCbFAcl3Li1dnpvDg9g323egdjCnUUgH/tJODRR/Y84iZSNRkg4FvHDVI/o7Q==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/storage": "0.10.1", + "@firebase/storage-types": "0.7.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + }, "@firebase/storage-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.5.0.tgz", - "integrity": "sha512-6Wv3Lu7s18hsgW7HG4BFwycTquZ3m/C8bjBoOsmPu0TD6M1GKwCzOC7qBdN7L6tRYPh8ipTj5+rPFrmhGfUVKA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.7.0.tgz", + "integrity": "sha512-n/8pYd82hc9XItV3Pa2KGpnuJ/2h/n/oTAaBberhe6GeyWQPnsmwwRK94W3GxUwBA/ZsszBAYZd7w7tTE+6XXA==", "requires": {} }, "@firebase/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.3.0.tgz", - "integrity": "sha512-SESvmYwuKOVCZ1ZxLbberbx+9cnbxpCa4CG2FUSQYqN6Ab8KyltegMDIsqMw5KyIBZ4n1phfHoOa22xo5NzAlQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.0.tgz", + "integrity": "sha512-oeoq/6Sr9btbwUQs5HPfeww97bf7qgBbkknbDTXpRaph2LZ23O9XLCE5tJy856SBmGQfO4xBZP8dyryLLM2nSQ==", "requires": { "tslib": "^2.1.0" } }, "@firebase/webchannel-wrapper": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.5.1.tgz", - "integrity": "sha512-dZMzN0uAjwJXWYYAcnxIwXqRTZw3o14hGe7O6uhwjD1ZQWPVYA5lASgnNskEBra0knVBsOXB4KXg+HnlKewN/A==" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.9.0.tgz", + "integrity": "sha512-BpiZLBWdLFw+qFel9p3Zs1jD6QmH7Ii4aTDu6+vx8ShdidChZUXqDhYJly4ZjSgQh54miXbBgBrk0S+jTIh/Qg==" }, "@google-analytics/data": { "version": "3.1.0", @@ -8690,6 +10108,313 @@ "google-gax": "^3.3.0" } }, + "@google-cloud/firestore": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz", + "integrity": "sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^2.24.1", + "protobufjs": "^6.8.6" + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.12.tgz", + "integrity": "sha512-JmvQ03OTSpVd9JTlj/K3IWHSz4Gk/JMLUTtW7Zb0KvO1LcOYGATh5cNuRYzCAeDR3O8wq+q8FZe97eO9MBrkUw==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", + "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^16.2.0" + } + }, + "protobufjs": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.0.tgz", + "integrity": "sha512-hYCqTDuII4iJ4stZqiuGCSU8xxWl5JeXYpwARGtn/tWcKCAro6h3WQz+xpsNbXW0UYqpmTQFEyFWO0G0Kjt64g==", + "optional": true, + "requires": { + "@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" + }, + "dependencies": { + "long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz", + "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==", + "optional": true + } + } + } + } + }, + "gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-gax": { + "version": "2.30.5", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.5.tgz", + "integrity": "sha512-Jey13YrAN2hfpozHzbtrwEfEHdStJh1GwaQ2+Akh1k0Tv/EuNVSuBtHZoKSBm5wBMvNsxTsEIZ/152NrYyZgxQ==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.6.0", + "@grpc/proto-loader": "^0.6.12", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.14.0", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^0.1.8", + "protobufjs": "6.11.3", + "retry-request": "^4.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true + }, + "proto3-json-serializer": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.9.tgz", + "integrity": "sha512-A60IisqvnuI45qNRygJjrnNjX2TMdQGMY+57tR3nul3ZgO2zXkR9OGR8AXxJhkqx84g0FTnrfi3D5fWMSdANdQ==", + "optional": true, + "requires": { + "protobufjs": "^6.11.2" + } + }, + "retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + } + } + }, + "@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/projectify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.1.tgz", + "integrity": "sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ==", + "optional": true + }, + "@google-cloud/promisify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.4.tgz", + "integrity": "sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA==", + "optional": true + }, + "@google-cloud/storage": { + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.20.5.tgz", + "integrity": "sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw==", + "optional": true, + "requires": { + "@google-cloud/paginator": "^3.0.7", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "abort-controller": "^3.0.0", + "arrify": "^2.0.0", + "async-retry": "^1.3.3", + "compressible": "^2.0.12", + "configstore": "^5.0.0", + "duplexify": "^4.0.0", + "ent": "^2.2.0", + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.14.1", + "hash-stream-validation": "^0.2.2", + "mime": "^3.0.0", + "mime-types": "^2.0.8", + "p-limit": "^3.0.1", + "pumpify": "^2.0.0", + "retry-request": "^4.2.2", + "stream-events": "^1.0.4", + "teeny-request": "^7.1.3", + "uuid": "^8.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "optional": true, + "requires": { + "node-forge": "^1.3.1" + } + }, + "gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "optional": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "optional": true, + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + } + } + }, "@grpc/grpc-js": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", @@ -8745,15 +10470,22 @@ } }, "@grpc/proto-loader": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.11.tgz", - "integrity": "sha512-MRiPjTjNgKxMupQ0M8mM9Mcljb2aZvE3Y/oEv+dacozIs2TwTdiPbvfkZpMeghfjGtoDJhDjyCtmFzJcjdDTUQ==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "requires": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", - "long": "^4.0.0 || ^5.2.0", - "protobufjs": "^6.10.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + } } }, "@humanwhocodes/config-array": { @@ -9012,6 +10744,11 @@ "fastq": "^1.6.0" } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@popperjs/core": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", @@ -9090,6 +10827,29 @@ "integrity": "sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==", "dev": true }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -9106,6 +10866,27 @@ "@types/trusted-types": "*" } }, + "@types/express": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", + "integrity": "sha512-LkKpqRZ7zqXJuvoELakaFYuETHjZkSol8EV6cNnyishutDBCCdv6+dsKPbKkCcIk57qRphOLY5sEgClw1bO3gA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.31", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -9125,6 +10906,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "requires": { + "@types/node": "*" + } + }, "@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", @@ -9157,6 +10946,11 @@ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -9193,6 +10987,16 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, "@types/react": { "version": "17.0.44", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.44.tgz", @@ -9203,6 +11007,15 @@ "csstype": "^3.0.2" } }, + "@types/react-highlight-words": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/react-highlight-words/-/react-highlight-words-0.16.4.tgz", + "integrity": "sha512-KITBX3xzheQLu2s3bUgLmRE7ekmhc52zRjRTwkKayQARh30L4fjEGzGm7ULK9TuX2LgxWWavZqyQGDGjAHbL3w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-is": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz", @@ -9224,6 +11037,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, "@types/trusted-types": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", @@ -9456,6 +11278,15 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "requires": { + "retry": "0.13.1" + } + }, "attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -9570,9 +11401,9 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "caniuse-lite": { - "version": "1.0.30001335", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz", - "integrity": "sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==" + "version": "1.0.30001449", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", + "integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==" }, "catharsis": { "version": "0.9.0", @@ -9667,11 +11498,34 @@ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz", "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==" }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "optional": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, "convert-source-map": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", @@ -9688,11 +11542,6 @@ "toggle-selection": "^1.0.6" } }, - "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" - }, "core-js-pure": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.4.tgz", @@ -9721,6 +11570,12 @@ "which": "^2.0.1" } }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true + }, "css-in-js-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", @@ -9793,6 +11648,14 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz", "integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==" }, + "dicer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.1.tgz", + "integrity": "sha512-ObioMtXnmjYs3aRtpIJt9rgQSPCIhKVkFPip+E9GUDyWl8N435znUxK/JfNwGZJ2wnn5JKQ7Ly3vOK5Q5dylGA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -9830,16 +11693,20 @@ "csstype": "^3.0.2" } }, - "dom-storage": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", - "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==" - }, "dompurify": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.6.tgz", "integrity": "sha512-OFP2u/3T1R5CEgWCEONuJ1a5+MFKnOYpkywpUSxv/dj1LeBT1erK+JwM7zK0ROy2BRhqVCf0LRw/kHqKuMkVGg==" }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "optional": true, + "requires": { + "is-obj": "^2.0.0" + } + }, "duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -9898,6 +11765,12 @@ "ansi-colors": "^4.1.1" } }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "optional": true + }, "entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -10533,9 +12406,9 @@ } }, "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "requires": { "websocket-driver": ">=0.5.1" } @@ -10580,25 +12453,87 @@ } }, "firebase": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-8.10.1.tgz", - "integrity": "sha512-84z/zqF8Y5IpUYN8nREZ/bxbGtF5WJDOBy4y0hAxRzGpB5+2tw9PQgtTnUzk6MQiVEf/WOniMUL3pCVXKsxALw==", - "requires": { - "@firebase/analytics": "0.6.18", - "@firebase/app": "0.6.30", - "@firebase/app-check": "0.3.2", - "@firebase/app-types": "0.6.3", - "@firebase/auth": "0.16.8", - "@firebase/database": "0.11.0", - "@firebase/firestore": "2.4.1", - "@firebase/functions": "0.6.16", - "@firebase/installations": "0.4.32", - "@firebase/messaging": "0.8.0", - "@firebase/performance": "0.4.18", - "@firebase/polyfill": "0.3.36", - "@firebase/remote-config": "0.1.43", - "@firebase/storage": "0.7.1", - "@firebase/util": "1.3.0" + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.16.0.tgz", + "integrity": "sha512-nNLpDwJvfP3crRc6AjnHH46TAkFzk8zimNVMJfYRCwAf5amOSGyU8duuc3IsJF6dQGiYLSfzfr2tMCsQa+rhKQ==", + "requires": { + "@firebase/analytics": "0.9.1", + "@firebase/analytics-compat": "0.2.1", + "@firebase/app": "0.9.1", + "@firebase/app-check": "0.6.1", + "@firebase/app-check-compat": "0.3.1", + "@firebase/app-compat": "0.2.1", + "@firebase/app-types": "0.9.0", + "@firebase/auth": "0.21.1", + "@firebase/auth-compat": "0.3.1", + "@firebase/database": "0.14.1", + "@firebase/database-compat": "0.3.1", + "@firebase/firestore": "3.8.1", + "@firebase/firestore-compat": "0.3.1", + "@firebase/functions": "0.9.1", + "@firebase/functions-compat": "0.3.1", + "@firebase/installations": "0.6.1", + "@firebase/installations-compat": "0.2.1", + "@firebase/messaging": "0.12.1", + "@firebase/messaging-compat": "0.2.1", + "@firebase/performance": "0.6.1", + "@firebase/performance-compat": "0.2.1", + "@firebase/remote-config": "0.4.1", + "@firebase/remote-config-compat": "0.2.1", + "@firebase/storage": "0.10.1", + "@firebase/storage-compat": "0.2.1", + "@firebase/util": "1.9.0" + }, + "dependencies": { + "@firebase/app-types": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz", + "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" + }, + "@firebase/database-compat": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.1.tgz", + "integrity": "sha512-sI7LNh0C8PCq9uUKjrBKLbZvqHTSjsf2LeZRxin+rHVegomjsOAYk9OzYwxETWh3URhpMkCM8KcTl7RVwAldog==", + "requires": { + "@firebase/component": "0.6.1", + "@firebase/database": "0.14.1", + "@firebase/database-types": "0.10.1", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.0", + "tslib": "^2.1.0" + } + } + } + }, + "firebase-admin": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.12.0.tgz", + "integrity": "sha512-AtA7OH5RbIFGoc0gZOQgaYC6cdjdhZv4w3XgWoupkPKO1HY+0GzixOuXDa75kFeoVyhIyo4PkLg/GAC1dC1P6w==", + "requires": { + "@firebase/database-compat": "^0.1.1", + "@firebase/database-types": "^0.7.2", + "@google-cloud/firestore": "^4.5.0", + "@google-cloud/storage": "^5.3.0", + "@types/node": ">=12.12.47", + "dicer": "^0.3.0", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.2", + "node-forge": "^0.10.0" + }, + "dependencies": { + "@firebase/database-types": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.3.tgz", + "integrity": "sha512-dSOJmhKQ0nL8O4EQMRNGpSExWCXeHtH57gGg0BfNAdWcKhC8/4Y+qfKLfWXzyHvrSecpLmO0SmAi/iK2D5fp5A==", + "requires": { + "@firebase/app-types": "0.6.3" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + } } }, "fitty": { @@ -10642,7 +12577,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "devOptional": true }, "functions-have-names": { "version": "1.2.3", @@ -11026,6 +12961,12 @@ "has-symbols": "^1.0.2" } }, + "hash-stream-validation": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true + }, "hast-to-hyperscript": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz", @@ -11175,6 +13116,17 @@ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==" }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -11190,9 +13142,9 @@ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, "idb": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", - "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.0.1.tgz", + "integrity": "sha512-UUxlE7vGWK5RfB/fDwEGgRf84DY/ieqNha6msMV99UsEMQhJ1RwbCd8AYBj3QMgnE3VZnfQvm4oKVCJTYlqIgg==" }, "ignore": { "version": "4.0.6", @@ -11218,7 +13170,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "devOptional": true }, "inflight": { "version": "1.0.6", @@ -11361,6 +13313,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "optional": true + }, "is-plain-obj": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz", @@ -11413,6 +13371,12 @@ "has-symbols": "^1.0.2" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "optional": true + }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -11441,6 +13405,14 @@ "@babel/runtime": "^7.15.4" } }, + "jose": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.6.tgz", + "integrity": "sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-cookie": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", @@ -11534,6 +13506,49 @@ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "peer": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "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": "^5.6.0" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "jsx-ast-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", @@ -11554,6 +13569,19 @@ "safe-buffer": "^5.0.1" } }, + "jwks-rsa": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.5.tgz", + "integrity": "sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==", + "requires": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^8.5.9", + "debug": "^4.3.4", + "jose": "^2.0.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + } + }, "jws": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", @@ -11606,6 +13634,11 @@ "type-check": "~0.4.0" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11639,12 +13672,52 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -11677,6 +13750,40 @@ "yallist": "^4.0.0" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "requires": { + "semver": "^6.0.0" + } + }, "markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -12036,6 +14143,27 @@ "picomatch": "^2.3.1" } }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "optional": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "optional": true, + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -12383,11 +14511,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, - "promise-polyfill": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz", - "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==" - }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -12466,6 +14589,32 @@ } } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -12516,12 +14665,10 @@ } }, "react-google-charts": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-3.0.15.tgz", - "integrity": "sha512-78s5xOQOJvL+jIewrWQZEHtlVk+5Yh4zZy+ODA1on1o1FaRjKWXxoo4n4JQl1XuqkF/A9NWque3KqM6pMggjzQ==", - "requires": { - "react-load-script": "^0.0.6" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-google-charts/-/react-google-charts-4.0.0.tgz", + "integrity": "sha512-9OG0EkBb9JerKEPQYdhmAXnhGLzOdOHOPS9j7l+P1a3z1kcmq9mGDa7PUoX/VQUY4IjZl2/81nsO4o+1cuYsuw==", + "requires": {} }, "react-highlight-words": { "version": "0.17.0", @@ -12538,24 +14685,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "react-load-script": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/react-load-script/-/react-load-script-0.0.6.tgz", - "integrity": "sha512-aRGxDGP9VoLxcsaYvKWIW+LRrMOzz2eEcubTS4NvQPPugjk2VvMhow0wWTkSl7RxookomD1MwcP4l5UStg5ShQ==", - "requires": {} - }, "react-swipeable": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.2.2.tgz", "integrity": "sha512-Oz7nSFrssvq2yvy05aNL3F+yBUqSvLsK6x1mu+rQFOpMdQVnt4izKt1vyjvvTb70q6GQOaSpaB6qniROW2MAzQ==", "requires": {} }, - "react-tilty": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/react-tilty/-/react-tilty-2.0.3.tgz", - "integrity": "sha512-/z23wWNndBpo6bMl5ktVj+Ght1bn+IMjSBIlCn7Fd3Te51MiSU/MRPcyefa6ghK01SxCO0ISf5xaVfTcjdTOag==", - "requires": {} - }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -12595,11 +14730,11 @@ } }, "reactfire": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/reactfire/-/reactfire-3.0.0.tgz", - "integrity": "sha512-syL0m52kMZQwlNYJ/dTOlZ6jxmMbl0+te64PBMcXl/eVOBndV7qP3h8sTE7UG+dZHIkVpV3U3cFU5+grYT1QJA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/reactfire/-/reactfire-4.2.2.tgz", + "integrity": "sha512-lxDFyr/Ugu907F/BRbRb7WRTmLSIlO528xeIChAhFipCsqtKZ8m3qceucACepUc2rcikzi3JYVhrMhzqpGOzHQ==", "requires": { - "rxfire": "5.0.0-rc.3", + "rxfire": "^6.0.3", "rxjs": "^6.6.3 || ^7.0.1" } }, @@ -12780,6 +14915,12 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, "retry-request": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", @@ -12830,9 +14971,9 @@ } }, "rxfire": { - "version": "5.0.0-rc.3", - "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-5.0.0-rc.3.tgz", - "integrity": "sha512-OkQQa/zZW6QWZTudNyz3U0DBEtdgmm+CodawPGq670K9OKQpQ0yN/wh645qnSRR/Z53bNLHe05ZWCnwK7LPQUQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rxfire/-/rxfire-6.0.3.tgz", + "integrity": "sha512-77nkyffHh7jgfi1YA/N9RI+kWxYpgKk6GRML1lyersvaqbJt4hkvWwk1rWib9Rb5Lr5mT+Ha45lu7nM79sJCZA==", "requires": { "tslib": "^1.9.0 || ~2.1.0" }, @@ -12845,9 +14986,9 @@ } }, "rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", "requires": { "tslib": "^2.1.0" } @@ -12934,6 +15075,12 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13041,11 +15188,25 @@ "stacktrace-gps": "^3.0.4" } }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "requires": { + "stubs": "^3.0.0" + } + }, "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13149,6 +15310,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, "style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -13182,11 +15349,11 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "swr": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/swr/-/swr-0.5.7.tgz", - "integrity": "sha512-Jh1Efgu8nWZV9rU4VLUMzBzcwaZgi4znqbVXvAtUy/0JzSiN6bNjLaJK8vhY/Rtp7a83dosz5YuehfBNwC/ZoQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.0.1.tgz", + "integrity": "sha512-6z4FpS9dKAay7axedlStsPahEw25nuMlVh4GHkuPpGptbmEEP8v/+kr0GkAE/7ErUs25U2VFOnZQz3AWfkmXdw==", "requires": { - "dequal": "2.0.2" + "use-sync-external-store": "^1.2.0" } }, "table": { @@ -13233,6 +15400,19 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "teeny-request": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.2.0.tgz", + "integrity": "sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw==", + "optional": true, + "requires": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "stream-events": "^1.0.5", + "uuid": "^8.0.0" + } + }, "templite": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/templite/-/templite-1.2.0.tgz", @@ -13361,6 +15541,15 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "optional": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz", @@ -13417,6 +15606,15 @@ "vfile": "^5.0.0" } }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "optional": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "unist-builder": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz", @@ -13487,11 +15685,23 @@ "object-assign": "^4.1.1" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true + }, "uvu": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz", @@ -13563,11 +15773,6 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" - }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -13642,16 +15847,29 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true + }, "xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13686,6 +15904,12 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "optional": true + }, "zwitch": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz", diff --git a/package.json b/package.json index 92ef275..563324e 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "@cougargrades/web", - "version": "1.0.7", + "version": "1.1.0", "private": true, "license": "MIT", "scripts": { "dev": "NEXT_PUBLIC_GIT_SHA=$(git rev-parse --short HEAD) NEXT_PUBLIC_VERSION=$npm_package_version NEXT_PUBLIC_BUILD_DATE=$(date) next dev", "build": "NEXT_PUBLIC_VERCEL_ENV=$VERCEL_ENV NEXT_PUBLIC_GIT_SHA=$VERCEL_GIT_COMMIT_SHA NEXT_PUBLIC_VERSION=$npm_package_version NEXT_PUBLIC_BUILD_DATE=$(date) next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "betterlint": "tsc --noEmit" }, "dependencies": { "@au5ton/use-atom-feed": "^1.0.4", @@ -21,7 +22,8 @@ "@mui/material": "^5.2.6", "@primer/css": "^19.1.1", "bootstrap": "^5.0.1", - "firebase": "^8.10.0", + "firebase": "^9.16.0", + "firebase-admin": "^9.12.0", "fitty": "^2.3.5", "gray-matter": "^4.0.3", "next": "~12.0.3", @@ -33,13 +35,13 @@ "react-dom": "17.0.2", "react-dropzone": "^11.3.4", "react-fitty": "^1.0.1", - "react-google-charts": "^3.0.15", + "react-google-charts": "^4.0.0", "react-highlight-words": "^0.17.0", "react-swipeable": "^6.1.2", - "react-tilty": "^2.0.3", + "@au5ton/react-tilty": "^2.0.4", "react-transition-group": "^4.4.2", "react-use": "^17.2.4", - "reactfire": "^3.0.0", + "reactfire": "^4.2.2", "recoil": "^0.5.2", "rehype-raw": "^6.1.1", "rehype-sanitize": "^5.0.1", @@ -49,12 +51,14 @@ "remark-rehype": "^10.1.0", "rosetta": "^1.1.0", "sass": "^1.45.2", + "swr": "^2.0.1", "timeago-react": "^3.0.2" }, "devDependencies": { "@au5ton/snooze": "^1.0.3", "@types/nprogress": "^0.2.0", "@types/react": "^17.0.11", + "@types/react-highlight-words": "^0.16.4", "eslint": "^7.28.0", "eslint-config-next": "^12.1.6", "next-transpile-modules": "^9.0.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 322d453..f3ab0a1 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,16 +1,14 @@ import Head from 'next/head' import dynamic from 'next/dynamic' +import type { AppProps } from 'next/app' +import { SWRConfig } from 'swr' import { ThemeProvider } from '@mui/material/styles' //import { FirebaseAppProvider } from 'reactfire' -const FirebaseAppProvider = dynamic(() => import('../lib/firebase').then(mod => mod.FirebaseAppProviderWrapper)) -const FirestorePreloader = dynamic(() => import('../lib/firebase').then(mod => mod.FirestorePreloader)) -//import { FirebaseAppProviderWrapper as FirebaseAppProvider } from '../lib/firebase' -//import { RealtimeClaimUpdater } from '../components/auth/RealtimeClaimUpdater' -const RealtimeClaimUpdater = dynamic(() => import('../components/auth/RealtimeClaimUpdater').then(mod => mod.RealtimeClaimUpdater)) -//import { AppCheck } from '../components/appcheck' -const AppCheck = dynamic(() => import('../components/appcheck').then(mod => mod.AppCheck)) -//import { RecoilRoot } from 'recoil' +// @ts-ignore +const FirebaseAppProvider = dynamic(() => import('../lib/firebase').then(mod => mod.FirebaseAppProviderWrapper)); +// @ts-ignore const RecoilRoot = dynamic(() => import('recoil').then(mod => mod.RecoilRoot)) +// @ts-ignore const PageViewLogger = dynamic(() => import('../components/pageviewlogger').then(mod => mod.PageViewLogger)) //import Layout from '../components/layout' // eslint-disable-next-line react/display-name @@ -27,7 +25,7 @@ import '../styles/nprogress-custom.scss' import '../styles/globals.scss' import '../styles/colors.scss' -export default function MyApp({ Component, pageProps }) { +export default function MyApp({ Component, pageProps }: AppProps) { const theme = useTheme(); return ( <> @@ -67,14 +65,17 @@ export default function MyApp({ Component, pageProps }) { - - - {/* */} - - - - - + fetch(resource, init).then(res => res.json()), + }}> + + {/* */} + + + + + + diff --git a/pages/_error.tsx b/pages/_error.tsx index 8d570c6..ba311a5 100644 --- a/pages/_error.tsx +++ b/pages/_error.tsx @@ -22,7 +22,7 @@ export default function ErrorPage(props: ErrorProps) { ErrorPage.getInitialProps = (ctx: NextPageContext): ErrorProps => { const { res, err } = ctx; - const statusCode = res ? res.statusCode : err ? err.statusCode : 404; + const statusCode = res && res.statusCode ? res.statusCode : err && err.statusCode ? err.statusCode : 404; const title = (res && res.statusMessage) ? res.statusMessage : err ? err.message : 'This page could not be found.'; return { statusCode, title } } diff --git a/pages/api/course/[courseName].ts b/pages/api/course/[courseName].ts new file mode 100644 index 0000000..d1345a6 --- /dev/null +++ b/pages/api/course/[courseName].ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../../lib/cache' +import { getCourseData } from '../../../lib/data/back/getCourseData'; +import { CourseResult } from '../../../lib/data/useCourseData' +import { extract } from '../../../lib/util' + +export default async function GetCourseData(req: NextApiRequest, res: NextApiResponse) { + const { courseName } = req.query; + const result = await getCourseData(extract(courseName)) + res.setHeader('Cache-Control', CACHE_CONTROL); + res.json(result); +} \ No newline at end of file diff --git a/pages/api/group/[groupId]/index.ts b/pages/api/group/[groupId]/index.ts new file mode 100644 index 0000000..b07d7d9 --- /dev/null +++ b/pages/api/group/[groupId]/index.ts @@ -0,0 +1,19 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../../../lib/cache' +import { getOneGroup } from '../../../../lib/data/back/getOneGroup'; +import { PopulatedGroupResult } from '../../../../lib/data/useAllGroups' +import { extract } from '../../../../lib/util' + +export default async function GetOneGroup(req: NextApiRequest, res: NextApiResponse) { + const { groupId } = req.query; + const result = await getOneGroup(extract(groupId)) + res.setHeader('Cache-Control', CACHE_CONTROL); + if (result) { + res.json(result); + } + else { + res.statusCode = 404; + res.send('Group Not Found' as any); + } +} \ No newline at end of file diff --git a/pages/api/group/[groupId]/sections.ts b/pages/api/group/[groupId]/sections.ts new file mode 100644 index 0000000..c2c4be1 --- /dev/null +++ b/pages/api/group/[groupId]/sections.ts @@ -0,0 +1,15 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../../../lib/cache' +import { getOneGroup } from '../../../../lib/data/back/getOneGroup'; +import { PopulatedGroupResult } from '../../../../lib/data/useAllGroups' +import { extract } from '../../../../lib/util' + +export default async function GetOneGroup(req: NextApiRequest, res: NextApiResponse) { + const { groupId } = req.query; + //const result = await getOneGroup(extract(groupId), true) + res.setHeader('Cache-Control', CACHE_CONTROL) + res.statusCode = 400; + res.end('Temporarily disabled') + //res.json(result); +} \ No newline at end of file diff --git a/pages/api/group/index.ts b/pages/api/group/index.ts new file mode 100644 index 0000000..f212911 --- /dev/null +++ b/pages/api/group/index.ts @@ -0,0 +1,11 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../../lib/cache' +import { getAllGroups } from '../../../lib/data/back/getAllGroups'; +import { AllGroupsResult } from '../../../lib/data/useAllGroups' + +export default async function GetAllGroups(req: NextApiRequest, res: NextApiResponse) { + const result = await getAllGroups(); + res.setHeader('Cache-Control', CACHE_CONTROL); + res.json(result); +} \ No newline at end of file diff --git a/pages/api/instructor/[instructorName].ts b/pages/api/instructor/[instructorName].ts new file mode 100644 index 0000000..0606cc8 --- /dev/null +++ b/pages/api/instructor/[instructorName].ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../../lib/cache' +import { getInstructorData } from '../../../lib/data/back/getInstructorData' +import { InstructorResult } from '../../../lib/data/useInstructorData' +import { extract } from '../../../lib/util' + +export default async function GetInstructorData(req: NextApiRequest, res: NextApiResponse) { + const { instructorName } = req.query; + const result = await getInstructorData(extract(instructorName)) + res.setHeader('Cache-Control', CACHE_CONTROL); + res.json(result); +} \ No newline at end of file diff --git a/pages/api/search.ts b/pages/api/search.ts new file mode 100644 index 0000000..f100d8d --- /dev/null +++ b/pages/api/search.ts @@ -0,0 +1,13 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../lib/cache' +import type { SearchResult } from '../../lib/data/useSearchResults' +import { getSearchResults } from '../../lib/data/back/getSearchResults' +import { extract } from '../../lib/util' + +export default async function Search(req: NextApiRequest, res: NextApiResponse) { + const { q } = req.query; + const data = q !== undefined ? await getSearchResults(extract(q)) : []; + res.setHeader('Cache-Control', CACHE_CONTROL); + res.json(data); +} \ No newline at end of file diff --git a/pages/api/top.ts b/pages/api/top.ts new file mode 100644 index 0000000..fcf52cb --- /dev/null +++ b/pages/api/top.ts @@ -0,0 +1,22 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next' +import { CACHE_CONTROL } from '../../lib/cache' +import { getTopResults, TopLimit, TopMetric, TopTime, TopTopic } from '../../lib/top_back' +import { CoursePlusMetrics, InstructorPlusMetrics } from '../../lib/trending' +import { extract } from '../../lib/util' + +export default async function Top(req: NextApiRequest, res: NextApiResponse<(CoursePlusMetrics | InstructorPlusMetrics)[]>) { + // extract query strings + const metric: TopMetric = extract(req.query['metric']) as TopMetric; + const topic: TopTopic = extract(req.query['topic']) as TopTopic; + const limit: TopLimit = parseInt(extract(req.query['limit'])) as TopLimit; + const time: TopTime = extract(req.query['time']) as TopTime; + // validate query strings + const valid: boolean = ['course', 'instructor'].includes(topic) + && !isNaN(limit) && limit > 0 && limit <= 100 + && ['totalEnrolled', 'activeUsers', 'screenPageViews'].includes(metric); + // fetch results + const data = valid ? await getTopResults({ metric, topic, limit, time }) : []; + res.setHeader('Cache-Control', CACHE_CONTROL); + res.json(data); +} \ No newline at end of file diff --git a/pages/api/trending.ts b/pages/api/trending.ts index 7b0a97e..88b4318 100644 --- a/pages/api/trending.ts +++ b/pages/api/trending.ts @@ -1,10 +1,11 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from 'next' -import { getTrending } from '../../lib/trending'; +import { CACHE_CONTROL } from '../../lib/cache' +import { SearchResult } from '../../lib/data/useSearchResults' +import { getTrendingResults } from '../../lib/trending' -export default async function Trending(req: NextApiRequest, res: NextApiResponse) { - const data = await getTrending(); - const maxAge = 86400; // 1 day in seconds - res.setHeader('Cache-Control', `s-maxage=${maxAge}, stale-while-revalidate=${maxAge}`); +export default async function Trending(req: NextApiRequest, res: NextApiResponse) { + const data = await getTrendingResults(); + res.setHeader('Cache-Control', CACHE_CONTROL); res.json(data); } \ No newline at end of file diff --git a/pages/c/[courseName].tsx b/pages/c/[courseName].tsx index 3734744..52393e0 100644 --- a/pages/c/[courseName].tsx +++ b/pages/c/[courseName].tsx @@ -2,6 +2,7 @@ import React from 'react' import Head from 'next/head' import { useRouter } from 'next/router' import { GetStaticPaths, GetStaticProps } from 'next' +import useSWR from 'swr/immutable' import Container from '@mui/material/Container' import Tooltip from '@mui/material/Tooltip' import Box from '@mui/material/Box' @@ -9,12 +10,12 @@ import Chip from '@mui/material/Chip' import Skeleton from '@mui/material/Skeleton' import Alert from '@mui/material/Alert' import AlertTitle from '@mui/material/AlertTitle' -import Tilty from 'react-tilty' +import Tilty from '@au5ton/react-tilty' import { Chart } from 'react-google-charts' import { Course, PublicationInfo } from '@cougargrades/types' import { PankoRow } from '../../components/panko' import { SectionPlus, useCourseData } from '../../lib/data/useCourseData' -import { onlyOne, getFirestoreDocument } from '../../lib/ssg' +import { getFirestoreDocument } from '../../lib/data/back/getFirestoreData' import { useRosetta } from '../../lib/i18n' import { Badge, BadgeSkeleton } from '../../components/badge' import { defaultComparator, EnhancedTable } from '../../components/datatable' @@ -22,12 +23,12 @@ import { Carousel } from '../../components/carousel' import { InstructorCard, InstructorCardShowMore, InstructorCardSkeleton } from '../../components/instructorcard' import { EnrollmentInfo } from '../../components/enrollment' import { CustomSkeleton } from '../../components/skeleton' -import { LinearProgressWithLabel } from '../../components/uploader/progress' +import { LoadingBoxIndeterminate, LoadingBoxLinearProgress } from '../../components/loading' import { TCCNSUpdateNotice } from '../../components/tccnsupdatenotice' -import { buildArgs } from '../../lib/environment' import styles from './course.module.scss' import interactivity from '../../styles/interactivity.module.scss' +import { extract } from '../../lib/util' export interface CourseProps { staticCourseName: string; @@ -45,15 +46,15 @@ export default function IndividualCourse({ staticCourseName, staticDescription, if(status === 'success') { // preload referenced areas - for(let item of data.relatedGroups) { + for(let item of data!.relatedGroups) { router.prefetch(item.href) } - for(let item of data.relatedInstructors) { + for(let item of data!.relatedInstructors) { router.prefetch(item.href) } } - const tccnsUpdateAsterisk = status === 'success' && data.tccnsUpdates.length > 0 ? * : null; + const tccnsUpdateAsterisk = status === 'success' && data!.tccnsUpdates.length > 0 ? * : null; return ( <> @@ -85,8 +86,8 @@ export default function IndividualCourse({ staticCourseName, staticDescription, { !isMissingProps ?

{staticDescription}

: } { !isMissingProps ?

{staticCourseName}{tccnsUpdateAsterisk}

: }
- {status === 'success' ? data.badges.map(e => ( - + {status === 'success' ? data!.badges.map(e => ( + {e.text} @@ -98,7 +99,7 @@ export default function IndividualCourse({ staticCourseName, staticDescription,
- { status === 'success' ? data.tccnsUpdates.map((value, index) => ( + { status === 'success' ? data!.tccnsUpdates.map((value, index) => ( )) : null}
@@ -108,35 +109,35 @@ export default function IndividualCourse({ staticCourseName, staticDescription, }
Sources:
- { status === 'success' ? data.publications.map(e => ( + { status === 'success' ? data!.publications.map(e => ( - )) : [1,2].map(e => )} + )) : [1,2].map(e => )} { status === 'success' ? <> - + : }

Basic Information

    -
  • Earliest record: { status === 'success' ? data.firstTaught : }
  • -
  • Latest record: { status === 'success' ? data.lastTaught : }
  • -
  • Number of instructors: { status === 'success' ? data.instructorCount : }
  • -
  • Number of sections: { status === 'success' ? data.sectionCount : }
  • +
  • Earliest record: { status === 'success' ? data!.firstTaught : }
  • +
  • Latest record: { status === 'success' ? data!.lastTaught : }
  • +
  • Number of instructors: { status === 'success' ? data!.instructorCount : }
  • +
  • Number of sections: { status === 'success' ? data!.sectionCount : }
  • -
  • Average number of students per section: { status === 'success' ? `~ ${data.classSize.toFixed(1)}` : }
  • +
  • Average number of students per section: { status === 'success' ? `~ ${data!.classSize.toFixed(1)}` : }

Related Groups

- { status === 'success' ? data.relatedGroups.map(e => ( + { status === 'success' ? data!.relatedGroups.map(e => ( - )) : [1].map(e => )} + )) : [1,2,3].map(e => )}

Related Instructors

- { status === 'success' && data.relatedInstructors.length > 0 ? data.relatedInstructors.slice(0,RELATED_INSTRUCTOR_LIMIT).map(e => + { status === 'success' && data!.relatedInstructors.length > 0 ? data!.relatedInstructors.slice(0,RELATED_INSTRUCTOR_LIMIT).map(e => ) : Array.from(new Array(RELATED_INSTRUCTOR_LIMIT).keys()).map(e => )} - { status === 'success' && data.relatedInstructors.length > RELATED_INSTRUCTOR_LIMIT ? : ''} + { status === 'success' && data!.relatedInstructors.length > RELATED_INSTRUCTOR_LIMIT ? : ''}

Data

@@ -144,24 +145,19 @@ export default function IndividualCourse({ staticCourseName, staticDescription,
{ - status === 'success' && data.dataChart.data.length > 1 ? + status === 'success' && data!.dataChart.data.length > 1 ? } - data={data.dataChart.data} - options={data.dataChart.options} + data={data!.dataChart.data} + options={data!.dataChart.options} // prevent ugly red box when there's no data yet on first-mount chartEvents={[{ eventName: 'error', callback: (event) => event.google.visualization.errors.removeError(event.eventArgs[0].id) }]} /> : - - Loading {status === 'success' ? data.sectionCount.toLocaleString() : ''} sections... -
- -
-
+ }
@@ -169,8 +165,8 @@ export default function IndividualCourse({ staticCourseName, staticDescription,
title="Past Sections" - columns={status === 'success' ? data.dataGrid.columns : []} - rows={status === 'success' ? data.dataGrid.rows : []} + columns={status === 'success' ? data!.dataGrid.columns : []} + rows={status === 'success' ? data!.dataGrid.rows : []} defaultOrder="desc" defaultOrderBy="term" /> @@ -199,10 +195,10 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async (context) => { //console.time('getStaticProps') const { params, locale } = context; - const { courseName } = params + const courseName = params?.courseName; const courseData = await getFirestoreDocument(`/catalog/${courseName}`) const description = courseData !== undefined ? courseData.description : '' - const recentPublication: PublicationInfo = courseData && courseData.publications !== undefined && + const recentPublication = courseData && courseData.publications !== undefined && Array.isArray(courseData.publications) && courseData.publications.length > 0 ? @@ -214,7 +210,7 @@ export const getStaticProps: GetStaticProps = async (context) => { return { props: { - staticCourseName: onlyOne(courseName), + staticCourseName: extract(courseName), staticDescription: description, staticHTML: content ?? '', doesNotExist: courseData === undefined, diff --git a/pages/c/course.module.scss b/pages/c/course.module.scss index 39723ca..e6c88f0 100644 --- a/pages/c/course.module.scss +++ b/pages/c/course.module.scss @@ -99,6 +99,7 @@ .chartWrap { overflow: auto; + margin-bottom: 30px; div { display: inline-block } diff --git a/pages/faq/[slug].tsx b/pages/faq/[slug].tsx index c7e9ccf..e69c7cf 100644 --- a/pages/faq/[slug].tsx +++ b/pages/faq/[slug].tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import Head from 'next/head' import { useRouter } from 'next/router' import { GetStaticPaths, GetStaticProps } from 'next' @@ -34,6 +34,14 @@ export default function FaqPost({ post, allPosts }: FaqPostProps) { router.push(`/faq/${other.slug}`, undefined, { scroll: false }) setTOCExpanded(false) } + + useEffect(() => { + for(let item of allPosts) { + const href = `/faq/${item.slug}` + router.prefetch(href) + } + },[allPosts]) + return ( <> @@ -75,9 +83,9 @@ export default function FaqPost({ post, allPosts }: FaqPostProps) { Frequently Asked Question: - + - Last modified: + Last modified: @@ -101,9 +109,9 @@ export const getStaticPaths: GetStaticPaths = async () => { } export const getStaticProps: GetStaticProps = async ({ params }) => { - const slug = Array.isArray(params.slug) ? params.slug[0] : params.slug + const slug = Array.isArray(params?.slug) ? params?.slug[0] : params?.slug const allPosts = getAllPosts(['slug','title']) - const postFound = allPosts.map(e => e.slug).includes(slug); + const postFound = allPosts.map(e => e.slug).includes(slug) && slug !== undefined; const post = postFound ? getPostBySlug(slug, ['slug','title', 'date','content']) : {}; if(postFound) { post.content = await markdownToHtml(post.content || '') diff --git a/pages/g/[groupId].tsx b/pages/g/[groupId].tsx index 6c33977..fd349f4 100644 --- a/pages/g/[groupId].tsx +++ b/pages/g/[groupId].tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react' import Head from 'next/head' import { useRouter } from 'next/router' import { GetStaticPaths, GetStaticProps } from 'next' +import useSWR from 'swr/immutable' import { useRecoilState } from 'recoil' import { Group } from '@cougargrades/types' import Container from '@mui/material/Container' @@ -12,16 +13,18 @@ import Alert from '@mui/material/Alert' import AlertTitle from '@mui/material/AlertTitle' import { PankoRow } from '../../components/panko' import { FakeLink } from '../../components/link' -import { getFirestoreCollection, getFirestoreDocument, onlyOne } from '../../lib/ssg' +import { getFirestoreCollection, getFirestoreDocument } from '../../lib/data/back/getFirestoreData' import { GroupNavSubheader, TableOfContentsWrap } from '../../components/groupnav' import { GroupContent, GroupContentSkeleton } from '../../components/groupcontent' -import { GroupResult, useAllGroups, useOneGroup } from '../../lib/data/useAllGroups' +import { AllGroupsResult, GroupResult, PopulatedGroupResult } from '../../lib/data/useAllGroups' import { buildArgs } from '../../lib/environment' import { useRosetta } from '../../lib/i18n' import { tocAtom } from '../../lib/recoil' +import { ObservableStatus } from '../../lib/data/Observable' import styles from './group.module.scss' import interactivity from '../../styles/interactivity.module.scss' +import { extract } from '../../lib/util' export interface GroupProps { staticGroupId: string; @@ -30,11 +33,16 @@ export interface GroupProps { doesNotExist?: boolean; } + export default function Groups({ staticGroupId, staticName, staticDescription, doesNotExist }: GroupProps) { const stone = useRosetta() const router = useRouter() - const { data, status } = useAllGroups(); - const { data: oneGroupData, status: oneGroupStatus } = useOneGroup(staticGroupId) + const { data, error: error2, isLoading: isLoading2 } = useSWR(`/api/group`) + const status: ObservableStatus = error2 ? 'error' : (isLoading2 || !data || !staticGroupId) ? 'loading' : 'success' + + const { data: oneGroupData, error, isLoading } = useSWR(`/api/group/${staticGroupId}`) + const oneGroupStatus: ObservableStatus = error ? 'error' : (isLoading || !oneGroupData || !staticGroupId) ? 'loading' : 'success' + const isMissingProps = staticGroupId === undefined const good = !isMissingProps && status === 'success' && oneGroupStatus === 'success' const [_, setTOCExpanded] = useRecoilState(tocAtom) @@ -45,10 +53,10 @@ export default function Groups({ staticGroupId, staticName, staticDescription, d } useEffect(() => { - if(good && data.categories.length > 0) { + if(good && data!.categories.length > 0) { // preload referenced areas - for(let key of data.categories) { - const cat = data.results[key]; + for(let key of data!.categories) { + const cat = data!.results[key]; for(let item of cat) { router.prefetch(item.href) } @@ -68,9 +76,9 @@ export default function Groups({ staticGroupId, staticName, staticDescription, d
@@ -126,13 +134,13 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async (context) => { const { params } = context; - const { groupId } = params + const groupId = params?.groupId; const groupData = await getFirestoreDocument(`/groups/${groupId}`) const name = groupData !== undefined ? groupData.name : '' const description = groupData !== undefined ? groupData.description : '' return { props: { - staticGroupId: onlyOne(groupId), + staticGroupId: extract(groupId), staticName: name, staticDescription: description, doesNotExist: groupData === undefined, diff --git a/pages/i/[instructorName].tsx b/pages/i/[instructorName].tsx index 0d13758..03d66aa 100644 --- a/pages/i/[instructorName].tsx +++ b/pages/i/[instructorName].tsx @@ -3,6 +3,7 @@ import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' import { GetStaticPaths, GetStaticProps } from 'next' +import useSWR from 'swr/immutable' import Box from '@mui/material/Box' import Chip from '@mui/material/Chip' import Button from '@mui/material/Button' @@ -11,7 +12,7 @@ import Skeleton from '@mui/material/Skeleton' import Container from '@mui/material/Container' import Typography from '@mui/material/Typography' import RateReviewIcon from '@mui/icons-material/RateReview' -import Tilty from 'react-tilty' +import Tilty from '@au5ton/react-tilty' import { Chart } from 'react-google-charts' import { Instructor } from '@cougargrades/types' import abbreviationMap from '@cougargrades/publicdata/bundle/edu.uh.publications.subjects/subjects.json' @@ -22,16 +23,16 @@ import { Carousel } from '../../components/carousel' import { InstructorCard, InstructorCardSkeleton } from '../../components/instructorcard' import { EnhancedTable } from '../../components/datatable' import { CustomSkeleton } from '../../components/skeleton' -import { LinearProgressWithLabel } from '../../components/uploader/progress' -import { onlyOne, getFirestoreDocument, getFirestoreCollection } from '../../lib/ssg' +import { getFirestoreCollection, getFirestoreDocument } from '../../lib/data/back/getFirestoreData' import { useRosetta } from '../../lib/i18n' -import { buildArgs } from '../../lib/environment' -import { useInstructorData } from '../../lib/data/useInstructorData' +import { InstructorResult, useInstructorData } from '../../lib/data/useInstructorData' import { SectionPlus } from '../../lib/data/useCourseData' import { CoursePlus } from '../../lib/data/useGroupData' +import { LoadingBoxIndeterminate } from '../../components/loading' import styles from './instructor.module.scss' import interactivity from '../../styles/interactivity.module.scss' +import { extract } from '../../lib/util' export interface InstructorProps { @@ -47,6 +48,16 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar const isMissingProps = staticInstructorName === undefined const RELATED_COURSE_LIMIT = 4; + if(status === 'success') { + // preload referenced areas + for(let item of data!.relatedGroups) { + router.prefetch(item.href) + } + for(let item of data!.relatedCourses) { + router.prefetch(item.href) + } + } + return ( <> @@ -61,8 +72,8 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar { !isMissingProps ? {staticInstructorName} : } { !isMissingProps ? {staticDepartmentText} : }
- {status === 'success' ? data.badges.map(e => ( - + {status === 'success' ? data!.badges.map(e => ( + {e.text} @@ -74,8 +85,8 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar
- { status === 'success' && data.rmpHref !== undefined ? <> - + { status === 'success' && data!.rmpHref !== undefined ? <> + @@ -84,27 +95,27 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar }
{ status === 'success' ? <> - + : }

Basic Information

    -
  • Earliest record: { status === 'success' ? data.firstTaught : }
  • -
  • Latest record: { status === 'success' ? data.lastTaught : }
  • -
  • Number of courses: { status === 'success' ? data.courseCount : }
  • -
  • Number of sections: { status === 'success' ? data.sectionCount : }
  • +
  • Earliest record: { status === 'success' ? data!.firstTaught : }
  • +
  • Latest record: { status === 'success' ? data!.lastTaught : }
  • +
  • Number of courses: { status === 'success' ? data!.courseCount : }
  • +
  • Number of sections: { status === 'success' ? data!.sectionCount : }
  • -
  • Average number of students per section: { status === 'success' ? `~ ${data.classSize.toFixed(1)}` : }
  • +
  • Average number of students per section: { status === 'success' ? `~ ${data!.classSize.toFixed(1)}` : }

Related Groups

- { status === 'success' ? data.relatedGroups.map(e => ( + { status === 'success' ? data!.relatedGroups.map(e => ( )) : [1].map(e => )}

Related Courses

- { status === 'success' ? data.relatedCourses.slice(0,RELATED_COURSE_LIMIT).map(e => + { status === 'success' ? data!.relatedCourses.slice(0,RELATED_COURSE_LIMIT).map(e => ) : Array.from(new Array(RELATED_COURSE_LIMIT).keys()).map(e => )}

Data

@@ -113,24 +124,19 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar
{ - status === 'success' && data.dataChart.data.length > 1 ? + status === 'success' && data!.dataChart.data.length > 0 ? } - data={data.dataChart.data} - options={data.dataChart.options} + data={data!.dataChart.data} + options={data!.dataChart.options} // prevent ugly red box when there's no data yet on first-mount chartEvents={[{ eventName: 'error', callback: (event) => event.google.visualization.errors.removeError(event.eventArgs[0].id) }]} /> : - - Loading {data.sectionCount.toLocaleString()} sections... -
- -
-
+ }
@@ -138,14 +144,14 @@ export default function IndividualInstructor({ staticInstructorName, staticDepar
title="Courses" - columns={status === 'success' ? data.courseDataGrid.columns : []} - rows={status === 'success' ? data.courseDataGrid.rows : []} + columns={status === 'success' ? data!.courseDataGrid.columns : []} + rows={status === 'success' ? data!.courseDataGrid.rows : []} defaultOrderBy="id" /> title="Past Sections" - columns={status === 'success' ? data.sectionDataGrid.columns : []} - rows={status === 'success' ? data.sectionDataGrid.rows : []} + columns={status === 'success' ? data!.sectionDataGrid.columns : []} + rows={status === 'success' ? data!.sectionDataGrid.rows : []} defaultOrder="desc" defaultOrderBy="term" /> @@ -172,14 +178,15 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async (context) => { //console.time('getStaticProps') const { params } = context; - const { instructorName } = params + //const { instructorName } = params + const instructorName = params?.instructorName; const instructorData = await getFirestoreDocument(`/instructors/${instructorName}`) const departmentText = getDepartmentText(instructorData) //console.timeEnd('getStaticProps') return { props: { - staticInstructorName: onlyOne(instructorName), + staticInstructorName: extract(instructorName), staticDepartmentText: departmentText, doesNotExist: instructorData === undefined, } @@ -189,8 +196,9 @@ export const getStaticProps: GetStaticProps = async (context) = function getDepartmentText(data: Instructor | undefined) { // sort department entries in descending by value if(data !== undefined) { - const text = Object - .entries(data.departments) // [string, number][] + const depts: [keyof typeof abbreviationMap, number][] = Object.entries(data.departments) as any; + + const text = depts // [string, number][] .sort((a, b) => b[1] - a[1]) // sort .slice(0, 3) // limit to 3 entries .map(e => abbreviationMap[e[0]]) // ['MATH'] => ['Mathematics'] diff --git a/pages/i/instructor.module.scss b/pages/i/instructor.module.scss index 7649dc7..9a1cded 100644 --- a/pages/i/instructor.module.scss +++ b/pages/i/instructor.module.scss @@ -86,6 +86,7 @@ .chartWrap { overflow: auto; + margin-bottom: 30px; div { display: inline-block } diff --git a/pages/index.tsx b/pages/index.tsx index a9fd868..33c5603 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,12 +1,10 @@ import React from 'react' import Image from 'next/image' import { useRecoilState } from 'recoil' -import { useFirestore } from 'reactfire' import Button from '@mui/material/Button' import { BlogNotifications } from '../components/blog' import { ExternalLink } from '../components/link' import { searchInputAtom } from '../lib/recoil' -import { FirestoreGuard } from '../lib/firebase' import slotmachine from '../public/slotmachine.svg' import wordcloud from '../public/wordcloud.svg' diff --git a/pages/top/[slug].tsx b/pages/top/[slug].tsx new file mode 100644 index 0000000..40decb5 --- /dev/null +++ b/pages/top/[slug].tsx @@ -0,0 +1,193 @@ +import React, { useEffect, useState } from 'react' +import Head from 'next/head' +import { useRouter } from 'next/router' +import { GetStaticPaths, GetStaticProps } from 'next' +import useSWR from 'swr/immutable' +import { useRecoilState } from 'recoil' +import Box from '@mui/material/Box' +import List from '@mui/material/List' +import Container from '@mui/material/Container' +import ListItemButton from '@mui/material/ListItemButton' +import ListItemText from '@mui/material/ListItemText' +import Divider from '@mui/material/Divider' +import Typography from '@mui/material/Typography' +import FormControl from '@mui/material/FormControl' +import InputLabel from '@mui/material/InputLabel' +import Select from '@mui/material/Select' +import MenuItem from '@mui/material/MenuItem' +import { FakeLink } from '../../components/link' +import { PankoRow } from '../../components/panko' +import { GroupNavSubheader, TableOfContentsWrap } from '../../components/groupnav' +import { FaqPostData } from '../../lib/faq' +import { tocAtom } from '../../lib/recoil' +import { useIsCondensed } from '../../lib/hook' +import { POPULAR_TABS, TopLimit, TopMetric, TopTime, TopTopic } from '../../lib/top_front' +import { ErrorBoxIndeterminate, LoadingBoxIndeterminate } from '../../components/loading' +import { TopResult, useTopResults } from '../../lib/data/useTopResults' +import { TopListItem } from '../../components/TopListItem' + +import styles from './slug.module.scss' +import interactivity from '../../styles/interactivity.module.scss' + +export interface FaqPostProps { + post: FaqPostData; + allPosts: FaqPostData[]; +} + +const parseInt2 = (x: string | number) => typeof x === 'number' ? x : parseInt(x) + +export default function TopPage({ post, allPosts }: FaqPostProps) { + const router = useRouter() + const [_, setTOCExpanded] = useRecoilState(tocAtom) + const condensed = useIsCondensed() + + const viewMetric: TopMetric = post?.slug?.includes('viewed') ? 'screenPageViews' : 'totalEnrolled' + const viewTopic: TopTopic = post?.slug?.includes('instructor') ? 'instructor' : 'course' + const [viewLimit, setViewLimit] = useState(10) + const [viewTime, setViewTime] = useState('all') + + const { data, status, error } = useTopResults({ metric: viewMetric, topic: viewTopic, limit: viewLimit, time: viewTime }) + + const handleClick = (other: FaqPostData) => { + router.push(`/top/${other.slug}`, undefined, { scroll: false }) + setTOCExpanded(false) + } + + useEffect(() => { + if(status === 'success') { + // preload referenced areas + for(let item of allPosts) { + const href = `/top/${item.slug}` + router.prefetch(href) + } + for(let item of data!) { + router.prefetch(item.href) + } + } + },[status,data,allPosts]) + + useEffect(() => { + if (viewMetric === 'totalEnrolled') { + setViewTime('all') + } + else { + setViewTime('lastMonth') + } + }, [viewMetric]) + + return ( + <> + + {router.isFallback ? `Popular / CougarGrades.io` : `${post.title} / CougarGrades.io Popular`} + + + + + +
+ +
+
+ + {post.title} + + + {post.content} + + + + Count + + + + Time Span + + + + + { + status === 'error' + ? <> + + + : status === 'loading' + ? <> + + + : <> + { data!.map((item, index, array) => ( + + + { index < (array.length - 1) ? : null } + + ))} + + } + +
+
+
+ + ); +} + +// See: https://nextjs.org/docs/basic-features/data-fetching#fallback-true +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: POPULAR_TABS.map(post => ({ + params: { + slug: post.slug + } + })), + fallback: false + } +} + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const slug = Array.isArray(params?.slug) ? params?.slug[0] : params?.slug + const post = POPULAR_TABS.find(post => post.slug === slug) + + return { + props: { + post: { + ...(post ?? { id: -1 }), + }, + allPosts: [ + ...POPULAR_TABS, + ] + } + }; +} diff --git a/pages/top/index.tsx b/pages/top/index.tsx new file mode 100644 index 0000000..37d4014 --- /dev/null +++ b/pages/top/index.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react' +import Head from 'next/head' +import { GetStaticProps } from 'next' +import { useRouter } from 'next/router' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import CircularProgress from '@mui/material/CircularProgress' +import ErrorIcon from '@mui/icons-material/Error' +import { FaqPostData, getAllPosts } from '../../lib/faq' +import { POPULAR_TABS } from '../../lib/top_front' + +export interface FaqIndexProps { + allPosts: FaqPostData[]; +} + +export default function Popular({ allPosts }: FaqIndexProps) { + const router = useRouter() + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + if(allPosts.length > 0 && allPosts[0].slug) { + router.replace(`/top/${allPosts[0].slug}`, undefined, { scroll: false }); + } + else { + setIsLoading(false) + } + }, []); + + return ( + <> + + Popular / CougarGrades.io + + +
+ + { + isLoading ? <> + Loading... + + : <> + No Popular pages were found + + + } + +
+ + ); +} + +export const getStaticProps: GetStaticProps = async ({ params }) => { + return { + props: { + allPosts: [ + ...POPULAR_TABS + ] + } + }; +} diff --git a/pages/top/slug.module.scss b/pages/top/slug.module.scss new file mode 100644 index 0000000..cf1e298 --- /dev/null +++ b/pages/top/slug.module.scss @@ -0,0 +1,62 @@ +.main { + margin: 16px; + @media(min-width: 767px) { + display: grid; + grid-template-columns: 15em minmax(0, 1fr); + max-width: 1300px; + width: 90%; + margin: 16px auto; + & > * { + padding-right: 8px; + padding-left: 8px; + } + } +} + +.articleContainer { + max-width: 750px; + padding-right: 2rem; + padding-left: 2rem; + margin-right: auto; + margin-left: 0; + + @media (max-width: 768px) { + padding-right: calc(1.5rem - 16px); + padding-left: calc(1.5rem - 16px); + padding-bottom: calc(1.5rem - 16px); + } +} + +.nav { + @media(min-width: 767px) { + position: sticky; + top: 2rem; + align-self: start; + margin-bottom: unset; + } +} + +.sidebarList { + padding-bottom: 0; +} + +.accordionRoot { + border-radius: 4px!important; + margin: 8px 0!important; + &::before { + display: none; + } +} + +.accordionRootExpanded { + margin-bottom: 16px!important; +} + +.controlBox { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + padding: 1rem 0; + //background-color: rebeccapurple; +} diff --git a/retired_pages/admin.tsx b/retired_pages/admin.tsx deleted file mode 100644 index 4957e20..0000000 --- a/retired_pages/admin.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { useSigninCheck } from 'reactfire' -import { UserAccountControl } from '../components/auth/UserAccountControl' -import { LoginForm } from '../components/auth/LoginForm' - -export default function AdminPanel() { - const { status, data: signInCheckResult } = useSigninCheck(); - return ( -
- { status === 'success' && signInCheckResult.signedIn ? <> - - : } -
- ); -} diff --git a/retired_pages/upload.tsx b/retired_pages/upload.tsx deleted file mode 100644 index 988fcc0..0000000 --- a/retired_pages/upload.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { useSigninCheck } from 'reactfire' -import { CustomClaimsCheck } from '../components/auth/CustomClaimsCheck' -import { LoginForm } from '../components/auth/LoginForm' -import { Uploader } from '../components/uploader/uploader' - -export default function Upload() { - const { status, data: signInCheckResult } = useSigninCheck(); - return ( -
- { status === 'success' && signInCheckResult.signedIn ? <> - - - - : } -
- ); -} diff --git a/retired_pages/utilities.tsx b/retired_pages/utilities.tsx deleted file mode 100644 index b21b4b7..0000000 --- a/retired_pages/utilities.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { useSigninCheck } from 'reactfire' -import { CustomClaimsCheck } from '../components/auth/CustomClaimsCheck' -import { LoginForm } from '../components/auth/LoginForm' -import { QueueManager } from '../components/uploader/queuemanager' - -export default function Queue() { - const { status, data: signInCheckResult } = useSigninCheck(); - return ( -
- { status === 'success' && signInCheckResult.signedIn ? <> - - - - : } -
- ); -} diff --git a/styles/new.css b/styles/new.css index 28cd838..b1d9a70 100644 --- a/styles/new.css +++ b/styles/new.css @@ -27,7 +27,7 @@ Small modifications were made. :root { --nc-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - --nc-font-mono: Consolas, monaco, 'Ubuntu Mono', 'Liberation Mono', 'Courier New', Courier, monospace; + --nc-font-mono: Consolas, monaco, 'Ubuntu Mono', 'Liberation Mono', 'Cascadia Mono', 'Courier New', Courier, monospace; /* Light theme */ --nc-tx-1: #000000; diff --git a/tsconfig.json b/tsconfig.json index a6d651f..fc441e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ ], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, @@ -17,7 +17,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, }, "include": [ "**/*.ts",