diff --git a/components/Container.tsx b/components/Container.tsx index 896cf4c7..a3ee606d 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -362,7 +362,8 @@ export default function Container(props: ContainerProps) { diff --git a/components/TeamCard.tsx b/components/TeamCard.tsx index eb9b5228..1475843f 100644 --- a/components/TeamCard.tsx +++ b/components/TeamCard.tsx @@ -10,8 +10,9 @@ export default function TeamCard(props: { team: Team | undefined }) { className="w-full bg-base-300 border-4 border-base-300 transition ease-in hover:border-primary" >

- {team?.league} Team {team?.number}{" "} - - {team?.users.length} members + {team?.league} {team?.alliance ? "Alliance" : "Team"}{" "} + {team?.number} -{" "} + {team?.users.length} members

); diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index 939a366f..502114c8 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -20,6 +20,8 @@ import { const SHOW_PICKLISTS_ON_TEAM_CARDS = false; const SHOW_CARD_IDS = false; +const api = new ClientApi(); + function TeamCard(props: { entry: PicklistEntry; draggable: boolean; @@ -297,8 +299,6 @@ export function TeamList(props: { ); } -const api = new ClientApi(); - export default function PicklistScreen(props: { teams: number[]; reports: Report[]; @@ -321,10 +321,16 @@ export default function PicklistScreen(props: { const teams = props.teams.map((team) => ({ number: team })); // Save picklists - useEffect( - () => savePicklistGroup(props.picklist._id, picklists, strikethroughs, api), - [props.picklist._id, picklists, strikethroughs], - ); + useEffect(() => { + if (loadingPicklists !== LoadState.Loaded) return; + savePicklistGroup(props.picklist._id, picklists, strikethroughs, api); + }, [ + props.picklist._id, + picklists, + strikethroughs, + LoadState.Loaded, + loadingPicklists, + ]); const updatePicklist = useCallback( (picklist: Picklist) => { diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index eada8b7f..f7343f11 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -5,6 +5,8 @@ import DbInterface, { WithStringOrObjectIdId, } from "./client/dbinterfaces/DbInterface"; import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface"; +import CachedDbInterface from "./client/dbinterfaces/CachedDbInterface"; +import { cacheOptions } from "./client/dbinterfaces/CachedDbInterface"; if (!process.env.MONGODB_URI) { // Necessary to allow connections from files running outside of Next @@ -28,10 +30,13 @@ clientPromise = global.clientPromise; export { clientPromise }; -export async function getDatabase(): Promise { +export async function getDatabase(): Promise { if (!global.interface) { await clientPromise; - const dbInterface = new MongoDBInterface(clientPromise); + const dbInterface = new CachedDbInterface( + new MongoDBInterface(clientPromise), + cacheOptions, + ); await dbInterface.init(); global.interface = dbInterface; diff --git a/lib/ResendUtils.ts b/lib/ResendUtils.ts index bac16f7b..a2a0f468 100644 --- a/lib/ResendUtils.ts +++ b/lib/ResendUtils.ts @@ -3,8 +3,7 @@ import { Resend } from "resend"; import { getDatabase } from "./MongoDB"; import { User } from "./Types"; import CollectionId from "./client/CollectionId"; - -const resend = new Resend(process.env.SMTP_PASSWORD); +import { ObjectId } from "bson"; export interface ResendInterface { createContact: (rawUser: NextAuthUser) => Promise; @@ -12,6 +11,12 @@ export interface ResendInterface { } export class ResendUtils implements ResendInterface { + private static resend: Resend; + + constructor() { + ResendUtils.resend ??= new Resend(process.env.SMTP_PASSWORD); + } + async createContact(rawUser: NextAuthUser) { const user = rawUser as User; @@ -26,7 +31,7 @@ export class ResendUtils implements ResendInterface { const nameParts = user.name?.split(" "); - const res = await resend.contacts.create({ + const res = await ResendUtils.resend.contacts.create({ email: user.email, firstName: nameParts[0], lastName: nameParts.length > 1 ? nameParts[1] : "", @@ -41,13 +46,16 @@ export class ResendUtils implements ResendInterface { } const db = await getDatabase(); - // Going around our own interface is a red flag, but it's 11 PM and I'm tired -Renato - db.db - ?.collection(CollectionId.Users) - .updateOne( - { email: user.email }, - { $set: { resendContactId: res.data.id } }, - ); + const id = (await db.findObject(CollectionId.Users, { email: user.email })) + ?._id; + if (!id) { + console.error("User not found in database", user.email); + return; + } + + db.updateObjectById(CollectionId.Users, new ObjectId(id), { + resendContactId: res.data.id, + }); } async emailDevelopers(subject: string, message: string) { @@ -56,7 +64,7 @@ export class ResendUtils implements ResendInterface { return; } - resend.emails.send({ + ResendUtils.resend.emails.send({ from: "Gearbox Server ", to: JSON.parse(process.env.DEVELOPER_EMAILS), // Environment variables are always strings, so we need to parse it subject, diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts index aadb7527..deb03646 100644 --- a/lib/TheBlueAlliance.ts +++ b/lib/TheBlueAlliance.ts @@ -12,6 +12,7 @@ import { import { NotLinkedToTba } from "./client/ClientUtils"; import { GameId, defaultGameId } from "./client/GameId"; import { games } from "./games"; +import DbInterface from "./client/dbinterfaces/DbInterface"; export namespace TheBlueAlliance { export interface SimpleTeam { @@ -209,7 +210,7 @@ export namespace TheBlueAlliance { export class Interface { req: Request; - db: Promise; + db: Promise; competitionPairings: CompetitonNameIdPair[] = []; diff --git a/lib/Types.ts b/lib/Types.ts index fab351ed..52539d0a 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -88,6 +88,7 @@ export class Team { tbaId: string | undefined; number: number; league: League = League.FRC; + alliance: boolean; owners: string[]; users: string[]; @@ -109,6 +110,7 @@ export class Team { tbaId: string | undefined, number: number, league: League = League.FRC, + alliance: boolean = false, owners: string[] = [], users: string[] = [], scouters: string[] = [], @@ -122,6 +124,7 @@ export class Team { this.tbaId = tbaId; this.number = number; this.league = league; + this.alliance = alliance; this.owners = owners; this.users = users; this.scouters = scouters; diff --git a/lib/UrlResolver.ts b/lib/UrlResolver.ts index 6670398b..d9279736 100644 --- a/lib/UrlResolver.ts +++ b/lib/UrlResolver.ts @@ -29,7 +29,7 @@ export interface ResolvedUrlData { * @param object - Any `Object` with a `_id` property * @returns - The same object, but with `_id` set as a string */ -export function SerializeDatabaseObject(object: any): any { +export function serializeDatabaseObject(object: any): any { if (!object) { return null; } @@ -46,8 +46,8 @@ export function SerializeDatabaseObject(object: any): any { return object; } -export function SerializeDatabaseObjects(objectArray: any[]): any[] { - return objectArray.map((obj) => SerializeDatabaseObject(obj)); +export function serializeDatabaseObjects(objectArray: any[]): any[] { + return objectArray.map((obj) => serializeDatabaseObject(obj)); } /** @@ -115,10 +115,10 @@ export default async function UrlResolver( // find these slugs, and convert them to a JSON safe condition // if they dont exist, simply return nothing const data: ResolvedUrlData = { - team: SerializeDatabaseObject(await promises[0]), - season: SerializeDatabaseObject(await promises[1]), - competition: SerializeDatabaseObject(await promises[2]), - report: SerializeDatabaseObject(await promises[3]), + team: serializeDatabaseObject(await promises[0]), + season: serializeDatabaseObject(await promises[1]), + competition: serializeDatabaseObject(await promises[2]), + report: serializeDatabaseObject(await promises[3]), }; return data; } catch (error) { diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index bb65b3af..03c2ea5e 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -222,7 +222,7 @@ export default class ClientApi extends NextApiTemplate { }); createTeam = createNextRoute< - [string, string, number, League], + [string, string, number, League, boolean], Team | undefined, ApiDependencies, void @@ -233,7 +233,7 @@ export default class ClientApi extends NextApiTemplate { res, { db: dbPromise, resend, userPromise }, authData, - [name, tbaId, number, league], + [name, tbaId, number, league, alliance], ) => { const user = (await userPromise)!; const db = await dbPromise; @@ -256,6 +256,7 @@ export default class ClientApi extends NextApiTemplate { tbaId, number, league, + alliance, [user._id!.toString()], [user._id!.toString()], [user._id!.toString()], @@ -2302,4 +2303,32 @@ export default class ClientApi extends NextApiTemplate { res.status(200).send(responseObj); }, }); + + getCacheStats = createNextRoute< + [], + object | undefined, + ApiDependencies, + void + >({ + isAuthorized: AccessLevels.IfDeveloper, + handler: async (req, res, {}, authData, args) => { + if (!global.cache) return res.status(200).send(undefined); + const stats = global.cache.getStats(); + return res.status(200).send(stats); + }, + }); + + getCachedValue = createNextRoute< + [string], + object | undefined, + ApiDependencies, + void + >({ + isAuthorized: AccessLevels.IfDeveloper, + handler: async (req, res, {}, authData, [key]) => { + if (!global.cache) return res.status(500).send({ error: "No cache" }); + const val = global.cache.get(key) as object | undefined; + return res.status(200).send(val); + }, + }); } diff --git a/lib/client/dbinterfaces/CachedDbInterface.ts b/lib/client/dbinterfaces/CachedDbInterface.ts new file mode 100644 index 00000000..8cd487ac --- /dev/null +++ b/lib/client/dbinterfaces/CachedDbInterface.ts @@ -0,0 +1,66 @@ +import { ObjectId } from "bson"; +import CollectionId, { CollectionIdToType } from "@/lib/client/CollectionId"; +import DbInterface, { + WithStringOrObjectIdId, +} from "@/lib/client/dbinterfaces/DbInterface"; +import { default as BaseCachedDbInterface } from "mongo-anywhere/CachedDbInterface"; +import NodeCache from "node-cache"; +import { CacheOperation } from "mongo-anywhere/CachedDbInterface"; + +export const cacheOptions: NodeCache.Options = { + stdTTL: 10 * 60, + useClones: false, +}; + +export default class CachedDbInterface + extends BaseCachedDbInterface> + implements DbInterface +{ + init(): Promise { + return super.init(Object.values(CollectionId)); + } + getTtl(operation: CacheOperation, collectionId: CollectionId): number { + if (operation === "count") return 3 * 60 * 60; + + return cacheOptions.stdTTL!; + } + addObject>( + collection: TId, + object: WithStringOrObjectIdId, + ): Promise { + return super.addObject(collection, object); + } + deleteObjectById(collection: CollectionId, id: ObjectId): Promise { + return super.deleteObjectById(collection, id); + } + updateObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId, newValues: Partial): Promise { + return super.updateObjectById(collection, id, newValues); + } + findObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId): Promise { + return super.findObjectById(collection, id); + } + findObject>( + collection: TId, + query: object, + ): Promise { + return super.findObject(collection, query); + } + findObjects>( + collection: TId, + query: object, + ): Promise { + return super.findObjects(collection, query); + } + countObjects( + collection: CollectionId, + query: object, + ): Promise { + return super.countObjects(collection, query); + } +} diff --git a/package-lock.json b/package-lock.json index d8eb49f4..1eeff82a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.1.20", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.1.20", + "version": "1.2.2", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", @@ -26,7 +26,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", - "mongo-anywhere": "^1.0.21", + "mongo-anywhere": "^1.1.5", "mongodb": "^5.0.0", "next": "^15.1.6", "next-auth": "^4.24.11", @@ -3588,6 +3588,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7870,13 +7878,14 @@ } }, "node_modules/mongo-anywhere": { - "version": "1.0.23", - "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.0.23.tgz", - "integrity": "sha512-n6N/fLRb2mylontUuxU4C57SFjULCqCXUUyeNbITRU2jZ8SH9brpZn3woEDwQ0DYZ/GXLHYcDqn2esZkAZTnKQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.5.tgz", + "integrity": "sha512-9qWUSv7kS2oD/MPLLV5Y4AjhTFE7VTkhdDWXsG9eeddOgmRHzRrKX5IV+C/ryDcRoJwcMMIIehnD96RsOse25g==", "dependencies": { "bson": "^5.0.0", "minimongo": "^6.19.0", - "mongodb": "^5.0.0" + "mongodb": "^5.0.0", + "node-cache": "^5.1.2" } }, "node_modules/mongodb": { @@ -8097,6 +8106,17 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 652bbc74..cb47b22d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.0", + "version": "1.2.2", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", @@ -35,7 +35,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", - "mongo-anywhere": "^1.0.21", + "mongo-anywhere": "^1.1.5", "mongodb": "^5.0.0", "next": "^15.1.6", "next-auth": "^4.24.11", diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx index fa932608..cefe76a4 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx @@ -19,7 +19,7 @@ import { GameId } from "@/lib/client/GameId"; import CollectionId from "@/lib/client/CollectionId"; import { games } from "@/lib/games"; import { getDatabase } from "@/lib/MongoDB"; -import UrlResolver, { SerializeDatabaseObject } from "@/lib/UrlResolver"; +import UrlResolver, { serializeDatabaseObject } from "@/lib/UrlResolver"; import { ObjectId } from "bson"; import { GetServerSideProps } from "next"; import Flex from "@/components/Flex"; @@ -330,7 +330,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { - pitReport: SerializeDatabaseObject(pitreport), + pitReport: makeObjSerializeable(serializeDatabaseObject(pitreport)), layout: makeObjSerializeable(game.pitReportLayout), teamNumber: resolved.team?.number, compName: resolved.competition?.name, diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx index 14f36293..e3b30af0 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pitstats.tsx @@ -7,7 +7,7 @@ import { QuantData, Report, } from "@/lib/Types"; -import { SerializeDatabaseObject } from "@/lib/UrlResolver"; +import { serializeDatabaseObject } from "@/lib/UrlResolver"; import { GetServerSideProps } from "next"; import { BsGearFill } from "react-icons/bs"; @@ -493,6 +493,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => { }); return { - props: { competition: SerializeDatabaseObject(comp) }, + props: { competition: serializeDatabaseObject(comp) }, }; }; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx index 78ab9dc0..58ad1c3c 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx @@ -8,7 +8,7 @@ import { Report, SubjectiveReport, } from "@/lib/Types"; -import { SerializeDatabaseObject } from "@/lib/UrlResolver"; +import { serializeDatabaseObject } from "@/lib/UrlResolver"; import ClientApi from "@/lib/api/ClientApi"; import CollectionId from "@/lib/client/CollectionId"; import { useCurrentSession } from "@/lib/client/useCurrentSession"; @@ -24,7 +24,7 @@ export default function Scouters(props: { const team = props.team; const comp = props.competition; - const { session, status } = useCurrentSession(); + const { session } = useCurrentSession(); const isManager = session?.user?._id ? team?.owners.includes(session.user?._id) : false; @@ -141,9 +141,9 @@ export default function Scouters(props: { setReports((reports) => { if (!reports) return reports; - const { _id, ...updated } = reports[comment.dbId]; + const { _id, ...old } = reports[comment.dbId]; promise = api.updateReport( - { data: { ...updated.data, comments: "" } }, + { data: { ...old.data, comments: "" } }, comment.dbId, ); @@ -154,7 +154,12 @@ export default function Scouters(props: { } function removePitComment(comment: Comment) { - return api.updatePitreport(comment.dbId, { comments: "" }); + const { _id, ...old } = data.pitReports.find( + (r) => r._id === comment.dbId, + )!; + return api.updatePitreport(comment.dbId, { + data: { ...old.data, comments: "" }, + }); } function removeSubjectiveComment(comment: Comment) { @@ -503,8 +508,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { - team: SerializeDatabaseObject(team), - competition: SerializeDatabaseObject(comp), + team: serializeDatabaseObject(team), + competition: serializeDatabaseObject(comp), }, }; }; diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 047de81c..1a588ba2 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -14,8 +14,8 @@ import { games } from "@/lib/games"; import CollectionId from "@/lib/client/CollectionId"; import { getDatabase } from "@/lib/MongoDB"; import UrlResolver, { - SerializeDatabaseObjects, - SerializeDatabaseObject, + serializeDatabaseObjects, + serializeDatabaseObject, } from "@/lib/UrlResolver"; import { ObjectId } from "bson"; import { GetServerSideProps } from "next"; @@ -193,9 +193,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => { submitted: true, }); - const pitReports = await db.findObjects(CollectionId.PitReports, { - _id: { $in: resolved.competition?.pitReports }, - }); + const pitReports = !resolved.competition + ? [] + : await db.findObjects(CollectionId.PitReports, { + _id: { + $in: resolved.competition.pitReports.map((id) => new ObjectId(id)), + }, + }); const subjectiveReports = await db.findObjects( CollectionId.SubjectiveReports, @@ -208,13 +212,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => { CollectionId.Picklists, new ObjectId(resolved.competition?.picklist), ); + console.log("Picklists", picklists); return { props: { - reports: SerializeDatabaseObjects(reports), - pitReports: SerializeDatabaseObjects(pitReports), - subjectiveReports: SerializeDatabaseObjects(subjectiveReports), - picklists: SerializeDatabaseObject(picklists), + reports: serializeDatabaseObjects(reports), + pitReports: serializeDatabaseObjects(pitReports), + subjectiveReports: serializeDatabaseObjects(subjectiveReports), + picklists: serializeDatabaseObject(picklists), competition: resolved.competition, }, }; diff --git a/pages/[teamSlug]/[seasonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/index.tsx index 0dc9b2ea..42a9a29b 100644 --- a/pages/[teamSlug]/[seasonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/index.tsx @@ -1,5 +1,5 @@ import ClientApi from "@/lib/api/ClientApi"; -import UrlResolver, { SerializeDatabaseObjects } from "@/lib/UrlResolver"; +import UrlResolver, { serializeDatabaseObjects } from "@/lib/UrlResolver"; import { GetServerSideProps } from "next"; import { Competition, Season, Team } from "@/lib/Types"; import Container from "@/components/Container"; @@ -106,7 +106,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { props: { team: team, season: season, - competitions: SerializeDatabaseObjects(comp), + competitions: serializeDatabaseObjects(comp), }, }; }; diff --git a/pages/[teamSlug]/createSeason.tsx b/pages/[teamSlug]/createSeason.tsx index 37caf01a..7b9e303d 100644 --- a/pages/[teamSlug]/createSeason.tsx +++ b/pages/[teamSlug]/createSeason.tsx @@ -1,5 +1,5 @@ import { League, Season, Team } from "../../lib/Types"; -import UrlResolver, { SerializeDatabaseObjects } from "@/lib/UrlResolver"; +import UrlResolver, { serializeDatabaseObjects } from "@/lib/UrlResolver"; import { GetServerSideProps } from "next"; import Container from "@/components/Container"; import { getDatabase } from "@/lib/MongoDB"; @@ -112,7 +112,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { team: resolved.team, - existingSeasons: SerializeDatabaseObjects(existingSeasons), + existingSeasons: serializeDatabaseObjects(existingSeasons), }, }; }; diff --git a/pages/[teamSlug]/index.tsx b/pages/[teamSlug]/index.tsx index 91b79295..cb1babc9 100644 --- a/pages/[teamSlug]/index.tsx +++ b/pages/[teamSlug]/index.tsx @@ -1,6 +1,6 @@ import UrlResolver, { - SerializeDatabaseObject, - SerializeDatabaseObjects, + serializeDatabaseObject, + serializeDatabaseObjects, } from "@/lib/UrlResolver"; import { GetServerSideProps } from "next"; import { useEffect, useState } from "react"; @@ -246,10 +246,12 @@ function Roster(props: TeamPageProps) { return ( -

View and Manage your Team

+

+ View and Manage your {team?.alliance ? "Alliance" : "Team"} +

{users?.length} total members

@@ -411,7 +413,11 @@ function Settings(props: TeamPageProps) { const updateTeam = async () => { setError(""); if (!validName(teamName, true)) { - setError("Invalid Team Name"); + { + props.team?.alliance + ? setError("Invalid Alliance Name") + : setError("Invalid Team Name"); + } return; } @@ -421,10 +427,12 @@ function Settings(props: TeamPageProps) { return ( -

Edit your teams configuration

+

+ Edit your {props.team?.alliance ? "Alliance's" : "Team's"} configuration +

{error}

-

Set your Team's Name:

+

Set your {props.team?.alliance ? "Alliance" : "Team"}'s Name:

@@ -440,7 +448,8 @@ function Settings(props: TeamPageProps) { className="btn btn-primary md:w-1/4" onClick={updateTeam} > - Update Team + + {props.team?.alliance ? "Update Alliance" : "Update Team"}
); @@ -460,7 +469,13 @@ export default function TeamIndex(props: TeamPageProps) { - Team {team?.number} + {props.team?.alliance ? "Alliance" : "Team"}{" "} + {team?.number}

@@ -500,16 +516,20 @@ export default function TeamIndex(props: TeamPageProps) {
{isFrc ? "FRC" : "FTC"}
- -
- - TBA -
- + {props.team?.alliance ? ( + <> + ) : ( + +
+ + TBA +
+ + )}
@@ -626,9 +646,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => { props: { team: resolved.team, users: makeObjSerializeable(users), - currentCompetition: SerializeDatabaseObject(comp), - currentSeason: SerializeDatabaseObject(currentSeason), - pastSeasons: SerializeDatabaseObjects(seasons), + currentCompetition: serializeDatabaseObject(comp), + currentSeason: serializeDatabaseObject(currentSeason), + pastSeasons: serializeDatabaseObjects(seasons), }, }; }; diff --git a/pages/createTeam.tsx b/pages/createTeam.tsx index e53304df..0859d0ac 100644 --- a/pages/createTeam.tsx +++ b/pages/createTeam.tsx @@ -28,6 +28,14 @@ export default function CreateTeam() { return; } + if (team.alliance) { + //If this ever becomes an issue it might work better to convert the number to a string and check the length - Davis. + if (!team.number || team.number < 10000) { + setError("Alliance numbers must be greater than six digits"); + return; + } + } + if (!team?.number) { setError("Must enter a team number"); return; @@ -52,6 +60,7 @@ export default function CreateTeam() { team.tbaId ?? NotLinkedToTba, team.number, team.league, + team.alliance == true, ); if (!newTeam) { @@ -89,7 +98,17 @@ export default function CreateTeam() { mode="col" className="md:h-full items-center md:justify-center max-sm:py-10" > - + +
{Object.values(League).map((league) => ( +
+ {cachedVals.map(({ key, val }) => ( +
+

{key}

+

{val ? JSON.stringify(val) : val}

+
+ ))} +
+ + ); +} diff --git a/pages/dev/index.tsx b/pages/dev/index.tsx new file mode 100644 index 00000000..092c7530 --- /dev/null +++ b/pages/dev/index.tsx @@ -0,0 +1,95 @@ +import Container from "@/components/Container"; +import { AuthenticationOptions } from "@/lib/Auth"; +import { User } from "@/lib/Types"; +import { serializeDatabaseObject } from "@/lib/UrlResolver"; +import { isDeveloper } from "@/lib/Utils"; +import { GetServerSideProps } from "next"; +import { getServerSession } from "next-auth"; +import Link from "next/link"; +import { makeObjSerializeable } from "../../lib/client/ClientUtils"; + +export default function DevIndex({ user }: { user: User }) { + return ( + +

Developer Dashboard

+

Hello, {user.name}.

+

Dev Pages

+
    +
  • + + Cache Stats and Lookup + +
  • +
  • + + Leveling Visualizer + +
  • +
  • + + LocalStorageDb Tester + +
  • +
  • + + QR Pamphlet Viewer + +
  • +
  • + + Speedtest + +
  • +
  • + + User Analytics + +
  • +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getServerSession( + context.req, + context.res, + AuthenticationOptions, + ); + + if (!session?.user || !isDeveloper(session.user.email!)) { + return { + redirect: { + destination: "/api/auth/login", + permanent: false, + }, + }; + } + + return { + props: { + user: makeObjSerializeable(serializeDatabaseObject(session.user)), + }, + }; +}; diff --git a/pages/dev/speedtest.tsx b/pages/dev/speedtest.tsx index 37a0a8ef..9e953972 100644 --- a/pages/dev/speedtest.tsx +++ b/pages/dev/speedtest.tsx @@ -1,7 +1,10 @@ import Container from "@/components/Container"; import ClientApi from "@/lib/api/ClientApi"; import { Round } from "@/lib/client/StatsMath"; +import { useCurrentSession } from "@/lib/client/useCurrentSession"; +import useInterval from "@/lib/client/useInterval"; import { useCallback, useEffect, useState } from "react"; +import toast from "react-hot-toast"; const api = new ClientApi(); @@ -23,17 +26,21 @@ const SPEED_TEST_LENGTH = 12000; const SPEED_TEST_PARALLEL_REQUESTS = 120; export default function SpeedTest() { - const [times, setTimes] = useState(); - const [resultsCompleted, setResultsCompleted] = useState( - undefined, - ); + const { session, status } = useCurrentSession(); + const [trialCountByThread, setTrialCountByThread] = useState(); + const [results, setResults] = useState([]); + const [avgResult, setAvgResult] = useState(); const runSpeedTest = useCallback(async () => { - setResultsCompleted(undefined); + if (!session?.user) { + toast.error("You must be logged in to run a speed test"); + return; + } - const newResults: SpeedTestResponse[] = []; + console.log("Running Speed Test"); + const newResults: SpeedTestResponse[] = []; const trialCountByThread: number[] = []; function onTrialComplete( @@ -43,6 +50,8 @@ export default function SpeedTest() { >, thread: number, ) { + delete newTimes.responseTimestamp; + newResults.push({ ...newTimes, dbTime: @@ -55,11 +64,11 @@ export default function SpeedTest() { totalTime: Object.values(newTimes).reduce((acc, time) => acc + time, 0), } as SpeedTestResponse); - setResultsCompleted(newResults.length); - trialCountByThread[thread]++; setTrialCountByThread(trialCountByThread); + setResults(newResults); + if (newResults.length < SPEED_TEST_LENGTH) api.speedTest().then((res) => onTrialComplete(res, thread)); } @@ -69,11 +78,12 @@ export default function SpeedTest() { trialCountByThread[thread] = 0; api.speedTest().then((res) => onTrialComplete(res, thread)); } + }, [session]); - while (newResults.length < SPEED_TEST_LENGTH) - await new Promise((resolve) => setTimeout(resolve, 25)); + const updateResults = useCallback(() => { + if (!results) return; - const avgTimes: typeof times = newResults.reduce( + const avgTimes: typeof avgResult = results.reduce( (acc, times) => { Object.entries(times).forEach(([key, value]) => { acc[key] += value; @@ -95,57 +105,64 @@ export default function SpeedTest() { ); Object.keys(avgTimes).forEach((key) => { - avgTimes[key] /= newResults.length; + avgTimes[key] /= results.length; }); - setTimes(avgTimes); - setResultsCompleted(undefined); - }, []); + setAvgResult(avgTimes); + }, [results]); - useEffect(() => { - runSpeedTest(); - }, [runSpeedTest]); + useInterval(updateResults, 1000); return ( - {resultsCompleted !== undefined && ( -
- {trialCountByThread!.map((count, index) => ( - - ))} -
- )} - {times ? ( +
- {Object.entries(times).map(([key, value]) => ( -
- {key}: {Round(value)}ms ({Round((value / times.totalTime) * 100)} - %) + {avgResult ? ( +
+
+ Results: {results.length}/{SPEED_TEST_LENGTH} Trials Complete +
+ {Object.entries(avgResult).map(([key, value]) => ( +
+ {key}: {Round(value)}ms ( + {Round((value / avgResult.totalTime) * 100)} + %) +
+ ))}
- ))} -
- ) : ( -
- Loading... {resultsCompleted}/{SPEED_TEST_LENGTH} Trials Complete + ) : ( +
Loading...
+ )} + +
Auth Status: {status}
- )} - + {results && trialCountByThread && ( +
+ {trialCountByThread!.map((count, index) => ( + + ))} +
+ )} +
); } diff --git a/pages/onboarding.tsx b/pages/onboarding.tsx index 0e0ec00f..888d441f 100644 --- a/pages/onboarding.tsx +++ b/pages/onboarding.tsx @@ -175,6 +175,7 @@ export default function Onboarding() { team?.tbaId ?? NotLinkedToTba, teamNumber, league, + false, ); if (!newTeam) { setErrorMsg("Failed to create team"); diff --git a/pages/profile.tsx b/pages/profile.tsx index 9cae10d8..d4e0f46c 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -14,7 +14,7 @@ import { FaPlus } from "react-icons/fa"; import { getDatabase } from "@/lib/MongoDB"; import CollectionId from "@/lib/client/CollectionId"; import { GetServerSideProps } from "next"; -import { SerializeDatabaseObject } from "@/lib/UrlResolver"; +import { serializeDatabaseObject } from "@/lib/UrlResolver"; import TeamCard from "@/components/TeamCard"; import { UpdateModal } from "@/components/UpdateModal"; import { Analytics } from "@/lib/client/Analytics"; @@ -240,7 +240,7 @@ export default function Profile(props: { teamList: Team[] }) { export const getServerSideProps: GetServerSideProps = async (context) => { const db = await getDatabase(); const teams = await db.findObjects(CollectionId.Teams, {}); - const serializedTeams = teams.map((team) => SerializeDatabaseObject(team)); + const serializedTeams = teams.map((team) => serializeDatabaseObject(team)); return { props: { teamList: serializedTeams }, diff --git a/tests/lib/api/ClientApi.test.ts b/tests/lib/api/ClientApi.test.ts index aa8e5eec..554eceb3 100644 --- a/tests/lib/api/ClientApi.test.ts +++ b/tests/lib/api/ClientApi.test.ts @@ -34,6 +34,7 @@ describe(`${ClientApi.name}.${api.requestToJoinTeam.name}`, () => { "tbaId", 1234, League.FRC, + false, [user._id!.toString()], [user._id!.toString()], ), @@ -131,7 +132,7 @@ describe(`${ClientApi.name}.${api.handleTeamJoinRequest.name}`, () => { const teamId = new ObjectId(); await db.addObject(CollectionId.Teams, { - ...new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ + ...new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, false, [ user._id!.toString(), ]), _id: teamId, @@ -163,7 +164,7 @@ describe(`${ClientApi.name}.${api.handleTeamJoinRequest.name}`, () => { const teamId = new ObjectId(); await db.addObject(CollectionId.Teams, { - ...new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ + ...new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, false, [ user._id!.toString(), ]), _id: teamId, @@ -578,9 +579,15 @@ describe(`${ClientApi.name}.${api.updateTeam.name}`, () => { test(`${ClientApi.name}.${api.updateTeam.name}: Updates team`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const newValues = { name: "Updated Team" }; @@ -623,9 +630,15 @@ describe(`${ClientApi.name}.${api.updateSeason.name}`, () => { test(`${ClientApi.name}.${api.updateSeason.name}: Updates season`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const season: Season = new Season( @@ -683,9 +696,15 @@ describe(`${ClientApi.name}.${api.updateReport.name}`, () => { test(`${ClientApi.name}.${api.updateReport.name}: Updates report`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const match: Match = new Match( @@ -754,9 +773,15 @@ describe(`${ClientApi.name}.${api.updatePitreport.name}`, () => { test(`${ClientApi.name}.${api.updatePitreport.name}: Updates pitreport`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const competition = { @@ -830,9 +855,15 @@ describe(`${ClientApi.name}.${api.setSlackWebhook.name}`, () => { test(`${ClientApi.name}.${api.setSlackWebhook.name}: Sets webhook URL when team does not already have one`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const webhookUrl = "test-webhook-url"; @@ -866,9 +897,15 @@ describe(`${ClientApi.name}.${api.setSlackWebhook.name}`, () => { test(`${ClientApi.name}.${api.setSlackWebhook.name}: Updates webhook URL when team already has one`, async () => { const { db, res, user } = await getTestApiUtils(); - const team = new Team("Test Team", "test-team", "tbaId", 1234, League.FRC, [ - user._id!.toString(), - ]); + const team = new Team( + "Test Team", + "test-team", + "tbaId", + 1234, + League.FRC, + false, + [user._id!.toString()], + ); await db.addObject(CollectionId.Teams, team); const webhookUrl = "test-webhook-url";