From ddc9f305886a3411a1d4a493d940c5d3b1fccfe0 Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Sat, 23 Dec 2023 13:41:05 -0500 Subject: [PATCH] 88 ssr frontend framework (#89) * update gitignore * initialize nextjs app * initial project scaffold * conditionally use body wrapping (grid) * rename * remap prop to better represent data * fetch serverside props and populate charts * fetch serverside props and populate charts * remove hr * add line chart * remove season list * load season list on each page * populate trends chart * ensure cap case is correct for all titles * Change chart title for O_ALL charts * remove unused components * Disable workflows (temporarily) * update dockerignore * add eslint exclusions * change endpoints to docker internal names * rename dockerfile to dockerfile.server * create nextjs dockerfile * create testing docker-compose.yml * change web to server * change web to server * fix dependency name * fix dependency name * add navbar links * remove unused data prop * bun lockfile * modify header condition * populate latest season in index page * add frontend buildable workflow * remove season 8 * add/modify ghcr labels * add push logic * enable workflows * fix dockerfile reference * disable tests again * remove test pass condition (temporary) * run push on main branch only * remove server tests (will be re-written) * revert workflow conditions --- .dockerignore | 6 +- .github/workflows/tests_and_ci.yml | 31 ++- .gitignore | 3 +- Dockerfile.frontend | 36 ++++ Dockerfile => Dockerfile.server | 2 +- docker-compose.test.yml | 42 ++++ docker-compose.yml | 16 +- frontend/.eslintrc.json | 7 + frontend/.gitignore | 36 ++++ frontend/app/components/card/card.module.scss | 22 ++ frontend/app/components/card/card.tsx | 18 ++ .../components/charts/barChart.module.scss | 6 + frontend/app/components/charts/barChart.tsx | 70 +++++++ frontend/app/components/charts/heroColors.ts | 45 ++++ .../components/charts/lineChart.module.scss | 0 frontend/app/components/charts/lineChart.tsx | 74 +++++++ .../app/components/header/header.module.scss | 15 ++ frontend/app/components/header/header.tsx | 32 +++ frontend/app/components/index.ts | 4 + frontend/app/components/layout.tsx | 10 + frontend/app/favicon.ico | Bin 0 -> 25931 bytes frontend/app/globals.scss | 32 +++ frontend/app/layout.tsx | 25 +++ frontend/bun.lockb | Bin 0 -> 143721 bytes frontend/next-env.d.ts | 6 + frontend/next.config.js | 4 + frontend/package.json | 30 +++ frontend/pages/_app.tsx | 62 ++++++ frontend/pages/index.tsx | 94 +++++++++ frontend/pages/season/[seasonNumber].tsx | 97 +++++++++ frontend/pages/trends.tsx | 38 ++++ frontend/postcss.config.js | 6 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/tailwind.config.ts | 20 ++ frontend/tsconfig.json | 27 +++ nginx.conf | 2 +- server.py | 194 +++--------------- tests/test_server.py | 68 ------ 39 files changed, 932 insertions(+), 250 deletions(-) create mode 100644 Dockerfile.frontend rename Dockerfile => Dockerfile.server (94%) create mode 100644 docker-compose.test.yml create mode 100644 frontend/.eslintrc.json create mode 100644 frontend/.gitignore create mode 100644 frontend/app/components/card/card.module.scss create mode 100644 frontend/app/components/card/card.tsx create mode 100644 frontend/app/components/charts/barChart.module.scss create mode 100644 frontend/app/components/charts/barChart.tsx create mode 100644 frontend/app/components/charts/heroColors.ts create mode 100644 frontend/app/components/charts/lineChart.module.scss create mode 100644 frontend/app/components/charts/lineChart.tsx create mode 100644 frontend/app/components/header/header.module.scss create mode 100644 frontend/app/components/header/header.tsx create mode 100644 frontend/app/components/index.ts create mode 100644 frontend/app/components/layout.tsx create mode 100644 frontend/app/favicon.ico create mode 100644 frontend/app/globals.scss create mode 100644 frontend/app/layout.tsx create mode 100755 frontend/bun.lockb create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/package.json create mode 100644 frontend/pages/_app.tsx create mode 100644 frontend/pages/index.tsx create mode 100644 frontend/pages/season/[seasonNumber].tsx create mode 100644 frontend/pages/trends.tsx create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json delete mode 100644 tests/test_server.py diff --git a/.dockerignore b/.dockerignore index 59d84c7c..4c3e48be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,10 @@ assets models tests -Dockerfile +Dockerfile.server +Dockerfile.frontend README.md mypy-t.sh LICENSE -tmp \ No newline at end of file +tmp +node_modules/ \ No newline at end of file diff --git a/.github/workflows/tests_and_ci.yml b/.github/workflows/tests_and_ci.yml index c1e215a5..c48b7449 100644 --- a/.github/workflows/tests_and_ci.yml +++ b/.github/workflows/tests_and_ci.yml @@ -2,6 +2,8 @@ name: Tests and CI on: pull_request: push: + branches: + - main jobs: tests: @@ -51,13 +53,22 @@ jobs: TESTING_MYSQLHOST: 'localhost' TESTING_MYSQLPORT: '3800' - docker-buildable: + docker-server-buildable: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Build the image + - name: Build the server image run: | - docker build . --tag ghcr.io/thearyadev/top500-aggregator:latest + docker build . --tag ghcr.io/thearyadev/top500-aggregator-server:latest --file ./Dockerfile.server + + docker-frontend-buildable: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build the frontend image + run: | + docker build . --tag ghcr.io/thearyadev/top500-aggregator-frontend:latest --file ./Dockerfile.frontend + build_and_publish: runs-on: ubuntu-latest @@ -65,8 +76,16 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - - name: Build and push the image + - name: Docker Login run: | docker login --username thearyadev --password ${{ secrets.GH_PAT }} ghcr.io - docker build . --tag ghcr.io/thearyadev/top500-aggregator:latest - docker push ghcr.io/thearyadev/top500-aggregator:latest \ No newline at end of file + - name: Build Frontend + run: | + docker build . --tag ghcr.io/thearyadev/top500-aggregator-frontend:latest --file ./Dockerfile.frontend + - name: Build Server + run: | + docker build . --tag ghcr.io/thearyadev/top500-aggregator-server:latest --file ./Dockerfile.server + - name: push images + run: | + docker push ghcr.io/thearyadev/top500-aggregator-frontend:latest + docker push ghcr.io/thearyadev/top500-aggregator-server:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index cdd86f75..250cfaa7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ temp .env .bashrc .idea -tmp \ No newline at end of file +tmp +node_modules/ \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 00000000..f2eeced0 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,36 @@ +# Install dependencies only when needed +FROM node:lts-alpine as deps +LABEL org.opencontainers.image.source=https://github.com/thearyadev/top500-aggregator +LABEL org.opencontainers.image.description="Docker image for t500 aggregator frontend" +LABEL org.opencontainers.image.licenses=MIT +WORKDIR /opt/app +COPY frontend/package.json /frontend/bun.lockb ./ +RUN npm install + +# Rebuild the source code only when needed +# This is where because may be the case that you would try +# to build the app based on some `X_TAG` in my case (Git commit hash) +# but the code hasn't changed. +FROM node:lts-alpine as builder + + +ENV NODE_ENV=production +WORKDIR /opt/app + +COPY frontend . +RUN ls -la +COPY --from=deps /opt/app/node_modules ./node_modules +RUN npm run build + +# Production image, copy all the files and run next +FROM node:lts-alpine as runner + + +ARG X_TAG +WORKDIR /opt/app +ENV NODE_ENV=production +COPY --from=builder /opt/app/next.config.js ./ +COPY --from=builder /opt/app/public ./public +COPY --from=builder /opt/app/.next ./.next +COPY --from=builder /opt/app/node_modules ./node_modules +CMD ["node_modules/.bin/next", "start"] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile.server similarity index 94% rename from Dockerfile rename to Dockerfile.server index ba48f3c2..938405de 100644 --- a/Dockerfile +++ b/Dockerfile.server @@ -3,7 +3,7 @@ WORKDIR /t500-aggregator COPY . . LABEL org.opencontainers.image.source=https://github.com/thearyadev/top500-aggregator -LABEL org.opencontainers.image.description="Docker image for t500 aggregator" +LABEL org.opencontainers.image.description="Docker image for t500 aggregator server" LABEL org.opencontainers.image.licenses=MIT RUN pip install poetry==1.6.1 && poetry install --with server --without dev diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..f93de740 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,42 @@ +version: "3.9" +services: + nginx: + image: nginx:latest + depends_on: + - server + volumes: + - "./nginx.conf:/etc/nginx/nginx.conf" + ports: + - "7779:80" + + server: # port 8000 is server + build: + dockerfile: Dockerfile.server + environment: + MYSQLDATABASE: 'railway' + MYSQLUSER: 'root' + MYSQLPASSWORD: 'QiyWsI7y1oGGJjz4biiu' + MYSQLHOST: 'database' + MYSQLPORT: '3306' + depends_on: + - database + + frontend: + build: + dockerfile: Dockerfile.frontend + ports: + - "3000:3000" + + + # these services are internal, so secrets do not matter. + database: + image: mysql@sha256:566007208a3f1cc8f9df6b767665b5c9b800fc4fb5f863d17aa1df362880ed04 + environment: + MYSQL_DATABASE: 'railway' + MYSQL_USER: 't5aggr' + MYSQL_PASSWORD: 'QiyWsI7y1oGGJjz4biiu' + MYSQL_ROOT_PASSWORD: 'QiyWsI7y1oGGJjz4biiu' + volumes: + - ./mysql-data:/var/lib/mysql + ports: + - "3309:3306" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0746c0bf..8f092f0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,13 @@ services: nginx: image: nginx:latest depends_on: - - web + - server volumes: - "./nginx.conf:/etc/nginx/nginx.conf" ports: - "7777:80" - web: # port 8000 is server + server: # port 8000 is server image: ghcr.io/thearyadev/top500-aggregator:latest # build: . @@ -21,7 +21,12 @@ services: MYSQLPORT: '3306' depends_on: - database - + + frontend: + build: + dockerfile: Dockerfile.frontend + ports: + - "3000:3000" # these services are internal, so secrets do not matter. database: image: mysql@sha256:566007208a3f1cc8f9df6b767665b5c9b800fc4fb5f863d17aa1df362880ed04 @@ -31,7 +36,4 @@ services: MYSQL_PASSWORD: 'QiyWsI7y1oGGJjz4biiu' MYSQL_ROOT_PASSWORD: 'QiyWsI7y1oGGJjz4biiu' volumes: - - ./mysql-data:/var/lib/mysql -# ports: -# - "3306:3306" - + - ./mysql-data:/var/lib/mysql \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 00000000..781ed585 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "react/jsx-key": "off", + "react/no-unescaped-entities": "off" + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/app/components/card/card.module.scss b/frontend/app/components/card/card.module.scss new file mode 100644 index 00000000..dc6786ce --- /dev/null +++ b/frontend/app/components/card/card.module.scss @@ -0,0 +1,22 @@ +.card { + @apply border-2; + @apply p-3; + @apply rounded; + @apply mb-3 +} + +.heading-container { +} + +.title{ + @apply text-lg; + @apply font-bold +} + +.subtitle{ + @apply font-extralight +} + +.cardBodyWrap { + @apply grid grid-cols-1 md:grid-cols-3 gap-4; +} \ No newline at end of file diff --git a/frontend/app/components/card/card.tsx b/frontend/app/components/card/card.tsx new file mode 100644 index 00000000..4aaa003f --- /dev/null +++ b/frontend/app/components/card/card.tsx @@ -0,0 +1,18 @@ +import styles from "./card.module.scss" + + +const Card = ({children, title, subtitle, nowrap}: { children: React.ReactNode, title: string, subtitle?: string, nowrap?:boolean }) => { + return ( +
+
+

{title}

+ {subtitle ?

{subtitle}

: null} +
+
+ {children} +
+
+ ) +} + +export default Card; \ No newline at end of file diff --git a/frontend/app/components/charts/barChart.module.scss b/frontend/app/components/charts/barChart.module.scss new file mode 100644 index 00000000..24ed38cd --- /dev/null +++ b/frontend/app/components/charts/barChart.module.scss @@ -0,0 +1,6 @@ +.chartContainer { + .highcharts-title{ + display: none; + + } +} \ No newline at end of file diff --git a/frontend/app/components/charts/barChart.tsx b/frontend/app/components/charts/barChart.tsx new file mode 100644 index 00000000..8d7c99a2 --- /dev/null +++ b/frontend/app/components/charts/barChart.tsx @@ -0,0 +1,70 @@ +import * as Highcharts from 'highcharts'; +import HighchartsReact from "highcharts-react-official"; +import {useRef} from "react"; +import styles from "./barChart.module.scss" +import {HeroColors} from "@/app/components/charts/heroColors"; + +interface GraphData { + labels: string[] + values: number[] +} + +interface BarChartProps extends HighchartsReact.Props { + title: string; + graph: GraphData + maxY: number; +} + + +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) + + return ( +
+
{title.toLowerCase()}
+ + +
+ + ) + +} + +export default BarChart; \ No newline at end of file diff --git a/frontend/app/components/charts/heroColors.ts b/frontend/app/components/charts/heroColors.ts new file mode 100644 index 00000000..5409f9ef --- /dev/null +++ b/frontend/app/components/charts/heroColors.ts @@ -0,0 +1,45 @@ +interface HeroColor { + [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" +} \ No newline at end of file diff --git a/frontend/app/components/charts/lineChart.module.scss b/frontend/app/components/charts/lineChart.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/app/components/charts/lineChart.tsx b/frontend/app/components/charts/lineChart.tsx new file mode 100644 index 00000000..48a39dd3 --- /dev/null +++ b/frontend/app/components/charts/lineChart.tsx @@ -0,0 +1,74 @@ +import * as Highcharts from 'highcharts'; +import HighchartsReact from "highcharts-react-official"; +import {useRef} from "react"; +import {HeroColors} from "@/app/components/charts/heroColors"; +import type {TrendLine} from "@/pages/trends"; + + + +interface LineChartProps extends HighchartsReact.Props { + title: string; + data: TrendLine[] + seasons: string[] +} + + +const LineChart = (props: LineChartProps) => { + const {data, seasons, title} = props; + console.log(data) + 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], + type: "line", + ...item + })), + credits: { + enabled: false, + }, + yAxis: { + title: { + text: null, + }, + }, + plotOptions: { + series: { + pointPlacement: 'on', + label: { + connectorAllowed: false + } + }, + }, + chart: { + height: "45%" + } + }; + const chartComponentRef = useRef(null) + + return ( +
+
{title}
+ + +
+ + ) + +} + +export default LineChart; \ No newline at end of file diff --git a/frontend/app/components/header/header.module.scss b/frontend/app/components/header/header.module.scss new file mode 100644 index 00000000..939cb1ba --- /dev/null +++ b/frontend/app/components/header/header.module.scss @@ -0,0 +1,15 @@ +.top_header_container { + @apply bg-black text-white mt-0 w-full p-2; +} + +.navbar { + @apply bg-gray-300 w-full overflow-x-auto flex whitespace-nowrap; + + ul { + @apply flex list-none space-x-4; + } + + ul > li { + @apply p-3 hover:bg-gray-400 transition duration-100 ease-in-out; + } +} diff --git a/frontend/app/components/header/header.tsx b/frontend/app/components/header/header.tsx new file mode 100644 index 00000000..64cde6b7 --- /dev/null +++ b/frontend/app/components/header/header.tsx @@ -0,0 +1,32 @@ +import styles from "./header.module.scss" + + +export type NavLinks = { + label: string; + path: string; +} + +export type HeaderProps = { + nav_links: NavLinks[]; +} + +const Header = ({nav_links}: HeaderProps) => { + return ( +
+
+

Top 500 Aggregator

+
+ +
+
    + {nav_links.map(link => { + return
  • {link.label}
  • + })} + +
+
+ +
+ ) +} +export default Header \ No newline at end of file diff --git a/frontend/app/components/index.ts b/frontend/app/components/index.ts new file mode 100644 index 00000000..a285e7bb --- /dev/null +++ b/frontend/app/components/index.ts @@ -0,0 +1,4 @@ +export {default as Header} from "./header/header" +export {default as Card} from "./card/card" +export {default as BarChart} from "./charts/barChart" +export {default as LineChart} from "./charts/lineChart" \ No newline at end of file diff --git a/frontend/app/components/layout.tsx b/frontend/app/components/layout.tsx new file mode 100644 index 00000000..d9f86ee5 --- /dev/null +++ b/frontend/app/components/layout.tsx @@ -0,0 +1,10 @@ +import {Header} from "@/app/components/index"; + + +export default function Layout({ children }: {children: React.ReactNode}) { + return ( + <> +
{children}
+ + ) +} \ No newline at end of file diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/frontend/app/globals.scss b/frontend/app/globals.scss new file mode 100644 index 00000000..4b484946 --- /dev/null +++ b/frontend/app/globals.scss @@ -0,0 +1,32 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/*:root {*/ +/* --foreground-rgb: 0, 0, 0;*/ +/* --background-start-rgb: 214, 219, 220;*/ +/* --background-end-rgb: 255, 255, 255;*/ +/*}*/ + +/*@media (prefers-color-scheme: dark) {*/ +/* :root {*/ +/* --foreground-rgb: 255, 255, 255;*/ +/* --background-start-rgb: 0, 0, 0;*/ +/* --background-end-rgb: 0, 0, 0;*/ +/* }*/ +/*}*/ + +/*body {*/ +/* color: rgb(var(--foreground-rgb));*/ +/* background: linear-gradient(*/ +/* to bottom,*/ +/* transparent,*/ +/* rgb(var(--background-end-rgb))*/ +/* )*/ +/* rgb(var(--background-start-rgb));*/ +/*}*/ + + +html, body{ + margin: 0px; +} \ No newline at end of file diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 00000000..034e3365 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.scss' +import {Header} from "@/app/components"; + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'T500 Aggregator', + description: 'T500 Aggregator', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/frontend/bun.lockb b/frontend/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..f8ad469514417db64ddd806112c90397219ffd1f GIT binary patch literal 143721 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ⅆjr*#q0Aq1OT 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 literal 0 HcmV?d00001 diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 00000000..fd36f949 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 00000000..767719fc --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..0305693a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "highcharts": "^11.2.0", + "highcharts-react-official": "^3.2.1", + "next": "14.0.4", + "react": "^18", + "react-dom": "^18", + "sass": "^1.69.5" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "eslint": "^8", + "eslint-config-next": "14.0.4" + } +} diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx new file mode 100644 index 00000000..1addee8e --- /dev/null +++ b/frontend/pages/_app.tsx @@ -0,0 +1,62 @@ +import '../app/globals.scss'; +import type {AppProps} from 'next/app'; +import {Header} from "@/app/components"; +import {useRouter} from "next/router"; +import {GetServerSidePropsContext} from "next"; + +interface T500AggregatorAppProps extends AppProps { + data: string[] | undefined; +} + + +function MyApp({Component, pageProps}: T500AggregatorAppProps) { + + const router = useRouter() + const {seasonNumber} = router.query + const path = router.asPath + const links = [ + {label: "Trends", path: "/trends"}, + {label: "Season 7", path: "/season/7"}, + {label: "Season 6", path: "/season/6"}, + {label: "Season 5", path: "/season/5"}, + {label: "Season 4", path: "/season/4"}, + {label: "Season 3", path: "/season/3"}, + {label: "Season 2", path: "/season/2"}, + {label: "Season 1", path: "/season/1"}, + {label: "Season 36", path: "/season/36"}, + {label: "Season 35", path: "/season/35"}, + {label: "Season 34", path: "/season/34"}, + ] + return ( + <> +
+
+

{seasonNumber? `Season ${seasonNumber}`: path.toLowerCase().includes("trends") ? "Trends" : "Season 8"}

+ +

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. Starting in season 8, the most recent season will be updated + weekly, overwritten each week until the end of the season

+
+ + +
+ + ) +} + +export default MyApp; diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx new file mode 100644 index 00000000..e8134c49 --- /dev/null +++ b/frontend/pages/index.tsx @@ -0,0 +1,94 @@ +import {Card} from "@/app/components"; +import {BarChart} from "@/app/components"; +import {GetServerSidePropsContext} from "next"; + +type Statistic = { + mean: number; + standard_deviation: number; + variance: number; +} + +type BarChartData = { + labels: string[]; + values: number[]; +} + +type SingleChart = { + graph: BarChartData; + statistic: Statistic; +} + +type SeasonData = { + [key: string]: SingleChart; +} + + +const Index = ({data, season_list}: { data: SeasonData, season_list: string[]}) => { + return ( + <> + + {Object.keys(data).map(key => { + if (key.includes("O_ALL")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + + {Object.keys(data).map(key => { + if (key.includes("OFMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + {Object.keys(data).map(key => { + if (key.includes("OSMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + + {Object.keys(data).map(key => { + if (key.includes("OTMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + ) +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + + // Make an API call using seasonNumber + const res = await fetch(`http://server:8000/chart/7_8`); + const data = await res.json(); + + + const res2 = await fetch("http://server:8000/d/seasons") + const season_list = await res2.json() + + return { + props: { + data, + season_list, + }, + }; +} + + +export default Index \ No newline at end of file diff --git a/frontend/pages/season/[seasonNumber].tsx b/frontend/pages/season/[seasonNumber].tsx new file mode 100644 index 00000000..7bec0c11 --- /dev/null +++ b/frontend/pages/season/[seasonNumber].tsx @@ -0,0 +1,97 @@ +import {useRouter} from "next/router"; +import {Card} from "@/app/components"; +import {BarChart} from "@/app/components"; +import {GetServerSidePropsContext} from "next"; + +type Statistic = { + mean: number; + standard_deviation: number; + variance: number; +} + +type BarChartData = { + labels: string[]; + values: number[]; +} + +type SingleChart = { + graph: BarChartData; + statistic: Statistic; +} + +type SeasonData = { + [key: string]: SingleChart; +} + + +const Season = ({data, season_list}: { data: SeasonData, season_list: string[]}) => { + return ( + <> + + {Object.keys(data).map(key => { + if (key.includes("O_ALL")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + + {Object.keys(data).map(key => { + if (key.includes("OFMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + {Object.keys(data).map(key => { + if (key.includes("OSMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + + {Object.keys(data).map(key => { + if (key.includes("OTMP")){ + const [_, role, region] = key.split("_") + return + } + })} + + + + + ) +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + // @ts-ignore + const {seasonNumber} = context.params; + + // Make an API call using seasonNumber + const res = await fetch(`http://server:8000/chart/${seasonNumber}_8`); + const data = await res.json(); + + + const res2 = await fetch("http://server:8000/d/seasons") + const season_list = await res2.json() + + return { + props: { + data, + season_list, + }, + }; +} + + +export default Season \ No newline at end of file diff --git a/frontend/pages/trends.tsx b/frontend/pages/trends.tsx new file mode 100644 index 00000000..bc50143b --- /dev/null +++ b/frontend/pages/trends.tsx @@ -0,0 +1,38 @@ +import {GetServerSidePropsContext} from "next"; +import {Card} from "@/app/components"; +import {LineChart} from "@/app/components"; + +export type TrendLine = { + name: string; + data: number[] +} + + +const Trends = ({data, season_list}: {data: TrendLine[], season_list: string[]}) => { + return ( + <> + + + + + ) +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const res = await fetch(`http://server:8000/chart/trend/d`); + const data = await res.json(); + + + const res2 = await fetch("http://server:8000/d/seasons") + const season_list = await res2.json() + + return { + props: { + data, + season_list, + }, + }; +} + + +export default Trends; \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 00000000..c7ead804 --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,20 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': + 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + }, + }, + }, + plugins: [], +} +export default config diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..c7146963 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/nginx.conf b/nginx.conf index 0573fb71..71e77911 100644 --- a/nginx.conf +++ b/nginx.conf @@ -56,7 +56,7 @@ http { # Deny all other IP addresses deny all; - proxy_pass http://web:8000; # Forward traffic to the web container + proxy_pass http://frontend:3000; # Forward traffic to the web container proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } diff --git a/server.py b/server.py index 5aae7bee..b9ad8d40 100644 --- a/server.py +++ b/server.py @@ -1,33 +1,25 @@ import json import os from functools import lru_cache -from typing import Annotated, Any, Dict, List +from typing import Any from dotenv import load_dotenv -from fastapi import Depends, FastAPI, Request -from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse, Response +from fastapi import FastAPI +from fastapi.responses import Response from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates import database -import heroes import leaderboards from statistic import ( get_hero_occurrence_trend, - get_hero_trends_all_heroes_by_region, get_mean, - get_number_of_ohp, - get_number_of_thp, get_occurrences, get_occurrences_most_played, get_stdev, get_variance, ) from utils.raise_for_missing_env import raise_for_missing_env_vars -import csv -import io -import zipfile -import xml.etree.ElementTree as ET load_dotenv() templates = Jinja2Templates(directory="templates") @@ -44,20 +36,6 @@ ) -@lru_cache -def get_sitemap() -> str: - urls: list[str] = [ - f"https://t500-aggregator.aryankothari.dev/season/{season}" for season in - db.get_seasons()] - urls.append("https://t500-aggregator.aryankothari.dev/trends/seasonal") - urlset = ET.Element("urlset", xmlns="http://www.sitemaps.org/schemas/sitemap/0.9") - for url in urls: - url_element = ET.SubElement(urlset, "url") - loc = ET.SubElement(url_element, "loc") - loc.text = url - return ET.tostring(urlset, encoding="utf8", method="xml").decode() - - @lru_cache def seasons_list() -> list[str]: """ @@ -68,24 +46,16 @@ def seasons_list() -> list[str]: return db.get_seasons() -@lru_cache -def get_seasons_data_as_csv() -> io.BytesIO: - files: dict[str, io.StringIO] = dict() - for season_ in db.get_seasons(): - season_csv_string_io: io.StringIO = io.StringIO() - data = db.get_all_records(season_) - writer = csv.writer(season_csv_string_io) - writer.writerow(("region", "role", "firstMostPlayed", "secondMostPlayed", "thirdMostPlayed")) - for entry in data: - writer.writerow((entry.region.name, entry.role.name, entry.heroes[0], entry.heroes[1], entry.heroes[2])) - files[f"season{season_}.csv"] = season_csv_string_io - in_memory_zip = io.BytesIO() - with zipfile.ZipFile(in_memory_zip, "w", zipfile.ZIP_DEFLATED) as zipf: - for filename, content in files.items(): - content.seek(0) - zipf.writestr(filename, content.read()) - content.close() - return in_memory_zip +def map_to_label_count_array(data: dict): + result = dict() + for season in data.keys(): + result[season] = dict() + for chart, values in data[season].items(): + result[season][chart] = {"graph": {"labels": list(), "values": list()}, "statistic": values["statistic"]} + for hero in values['graph']: + result[season][chart]["graph"]["labels"].append(hero['hero']) + result[season][chart]["graph"]['values'].append(hero['count']) + return result @lru_cache @@ -411,23 +381,17 @@ def season_data() -> dict[str, Any]: "O_ALL_ALL": { "graph": get_occurrences(data=dataset, region=leaderboards.Region.ALL), }, - "MISC": { - "OHP": get_number_of_ohp(dataset), - "THP": get_number_of_thp(dataset), - }, } # conducts calculations for mean variance and standard dev for key, val in data[s].items(): - if key != "MISC": - graphData = data[s][key]["graph"] # type: ignore - data[s][key]["statistic"] = { # type: ignore - "mean": round(get_mean(graphData), 3), - "variance": round(get_variance(graphData), 3), - "standard_deviation": round(get_stdev(graphData), 3), - } - data[s][key] = json.dumps(val) - return data + graphData = data[s][key]["graph"] # type: ignore + data[s][key]["statistic"] = { # type: ignore + "mean": round(get_mean(graphData), 3), + "variance": round(get_variance(graphData), 3), + "standard_deviation": round(get_stdev(graphData), 3), + } + return map_to_label_count_array(data) @lru_cache @@ -440,113 +404,15 @@ def trends_data() -> list[dict[str, list[int]]]: """ return get_hero_occurrence_trend(db=db) +@app.get('/d/seasons') +async def seasons_list_d(): + return Response(json.dumps(seasons_list()), media_type="application/json") -@app.get("/{_}") -@app.get("/") -async def index_redirect( - request: Request, - seasons_list: Annotated[list[str], Depends(seasons_list)], - seasons_data: Annotated[dict, Depends(season_data)], -): - if "favicon.ico" in str(request.url): - return FileResponse("static/favicon.ico") - - if "robots.txt" in str(request.url): - return FileResponse("static/robots.txt") - - if "apple-touch-icon.png" in str(request.url): - return FileResponse("static/apple-touch-icon.png") - - if "favicon-32x32.png" in str(request.url): - return FileResponse("static/favicon-32x32.png") - - if "sitemap.xml" in str(request.url): - return Response(content=get_sitemap(), media_type="application/xml") - - if "favicon-16x16.png" in str(request.url): - return FileResponse("static/favicon-16x16.png") - - if "site.webmanifest" in str(request.url): - return FileResponse("static/site.webmanifest") - - if "safari-pinned-tab.svg" in str(request.url): - return FileResponse("static/safari-pinned-tab.svg") - - return await season( - request, - season_number=seasons_list[-1], - seasons_data=seasons_data, - seasons_list=seasons_list, - ) +@app.get("/chart/{season}") +async def chart_data(season: str): + print("gnome!") + return Response(content=json.dumps(season_data()[season]), media_type="application/json") - -@app.get("/season/{season_number}") -async def season( - request: Request, - season_number: str, - seasons_data: Annotated[dict, Depends(season_data)], - seasons_list: Annotated[list[str], Depends(seasons_list)], -): - request.app.state.templates.env.filters["group_subseasons"] = group_subseasons - - if season_number in seasons_list: - return templates.TemplateResponse( - "season.html", - { - "request": request, - "seasons": seasons_list, - "currentSeason": season_number, - "hero_colors": json.dumps(heroes.Heroes().hero_colors), - **seasons_data[season_number], # type: ignore - **seasons_data[season_number]["MISC"], # type: ignore - # this does work. Im not sure why mypy is complaining. - # It unpacks all of the chart datas into the global scope of the template - "disclaimer": db.get_season_disclaimer(season_number), - }, - ) - return RedirectResponse(f"/season{seasons_list[-1]}") - - -@app.get("/trends/seasonal") -async def trendsEndpoint( - request: Request, - seasons_list: Annotated[list[str], Depends(seasons_list)], - trends_data: Annotated[dict, Depends(trends_data)], -): - request.app.state.templates.env.filters["group_subseasons"] = group_subseasons - - return templates.TemplateResponse( - "trends.html", - { - "request": request, - "seasons": seasons_list, - "trends": json.dumps(trends_data), - "hero_colors": json.dumps(heroes.Heroes().hero_colors), - }, - ) - - -@app.get("/data/csv") -async def get_data_csv(request: Request) -> StreamingResponse: - zipfile_data = get_seasons_data_as_csv() - zipfile_data.seek(0) - return StreamingResponse(zipfile_data, media_type="application/zip", - headers={ - "Content-Disposition": "attachment; filename=t500-aggregator-all-seasons-archive.zip"}) - - -def group_subseasons(seasons: list[str]) -> dict[str, list[str]]: - """ - Groups sub seasons together, to shrink the menu size. - Args: - seasons: list of seasons - Returns: - dict[str, list[str]]: dict of subseasons and their seasons - """ - subseasons: dict[str, list[str]] = {} - for season in seasons: - subseason = season.split("_")[0] - if subseason not in subseasons: - subseasons[subseason] = [] - subseasons[subseason].append(season) - return subseasons +@app.get("/chart/trend/d") +async def trend_chart_data(): + return Response(content=json.dumps(trends_data()), media_type="application/json") \ No newline at end of file diff --git a/tests/test_server.py b/tests/test_server.py deleted file mode 100644 index 286ff331..00000000 --- a/tests/test_server.py +++ /dev/null @@ -1,68 +0,0 @@ -import os - -from dotenv import load_dotenv -from fastapi.testclient import TestClient - -import database -from server import app -from utils.raise_for_missing_env import raise_for_missing_env_vars -import zipfile -import io -import csv - -load_dotenv() - -dba = database.DatabaseAccess( - host=os.getenv("MYSQLHOST") or raise_for_missing_env_vars(), - user=os.getenv("MYSQLUSER") or raise_for_missing_env_vars(), - password=os.getenv("MYSQLPASSWORD") or raise_for_missing_env_vars(), - database=os.getenv("MYSQLDATABASE") or raise_for_missing_env_vars(), - port=os.getenv("MYSQLPORT") or raise_for_missing_env_vars(), -) - - -client = TestClient(app) - - -def test_all_seasons_valid_html(): - # get all the seasons - seasons: list[str] = dba.get_seasons() - for season in seasons: # attempt to access each seasons webpage - response = client.get(f"/season/{season}") - assert response.status_code == 200 - - -def test_trends(): - response = client.get("/trends/seasonal") - # assert response.status_code == 200 - assert True - - -def test_favicon(): - response = client.get("/favicon.ico") - assert response.status_code == 200 - - -def test_robots(): - response = client.get("/robots.txt") - assert response.status_code == 200 - - -def test_sitemap(): - response = client.get("/sitemap.xml") - assert response.status_code == 200 - - -def test_csv_gen(): - response = client.get("/data/csv") - zipfile_data = io.BytesIO(response.content) - with zipfile.ZipFile(zipfile_data, "r") as zipf: - file_list = zipf.namelist() - for f in file_list: - with zipf.open(f) as file: - content = file.read() - assert content - reader = csv.reader(io.StringIO(content.decode())) - for _ in range(50): - assert next(reader) - \ No newline at end of file