From a094d54032e5a3128d437c8415195bf296cc11c8 Mon Sep 17 00:00:00 2001 From: Giovanni Martire Date: Thu, 21 Dec 2023 12:17:08 +0100 Subject: [PATCH 1/4] Implementation of tournament related graphs --- app/dashboard/page.tsx | 44 ++--- components/Charts/AreaChart/AreaChart.tsx | 4 +- components/Charts/BarChart/BarChart.tsx | 70 ++++++-- .../Charts/RadarChart/RadarChart.module.css | 3 + components/Charts/RadarChart/RadarChart.tsx | 152 ++++++++++++++++++ components/Dashboard/GridCard/GridCard.tsx | 8 +- 6 files changed, 248 insertions(+), 33 deletions(-) create mode 100644 components/Charts/RadarChart/RadarChart.module.css create mode 100644 components/Charts/RadarChart/RadarChart.tsx diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 3a14fca..e6fb371 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -2,6 +2,7 @@ import AreaChart from '@/components/Charts/AreaChart/AreaChart'; import BarChart from '@/components/Charts/BarChart/BarChart'; import DoughnutChart from '@/components/Charts/DoughnutChart/DoughnutChart'; import PlayersBarChart from '@/components/Charts/PlayersBarChart/PlayersBarChart'; +import RadarChart from '@/components/Charts/RadarChart/RadarChart'; import FilterButtons from '@/components/Dashboard/FilterButtons/FilterButtons'; import GridCard from '@/components/Dashboard/GridCard/GridCard'; import UserTotalMatches from '@/components/Dashboard/Matches/UserTotalMatches/UserTotalMatches'; @@ -30,7 +31,7 @@ export default async function page({ return (
-
+ {/*
Your TR (tournament rating) is calculated based on your match cost relative to other players in your matches, see{' '} @@ -59,7 +60,7 @@ export default async function page({ "If you've never played osu! tournaments before, welcome! You can start earning TR simply by playing in osu! tournament matches." } /> -
+
*/}
@@ -158,8 +159,8 @@ export default async function page({
- - + +
@@ -174,10 +175,6 @@ export default async function page({ {data?.matchStats.bestTeammateName}
-
- Worst teammate (to remove) - ?? -
@@ -190,13 +187,9 @@ export default async function page({ Best opponent TO DO
-
- Worst opponent (to remove) - ?? -
- - + + @@ -211,8 +204,11 @@ export default async function page({ - - + + @@ -220,11 +216,19 @@ export default async function page({ - - + + - - + +
diff --git a/components/Charts/AreaChart/AreaChart.tsx b/components/Charts/AreaChart/AreaChart.tsx index 0517cce..5af7864 100644 --- a/components/Charts/AreaChart/AreaChart.tsx +++ b/components/Charts/AreaChart/AreaChart.tsx @@ -230,9 +230,9 @@ export default function AreaChart({ fill: true, label: '', data: dataForGraph, - borderWidth: 2, + borderWidth: 3, borderColor: `hsla(${colors[0]}, 0.6)`, - backgroundColor: `hsla(${colors[1]}, 0.6)`, + backgroundColor: 'transparent' /* `hsla(${colors[1]}, 0.6)` */, font: font, }, ], diff --git a/components/Charts/BarChart/BarChart.tsx b/components/Charts/BarChart/BarChart.tsx index 398b8cb..8d199cb 100644 --- a/components/Charts/BarChart/BarChart.tsx +++ b/components/Charts/BarChart/BarChart.tsx @@ -22,8 +22,19 @@ ChartJS.register( Legend ); -export default function BarChart({ mainAxe = 'x' }: { mainAxe: any }) { +export default function BarChart({ + mainAxe = 'x', + bestTournamentPerformances, + worstTournamentPerformances, + teamSizes, +}: { + mainAxe: any; + bestTournamentPerformances?: any; + worstTournamentPerformances?: any; + teamSizes?: any; +}) { const [font, setFont] = useState(''); + const [color, setColor] = useState(''); /* get variables of colors from CSS */ useEffect(() => { @@ -32,6 +43,9 @@ export default function BarChart({ mainAxe = 'x' }: { mainAxe: any }) { '--font-families' ) ); + setColor( + getComputedStyle(document.documentElement).getPropertyValue('--green-400') + ); }, []); const options = { @@ -52,7 +66,7 @@ export default function BarChart({ mainAxe = 'x' }: { mainAxe: any }) { display: false, }, tooltip: { - enabled: false, + enabled: true, }, }, scales: { @@ -63,6 +77,7 @@ export default function BarChart({ mainAxe = 'x' }: { mainAxe: any }) { family: font, }, }, + grace: '2%', /* border: { color: 'transparent', }, */ @@ -74,27 +89,62 @@ export default function BarChart({ mainAxe = 'x' }: { mainAxe: any }) { family: font, }, }, + grace: '10%', + stepsSize: 1, }, }, }; - const labels = ['HR', 'NM', 'HD', 'FM']; + var labels = ['HR', 'NM', 'HD', 'FM']; + var dataScores = ['']; /* labels.map(() => Math.ceil(Math.random() * 20)) */ + + if (bestTournamentPerformances) { + labels.length = 0; + bestTournamentPerformances.map((tournament: any, index: any) => { + labels[index] = tournament.tournamentName; + dataScores[index] = tournament.matchCost.toFixed(2); + return; + }); + } + + if (worstTournamentPerformances) { + labels.length = 0; + worstTournamentPerformances.map((tournament: any, index: any) => { + labels[index] = tournament.tournamentName; + dataScores[index] = tournament.matchCost.toFixed(2); + return; + }); + } + + if (teamSizes) { + Object.keys(teamSizes).map((data: any, index: any) => { + labels[index] = data.replace('count', ''); + return; + }); + + Object.values(teamSizes).map((data: any, index: any) => { + dataScores[index] = data; + return; + }); + } const data = { labels, datasets: [ { label: 'Dataset 1', - data: labels.map(() => Math.ceil(Math.random() * 100)), - backgroundColor: [ - 'rgba(255, 99, 132)', - 'rgba(54, 162, 235)', - 'rgba(255, 206, 86)', - 'rgba(25, 156, 86)', - ], + data: dataScores, + backgroundColor: teamSizes + ? [ + 'rgba(120, 227, 117, 1)', + 'rgba(76, 148, 255, 1)', + 'rgba(227, 117, 117, 1)', + ] + : `hsla(${color})`, /* barThickness: 30, */ /* maxBarThickness: 30, */ beginAtZero: true, + padding: 10, }, ], }; diff --git a/components/Charts/RadarChart/RadarChart.module.css b/components/Charts/RadarChart/RadarChart.module.css new file mode 100644 index 0000000..ec6dbcb --- /dev/null +++ b/components/Charts/RadarChart/RadarChart.module.css @@ -0,0 +1,3 @@ +.radarChart { + height: 20rem; +} diff --git a/components/Charts/RadarChart/RadarChart.tsx b/components/Charts/RadarChart/RadarChart.tsx new file mode 100644 index 0000000..ab267fa --- /dev/null +++ b/components/Charts/RadarChart/RadarChart.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { + Chart as ChartJS, + Filler, + Legend, + LineElement, + PointElement, + RadialLinearScale, + Tooltip, +} from 'chart.js'; +import { useEffect, useState } from 'react'; +import { Radar } from 'react-chartjs-2'; +import styles from './RadarChart.module.css'; + +ChartJS.register( + RadialLinearScale, + PointElement, + LineElement, + Filler, + Tooltip, + Legend +); + +export default function RadarChart({ + winrateModData, + averageModScore, +}: { + winrateModData?: any; + averageModScore?: any; +}) { + const [colors, setColors] = useState([]); + const [font, setFont] = useState(''); + + /* get variables of colors from CSS */ + useEffect(() => { + setColors([ + getComputedStyle(document.documentElement).getPropertyValue('--blue-600'), + getComputedStyle(document.documentElement).getPropertyValue('--blue-400'), + ]); + setFont( + getComputedStyle(document.documentElement).getPropertyValue( + '--font-families' + ) + ); + }, []); + + let labels = ['NM', 'HD', 'HR', 'DT', 'EZ']; + let values = [95, 90, 22, 10, 50]; + + if (winrateModData) { + labels = ['NM', 'HD', 'HR', 'DT', 'EZ']; + values = [ + (winrateModData.playedNM?.winrate * 100) | 0, + (winrateModData.playedHD?.winrate * 100) | 0, + (winrateModData.playedHR?.winrate * 100) | 0, + (winrateModData.playedDT?.winrate * 100) | 0, + (winrateModData.playedEZ?.winrate * 100) | 0, + ]; + } + + if (averageModScore) { + labels = ['NM', 'HD', 'HR', 'DT', 'EZ']; + values = [ + averageModScore.playedNM?.normalizedAverageScore.toFixed(0) | 0, + averageModScore.playedHD?.normalizedAverageScore.toFixed(0) | 0, + averageModScore.playedHR?.normalizedAverageScore.toFixed(0) | 0, + averageModScore.playedDT?.normalizedAverageScore.toFixed(0) | 0, + averageModScore.playedEZ?.normalizedAverageScore.toFixed(0) | 0, + ]; + } + + const data = { + labels: labels, + datasets: [ + { + label: winrateModData + ? 'Winrate %' + : averageModScore + ? 'AVG Score' + : 'Winrate %', + data: values, + backgroundColor: `hsla(${colors[0]}, 0.15)`, + borderWidth: 0, + }, + ], + }; + + const options = { + elements: { + line: { + borderWidth: 0, + }, + point: { + pointBackgroundColor: `hsla(${colors[1]})`, + pointBorderWidth: 0, + }, + }, + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + position: 'right' as const, + }, + title: { + display: false, + }, + tooltip: { + enabled: true, + }, + }, + + layout: { + padding: { + left: 0, + }, + }, + scales: { + r: { + backgroundColor: 'rgb(250,250,250)', + beginAtZero: true, + angleLines: { + borderDash: (context: any) => { + const space = context.scale.yCenter - context.scale.top - 30; + const ticksLength = context.scale.ticks.length - 1; + const spaceInPx = space / ticksLength; + return [0, 0, 0, spaceInPx, 2500]; + }, + }, + min: 0, + max: winrateModData ? 100 : averageModScore ? 1200000 : 100, + ticks: { + stepSize: winrateModData ? 25 : averageModScore ? 300000 : 25, + callback: (value: any, tick: any, values: any) => { + return ''; + }, + showLabelBackdrop: (context: any) => { + return false; + }, + /* maxTicksLimit: 4, */ + }, + }, + }, + }; + + return ( +
+ +
+ ); +} diff --git a/components/Dashboard/GridCard/GridCard.tsx b/components/Dashboard/GridCard/GridCard.tsx index 1cf7513..8e56e4f 100644 --- a/components/Dashboard/GridCard/GridCard.tsx +++ b/components/Dashboard/GridCard/GridCard.tsx @@ -10,7 +10,13 @@ export default function GridCard({ children?: React.ReactNode; }) { return ( -
+

{title}

{children}
From bdecc27470b840bb932aef533927931a31a71223 Mon Sep 17 00:00:00 2001 From: Giovanni Martire Date: Sun, 7 Jan 2024 15:48:53 +0100 Subject: [PATCH 2/4] Tournament submission toast implementation --- app/actions.ts | 3 +- components/Form/Form.tsx | 2 +- .../SubmitMatches/MatchForm/MatchForm.tsx | 385 ++++++++++-------- components/Toast/Toast.module.css | 100 +++++ components/Toast/Toast.tsx | 18 + 5 files changed, 332 insertions(+), 176 deletions(-) create mode 100644 components/Toast/Toast.module.css create mode 100644 components/Toast/Toast.tsx diff --git a/app/actions.ts b/app/actions.ts index 6fd37da..15561c4 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -111,7 +111,8 @@ export async function saveTournamentMatches( ids: matchIDs, }); - let isSubmissionVerified = formData.get('verifierCheckBox') ?? false; + let isSubmissionVerified = + formData.get('verifierCheckBox') == 'on' ?? false; await fetch( `${process.env.REACT_APP_API_URL}/matches/batch?verified=${isSubmissionVerified}`, diff --git a/components/Form/Form.tsx b/components/Form/Form.tsx index ebc0d00..2ac9ab2 100644 --- a/components/Form/Form.tsx +++ b/components/Form/Form.tsx @@ -8,7 +8,7 @@ export default function Form({ children: React.ReactNode; }) { return ( -
+ {children}
); diff --git a/components/SubmitMatches/MatchForm/MatchForm.tsx b/components/SubmitMatches/MatchForm/MatchForm.tsx index 9760689..f5be6fc 100644 --- a/components/SubmitMatches/MatchForm/MatchForm.tsx +++ b/components/SubmitMatches/MatchForm/MatchForm.tsx @@ -3,8 +3,9 @@ import { saveTournamentMatches } from '@/app/actions'; import Form from '@/components/Form/Form'; import InfoIcon from '@/components/Form/InfoIcon/InfoIcon'; +import Toast from '@/components/Toast/Toast'; import clsx from 'clsx'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useFormState, useFormStatus } from 'react-dom'; import styles from './MatchForm.module.css'; @@ -27,197 +28,233 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) { const [rulesAccepted, setRulesAccepted] = useState(false); const [verifierAccepted, setVerifierAccepted] = useState(false); + const [showToast, setShowToast] = useState(false); + + useEffect(() => { + if (state?.status) { + setShowToast(true); + setTimeout(() => { + setShowToast(false); + }, 6000); + } + + if (state?.status === 'success') { + document.getElementById('tournament-form')?.reset(); + } + + return () => {}; + }, [state]); return ( -
-
-
-
-

Tournament

-

- We’re currently prioritizing badged tournaments, but you can - submit an unbadged tournament as well as long as it follows the - rules. -

-
-
-
-
- - {state?.errors?.mode} - -
+ <> +
+ +
+
+

Tournament

+

+ We’re currently prioritizing badged tournaments, but you can + submit an unbadged tournament as well as long as it follows the + rules. +

-
-
- - - {state?.errors?.forumPost} - - +
+
+
+ + + {state?.errors?.mode} + + +
-
-
-
- - - {state?.errors?.tournamentName} - - +
+
+ + + {state?.errors?.forumPost} + + +
-
- - - {state?.errors?.abbreviation} - - +
+
+ + + {state?.errors?.tournamentName} + + +
+
+ + + {state?.errors?.abbreviation} + + +
-
-
-
-
-
-
-
- - - {state?.errors?.teamSize} - - +
+
+ + + {state?.errors?.teamSize} + + +
-
-
-
-

- Match links - {/* // ? ADD INFO TO MATCH */} - -

-
-
-
-
- {state?.errors?.ids} - -
-
-
- setRulesAccepted(e.target.checked)} - /> - setRulesAccepted(!rulesAccepted)}> - I read the rules and I understand that submitting irrelevant - matches can lead to a restriction - +
+
+

+ Match links + {/* // ? ADD INFO TO MATCH */} + +

- {(userRoles.includes('MatchVerifier') || - userRoles.includes('Admin')) && ( +
+
+
+ + {state?.errors?.ids} + + +
+
{ - console.log(e.target.checked); - setVerifierAccepted(e.target.checked); - }} + name="rulesCheckBox" + id="rulesCheckBox" + /* defaultChecked={rulesAccepted} */ + checked={rulesAccepted} + onChange={(e) => setRulesAccepted(e.target.checked)} /> - setVerifierAccepted(!verifierAccepted)}> - Force verify + setRulesAccepted(!rulesAccepted)}> + I read the rules and I understand that submitting irrelevant + matches can lead to a restriction
- )} -
-
- - {state?.errors?.serverError} - + {(userRoles.includes('MatchVerifier') || + userRoles.includes('Admin')) && ( +
+ { + console.log(e.target.checked); + setVerifierAccepted(e.target.checked); + }} + /> + setVerifierAccepted(!verifierAccepted)}> + Force verify + +
+ )} +
+
+ + {state?.errors?.serverError} + +
+
- -
- -
+ +
+ {showToast && ( + + )} + ); } diff --git a/components/Toast/Toast.module.css b/components/Toast/Toast.module.css new file mode 100644 index 0000000..9c17819 --- /dev/null +++ b/components/Toast/Toast.module.css @@ -0,0 +1,100 @@ +.toast { + width: 22em; + height: auto; + padding: 1em 3em; + background-color: rgb(63, 63, 168); + color: white; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + position: fixed; + bottom: 0; + left: 50%; + align-items: center; + margin-left: -11em; + border-radius: 0.3rem; + font-family: var(--font-families); + transform: translateY(2em); + opacity: 0; + -webkit-animation: fadein 0.5s, stay 5s 0.5s, fadeout 0.5s 5s; + animation: fadein 0.5s, stay 5s 0.5s, fadeout 0.5s 5s; +} + +.toast.success { + background-color: hsla(var(--green-400)); + color: hsla(var(--green-900)); + font-weight: 500; +} + +.toast.error { + background-color: hsla(var(--red-400)); + color: hsla(var(--red-900)); + font-weight: 500; +} + +@-webkit-keyframes fadein { + from { + transform: translateY(2em); + opacity: 0; + } + to { + transform: translateY(-1em); + opacity: 1; + } +} + +@keyframes fadein { + from { + transform: translateY(2em); + opacity: 0; + } + to { + transform: translateY(-1em); + opacity: 1; + } +} + +@-webkit-keyframes stay { + from { + transform: translateY(-1em); + opacity: 1; + } + to { + transform: translateY(-1em); + opacity: 1; + } +} + +@keyframes stay { + from { + transform: translateY(-1em); + opacity: 1; + } + to { + transform: translateY(-1em); + opacity: 1; + } +} + +@-webkit-keyframes fadeout { + from { + transform: translateY(-1em); + opacity: 1; + } + to { + transform: translateY(2em); + opacity: 0; + } +} + +@keyframes fadeout { + from { + transform: translateY(-1em); + opacity: 1; + } + to { + transform: translateY(2em); + opacity: 0; + } +} diff --git a/components/Toast/Toast.tsx b/components/Toast/Toast.tsx new file mode 100644 index 0000000..556e36a --- /dev/null +++ b/components/Toast/Toast.tsx @@ -0,0 +1,18 @@ +import clsx from 'clsx'; +import styles from './Toast.module.css'; + +export default function Toast({ + status, + message, +}: { + status: string; + message: string; +}) { + return ( +
+ {message} +
+ ); +} From 1de327d756b5322629778f71a6f479f2025b8159 Mon Sep 17 00:00:00 2001 From: Giovanni Martire Date: Sun, 7 Jan 2024 16:08:20 +0100 Subject: [PATCH 3/4] Fix toast --- components/SubmitMatches/MatchForm/MatchForm.tsx | 9 +++++++-- components/Toast/Toast.module.css | 4 ++-- components/Toast/Toast.tsx | 9 ++++++++- lib/types.ts | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/components/SubmitMatches/MatchForm/MatchForm.tsx b/components/SubmitMatches/MatchForm/MatchForm.tsx index f5be6fc..db687aa 100644 --- a/components/SubmitMatches/MatchForm/MatchForm.tsx +++ b/components/SubmitMatches/MatchForm/MatchForm.tsx @@ -31,15 +31,20 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) { const [showToast, setShowToast] = useState(false); useEffect(() => { - if (state?.status) { + // Shows toast for both success or error, but need better implementation for errors + /* if (state?.status) { setShowToast(true); setTimeout(() => { setShowToast(false); }, 6000); - } + } */ if (state?.status === 'success') { document.getElementById('tournament-form')?.reset(); + setShowToast(true); + setTimeout(() => { + setShowToast(false); + }, 6000); } return () => {}; diff --git a/components/Toast/Toast.module.css b/components/Toast/Toast.module.css index 9c17819..7004d29 100644 --- a/components/Toast/Toast.module.css +++ b/components/Toast/Toast.module.css @@ -1,5 +1,5 @@ .toast { - width: 22em; + width: 26em; height: auto; padding: 1em 3em; background-color: rgb(63, 63, 168); @@ -12,7 +12,7 @@ bottom: 0; left: 50%; align-items: center; - margin-left: -11em; + margin-left: -13em; border-radius: 0.3rem; font-family: var(--font-families); transform: translateY(2em); diff --git a/components/Toast/Toast.tsx b/components/Toast/Toast.tsx index 556e36a..c7d3630 100644 --- a/components/Toast/Toast.tsx +++ b/components/Toast/Toast.tsx @@ -10,7 +10,14 @@ export default function Toast({ }) { return (
{message}
diff --git a/lib/types.ts b/lib/types.ts index 93533c7..1bd52b0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -44,7 +44,7 @@ export const MatchesSubmitFormSchema = z.object({ .number({ required_error: 'osu! match link or lobby id required', invalid_type_error: - 'The format must be an osu! match link, or lobby id. “Text” is not a valid url', + 'Failed to parse one or more entries, ensure all entries are match IDs or osu! match URLs only, one per line', }) .positive() ) From 5845048979432fabb256b3d02a3debbbfef9d28c Mon Sep 17 00:00:00 2001 From: Giovanni Martire Date: Sun, 7 Jan 2024 16:21:17 +0100 Subject: [PATCH 4/4] Form tooltips reword --- .../SubmitMatches/MatchForm/MatchForm.tsx | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/components/SubmitMatches/MatchForm/MatchForm.tsx b/components/SubmitMatches/MatchForm/MatchForm.tsx index db687aa..f22cfad 100644 --- a/components/SubmitMatches/MatchForm/MatchForm.tsx +++ b/components/SubmitMatches/MatchForm/MatchForm.tsx @@ -128,9 +128,29 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) {
{state?.errors?.rankRangeLowerBound} @@ -151,11 +171,21 @@ export default function MatchForm({ userRoles }: { userRoles: Array }) { Team size

- The number of players per team that play in match - at a time -- for example, enter 3 for a 3v3 team size 6 - tournament and 1 for a 1v1. Remember not to include - battle royale matches or matches that are played in - head-to-head mode with more than two players. + + The maximum number of players per team allowed in the + lobby at the same time. This is not the same as the + maximum roster size (Team Size / TS). Ask us in the + Discord if you're not sure. + +
+
+ Examples: +

    +
  • 1v1 TS 1 => 1v1
  • +
  • 1v1 TS 4 => 1v1
  • +
  • 3v3 TS 6 => 3v3
  • +
  • osu! World Cups: 4v4 TS 8 => 4v4
  • +