diff --git a/app/[eventId]/[teamNumber]/client-page.tsx b/app/[eventId]/[teamNumber]/client-page.tsx
index 992e651..35713a6 100644
--- a/app/[eventId]/[teamNumber]/client-page.tsx
+++ b/app/[eventId]/[teamNumber]/client-page.tsx
@@ -15,7 +15,7 @@ import {zodResolver} from "@hookform/resolvers/zod";
import {useForm} from "react-hook-form";
import {Form, FormField, FormItem, FormMessage} from "@/components/ui/form";
import UpdateTeamData from "@/lib/database/update-team-data";
-import {Loader2, MoreVertical, ScanEye} from "lucide-react";
+import {ArrowLeftRight, Loader2, MoreVertical, ScanEye} from "lucide-react";
import {cn} from "@/lib/utils";
import KeyBindListener from "@/components/key-bind-listener";
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
@@ -154,6 +154,12 @@ export default function ClientPage({
View Overview
+
+
+
+ Compare
+
+
@@ -232,7 +238,11 @@ export default function ClientPage({
Statistics
{statistics}
-
+ {
+ matches.length != 0 && (
+
+ )
+ }
Events
{events}
diff --git a/app/[eventId]/overview/[teamNumber]/client-page.tsx b/app/[eventId]/overview/[teamNumber]/client-page.tsx
index 8a571f5..437a3a7 100644
--- a/app/[eventId]/overview/[teamNumber]/client-page.tsx
+++ b/app/[eventId]/overview/[teamNumber]/client-page.tsx
@@ -11,7 +11,7 @@ import StatusBadge from "@/components/status-badge";
import {TeamStatus} from "@/lib/database/set-team-statues";
import RichTextarea from "@/components/rich-textarea";
import React, {ReactNode} from "react";
-import {ArrowDown, ArrowUp, Edit, Minus, MoreVertical} from "lucide-react";
+import {ArrowDown, ArrowLeftRight, ArrowUp, Edit, Minus, MoreVertical} from "lucide-react";
import {percentile, withOrdinalSuffix} from "@/lib/utils";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {Button} from "@/components/ui/button";
@@ -47,33 +47,6 @@ export default function ClientPage({
));
- function epaValue(epa: number, deviation: number) {
- let arrow;
- let placement;
- if (deviation > 0.2) {
- arrow = ();
- placement = "Above";
- } else if (deviation > -0.2) {
- arrow = ();
- placement = "Around";
- } else {
- arrow = ();
- placement = "Below";
- }
-
- return (
-
- {epa.toFixed(1)}
- {arrow}
-
- }
- content={`${placement} average; ${withOrdinalSuffix((percentile(deviation) * 100))} percentile`}
- />
- );
- }
-
return (
<>
@@ -95,6 +68,12 @@ export default function ClientPage({
Make Changes
+
+
+
+ Compare
+
+
@@ -254,9 +233,9 @@ export default function ClientPage({
+
)}
-
Events
{
@@ -283,4 +262,31 @@ export default function ClientPage({
{previousSeasons}
>
);
+}
+
+export function epaValue(epa: number, deviation: number) {
+ let arrow;
+ let placement;
+ if (deviation > 0.2) {
+ arrow = ();
+ placement = "Above";
+ } else if (deviation > -0.2) {
+ arrow = ();
+ placement = "Around";
+ } else {
+ arrow = ();
+ placement = "Below";
+ }
+
+ return (
+
+ {epa.toFixed(1)}
+ {arrow}
+
+ }
+ content={`${placement} average; ${withOrdinalSuffix((percentile(deviation) * 100))} percentile`}
+ />
+ );
}
\ No newline at end of file
diff --git a/app/[eventId]/overview/compare/client-page.tsx b/app/[eventId]/overview/compare/client-page.tsx
new file mode 100644
index 0000000..5950964
--- /dev/null
+++ b/app/[eventId]/overview/compare/client-page.tsx
@@ -0,0 +1,308 @@
+'use client'
+
+import {Team, TeamEntry, TeamEvent} from "@prisma/client";
+import React from "react";
+import {Separator} from "@/components/ui/separator";
+import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
+import {Button} from "@/components/ui/button";
+import {Edit, MoreVertical, ScanEye} from "lucide-react";
+import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
+import ThreatGrade from "@/components/threat-grade";
+import {ThreatGradeType} from "@/lib/database/set-threat-grade";
+import StatusBadge from "@/components/status-badge";
+import {TeamStatus} from "@/lib/database/set-team-statues";
+import RichTextarea from "@/components/rich-textarea";
+import QuickTooltip from "@/components/quick-tooltip";
+import {epaValue} from "@/app/[eventId]/overview/[teamNumber]/client-page";
+import EPAOverTime from "@/app/[eventId]/overview/compare/epa-over-time";
+import PreviousSeasons, {columns, Year} from "@/components/previous-seasons";
+
+export default function ClientPage({eventId, a, b}: {
+ eventId: number,
+ a: {
+ team: Team,
+ entry: TeamEntry
+ matches: any[],
+ events: TeamEvent[],
+ previousYears: Year[]
+ },
+ b: {
+ team: Team,
+ entry: TeamEntry
+ matches: any[],
+ events: TeamEvent[],
+ previousYears: Year[]
+ }
+}) {
+ function teamHeader(teamData: typeof a | typeof b) {
+ return (
+
+ );
+ }
+
+ function teamData(teamData: typeof a | typeof b) {
+ return (
+
+
+
+ Team Details
+
+
+
+
Region
+
{teamData.team.state}
+
+
+
School
+
+ {teamData.team.school?.replace("&", "and")}
+
+
+
+
Rookie year
+
{teamData.team.rookieYear}
+
+
+
+
+
+ Threat Grade
+
+
+
+
+
+
+
+ Status
+
+
+
+
+
+ {
+ teamData.entry.notes != "[{\"children\":[{\"text\":\"\"}],\"type\":\"p\"}]" && (
+
+
+ Notes
+
+
+
+
+
+ )
+ }
+
+ );
+ }
+
+ function statistics(teamData: typeof a | typeof b) {
+ return (
+
+
Statistics
+
+ {teamData.matches.length == 0 ? (
+
+ This team has not competed in any matches this season, yet.
+
+ ) : (
+
+
+
+ Record
+
+
+
+
Wins
+
{teamData.entry.wins}
+
+
+
Losses
+
{teamData.entry.losses}
+
+
+
Total
+
{(teamData.entry.wins ?? 0) + (teamData.entry.ties ?? 0) + (teamData.entry.losses ?? 0)}
+
+
+
Win rate
+
+
+ {((teamData.entry.wins ?? 0) / ((teamData.entry.wins ?? 0) + (teamData.entry.losses ?? 0)) * 100).toFixed()}
+ of 100
+ >
+ }
+ content={`Theoretically would win ${
+ ((teamData.entry.wins ?? 0) / ((teamData.entry.wins ?? 0) + (teamData.entry.losses ?? 0)) * 100).toFixed()
+ } out of 100 matches if put up against the same opponents`}
+ />
+
+
+
+
+
+
+
+
+
+
+
World
+
{teamData.entry.worldRank} of {teamData.entry.worldTotal}
+
+
+
+
Country
+
{teamData.entry.countyRank} of {teamData.entry.countyTotal}
+
+
+
District
+
{teamData.entry.districtRank} of {teamData.entry.districtTotal}
+
+
+
Event
+
{teamData.entry.eventRank} of {teamData.entry.eventTotal}
+
+
+
+
+
+
+ Expected Points Added
+ }/>
+
+
+
+
Auto
+ {epaValue(teamData.entry.autoEPA ?? 0, teamData.entry.autoDeviation ?? 0)}
+
+
+
Teleop
+ {epaValue(teamData.entry.teleopEPA ?? 0, teamData.entry.teleopDeviation ?? 0)}
+
+
+
Endgame
+ {epaValue(teamData.entry.endgameEPA ?? 0, teamData.entry.endgameDeviation ?? 0)}
+
+
+
Total
+ {epaValue(teamData.entry.totalEPA ?? 0, teamData.entry.totalDeviation ?? 0)}
+
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+ <>
+
+ {teamHeader(a)}
+ {teamData(a)}
+ {statistics(a)}
+ {
+ a.previousYears.length == 0 ? (
+
+ This team has not competed in any previous seasons, yet.
+
+ ) : (
+
+ )
+ }
+ {teamHeader(b)}
+ {teamData(b)}
+ {statistics(b)}
+ {
+ b.previousYears.length == 0 ? (
+
+ This team has not competed in any previous seasons, yet.
+
+ ) : (
+
+ )
+ }
+
+
+
+ {teamHeader(a)}
+ {teamHeader(b)}
+
+
+ {teamData(a)}
+ {teamData(b)}
+
+
+ {statistics(a)}
+ {statistics(b)}
+
+
+
+
+
+ {
+ a.previousYears.length == 0 ? (
+
+ This team has not competed in any previous seasons, yet.
+
+ ) : (
+
+ )
+ }
+ {
+ b.previousYears.length == 0 ? (
+
+ This team has not competed in any previous seasons, yet.
+
+ ) : (
+
+ )
+ }
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/[eventId]/overview/compare/epa-over-time.tsx b/app/[eventId]/overview/compare/epa-over-time.tsx
new file mode 100644
index 0000000..6a53d41
--- /dev/null
+++ b/app/[eventId]/overview/compare/epa-over-time.tsx
@@ -0,0 +1,141 @@
+import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
+import React, {useState} from "react";
+import {CartesianGrid, Line, LineChart, XAxis, YAxis} from "recharts";
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent
+} from "@/components/ui/chart";
+import {Team, TeamEvent} from "@prisma/client";
+import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
+
+export default function EPAOverTime({a, b}: {
+ a: {
+ team: Team,
+ matches: any[],
+ events: TeamEvent[]
+ },
+ b: {
+ team: Team,
+ matches: any[],
+ events: TeamEvent[]
+ }
+}) {
+ const [type, setType] = useState("totalEPA");
+
+ const chartConfig = {
+ a: {
+ label: a.team.number
+ },
+ b: {
+ label: b.team.number
+ }
+ } satisfies ChartConfig
+
+ const matches = [
+ ...a.matches.map(value => ({...value, type: "a"})),
+ ...b.matches.map(value => ({...value, type: "b"}))
+ ].filter(value => value.totalEPA >= 0).sort((a, b) => a.startTime - b.startTime);
+
+ const chart: any[] = [];
+ let lastA: number | undefined = undefined;
+ let lastB: number | undefined = undefined;
+
+ matches.forEach(value => {
+ if (value.type === "a") {
+ lastA = value[type];
+ } else if (value.type === "b") {
+ lastB = value[type];
+ }
+
+ chart.push({
+ time: value.startTime,
+ a: lastA,
+ b: lastB
+ });
+ });
+
+ return (
+
+
+
+ EPA Over Time
+
+
+
+
+
+
+
+
+ }
+ labelFormatter={(value) => {
+ return new Date(value * 1000).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ hour12: true
+ }).replace(" at ", ", ");
+ }}
+ cursor={false}
+ />
+ {
+ return `${
+ new Date(value * 1000).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric"
+ })
+ } `;
+ }}
+ />
+
+
+
+ }/>
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/[eventId]/overview/compare/page.tsx b/app/[eventId]/overview/compare/page.tsx
new file mode 100644
index 0000000..8fe8221
--- /dev/null
+++ b/app/[eventId]/overview/compare/page.tsx
@@ -0,0 +1,216 @@
+'use server'
+
+import prisma from "@/lib/prisma";
+import NotFound from "@/app/not-found";
+import React from "react";
+import ClientPage from "@/app/[eventId]/overview/compare/client-page";
+import {TeamDropdown} from "@/app/[eventId]/overview/compare/team-dropdown";
+import Back from "@/components/back";
+
+export default async function Compare({params, searchParams}: {
+ params: { eventId: string }
+ searchParams: { a?: string, b?: string }
+}) {
+ const event = await prisma.event.findUnique(
+ {
+ where: {
+ id: +params.eventId
+ }
+ }
+ );
+ if (!event) return ;
+
+ const eventTeams = await Promise.all(
+ (await prisma.teamEntry.findMany(
+ {
+ where: {
+ eventId: event.id
+ }
+ }
+ )).map(async value => (await prisma.team.findMany(
+ {
+ where: {
+ number: value.teamNumber ?? undefined
+ }
+ }
+ ))[0])
+ );
+
+ function teamSelector(a: string | undefined, b: string | undefined) {
+ if (!event) return;
+
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ if (!searchParams.a || !searchParams.b) {
+ return (
+ <>
+ {teamSelector(searchParams.a, searchParams.b)}
+
+ Select two teams to continue.
+
+ >
+ );
+ }
+
+ const a = await prisma.team.findUnique(
+ {
+ where: {
+ number: +searchParams.a
+ }
+ }
+ );
+ const b = await prisma.team.findUnique(
+ {
+ where: {
+ number: +searchParams.b
+ }
+ }
+ );
+ if (!a || !b) return ;
+
+ const aEntry = await prisma.teamEntry.findMany(
+ {
+ where: {
+ eventId: +params.eventId,
+ teamNumber: a.number
+ }
+ }
+ );
+ const bEntry = await prisma.teamEntry.findMany(
+ {
+ where: {
+ eventId: +params.eventId,
+ teamNumber: b.number
+ }
+ }
+ );
+ if (!aEntry || !bEntry) return ;
+
+ const aMatches = await Promise.all(
+ (await prisma.matchEntry.findMany({
+ where: {
+ eventId: aEntry[0].eventId,
+ teamEntryId: aEntry[0].id,
+ },
+ })).map(async (value) => {
+ const match = await prisma.match.findUnique({
+ where: {
+ key: value.matchKey ?? undefined,
+ },
+ });
+ return {
+ ...value,
+ ...(match ?? {}), // Safely spread the match object if it exists, otherwise spread an empty object
+ };
+ })
+ );
+ const bMatches = await Promise.all(
+ (await prisma.matchEntry.findMany({
+ where: {
+ eventId: bEntry[0].eventId,
+ teamEntryId: bEntry[0].id,
+ },
+ })).map(async (value) => {
+ const match = await prisma.match.findUnique({
+ where: {
+ key: value.matchKey ?? undefined,
+ },
+ });
+ return {
+ ...value,
+ ...(match ?? {}), // Safely spread the match object if it exists, otherwise spread an empty object
+ };
+ })
+ );
+
+ const aEvents = await prisma.teamEvent.findMany(
+ {
+ where: {
+ teamNumber: a.number
+ }
+ }
+ );
+ const bEvents = await prisma.teamEvent.findMany(
+ {
+ where: {
+ teamNumber: b.number
+ }
+ }
+ );
+
+ const aPastSeasons = (await prisma.teamPastSeason.findMany(
+ {
+ where: {
+ teamNumber: a.number,
+ year: {
+ lt: event.year
+ }
+ }
+ }
+ )).map(value => ({
+ year: value.year,
+ winRate: value.winrate,
+ rank: {
+ rank: value.rank,
+ of: value.totalTeams
+ },
+ epa: {
+ epa: value.epa,
+ percentile: value.percentile
+ }
+ })).sort((a, b) => b.year - a.year);
+ const bPastSeasons = (await prisma.teamPastSeason.findMany(
+ {
+ where: {
+ teamNumber: b.number,
+ year: {
+ lt: event.year
+ }
+ }
+ }
+ )).map(value => ({
+ year: value.year,
+ winRate: value.winrate,
+ rank: {
+ rank: value.rank,
+ of: value.totalTeams
+ },
+ epa: {
+ epa: value.epa,
+ percentile: value.percentile
+ }
+ })).sort((a, b) => b.year - a.year);
+
+ return (
+ <>
+ {teamSelector(searchParams.a, searchParams.b)}
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/[eventId]/overview/compare/team-dropdown.tsx b/app/[eventId]/overview/compare/team-dropdown.tsx
new file mode 100644
index 0000000..abb8a22
--- /dev/null
+++ b/app/[eventId]/overview/compare/team-dropdown.tsx
@@ -0,0 +1,102 @@
+'use client'
+
+import * as React from "react"
+
+import {Button} from "@/components/ui/button"
+import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,} from "@/components/ui/command"
+import {Drawer, DrawerContent, DrawerTrigger,} from "@/components/ui/drawer"
+import {Popover, PopoverContent, PopoverTrigger,} from "@/components/ui/popover"
+import {Team} from "@prisma/client";
+import {ChevronsUpDown} from "lucide-react";
+
+export function TeamDropdown({eventId, teams, a, b, side}: {
+ eventId: number,
+ teams: Team[],
+ a: string | undefined,
+ b: string | undefined,
+ side: "a" | "b"
+}) {
+ const [open, setOpen] = React.useState(false);
+ const [selectedTeam, setSelectedTeam] = React.useState(
+ teams.find(value => `${value.number}` == (side == "a" ? a : b)) ?? null
+ );
+
+ if (typeof window !== "undefined" && window.innerWidth > 768) {
+ return (
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function StatusList({
+ setOpen,
+ teams,
+ eventId,
+ a,
+ b,
+ side
+ }: {
+ setOpen: (open: boolean) => void
+ teams: Team[],
+ eventId: number, a: string | undefined, b: string | undefined, side: "a" | "b"
+}) {
+ return (
+
+
+
+ No results found.
+
+ {teams.map((team) => (
+ !((side == "a" && b == `${team.number}`) || (side == "b" && a == `${team.number}`)) && (
+ {
+ setOpen(false)
+ if (side == "a") {
+ a = value;
+ } else {
+ b = value;
+ }
+ window.open(`/${eventId}/overview/compare?${side == "a" ? `a=${team.number}` : (a ? `a=${a}` : "")}&${side == "b" ? `b=${team.number}` : (b ? `b=${b}` : "")}`, "_self");
+ }}
+ >
+ {label(team)}
+
+ )
+ ))}
+
+
+
+ );
+}
+
+function label(team: Team) {
+ return `Team ${team.number}, ${team.name}`;
+}
\ No newline at end of file
diff --git a/app/[eventId]/overview/page.tsx b/app/[eventId]/overview/page.tsx
index 8dda23f..05b9626 100644
--- a/app/[eventId]/overview/page.tsx
+++ b/app/[eventId]/overview/page.tsx
@@ -7,6 +7,7 @@ import React from "react";
import OverviewCharts from "@/app/[eventId]/overview/overview-charts";
import ScoutingCharts from "@/app/[eventId]/overview/scouting-charts";
import Teams from "@/app/[eventId]/overview/teams";
+import {ArrowLeftRight} from "lucide-react";
export default async function Overview({params}: { params: { eventId: string } }) {
if (!+params.eventId) return NotFound();
@@ -59,7 +60,12 @@ export default async function Overview({params}: { params: { eventId: string } }
Scouting Overview
- Team Overviews
+
>
diff --git a/components/epa-over-time.tsx b/components/epa-over-time.tsx
index af4ad9c..3fdc941 100644
--- a/components/epa-over-time.tsx
+++ b/components/epa-over-time.tsx
@@ -36,11 +36,11 @@ export default function EPAOverTime({matches, events}: {
.sort((a, b) => a.startTime - b.startTime)
.map(value => ({
...value,
- xAxis: `${events.find(value1 => value1.eventKey == value.eventKey)?.name ?? ""}\nMatch ${value.matchNumber}`
+ xAxis: `${events.find(value1 => value1.eventKey == value.eventKey)?.name ?? ""} \nMatch ${value.matchNumber}`
}));
return (
-
+
EPA Over Time