diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 781ed585..2c163ae1 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,7 +1,7 @@ { - "extends": "next/core-web-vitals", - "rules": { - "react/jsx-key": "off", - "react/no-unescaped-entities": "off" - } + "extends": "next/core-web-vitals", + "rules": { + "react/jsx-key": "off", + "react/no-unescaped-entities": "off" + } } diff --git a/frontend/app/components/card/card.module.scss b/frontend/app/components/card/card.module.scss index d4a59488..0b52ef66 100644 --- a/frontend/app/components/card/card.module.scss +++ b/frontend/app/components/card/card.module.scss @@ -1,22 +1,22 @@ .card { - @apply border-2; - @apply p-3; - @apply rounded; - @apply mb-3; + @apply border-2; + @apply p-3; + @apply rounded; + @apply mb-3; } .heading-container { } .title { - @apply text-lg; - @apply font-bold; + @apply text-lg; + @apply font-bold; } .subtitle { - @apply font-extralight text-center; + @apply font-extralight text-center; } .cardBodyWrap { - @apply grid grid-cols-1 md:grid-cols-3 gap-4; + @apply grid grid-cols-1 md:grid-cols-3 gap-4; } diff --git a/frontend/app/components/card/card.tsx b/frontend/app/components/card/card.tsx index 2ae69c9f..c7879720 100644 --- a/frontend/app/components/card/card.tsx +++ b/frontend/app/components/card/card.tsx @@ -2,30 +2,33 @@ import styles from "./card.module.scss"; import classNames from "classnames"; const Card = ({ - children, - title, - subtitle, - nowrap, - className, + children, + title, + subtitle, + nowrap, + className, }: { - children: React.ReactNode; - title: string; - subtitle?: string; - nowrap?: boolean; - className?: string | string[] + children: React.ReactNode; + title: string; + subtitle?: string; + nowrap?: boolean; + className?: string | string[]; }) => { + const cardStyle = classNames(styles.card, className); - const cardStyle = classNames(styles.card, className) - - return ( -
-
-

{title}

- {subtitle ?

{subtitle}

: null} -
-
{children}
-
- ); + return ( +
+
+

{title}

+ {subtitle ? ( +

{subtitle}

+ ) : null} +
+
+ {children} +
+
+ ); }; export default Card; diff --git a/frontend/app/components/charts/barChart.module.scss b/frontend/app/components/charts/barChart.module.scss index 0d6f9669..f0305cc3 100644 --- a/frontend/app/components/charts/barChart.module.scss +++ b/frontend/app/components/charts/barChart.module.scss @@ -1,5 +1,5 @@ .chartContainer { - .highcharts-title { - display: none; - } + .highcharts-title { + display: none; + } } diff --git a/frontend/app/components/charts/barChart.tsx b/frontend/app/components/charts/barChart.tsx index f9bb19f2..3ebe09b4 100644 --- a/frontend/app/components/charts/barChart.tsx +++ b/frontend/app/components/charts/barChart.tsx @@ -2,71 +2,92 @@ import * as Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import styles from "./barChart.module.scss"; import { HeroColors } from "@/app/components/charts/heroColors"; import classNames from "classnames"; interface GraphData { - labels: string[]; - values: number[]; + labels: string[]; + values: number[]; } interface BarChartProps extends HighchartsReact.Props { - title: string; - graph: GraphData; - maxY: number; - className?: string | string[] + title: string; + graph: GraphData; + maxY: number; + className?: string | string[]; } - const BarChart = (props: BarChartProps) => { - const { title, graph, maxY } = props; - const options: Highcharts.Options = { - title: { - // @ts-ignore - text: null, - margin: 0, - }, - legend: { - enabled: false, - }, - xAxis: { - categories: graph.labels, - }, - series: [ - { - type: "column", - name: "Occurrences", - data: graph.values.map((item, index) => { - return { y: item, color: HeroColors[graph.labels[index]] }; // do lookup - }), - }, - ], - credits: { - enabled: false, - }, - yAxis: { - min: 0, - max: maxY, - title: { - text: null, - }, - }, - }; - const chartComponentRef = useRef(null); - const chartContainerStyle = classNames(styles.chartContainer, props.className) - return ( -
-
{title.toLowerCase()}
- -
- ); + const { title, graph, maxY } = props; + const options: Highcharts.Options = { + title: { + // @ts-ignore + text: null, + margin: 0, + }, + legend: { + enabled: false, + }, + xAxis: { + categories: graph.labels, + }, + series: [ + { + type: "column", + name: "Occurrences", + data: graph.values.map((item, index) => { + return { y: item, color: HeroColors[graph.labels[index]] }; // do lookup + }), + }, + ], + credits: { + enabled: false, + }, + yAxis: { + min: 0, + max: maxY, + title: { + text: null, + }, + }, + chart: { + events: { + load: () => { + setLoading(false); + }, + }, + }, + }; + const chartComponentRef = useRef(null); + const chartContainerStyle = classNames( + styles.chartContainer, + props.className, + ); // meow + const [loading, setLoading] = useState(true); + return ( +
+
+ {title.toLowerCase()} +
+
+ +
+ +
+

Loading...

+
+
+ ); }; export default BarChart; diff --git a/frontend/app/components/charts/heroColors.ts b/frontend/app/components/charts/heroColors.ts index 820191b7..75a35eff 100644 --- a/frontend/app/components/charts/heroColors.ts +++ b/frontend/app/components/charts/heroColors.ts @@ -1,45 +1,45 @@ interface HeroColor { - [key: string]: string; + [key: string]: string; } export const HeroColors: HeroColor = { - Ana: "#8796B6", - Ashe: "#808284", - Baptiste: "#7DB8CE", - Bastion: "#8c998c", - Blank: "#000000", - Brigitte: "#957D7E", - Cassidy: "#b77e80", - "D.Va": "#F59CC8", - Doomfist: "#947F80", - Echo: "#A4CFF9", - Genji: "#9FF67D", - Hanzo: "#BDB894", - Illari: "#B3A58A", - "Junker Queen": "#89B2D5", - Junkrat: "#F0BE7C", - Kiriko: "#D4868F", - Lucio: "#91CD7D", - Mei: "#81AFED", - Mercy: "#F4EFBF", - Moira: "#9d86e5", - Orisa: "#76967B", - Pharah: "#768BC8", - Ramattra: "#9B89CE", - Reaper: "#8D797E", - Reinhardt: "#9EA8AB", - Roadhog: "#BC977E", - Sigma: "#9CA7AA", - Sojourn: "#D88180", - "Soldier 76": "#838A9C", - Sombra: "#887EBC", - Symmetra: "#98C1D1", - Torbjorn: "#C38786", - Tracer: "#DE9D7D", - Widowmaker: "#A583AB", - Winston: "#A7AABE", - "Wrecking Ball": "#E09C7C", - Zarya: "#F291BB", - Zenyatta: "#F5EC91", - LifeWeaver: "#E0B6C5", - Mauga: "#E0B6C5", + Ana: "#8796B6", + Ashe: "#808284", + Baptiste: "#7DB8CE", + Bastion: "#8c998c", + Blank: "#000000", + Brigitte: "#957D7E", + Cassidy: "#b77e80", + "D.Va": "#F59CC8", + Doomfist: "#947F80", + Echo: "#A4CFF9", + Genji: "#9FF67D", + Hanzo: "#BDB894", + Illari: "#B3A58A", + "Junker Queen": "#89B2D5", + Junkrat: "#F0BE7C", + Kiriko: "#D4868F", + Lucio: "#91CD7D", + Mei: "#81AFED", + Mercy: "#F4EFBF", + Moira: "#9d86e5", + Orisa: "#76967B", + Pharah: "#768BC8", + Ramattra: "#9B89CE", + Reaper: "#8D797E", + Reinhardt: "#9EA8AB", + Roadhog: "#BC977E", + Sigma: "#9CA7AA", + Sojourn: "#D88180", + "Soldier 76": "#838A9C", + Sombra: "#887EBC", + Symmetra: "#98C1D1", + Torbjorn: "#C38786", + Tracer: "#DE9D7D", + Widowmaker: "#A583AB", + Winston: "#A7AABE", + "Wrecking Ball": "#E09C7C", + Zarya: "#F291BB", + Zenyatta: "#F5EC91", + LifeWeaver: "#E0B6C5", + Mauga: "#E0B6C5", }; diff --git a/frontend/app/components/charts/lineChart.tsx b/frontend/app/components/charts/lineChart.tsx index 2e742470..55ebb159 100644 --- a/frontend/app/components/charts/lineChart.tsx +++ b/frontend/app/components/charts/lineChart.tsx @@ -2,70 +2,85 @@ import * as Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import { HeroColors } from "@/app/components/charts/heroColors"; import type { TrendLine } from "@/app/utils/serverSideProps"; import classNames from "classnames"; interface LineChartProps extends HighchartsReact.Props { - title: string; - data: TrendLine[]; - seasons: string[]; - className: string; + title: string; + data: TrendLine[]; + seasons: string[]; + className: string; } const LineChart = (props: LineChartProps) => { - const { data, seasons, title } = props; - const options: Highcharts.Options = { - title: { - // @ts-ignore - text: null, - margin: 0, - }, - xAxis: { - categories: seasons, - min: 0, - max: 9, - }, - series: data.map((item) => ({ - color: HeroColors[item.name] ?? null, - type: "line", - ...item, - })), - credits: { - enabled: false, - }, - yAxis: { - title: { - text: null, - }, - }, - plotOptions: { - series: { - pointPlacement: "on", - label: { - connectorAllowed: false, + const { data, seasons, title } = props; + const options: Highcharts.Options = { + title: { + // @ts-ignore + text: null, + margin: 0, }, - }, - }, - chart: { - height: "45%", - }, - }; - const chartComponentRef = useRef(null); + xAxis: { + categories: seasons, + min: 0, + max: 9, + }, + series: data.map((item) => ({ + color: HeroColors[item.name] ?? null, + type: "line", + ...item, + })), + credits: { + enabled: false, + }, + yAxis: { + title: { + text: null, + }, + }, + plotOptions: { + series: { + pointPlacement: "on", + label: { + connectorAllowed: false, + }, + }, + }, + chart: { + height: "45%", + events: { + load: () => { + setLoading(false); + }, + }, + }, + }; + const chartComponentRef = useRef(null); + const [loading, setLoading] = useState(true); + return ( +
+
{title}
+ +
+ +
- return ( -
-
{title}
- -
- ); +
+

Loading...

+
+
+ ); }; export default LineChart; diff --git a/frontend/app/components/footer/footer.tsx b/frontend/app/components/footer/footer.tsx index 143a7ce6..8bbf2753 100644 --- a/frontend/app/components/footer/footer.tsx +++ b/frontend/app/components/footer/footer.tsx @@ -1,18 +1,18 @@ import React from "react"; const Footer = () => { - return ( -
- - Support this project on Github - -
- ); + > + + Support this project on Github + + + ); }; export default Footer; diff --git a/frontend/app/components/header/header.module.scss b/frontend/app/components/header/header.module.scss index 4ab59e3c..5e1103cb 100644 --- a/frontend/app/components/header/header.module.scss +++ b/frontend/app/components/header/header.module.scss @@ -1,21 +1,21 @@ .top_header_container { - @apply bg-black text-white mt-0 w-full p-2; + @apply bg-black text-white mt-0 w-full p-2; } .navbar { - @apply bg-gray-300 w-full overflow-x-auto flex whitespace-nowrap; - scrollbar-width: none; - -ms-overflow-style: none; + @apply bg-gray-300 w-full overflow-x-auto flex whitespace-nowrap; + scrollbar-width: none; + -ms-overflow-style: none; - ul { - @apply flex list-none space-x-4; - } + ul { + @apply flex list-none space-x-4; + } - ul > li { - @apply hover:bg-gray-400 transition duration-100 ease-in-out; - } + ul > li { + @apply hover:bg-gray-400 transition duration-100 ease-in-out; + } } .navbar::-webkit-scrollbar { - display: none; + display: none; } diff --git a/frontend/app/components/header/header.tsx b/frontend/app/components/header/header.tsx index 1c664b26..fde2d7f4 100644 --- a/frontend/app/components/header/header.tsx +++ b/frontend/app/components/header/header.tsx @@ -3,49 +3,51 @@ import styles from "./header.module.scss"; import { fetchSeasonList } from "@/app/utils/serverSideProps"; type NavLinks = { - label: string; - path: string; + label: string; + path: string; }; const Header = async () => { - const seasons = await fetchSeasonList(); - const navLinks: NavLinks[] = seasons.reverse().map((seasonNum) => { - seasonNum = seasonNum.replace("_8", ""); - return { label: `Season ${seasonNum}`, path: `/season/${seasonNum}` }; - }); - navLinks.unshift({ label: "Trends", path: "/trends" }); - return ( -
-
- -

Top 500 Aggregator

- -
+ const seasons = await fetchSeasonList(); + const navLinks: NavLinks[] = seasons.reverse().map((seasonNum) => { + seasonNum = seasonNum.replace("_8", ""); + return { label: `Season ${seasonNum}`, path: `/season/${seasonNum}` }; + }); + navLinks.unshift({ label: "Trends", path: "/trends" }); + return ( +
+
+ +

Top 500 Aggregator

+ +
-
- ); + {!navLinks.length && ( +
  • +

    + you're not supposed to see this... +

    +
  • + )} + + +
    + ); }; export default Header; diff --git a/frontend/app/components/topmatter/topmatter.tsx b/frontend/app/components/topmatter/topmatter.tsx index 258ba124..fc6ab7d4 100644 --- a/frontend/app/components/topmatter/topmatter.tsx +++ b/frontend/app/components/topmatter/topmatter.tsx @@ -1,53 +1,53 @@ const TopMatter = ({ seasonNumber }: { seasonNumber: string }) => { - let indicator = ""; - if (seasonNumber == "trends") { - indicator = "Trends"; - } else { - indicator = `Season: ${seasonNumber}`; - } + let indicator = ""; + if (seasonNumber == "trends") { + indicator = "Trends"; + } else { + indicator = `Season: ${seasonNumber}`; + } - return ( -
    -

    {indicator}

    -

    - Welcome to Overwatch 2 Top 500 Aggregator -

    -

    - The data available on this page is not 100% accurate. Data collection - involves computer vision and image classification using a neural - network, and as such, there is a slight error rate. This error rate is - most apparent in some charts where the incorrect role appears, such as a - support chart containing Echo. More information on data collection is - available on my{" "} - - GitHub - {" "} - page. -

    -

    - What is this data? -

    -

    - The data below is taken from the in-game top 500 leaderboards. The - information available there includes a single player's matches played - and their top 3 heroes played. The charts and categories below represent - data for the hero "slot" in a top 500 leaderboard entry. For example, - they count the number of people who have Kiriko as their most played - hero, or Widowmaker as their second most played hero. The data is a - high-level overview of what heroes are popular or unpopular and is by no - means an accurate representation of hero pick rates. -

    -

    - When is the data updated? -

    -

    - The dataset is updated once per season.

    -
    -
    - ); + return ( +
    +

    {indicator}

    +

    + Welcome to Overwatch 2 Top 500 Aggregator +

    +

    + The data available on this page is not 100% accurate. Data + collection involves computer vision and image classification + using a neural network, and as such, there is a slight error + rate. This error rate is most apparent in some charts where the + incorrect role appears, such as a support chart containing Echo. + More information on data collection is available on my{" "} + + GitHub + {" "} + page. +

    +

    + What is this data? +

    +

    + The data below is taken from the in-game top 500 leaderboards. + The information available there includes a single player's + matches played and their top 3 heroes played. The charts and + categories below represent data for the hero "slot" in a top 500 + leaderboard entry. For example, they count the number of people + who have Kiriko as their most played hero, or Widowmaker as + their second most played hero. The data is a high-level overview + of what heroes are popular or unpopular and is by no means an + accurate representation of hero pick rates. +

    +

    + When is the data updated? +

    +

    The dataset is updated once per season.

    +
    +
    + ); }; export default TopMatter; diff --git a/frontend/app/globals.scss b/frontend/app/globals.scss index 314912d7..c390e6f6 100644 --- a/frontend/app/globals.scss +++ b/frontend/app/globals.scss @@ -28,9 +28,9 @@ html, body { - margin: 0px; + margin: 0px; } main { - @apply mt-5 lg:ml-3 lg:mr-3 sm:ml-1 sm:mr-1; + @apply mt-5 lg:ml-3 lg:mr-3 sm:ml-1 sm:mr-1; } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index e744fa37..3faf82ab 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -6,23 +6,23 @@ import { Footer, Header } from "@/app/components"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "T500 Aggregator", - description: "T500 Aggregator", + title: "T500 Aggregator", + description: "T500 Aggregator", }; // export const dynamic = "force-dynamic"; export default function RootLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - return ( - - -
    - {children} -